diff --git a/.gitattributes b/.gitattributes index 8786a737ef760006e8252b4ff43287cfc055fcbd..3c058e0a4d92e264a895edd6b8556562ffa8653a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -218,3 +218,12 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text .venv/lib/python3.11/site-packages/pip/_vendor/distlib/t64.exe filter=lfs diff=lfs merge=lfs -text .venv/lib/python3.11/site-packages/pip/_vendor/distlib/w64-arm.exe filter=lfs diff=lfs merge=lfs -text .venv/lib/python3.11/site-packages/pip/_vendor/distlib/w64.exe filter=lfs diff=lfs merge=lfs -text +.venv/lib/python3.11/site-packages/scanpy/datasets/10x_pbmc68k_reduced.h5ad filter=lfs diff=lfs merge=lfs -text +.venv/lib/python3.11/site-packages/scikit_learn.libs/libgomp-e985bcbb.so.1.0.0 filter=lfs diff=lfs merge=lfs -text +.venv/lib/python3.11/site-packages/scipy/_cyutility.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text +.venv/lib/python3.11/site-packages/scipy/_lib/_uarray/_uarray.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text +.venv/lib/python3.11/site-packages/scipy/cluster/_hierarchy.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text +.venv/lib/python3.11/site-packages/scipy/cluster/_optimal_leaf_ordering.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text +.venv/lib/python3.11/site-packages/scipy/cluster/_vq.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text +.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/pypocketfft.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text +.venv/lib/python3.11/site-packages/scipy/fftpack/convolve.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text diff --git a/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/INSTALLER b/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..5c69047b2eb8235994febeeae1da4a82365a240a --- /dev/null +++ b/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/INSTALLER @@ -0,0 +1 @@ +uv \ No newline at end of file diff --git a/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/LICENSE.txt b/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/LICENSE.txt new file mode 100644 index 0000000000000000000000000000000000000000..5f1c11289f6a54cb07ebdbf31d02e8e81b18b07f --- /dev/null +++ b/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2003-2019 Stuart Bishop + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/METADATA b/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..cccefe1da805237142d593d4c1a1c113a452a27f --- /dev/null +++ b/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/METADATA @@ -0,0 +1,648 @@ +Metadata-Version: 2.1 +Name: pytz +Version: 2026.2 +Summary: World timezone definitions, modern and historical +Home-page: http://pythonhosted.org/pytz +Download-URL: https://pypi.org/project/pytz/ +Author: Stuart Bishop +Author-email: stuart@stuartbishop.net +Maintainer: Stuart Bishop +Maintainer-email: stuart@stuartbishop.net +License: MIT +Keywords: timezone,tzinfo,datetime,olson,time +Platform: Independent +Classifier: Development Status :: 6 - Mature +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Natural Language :: English +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.4 +Classifier: Programming Language :: Python :: 2.5 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.1 +Classifier: Programming Language :: Python :: 3.2 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Topic :: Software Development :: Libraries :: Python Modules +License-File: LICENSE.txt + +pytz - World Timezone Definitions for Python +============================================ + +:Author: Stuart Bishop + +Introduction +~~~~~~~~~~~~ + +pytz brings the Olson tz database into Python. This library allows +accurate and cross platform timezone calculations using Python 2.4 +or higher. It also solves the issue of ambiguous times at the end +of daylight saving time, which you can read more about in the Python +Library Reference (``datetime.tzinfo``). + +Almost all of the Olson timezones are supported. + +.. note:: + + Projects using Python 3.9 or later should be using the support + now included as part of the standard library, and third party + packages work with it such as `tzdata `_. + pytz offers no advantages beyond backwards compatibility with + code written for earlier versions of Python. + +.. note:: + + This library differs from the documented Python API for + tzinfo implementations; if you want to create local wallclock + times you need to use the ``localize()`` method documented in this + document. In addition, if you perform date arithmetic on local + times that cross DST boundaries, the result may be in an incorrect + timezone (ie. subtract 1 minute from 2002-10-27 1:00 EST and you get + 2002-10-27 0:59 EST instead of the correct 2002-10-27 1:59 EDT). A + ``normalize()`` method is provided to correct this. Unfortunately these + issues cannot be resolved without modifying the Python datetime + implementation (see PEP-431). + + +Installation +~~~~~~~~~~~~ + +This package can either be installed using ``pip`` or from a tarball using the +standard Python distutils. + +If you are installing using ``pip``, you don't need to download anything as the +latest version will be downloaded for you from PyPI:: + + pip install pytz + +If you are installing from a tarball, run the following command as an +administrative user:: + + python setup.py install + + +pytz for Enterprise +~~~~~~~~~~~~~~~~~~~ + +Available as part of the Tidelift Subscription. + +The maintainers of pytz and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. `Learn more. `_. + + +Example & Usage +~~~~~~~~~~~~~~~ + +Localized times and date arithmetic +----------------------------------- + +>>> from datetime import datetime, timedelta +>>> from pytz import timezone +>>> import pytz +>>> utc = pytz.utc +>>> utc.zone +'UTC' +>>> eastern = timezone('US/Eastern') +>>> eastern.zone +'US/Eastern' +>>> amsterdam = timezone('Europe/Amsterdam') +>>> fmt = '%Y-%m-%d %H:%M:%S %Z%z' + +This library only supports two ways of building a localized time. The +first is to use the ``localize()`` method provided by the pytz library. +This is used to localize a naive datetime (datetime with no timezone +information): + +>>> loc_dt = eastern.localize(datetime(2002, 10, 27, 6, 0, 0)) +>>> print(loc_dt.strftime(fmt)) +2002-10-27 06:00:00 EST-0500 + +The second way of building a localized time is by converting an existing +localized time using the standard ``astimezone()`` method: + +>>> ams_dt = loc_dt.astimezone(amsterdam) +>>> ams_dt.strftime(fmt) +'2002-10-27 12:00:00 CET+0100' + +Unfortunately using the tzinfo argument of the standard datetime +constructors ''does not work'' with pytz for many timezones. + +>>> datetime(2002, 10, 27, 12, 0, 0, tzinfo=amsterdam).strftime(fmt) # /!\ Does not work this way! +'2002-10-27 12:00:00 LMT+0018' + +It is safe for timezones without daylight saving transitions though, such +as UTC: + +>>> datetime(2002, 10, 27, 12, 0, 0, tzinfo=pytz.utc).strftime(fmt) # /!\ Not recommended except for UTC +'2002-10-27 12:00:00 UTC+0000' + +The preferred way of dealing with times is to always work in UTC, +converting to localtime only when generating output to be read +by humans. + +>>> utc_dt = datetime(2002, 10, 27, 6, 0, 0, tzinfo=utc) +>>> loc_dt = utc_dt.astimezone(eastern) +>>> loc_dt.strftime(fmt) +'2002-10-27 01:00:00 EST-0500' + +This library also allows you to do date arithmetic using local +times, although it is more complicated than working in UTC as you +need to use the ``normalize()`` method to handle daylight saving time +and other timezone transitions. In this example, ``loc_dt`` is set +to the instant when daylight saving time ends in the US/Eastern +timezone. + +>>> before = loc_dt - timedelta(minutes=10) +>>> before.strftime(fmt) +'2002-10-27 00:50:00 EST-0500' +>>> eastern.normalize(before).strftime(fmt) +'2002-10-27 01:50:00 EDT-0400' +>>> after = eastern.normalize(before + timedelta(minutes=20)) +>>> after.strftime(fmt) +'2002-10-27 01:10:00 EST-0500' + +Creating local times is also tricky, and the reason why working with +local times is not recommended. Unfortunately, you cannot just pass +a ``tzinfo`` argument when constructing a datetime (see the next +section for more details) + +>>> dt = datetime(2002, 10, 27, 1, 30, 0) +>>> dt1 = eastern.localize(dt, is_dst=True) +>>> dt1.strftime(fmt) +'2002-10-27 01:30:00 EDT-0400' +>>> dt2 = eastern.localize(dt, is_dst=False) +>>> dt2.strftime(fmt) +'2002-10-27 01:30:00 EST-0500' + +Converting between timezones is more easily done, using the +standard astimezone method. + +>>> utc_dt = datetime.fromtimestamp(1143408899, tz=utc) +>>> utc_dt.strftime(fmt) +'2006-03-26 21:34:59 UTC+0000' +>>> au_tz = timezone('Australia/Sydney') +>>> au_dt = utc_dt.astimezone(au_tz) +>>> au_dt.strftime(fmt) +'2006-03-27 08:34:59 AEDT+1100' +>>> utc_dt2 = au_dt.astimezone(utc) +>>> utc_dt2.strftime(fmt) +'2006-03-26 21:34:59 UTC+0000' +>>> utc_dt == utc_dt2 +True + +You can take shortcuts when dealing with the UTC side of timezone +conversions. ``normalize()`` and ``localize()`` are not really +necessary when there are no daylight saving time transitions to +deal with. + +>>> utc_dt = datetime.fromtimestamp(1143408899, tz=utc) +>>> utc_dt.strftime(fmt) +'2006-03-26 21:34:59 UTC+0000' +>>> au_tz = timezone('Australia/Sydney') +>>> au_dt = au_tz.normalize(utc_dt.astimezone(au_tz)) +>>> au_dt.strftime(fmt) +'2006-03-27 08:34:59 AEDT+1100' +>>> utc_dt2 = au_dt.astimezone(utc) +>>> utc_dt2.strftime(fmt) +'2006-03-26 21:34:59 UTC+0000' + + +``tzinfo`` API +-------------- + +The ``tzinfo`` instances returned by the ``timezone()`` function have +been extended to cope with ambiguous times by adding an ``is_dst`` +parameter to the ``utcoffset()``, ``dst()`` && ``tzname()`` methods. + +>>> tz = timezone('America/St_Johns') + +>>> normal = datetime(2009, 9, 1) +>>> ambiguous = datetime(2009, 10, 31, 23, 30) + +The ``is_dst`` parameter is ignored for most timestamps. It is only used +during DST transition ambiguous periods to resolve that ambiguity. + +>>> print(tz.utcoffset(normal, is_dst=True)) +-1 day, 21:30:00 +>>> print(tz.dst(normal, is_dst=True)) +1:00:00 +>>> tz.tzname(normal, is_dst=True) +'NDT' + +>>> print(tz.utcoffset(ambiguous, is_dst=True)) +-1 day, 21:30:00 +>>> print(tz.dst(ambiguous, is_dst=True)) +1:00:00 +>>> tz.tzname(ambiguous, is_dst=True) +'NDT' + +>>> print(tz.utcoffset(normal, is_dst=False)) +-1 day, 21:30:00 +>>> tz.dst(normal, is_dst=False).seconds +3600 +>>> tz.tzname(normal, is_dst=False) +'NDT' + +>>> print(tz.utcoffset(ambiguous, is_dst=False)) +-1 day, 20:30:00 +>>> tz.dst(ambiguous, is_dst=False) +datetime.timedelta(0) +>>> tz.tzname(ambiguous, is_dst=False) +'NST' + +If ``is_dst`` is not specified, ambiguous timestamps will raise +an ``pytz.exceptions.AmbiguousTimeError`` exception. + +>>> print(tz.utcoffset(normal)) +-1 day, 21:30:00 +>>> print(tz.dst(normal)) +1:00:00 +>>> tz.tzname(normal) +'NDT' + +>>> import pytz.exceptions +>>> try: +... tz.utcoffset(ambiguous) +... except pytz.exceptions.AmbiguousTimeError: +... print('pytz.exceptions.AmbiguousTimeError: %s' % ambiguous) +pytz.exceptions.AmbiguousTimeError: 2009-10-31 23:30:00 +>>> try: +... tz.dst(ambiguous) +... except pytz.exceptions.AmbiguousTimeError: +... print('pytz.exceptions.AmbiguousTimeError: %s' % ambiguous) +pytz.exceptions.AmbiguousTimeError: 2009-10-31 23:30:00 +>>> try: +... tz.tzname(ambiguous) +... except pytz.exceptions.AmbiguousTimeError: +... print('pytz.exceptions.AmbiguousTimeError: %s' % ambiguous) +pytz.exceptions.AmbiguousTimeError: 2009-10-31 23:30:00 + + +Problems with Localtime +~~~~~~~~~~~~~~~~~~~~~~~ + +The major problem we have to deal with is that certain datetimes +may occur twice in a year. For example, in the US/Eastern timezone +on the last Sunday morning in October, the following sequence +happens: + + - 01:00 EDT occurs + - 1 hour later, instead of 2:00am the clock is turned back 1 hour + and 01:00 happens again (this time 01:00 EST) + +In fact, every instant between 01:00 and 02:00 occurs twice. This means +that if you try and create a time in the 'US/Eastern' timezone +the standard datetime syntax, there is no way to specify if you meant +before of after the end-of-daylight-saving-time transition. Using the +pytz custom syntax, the best you can do is make an educated guess: + +>>> loc_dt = eastern.localize(datetime(2002, 10, 27, 1, 30, 00)) +>>> loc_dt.strftime(fmt) +'2002-10-27 01:30:00 EST-0500' + +As you can see, the system has chosen one for you and there is a 50% +chance of it being out by one hour. For some applications, this does +not matter. However, if you are trying to schedule meetings with people +in different timezones or analyze log files it is not acceptable. + +The best and simplest solution is to stick with using UTC. The pytz +package encourages using UTC for internal timezone representation by +including a special UTC implementation based on the standard Python +reference implementation in the Python documentation. + +The UTC timezone unpickles to be the same instance, and pickles to a +smaller size than other pytz tzinfo instances. The UTC implementation +can be obtained as pytz.utc, pytz.UTC, or pytz.timezone('UTC'). + +>>> import pickle, pytz +>>> dt = datetime(2005, 3, 1, 14, 13, 21, tzinfo=utc) +>>> naive = dt.replace(tzinfo=None) +>>> p = pickle.dumps(dt, 1) +>>> naive_p = pickle.dumps(naive, 1) +>>> len(p) - len(naive_p) +17 +>>> new = pickle.loads(p) +>>> new == dt +True +>>> new is dt +False +>>> new.tzinfo is dt.tzinfo +True +>>> pytz.utc is pytz.UTC is pytz.timezone('UTC') +True + +Note that some other timezones are commonly thought of as the same (GMT, +Greenwich, Universal, etc.). The definition of UTC is distinct from these +other timezones, and they are not equivalent. For this reason, they will +not compare the same in Python. + +>>> utc == pytz.timezone('GMT') +False + +See the section `What is UTC`_, below. + +If you insist on working with local times, this library provides a +facility for constructing them unambiguously: + +>>> loc_dt = datetime(2002, 10, 27, 1, 30, 00) +>>> est_dt = eastern.localize(loc_dt, is_dst=True) +>>> edt_dt = eastern.localize(loc_dt, is_dst=False) +>>> print(est_dt.strftime(fmt) + ' / ' + edt_dt.strftime(fmt)) +2002-10-27 01:30:00 EDT-0400 / 2002-10-27 01:30:00 EST-0500 + +If you pass None as the is_dst flag to localize(), pytz will refuse to +guess and raise exceptions if you try to build ambiguous or non-existent +times. + +For example, 1:30am on 27th Oct 2002 happened twice in the US/Eastern +timezone when the clocks where put back at the end of Daylight Saving +Time: + +>>> dt = datetime(2002, 10, 27, 1, 30, 00) +>>> try: +... eastern.localize(dt, is_dst=None) +... except pytz.exceptions.AmbiguousTimeError: +... print('pytz.exceptions.AmbiguousTimeError: %s' % dt) +pytz.exceptions.AmbiguousTimeError: 2002-10-27 01:30:00 + +Similarly, 2:30am on 7th April 2002 never happened at all in the +US/Eastern timezone, as the clocks where put forward at 2:00am skipping +the entire hour: + +>>> dt = datetime(2002, 4, 7, 2, 30, 00) +>>> try: +... eastern.localize(dt, is_dst=None) +... except pytz.exceptions.NonExistentTimeError: +... print('pytz.exceptions.NonExistentTimeError: %s' % dt) +pytz.exceptions.NonExistentTimeError: 2002-04-07 02:30:00 + +Both of these exceptions share a common base class to make error handling +easier: + +>>> isinstance(pytz.AmbiguousTimeError(), pytz.InvalidTimeError) +True +>>> isinstance(pytz.NonExistentTimeError(), pytz.InvalidTimeError) +True + + +A special case is where countries change their timezone definitions +with no daylight savings time switch. For example, in 1915 Warsaw +switched from Warsaw time to Central European time with no daylight savings +transition. So at the stroke of midnight on August 5th 1915 the clocks +were wound back 24 minutes creating an ambiguous time period that cannot +be specified without referring to the timezone abbreviation or the +actual UTC offset. In this case midnight happened twice, neither time +during a daylight saving time period. pytz handles this transition by +treating the ambiguous period before the switch as daylight savings +time, and the ambiguous period after as standard time. + + +>>> warsaw = pytz.timezone('Europe/Warsaw') +>>> amb_dt1 = warsaw.localize(datetime(1915, 8, 4, 23, 59, 59), is_dst=True) +>>> amb_dt1.strftime(fmt) +'1915-08-04 23:59:59 WMT+0124' +>>> amb_dt2 = warsaw.localize(datetime(1915, 8, 4, 23, 59, 59), is_dst=False) +>>> amb_dt2.strftime(fmt) +'1915-08-04 23:59:59 CET+0100' +>>> switch_dt = warsaw.localize(datetime(1915, 8, 5, 00, 00, 00), is_dst=False) +>>> switch_dt.strftime(fmt) +'1915-08-05 00:00:00 CET+0100' +>>> str(switch_dt - amb_dt1) +'0:24:01' +>>> str(switch_dt - amb_dt2) +'0:00:01' + +The best way of creating a time during an ambiguous time period is +by converting from another timezone such as UTC: + +>>> utc_dt = datetime(1915, 8, 4, 22, 36, tzinfo=pytz.utc) +>>> utc_dt.astimezone(warsaw).strftime(fmt) +'1915-08-04 23:36:00 CET+0100' + +The standard Python way of handling all these ambiguities is not to +handle them, such as demonstrated in this example using the US/Eastern +timezone definition from the Python documentation (Note that this +implementation only works for dates between 1987 and 2006 - it is +included for tests only!): + +>>> from pytz.reference import Eastern # pytz.reference only for tests +>>> dt = datetime(2002, 10, 27, 0, 30, tzinfo=Eastern) +>>> str(dt) +'2002-10-27 00:30:00-04:00' +>>> str(dt + timedelta(hours=1)) +'2002-10-27 01:30:00-05:00' +>>> str(dt + timedelta(hours=2)) +'2002-10-27 02:30:00-05:00' +>>> str(dt + timedelta(hours=3)) +'2002-10-27 03:30:00-05:00' + +Notice the first two results? At first glance you might think they are +correct, but taking the UTC offset into account you find that they are +actually two hours appart instead of the 1 hour we asked for. + +>>> from pytz.reference import UTC # pytz.reference only for tests +>>> str(dt.astimezone(UTC)) +'2002-10-27 04:30:00+00:00' +>>> str((dt + timedelta(hours=1)).astimezone(UTC)) +'2002-10-27 06:30:00+00:00' + + +Country Information +~~~~~~~~~~~~~~~~~~~ + +A mechanism is provided to access the timezones commonly in use +for a particular country, looked up using the ISO 3166 country code. +It returns a list of strings that can be used to retrieve the relevant +tzinfo instance using ``pytz.timezone()``: + +>>> print(' '.join(pytz.country_timezones['nz'])) +Pacific/Auckland Pacific/Chatham + +The Olson database comes with a ISO 3166 country code to English country +name mapping that pytz exposes as a dictionary: + +>>> print(pytz.country_names['nz']) +New Zealand + + +What is UTC +~~~~~~~~~~~ + +'UTC' is `Coordinated Universal Time`_. It is a successor to, but distinct +from, Greenwich Mean Time (GMT) and the various definitions of Universal +Time. UTC is now the worldwide standard for regulating clocks and time +measurement. + +All other timezones are defined relative to UTC, and include offsets like +UTC+0800 - hours to add or subtract from UTC to derive the local time. No +daylight saving time occurs in UTC, making it a useful timezone to perform +date arithmetic without worrying about the confusion and ambiguities caused +by daylight saving time transitions, your country changing its timezone, or +mobile computers that roam through multiple timezones. + +.. _Coordinated Universal Time: https://en.wikipedia.org/wiki/Coordinated_Universal_Time + + +Helpers +~~~~~~~ + +There are two lists of timezones provided. + +``all_timezones`` is the exhaustive list of the timezone names that can +be used. + +>>> from pytz import all_timezones +>>> len(all_timezones) >= 500 +True +>>> 'Etc/Greenwich' in all_timezones +True + +``common_timezones`` is a list of useful, current timezones. It doesn't +contain deprecated zones or historical zones, except for a few I've +deemed in common usage, such as US/Eastern (open a bug report if you +think other timezones are deserving of being included here). It is also +a sequence of strings. + +>>> from pytz import common_timezones +>>> len(common_timezones) < len(all_timezones) +True +>>> 'Etc/Greenwich' in common_timezones +False +>>> 'Australia/Melbourne' in common_timezones +True +>>> 'US/Eastern' in common_timezones +True +>>> 'Canada/Eastern' in common_timezones +True +>>> 'Australia/Yancowinna' in all_timezones +True +>>> 'Australia/Yancowinna' in common_timezones +False + +Both ``common_timezones`` and ``all_timezones`` are alphabetically +sorted: + +>>> common_timezones_dupe = common_timezones[:] +>>> common_timezones_dupe.sort() +>>> common_timezones == common_timezones_dupe +True +>>> all_timezones_dupe = all_timezones[:] +>>> all_timezones_dupe.sort() +>>> all_timezones == all_timezones_dupe +True + +``all_timezones`` and ``common_timezones`` are also available as sets. + +>>> from pytz import all_timezones_set, common_timezones_set +>>> 'US/Eastern' in all_timezones_set +True +>>> 'US/Eastern' in common_timezones_set +True +>>> 'Australia/Victoria' in common_timezones_set +False + +You can also retrieve lists of timezones used by particular countries +using the ``country_timezones()`` function. It requires an ISO-3166 +two letter country code. + +>>> from pytz import country_timezones +>>> print(' '.join(country_timezones('ch'))) +Europe/Zurich +>>> print(' '.join(country_timezones('CH'))) +Europe/Zurich + + +Internationalization - i18n/l10n +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Pytz is an interface to the IANA database, which uses ASCII names. The `Unicode Consortium's Unicode Locales (CLDR) `_ +project provides translations. Python packages such as +`Babel `_ +and Thomas Khyn's `l18n `_ package can be used +to access these translations from Python. + + +License +~~~~~~~ + +MIT license. + +This code is also available as part of Zope 3 under the Zope Public +License, Version 2.1 (ZPL). + +I'm happy to relicense this code if necessary for inclusion in other +open source projects. + + +Latest Versions +~~~~~~~~~~~~~~~ + +This package will be updated after releases of the Olson timezone +database. The latest version can be downloaded from the `Python Package +Index `_. The code that is used +to generate this distribution is hosted on Github and available +using git:: + + git clone https://github.com/stub42/pytz.git + +Announcements of new releases are made on +`Launchpad `_, and the +`Atom feed `_ +hosted there. + + +Bugs, Feature Requests & Patches +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Bugs should be reported on `Github `_. +Feature requests are unlikely to be considered, and efforts instead directed +to timezone support now built into Python or packages that work with it. + + +Security Issues +~~~~~~~~~~~~~~~ + +Reports about security issues can be made via `Tidelift `_. + + +Issues & Limitations +~~~~~~~~~~~~~~~~~~~~ + +- This project is in maintenance mode. Projects using Python 3.9 or later + are best served by using the timezone functionality now included in core + Python and packages that work with it such as `tzdata `_. + +- Offsets from UTC are rounded to the nearest whole minute, so timezones + such as Europe/Amsterdam pre 1937 will be up to 30 seconds out. This + was a limitation of the Python datetime library. + +- If you think a timezone definition is incorrect, I probably can't fix + it. pytz is a direct translation of the Olson timezone database, and + changes to the timezone definitions need to be made to this source. + If you find errors they should be reported to the time zone mailing + list, linked from http://www.iana.org/time-zones. + + +Further Reading +~~~~~~~~~~~~~~~ + +More info than you want to know about timezones: +https://data.iana.org/time-zones/tz-link.html + + +Contact +~~~~~~~ + +Stuart Bishop diff --git a/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/RECORD b/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..7c8a9087bedf02f144001c68d1b6bdc65c582ca1 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/RECORD @@ -0,0 +1,618 @@ +pytz-2026.2.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 +pytz-2026.2.dist-info/LICENSE.txt,sha256=vosaN-vibFkqkPbA6zMQOn84POL010mMCvmlJpkKB7g,1088 +pytz-2026.2.dist-info/METADATA,sha256=VrHkkz5DC_UkzCzGYhvNZM2hKUftIpeNDuJBzX8C9Yg,22376 +pytz-2026.2.dist-info/RECORD,, +pytz-2026.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pytz-2026.2.dist-info/WHEEL,sha256=-G_t0oGuE7UD0DrSpVZnq1hHMBV9DD2XkS5v7XpmTnk,110 +pytz-2026.2.dist-info/top_level.txt,sha256=6xRYlt934v1yHb1JIrXgHyGxn3cqACvd-yE8ski_kcc,5 +pytz-2026.2.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 +pytz/__init__.py,sha256=uxsAG0BCjZxbYX_Ch0twmDT54P5xtqC-2ncwHGo5DpI,35529 +pytz/exceptions.py,sha256=434ZcuLlpLQY9mWoGq7zJMV1TyiYvVgpKBU1qZkbDjM,1571 +pytz/lazy.py,sha256=toeR5uDWKBj6ezsUZ4elNP6CEMtK7CO2jS9A30nsFbo,5404 +pytz/reference.py,sha256=zUtCki7JFEmrzrjNsfMD7YL0lWDxynKc1Ubo4iXSs74,3778 +pytz/tzfile.py,sha256=K2y7pZs4vydpZVftrfAA_-hgw17y1Szc7z_QCse6udU,4723 +pytz/tzinfo.py,sha256=XfaVOoO3KsCvtUYaCd0fvgBXWZ8tgevGYUoBh_uiE60,19340 +pytz/zoneinfo/Africa/Abidjan,sha256=0u-sTl8j2IyV1ywdtCgHFw9S9D3ZiiBa9akqkbny2Zc,148 +pytz/zoneinfo/Africa/Accra,sha256=0u-sTl8j2IyV1ywdtCgHFw9S9D3ZiiBa9akqkbny2Zc,148 +pytz/zoneinfo/Africa/Addis_Ababa,sha256=yJsuJTqJJqbOz37_NOS_zbf-JNr_IthHGMMN7sDqSWg,265 +pytz/zoneinfo/Africa/Algiers,sha256=vaFpjNVCwObnbfu82rOQzdJvN6nVgmpXpQ1aqzfzsqY,735 +pytz/zoneinfo/Africa/Asmara,sha256=yJsuJTqJJqbOz37_NOS_zbf-JNr_IthHGMMN7sDqSWg,265 +pytz/zoneinfo/Africa/Asmera,sha256=yJsuJTqJJqbOz37_NOS_zbf-JNr_IthHGMMN7sDqSWg,265 +pytz/zoneinfo/Africa/Bamako,sha256=0u-sTl8j2IyV1ywdtCgHFw9S9D3ZiiBa9akqkbny2Zc,148 +pytz/zoneinfo/Africa/Bangui,sha256=z_6wKCzL1_ug5JP_hneh5abdUZeIUELkN_ladz-ESEY,235 +pytz/zoneinfo/Africa/Banjul,sha256=0u-sTl8j2IyV1ywdtCgHFw9S9D3ZiiBa9akqkbny2Zc,148 +pytz/zoneinfo/Africa/Bissau,sha256=IjuxDP6EZiDHFvl_bHS6NN7sdRxLKXllooBC829poak,194 +pytz/zoneinfo/Africa/Blantyre,sha256=RE7TpxBBS8a_Q-sn5ZHaSdO-PbFTRJpqDJRz9-Of28s,149 +pytz/zoneinfo/Africa/Brazzaville,sha256=z_6wKCzL1_ug5JP_hneh5abdUZeIUELkN_ladz-ESEY,235 +pytz/zoneinfo/Africa/Bujumbura,sha256=RE7TpxBBS8a_Q-sn5ZHaSdO-PbFTRJpqDJRz9-Of28s,149 +pytz/zoneinfo/Africa/Cairo,sha256=Lft-GCLQhaSJm9VqUmsEFoHIS1Vhfa7pFJn9GZCpifs,2399 +pytz/zoneinfo/Africa/Casablanca,sha256=4RqVbw_F3ZucopIC2ivAJ8WDwj5wRODAB67tBpdXcgA,2429 +pytz/zoneinfo/Africa/Ceuta,sha256=Cw-2_nFDGbN8WqIsVpcauyZooWX8j3Kmx2PnC0fHut8,2052 +pytz/zoneinfo/Africa/Conakry,sha256=0u-sTl8j2IyV1ywdtCgHFw9S9D3ZiiBa9akqkbny2Zc,148 +pytz/zoneinfo/Africa/Dakar,sha256=0u-sTl8j2IyV1ywdtCgHFw9S9D3ZiiBa9akqkbny2Zc,148 +pytz/zoneinfo/Africa/Dar_es_Salaam,sha256=yJsuJTqJJqbOz37_NOS_zbf-JNr_IthHGMMN7sDqSWg,265 +pytz/zoneinfo/Africa/Djibouti,sha256=yJsuJTqJJqbOz37_NOS_zbf-JNr_IthHGMMN7sDqSWg,265 +pytz/zoneinfo/Africa/Douala,sha256=z_6wKCzL1_ug5JP_hneh5abdUZeIUELkN_ladz-ESEY,235 +pytz/zoneinfo/Africa/El_Aaiun,sha256=UWCCqQLJxd8qsTYw82kz9W1suwW5TRgnZw31sDWDz20,2295 +pytz/zoneinfo/Africa/Freetown,sha256=0u-sTl8j2IyV1ywdtCgHFw9S9D3ZiiBa9akqkbny2Zc,148 +pytz/zoneinfo/Africa/Gaborone,sha256=RE7TpxBBS8a_Q-sn5ZHaSdO-PbFTRJpqDJRz9-Of28s,149 +pytz/zoneinfo/Africa/Harare,sha256=RE7TpxBBS8a_Q-sn5ZHaSdO-PbFTRJpqDJRz9-Of28s,149 +pytz/zoneinfo/Africa/Johannesburg,sha256=bBvMdSZo53WFowiuhUO9C8zY6BOGViboCb-U8_49l34,246 +pytz/zoneinfo/Africa/Juba,sha256=UVnIqEPJwHLTMC-r5qZQHNv9opoYVsKdq-ta_5XUw_Q,679 +pytz/zoneinfo/Africa/Kampala,sha256=yJsuJTqJJqbOz37_NOS_zbf-JNr_IthHGMMN7sDqSWg,265 +pytz/zoneinfo/Africa/Khartoum,sha256=MYWDoJ3AcCItZdApoeOgtWWDDxquwTon5v5TOGP70-o,679 +pytz/zoneinfo/Africa/Kigali,sha256=RE7TpxBBS8a_Q-sn5ZHaSdO-PbFTRJpqDJRz9-Of28s,149 +pytz/zoneinfo/Africa/Kinshasa,sha256=z_6wKCzL1_ug5JP_hneh5abdUZeIUELkN_ladz-ESEY,235 +pytz/zoneinfo/Africa/Lagos,sha256=z_6wKCzL1_ug5JP_hneh5abdUZeIUELkN_ladz-ESEY,235 +pytz/zoneinfo/Africa/Libreville,sha256=z_6wKCzL1_ug5JP_hneh5abdUZeIUELkN_ladz-ESEY,235 +pytz/zoneinfo/Africa/Lome,sha256=0u-sTl8j2IyV1ywdtCgHFw9S9D3ZiiBa9akqkbny2Zc,148 +pytz/zoneinfo/Africa/Luanda,sha256=z_6wKCzL1_ug5JP_hneh5abdUZeIUELkN_ladz-ESEY,235 +pytz/zoneinfo/Africa/Lubumbashi,sha256=RE7TpxBBS8a_Q-sn5ZHaSdO-PbFTRJpqDJRz9-Of28s,149 +pytz/zoneinfo/Africa/Lusaka,sha256=RE7TpxBBS8a_Q-sn5ZHaSdO-PbFTRJpqDJRz9-Of28s,149 +pytz/zoneinfo/Africa/Malabo,sha256=z_6wKCzL1_ug5JP_hneh5abdUZeIUELkN_ladz-ESEY,235 +pytz/zoneinfo/Africa/Maputo,sha256=RE7TpxBBS8a_Q-sn5ZHaSdO-PbFTRJpqDJRz9-Of28s,149 +pytz/zoneinfo/Africa/Maseru,sha256=bBvMdSZo53WFowiuhUO9C8zY6BOGViboCb-U8_49l34,246 +pytz/zoneinfo/Africa/Mbabane,sha256=bBvMdSZo53WFowiuhUO9C8zY6BOGViboCb-U8_49l34,246 +pytz/zoneinfo/Africa/Mogadishu,sha256=yJsuJTqJJqbOz37_NOS_zbf-JNr_IthHGMMN7sDqSWg,265 +pytz/zoneinfo/Africa/Monrovia,sha256=-VsJW5cU4KdvfgYaQVv4lcuzmaKIVFMd42nO6RXOBdU,208 +pytz/zoneinfo/Africa/Nairobi,sha256=yJsuJTqJJqbOz37_NOS_zbf-JNr_IthHGMMN7sDqSWg,265 +pytz/zoneinfo/Africa/Ndjamena,sha256=8T3A0Zm9Gj0Bvm6rd88t3GAXKiKdGUfHlIqYlkYI0KM,199 +pytz/zoneinfo/Africa/Niamey,sha256=z_6wKCzL1_ug5JP_hneh5abdUZeIUELkN_ladz-ESEY,235 +pytz/zoneinfo/Africa/Nouakchott,sha256=0u-sTl8j2IyV1ywdtCgHFw9S9D3ZiiBa9akqkbny2Zc,148 +pytz/zoneinfo/Africa/Ouagadougou,sha256=0u-sTl8j2IyV1ywdtCgHFw9S9D3ZiiBa9akqkbny2Zc,148 +pytz/zoneinfo/Africa/Porto-Novo,sha256=z_6wKCzL1_ug5JP_hneh5abdUZeIUELkN_ladz-ESEY,235 +pytz/zoneinfo/Africa/Sao_Tome,sha256=MdjxpQ268uzJ7Zx1ZroFUtRUwqsJ6F_yY3AYV9FXw1I,254 +pytz/zoneinfo/Africa/Timbuktu,sha256=0u-sTl8j2IyV1ywdtCgHFw9S9D3ZiiBa9akqkbny2Zc,148 +pytz/zoneinfo/Africa/Tripoli,sha256=W1dptGD70T7ppGoo0fczFQeDiIp0nultLNPV66MwB2c,625 +pytz/zoneinfo/Africa/Tunis,sha256=OFVMEM4eYT2Ez0beuhEUCTSIpcFldWxsV2uEoTZIUNI,689 +pytz/zoneinfo/Africa/Windhoek,sha256=xuhvudrMH4alnVmouSTQI8YL8F_HbgsF2EQ7AZKzuHs,955 +pytz/zoneinfo/America/Adak,sha256=IB1DhwJQAKbhPJ9jHLf8zW5Dad7HIkBS-dhv64E1OlM,2356 +pytz/zoneinfo/America/Anchorage,sha256=oZA1NSPS2BWdymYpnCHFO8BlYVS-ll5KLg2Ez9CbETs,2371 +pytz/zoneinfo/America/Anguilla,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/Antigua,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/Araguaina,sha256=G6v9wYFZ8EB4WQfIsqRbbiiKd2b27j7Zt5dFjBbzx2o,870 +pytz/zoneinfo/America/Argentina/Buenos_Aires,sha256=JmU8lBwmy29gR6OmeytvFdMRx6ObJKnYNHmLyMmXX2M,1062 +pytz/zoneinfo/America/Argentina/Catamarca,sha256=uMCJXXGYmNESHVvj5RYBZ0McrOdE14hwm17l25MgRW0,1062 +pytz/zoneinfo/America/Argentina/ComodRivadavia,sha256=uMCJXXGYmNESHVvj5RYBZ0McrOdE14hwm17l25MgRW0,1062 +pytz/zoneinfo/America/Argentina/Cordoba,sha256=uniNihhMHnr4XK4WpwiPUnrAT0YPmvzqB6f0hRLtXvY,1062 +pytz/zoneinfo/America/Argentina/Jujuy,sha256=PGmAehypCxj0XCenCSWqylDIPbKLK0DlrwJK_24D590,1034 +pytz/zoneinfo/America/Argentina/La_Rioja,sha256=Um6XoVXhsr62ad1mWuebe6NY0ZHauBdR9tMGDgqCOHg,1076 +pytz/zoneinfo/America/Argentina/Mendoza,sha256=xcOVtvRyVYFAU90y2QYwpyQhpMLyAp7-Fxvku4kgl0c,1062 +pytz/zoneinfo/America/Argentina/Rio_Gallegos,sha256=F9ZKR4o8gLHX7QBuIjMapGIdmzJxpqwbouPgZ5MqDpY,1062 +pytz/zoneinfo/America/Argentina/Salta,sha256=h1KYrDNIapvDkYhi1PaB8WD1qWOe4vhhgDJWDCGV4jc,1034 +pytz/zoneinfo/America/Argentina/San_Juan,sha256=AI2GltA80mPNzhHxYycuEwIbO1ANXyIqBQZMpjqKqdQ,1076 +pytz/zoneinfo/America/Argentina/San_Luis,sha256=2ItGRcLVK2wx8MyJsHbIBBeAkU4B-MN5x1ZxNyZ7UJE,1088 +pytz/zoneinfo/America/Argentina/Tucuman,sha256=twO-FqtNJV8XOzWTvFQ-xnEcWCoDUHY3gpVIG0Mzbf8,1090 +pytz/zoneinfo/America/Argentina/Ushuaia,sha256=A6IbpVlY9IIPoSKMFRR9DMROdwXUSDc2HsASueOSnqo,1062 +pytz/zoneinfo/America/Aruba,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/Asuncion,sha256=9yfGiENhXNgd2_0xHVsF-hLeJkGSqRs-zD9O945KJWc,1644 +pytz/zoneinfo/America/Atikokan,sha256=kayA_pdpMcSQ0FjIzotdcf-m1JYfbKE-qcFT8LC8zqA,182 +pytz/zoneinfo/America/Atka,sha256=IB1DhwJQAKbhPJ9jHLf8zW5Dad7HIkBS-dhv64E1OlM,2356 +pytz/zoneinfo/America/Bahia,sha256=qi7dA6FofDhLxVMmd2L8bK3HeaQnc9X-jiijwyfhs3g,1010 +pytz/zoneinfo/America/Bahia_Banderas,sha256=MvrXGJ5LzaHOeguJqxszxjxMhVafGVbk-ojXEc7_YEI,1100 +pytz/zoneinfo/America/Barbados,sha256=ima-Qrrhazu4Qfvu2Z0-e6E-GTiYknuJBu6c2yVG9LE,436 +pytz/zoneinfo/America/Belem,sha256=aZMUgtFDdHNISpqyQRYbmS2IBD-BAS3CaJnhu6onLCY,562 +pytz/zoneinfo/America/Belize,sha256=pkfLY2KfPchbeJa1pWcXmWAwp4ZlRvxWLVezXnrbkws,1614 +pytz/zoneinfo/America/Blanc-Sablon,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/Boa_Vista,sha256=dMtaG11kGlJrgJJgGWEDZZAmnO_HfT3L4X8pI72LLFY,618 +pytz/zoneinfo/America/Bogota,sha256=Z1ernZZGQxulE8KFWHYWcM3SV1jn2_QEc1Q0OJzHRak,232 +pytz/zoneinfo/America/Boise,sha256=7HQsNPJiUheQgFz5kVLvTnf5xhXAYaeANqDskxKz2Vs,2410 +pytz/zoneinfo/America/Buenos_Aires,sha256=JmU8lBwmy29gR6OmeytvFdMRx6ObJKnYNHmLyMmXX2M,1062 +pytz/zoneinfo/America/Cambridge_Bay,sha256=_4xRlX3WdVpEcqoT6myD7NeTCXnn9OYk_iH006bwULo,2254 +pytz/zoneinfo/America/Campo_Grande,sha256=gINiXg5i2e6Rh2Nbo2bFqhPAJL4F4cAqGnBankXTDXw,1430 +pytz/zoneinfo/America/Cancun,sha256=EdV0Nw2WjM7VnjFHoq5jsSbLuuE7eP1OE74utEyWJG4,864 +pytz/zoneinfo/America/Caracas,sha256=mUNMFdDzZLav_ePA1ocBdmqVBierkeEszTIFpNCm5J0,250 +pytz/zoneinfo/America/Catamarca,sha256=uMCJXXGYmNESHVvj5RYBZ0McrOdE14hwm17l25MgRW0,1062 +pytz/zoneinfo/America/Cayenne,sha256=4k7Iv1woX4atqePKrcvMQD2Vk9Tmma7rW_AW_R62pCc,184 +pytz/zoneinfo/America/Cayman,sha256=kayA_pdpMcSQ0FjIzotdcf-m1JYfbKE-qcFT8LC8zqA,182 +pytz/zoneinfo/America/Chicago,sha256=_roybr6I6sIAF6cYdIxGxoRpoef153Fty48dQ6bm9oY,3592 +pytz/zoneinfo/America/Chihuahua,sha256=3Ngzbedg8AzAqxsbQSG0jVRx-LxYlw1i3kx-Yzl-2Ic,1102 +pytz/zoneinfo/America/Ciudad_Juarez,sha256=ir4b27DiFrhL0H4fZQ92nEa-BBoPfLWIz3phU373dgE,1538 +pytz/zoneinfo/America/Coral_Harbour,sha256=kayA_pdpMcSQ0FjIzotdcf-m1JYfbKE-qcFT8LC8zqA,182 +pytz/zoneinfo/America/Cordoba,sha256=uniNihhMHnr4XK4WpwiPUnrAT0YPmvzqB6f0hRLtXvY,1062 +pytz/zoneinfo/America/Costa_Rica,sha256=74rYa6lrgIkyls9PkHo8SCYl9oOqiuG5S7MWdnJelP4,316 +pytz/zoneinfo/America/Coyhaique,sha256=beZXU6Lw5jx2Wp3dNRkFiofXGma2dM6DsqvJT9CSHdc,2126 +pytz/zoneinfo/America/Creston,sha256=illz0sYuLL8lIPK0Tkou6dL0Vck_D0W_3rRTOvFYRmQ,360 +pytz/zoneinfo/America/Cuiaba,sha256=GRJqkhRXNsOUcgjZddQxRIJdRYaw9pM_YLWbun88dkg,1402 +pytz/zoneinfo/America/Curacao,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/Danmarkshavn,sha256=YRZAfUCoVtaL1L-MYMYMH1wyOaVQnfUo_gFnvMXSuzw,698 +pytz/zoneinfo/America/Dawson,sha256=rAHhyuMuyjf_eyA2SBG76MRBf_fj_xi5FAuiWVQgJhw,1614 +pytz/zoneinfo/America/Dawson_Creek,sha256=aJXCyP4j3ggE4wGCN-LrS9hpD_5zWHzQTeSAKTWEPUM,1050 +pytz/zoneinfo/America/Denver,sha256=MugZwApDs8NI9TnXANQlUE8guNBowWQY0m-ptpPndck,2460 +pytz/zoneinfo/America/Detroit,sha256=hecz8yqY2Cj5B61G3gLZdAVZvRgK9l0P90c_gN-uD5g,2230 +pytz/zoneinfo/America/Dominica,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/Edmonton,sha256=-TkIfc3QlvaCf0p8COZ43Y1HRBAl-nARUi-JdXeK1vE,2332 +pytz/zoneinfo/America/Eirunepe,sha256=j5eExkjFaqtC-D8XK0rGzoF9yEgbSlTbPqVG9WKhEa8,642 +pytz/zoneinfo/America/El_Salvador,sha256=gvGN8Lkj-sGm2_rs8OUjAMf1oMtKp2Xes6UfWT0WqgU,224 +pytz/zoneinfo/America/Ensenada,sha256=jC57Mhcm5jRZEto5Z5a9s2cr_EvRqRxl7lizjgBNTKU,2906 +pytz/zoneinfo/America/Fort_Nelson,sha256=erfODr3DrSpz65kAdO7Ts2dGbZxvddEP6gx4BX3y2J0,2240 +pytz/zoneinfo/America/Fort_Wayne,sha256=kNKy9Kj9ICsiYYfCCbAggzMA7exf-GpGPMxoXocHUyw,1682 +pytz/zoneinfo/America/Fortaleza,sha256=rjiSB0q1cBuMDOM9orW_uwe5UOLBwTlfjFotwOYe1mU,702 +pytz/zoneinfo/America/Glace_Bay,sha256=G8DGLGCapH_aYCF_OhaL5Qonf7FOAgAPwelO5htCWBc,2192 +pytz/zoneinfo/America/Godthab,sha256=KGXrMN-YkYpVCgLdpcfwMFQ77EsRAGsjUCG3yAUvVfw,1889 +pytz/zoneinfo/America/Goose_Bay,sha256=JgaLueghSvX2g725FOfIgpgvsqxZGykWOhAZWGpQZRY,3210 +pytz/zoneinfo/America/Grand_Turk,sha256=4YOFEPK60Bel2_fCsY6vSZxUcMJKjiKtyOf_Q0khEwU,1834 +pytz/zoneinfo/America/Grenada,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/Guadeloupe,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/Guatemala,sha256=dugUgCd6QY52yHkHuUP4jRWzo5x439IQigaYCvEF46Q,280 +pytz/zoneinfo/America/Guayaquil,sha256=j2UuIo-4RgSOlTNfu77mhZ92waNTeKFSvmoVemJooT0,232 +pytz/zoneinfo/America/Guyana,sha256=R0bOvCRDC8SRIexmhsduPdHbbRPwI2GviD9otExiUrk,248 +pytz/zoneinfo/America/Halifax,sha256=TZpmc5PwWoLfTfQoQ_b3U17BE2iVKSeNkR0Ho8mbTn8,3424 +pytz/zoneinfo/America/Havana,sha256=HUQeAuKBsEkI5SLZjqynXICOUVOajkKzKH5r-Ov5Odc,2416 +pytz/zoneinfo/America/Hermosillo,sha256=ixYKestLmS7gWobk9Kq6FtLZo1yqbWActrFUKluzctw,388 +pytz/zoneinfo/America/Indiana/Indianapolis,sha256=kNKy9Kj9ICsiYYfCCbAggzMA7exf-GpGPMxoXocHUyw,1682 +pytz/zoneinfo/America/Indiana/Knox,sha256=CsvZ5BKw2qVav3x_F8CU9taJdDk7jX41Cfsqms6jXV8,2444 +pytz/zoneinfo/America/Indiana/Marengo,sha256=f3tQ-lgMSUA7nvn64pXhKtJL7mWzGajoCega5MEJSbI,1738 +pytz/zoneinfo/America/Indiana/Petersburg,sha256=A88OHuM0Rg3iMLHjKgXq_d2jZCdVSytUQs-9W0KcFyQ,1920 +pytz/zoneinfo/America/Indiana/Tell_City,sha256=4dWqAr9Y2BXfL4pAQk-81c3gGl2cNdHXOD7_wJhhhn8,1700 +pytz/zoneinfo/America/Indiana/Vevay,sha256=H7VR2G-_sD_C5Rm4P3g1iRC1FWCPg4m0MGD3P1PLzsk,1430 +pytz/zoneinfo/America/Indiana/Vincennes,sha256=62mAxT7APFCaoygflnEzdOpe-fuW1yObI6m6EUUcS7A,1710 +pytz/zoneinfo/America/Indiana/Winamac,sha256=aZGM2jR8CH9BHSUq7XygiweDd6dorXLPXg246XsbR6s,1794 +pytz/zoneinfo/America/Indianapolis,sha256=kNKy9Kj9ICsiYYfCCbAggzMA7exf-GpGPMxoXocHUyw,1682 +pytz/zoneinfo/America/Inuvik,sha256=6J-mapDnrk9A1LtswoE34tqSy_ufedcEBNxixkrEjIo,2074 +pytz/zoneinfo/America/Iqaluit,sha256=feOnxAN0N0r-M1qlkrA4JMyawoc0tqae0iiBCPDAs4k,2202 +pytz/zoneinfo/America/Jamaica,sha256=wlagieUPRf5-beie-h7QsONbNzjGsm8vMs8uf28pw28,482 +pytz/zoneinfo/America/Jujuy,sha256=PGmAehypCxj0XCenCSWqylDIPbKLK0DlrwJK_24D590,1034 +pytz/zoneinfo/America/Juneau,sha256=k7hxb0aGRnfnE-DBi3LkcjAzRPyAf0_Hw0vVFfjGeb0,2353 +pytz/zoneinfo/America/Kentucky/Louisville,sha256=tP072xV_n_vIQjxxcJ77AGeGj6yL1KPpn3fwids9g1U,2788 +pytz/zoneinfo/America/Kentucky/Monticello,sha256=LtdyCo85BrXQs6rlH61Ym-8KqWHH6PwAOjD0QxhIdzM,2368 +pytz/zoneinfo/America/Knox_IN,sha256=CsvZ5BKw2qVav3x_F8CU9taJdDk7jX41Cfsqms6jXV8,2444 +pytz/zoneinfo/America/Kralendijk,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/La_Paz,sha256=hqfD8LQHupdZhji2e93_9pOQAT-R7muzzjP0nyfbFXY,218 +pytz/zoneinfo/America/Lima,sha256=HHgTnDUnCZzibvL0MrG8qyOuvjmYYw3e3R5VbnxMZs8,392 +pytz/zoneinfo/America/Los_Angeles,sha256=aJd7ua1tGG_vxser02AQpm4wAI3LLTdgh6QcSYYecmg,2852 +pytz/zoneinfo/America/Louisville,sha256=tP072xV_n_vIQjxxcJ77AGeGj6yL1KPpn3fwids9g1U,2788 +pytz/zoneinfo/America/Lower_Princes,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/Maceio,sha256=3R5DlSe32kQDmoSVIWpcyk2o7qohr-rliwqDSGFIMyQ,730 +pytz/zoneinfo/America/Managua,sha256=xBzF01AHn2E2fD8Qdy-DHFe36UqoeNpKPfChduBKWdk,430 +pytz/zoneinfo/America/Manaus,sha256=F6RLOOeOi9lymZiQmQ9pR8tFpPZ6EguNdPfOc6BhXDE,590 +pytz/zoneinfo/America/Marigot,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/Martinique,sha256=fMs80kOU2YFvC0f9y2eje97JeAtTYBamXrnlTunNLzQ,232 +pytz/zoneinfo/America/Matamoros,sha256=fq-PqdmZrQ98UsFmHA9ivjBZv5GEBRTOuLQ5Cu5ajW8,1418 +pytz/zoneinfo/America/Mazatlan,sha256=BWH2NqVPA1PsyELPN_2BF8KllrsmQkqg1eujsQvnnx8,1060 +pytz/zoneinfo/America/Mendoza,sha256=xcOVtvRyVYFAU90y2QYwpyQhpMLyAp7-Fxvku4kgl0c,1062 +pytz/zoneinfo/America/Menominee,sha256=Arv9WLbfhNcpRsUjHDU757BEdwlp08Gt30AixG3gZ04,2274 +pytz/zoneinfo/America/Merida,sha256=SVNEHCazjomftnuPVBayFI-E-IQ0WmluHfTpHP0h3d0,1004 +pytz/zoneinfo/America/Metlakatla,sha256=twmieGTVY2V-U8nFxqvx7asYv8GVjeWdLtrOI7UApVI,1423 +pytz/zoneinfo/America/Mexico_City,sha256=Uog2-FMWz2o12jR6sK9vemJamLeo6OEFMQR3s0xTxkc,1222 +pytz/zoneinfo/America/Miquelon,sha256=l5txBJYe9HTRZlILcbSL_HNDYrjUb0ouecNy7QEkg9c,1652 +pytz/zoneinfo/America/Moncton,sha256=Wmv-bk9aKKcWWzOpc1UFu67HOfwaIk2Wmh3LgqGctys,3154 +pytz/zoneinfo/America/Monterrey,sha256=YixTESJubf6ZBUXy6g32hAM2gR4GXXPqOU4tv0L3kG0,1114 +pytz/zoneinfo/America/Montevideo,sha256=dQEBE4mjZPtyRjKXK6Z-bMHJdFqpwhIzxDH4x04rKYk,1496 +pytz/zoneinfo/America/Montreal,sha256=pYehoWB0Ofe6woPhgV8r26-5ZJpFPRjgbC5E5pltiI8,3494 +pytz/zoneinfo/America/Montserrat,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/Nassau,sha256=pYehoWB0Ofe6woPhgV8r26-5ZJpFPRjgbC5E5pltiI8,3494 +pytz/zoneinfo/America/New_York,sha256=6e0H177gx2qdRC0JHvHwFmj-58TyYBTAqGixn-bBipU,3552 +pytz/zoneinfo/America/Nipigon,sha256=pYehoWB0Ofe6woPhgV8r26-5ZJpFPRjgbC5E5pltiI8,3494 +pytz/zoneinfo/America/Nome,sha256=2izM3-P-PqJ9za6MdhzFfMvPFNq7Gim69tAvEwPeY2s,2367 +pytz/zoneinfo/America/Noronha,sha256=feeRAijQqKylZgqe84nKhsFLycT5zIBm7mLIvdyGw4w,702 +pytz/zoneinfo/America/North_Dakota/Beulah,sha256=qtgbqNu8M3AkHF2n-_oSps1pYT4SxgclbkkPKbXaBHs,2396 +pytz/zoneinfo/America/North_Dakota/Center,sha256=9ZWbK9YKkquULyBUFS3Lr_idxbt7V7y4W4EO0Kn20sw,2396 +pytz/zoneinfo/America/North_Dakota/New_Salem,sha256=DH_bsQfuUnK2obdb06KgisO4XLqht12BXdrgUsZZveg,2396 +pytz/zoneinfo/America/Nuuk,sha256=KGXrMN-YkYpVCgLdpcfwMFQ77EsRAGsjUCG3yAUvVfw,1889 +pytz/zoneinfo/America/Ojinaga,sha256=b38Q_7VdkCZzaVwb7OXuddihJAzUKPTTqXcmpBm1ntE,1524 +pytz/zoneinfo/America/Panama,sha256=kayA_pdpMcSQ0FjIzotdcf-m1JYfbKE-qcFT8LC8zqA,182 +pytz/zoneinfo/America/Pangnirtung,sha256=feOnxAN0N0r-M1qlkrA4JMyawoc0tqae0iiBCPDAs4k,2202 +pytz/zoneinfo/America/Paramaribo,sha256=Z7UZvNlgd-qEUHjEPYXIkLNTgjMcCzk9EfUUEmUyd7M,248 +pytz/zoneinfo/America/Phoenix,sha256=illz0sYuLL8lIPK0Tkou6dL0Vck_D0W_3rRTOvFYRmQ,360 +pytz/zoneinfo/America/Port-au-Prince,sha256=09ZAJd4IOiMpfdpUuF1U44R_hRt6BvpAkFXOnYO9yOM,1434 +pytz/zoneinfo/America/Port_of_Spain,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/Porto_Acre,sha256=0gpJUl46hQbp0P6Xj1S0NArIWeAryuuDXjsldvB5GHE,614 +pytz/zoneinfo/America/Porto_Velho,sha256=uSMV2hZWj-VyBhFBwC950wcThfN3jq6KlycESmQTLOA,562 +pytz/zoneinfo/America/Puerto_Rico,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/Punta_Arenas,sha256=tR5uIf1351AWFqrqNtmXnhQWnKREmJaZqKBzaWRVMTQ,1902 +pytz/zoneinfo/America/Rainy_River,sha256=7P-_YQrneFcon7QKSTOnkiGjEppFDn3Z48MJ1qq8VBw,2868 +pytz/zoneinfo/America/Rankin_Inlet,sha256=nXgqjL3O2BV0em-Xk8qVRRZb_X0yQmHE6vmSSvI9Kzc,2066 +pytz/zoneinfo/America/Recife,sha256=bJ_HE0-JFio4-owpZ0pLO8U3ai0fiGu8QHL0DexLiLc,702 +pytz/zoneinfo/America/Regina,sha256=yjqT08pHbICYe83H8JmtaDBvCFqRv7Tfze3Y8xuXukw,980 +pytz/zoneinfo/America/Resolute,sha256=CnMU2dBI-63vt8-J0Q1Ropx-8b9pRCLjhvrycMIedGg,2066 +pytz/zoneinfo/America/Rio_Branco,sha256=0gpJUl46hQbp0P6Xj1S0NArIWeAryuuDXjsldvB5GHE,614 +pytz/zoneinfo/America/Rosario,sha256=uniNihhMHnr4XK4WpwiPUnrAT0YPmvzqB6f0hRLtXvY,1062 +pytz/zoneinfo/America/Santa_Isabel,sha256=jC57Mhcm5jRZEto5Z5a9s2cr_EvRqRxl7lizjgBNTKU,2906 +pytz/zoneinfo/America/Santarem,sha256=VmZP9S5pPucFxyqAOV908EmWXQZvgCgWLmlJJTUl0LE,588 +pytz/zoneinfo/America/Santiago,sha256=0CDw13dCMUsoquMupoJgupkzAUNhDK6E0lVxURA7osA,2515 +pytz/zoneinfo/America/Santo_Domingo,sha256=DKtaEj8fQ92ybITTWU4Bm160S9pzJmUVbjaWRnenxU4,458 +pytz/zoneinfo/America/Sao_Paulo,sha256=BMBnRO4_4HjvO4t3njjrMGZr-ZPmegkvyvL8KPY6ZM4,1430 +pytz/zoneinfo/America/Scoresbysund,sha256=K-qkiMCCFgOe8ccPMABA-lDjc9vb6wpluBOCVfiBdLI,1935 +pytz/zoneinfo/America/Shiprock,sha256=MugZwApDs8NI9TnXANQlUE8guNBowWQY0m-ptpPndck,2460 +pytz/zoneinfo/America/Sitka,sha256=aiS7Fk37hZpzZ9VkeJQeF-BqTLRC1QOTCgMAJwT8UxA,2329 +pytz/zoneinfo/America/St_Barthelemy,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/St_Johns,sha256=r1-17uKv27eZ3JsVkw_DLZQbo6wvjuuVu7C2pDsmOgI,3655 +pytz/zoneinfo/America/St_Kitts,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/St_Lucia,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/St_Thomas,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/St_Vincent,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/Swift_Current,sha256=RRKOF7vZC8VvYxD8PP4J1_hUPayKBP7Lu80avRkfPDY,560 +pytz/zoneinfo/America/Tegucigalpa,sha256=EzOz7ntTlreMq69JZ2CcAb8Ps98V9bUMN480tpPIyw4,252 +pytz/zoneinfo/America/Thule,sha256=8xuPRaZU8RgO5ECqFYHYmnHioc81sBOailkVu8Y02i8,1502 +pytz/zoneinfo/America/Thunder_Bay,sha256=pYehoWB0Ofe6woPhgV8r26-5ZJpFPRjgbC5E5pltiI8,3494 +pytz/zoneinfo/America/Tijuana,sha256=jC57Mhcm5jRZEto5Z5a9s2cr_EvRqRxl7lizjgBNTKU,2906 +pytz/zoneinfo/America/Toronto,sha256=pYehoWB0Ofe6woPhgV8r26-5ZJpFPRjgbC5E5pltiI8,3494 +pytz/zoneinfo/America/Tortola,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/Vancouver,sha256=4YLiDsZ8RqfdCu2z1F-xOV7csU_yryrGlKV3azB77TE,2590 +pytz/zoneinfo/America/Virgin,sha256=hJHlV_-AGoMGUWuMpZRv9fLmghrzFHfrR9fRkcxaZJc,246 +pytz/zoneinfo/America/Whitehorse,sha256=TrR6PCnYG-mSClBMohqlP8qnYhXMUsydI-L-quXFxyM,1614 +pytz/zoneinfo/America/Winnipeg,sha256=7P-_YQrneFcon7QKSTOnkiGjEppFDn3Z48MJ1qq8VBw,2868 +pytz/zoneinfo/America/Yakutat,sha256=tFwnKbvwhyyn4LNTAn5ye_JWDdxjCerNDt7oOwUwO2M,2305 +pytz/zoneinfo/America/Yellowknife,sha256=-TkIfc3QlvaCf0p8COZ43Y1HRBAl-nARUi-JdXeK1vE,2332 +pytz/zoneinfo/Antarctica/Casey,sha256=VeaLOxTfDyjfGXq5Ul95JEIMXNWHSW-0N3yOoS7VK-c,423 +pytz/zoneinfo/Antarctica/Davis,sha256=XB12dEq0Q-3XkzBNTNC7G1fzH-WxxctIuZqI3zp8ypI,283 +pytz/zoneinfo/Antarctica/DumontDUrville,sha256=nB36HBWZTdh3TlP0DLFNz1KRQ0aHIfHbp7LC4Urp9fA,172 +pytz/zoneinfo/Antarctica/Macquarie,sha256=ie7RlaU8RHTorVVj-MX8StKMqx_oXf4UH2PUqpzcwe0,2260 +pytz/zoneinfo/Antarctica/Mawson,sha256=EjIFbqRdr2ZJBaI1XvoWRptnnW1LFrlhydxDDuIQjSI,185 +pytz/zoneinfo/Antarctica/McMurdo,sha256=gADjoyPo_QISQU6UJrAgcHp3HDaMoOFRdH-d23uBSyc,2437 +pytz/zoneinfo/Antarctica/Palmer,sha256=HTZY0M8td7oUx5REPgRCHuqKg5V3fjJEi4lYBNL4Etg,1404 +pytz/zoneinfo/Antarctica/Rothera,sha256=_9NY-f8vkozQYrjbUHP5YjcICg0-LuyA9PnIeK123RU,150 +pytz/zoneinfo/Antarctica/South_Pole,sha256=gADjoyPo_QISQU6UJrAgcHp3HDaMoOFRdH-d23uBSyc,2437 +pytz/zoneinfo/Antarctica/Syowa,sha256=oCKH7uafN8R1o-ijXGoT5U1JZxwvoLzJu_2Cqyi2hUM,151 +pytz/zoneinfo/Antarctica/Troll,sha256=fjcYppwr1FnjEssee-RLgGOANzoUyfjse-RGK46PR2E,1148 +pytz/zoneinfo/Antarctica/Vostok,sha256=KfftwdzK6PkMDz0d-D3z4HKIBgY9KqsqHnTnqsPMrUg,213 +pytz/zoneinfo/Arctic/Longyearbyen,sha256=XuR19xoPwaMvrrhJ-MOcbnqmbW1B7HQrl7OnQ2s7BwE,2298 +pytz/zoneinfo/Asia/Aden,sha256=oCKH7uafN8R1o-ijXGoT5U1JZxwvoLzJu_2Cqyi2hUM,151 +pytz/zoneinfo/Asia/Almaty,sha256=lPLWXk2f1mWYRQZFkIrq_5HkhocsUBis0M-yhdDHcBQ,983 +pytz/zoneinfo/Asia/Amman,sha256=Qv4cXXw7KBQWE882cgj0kjQ3wh1vpV1orJ2v2Jjxr2U,1433 +pytz/zoneinfo/Asia/Anadyr,sha256=WqKnHo5IHSWZ08d2sS5ytHtv0MQMoczP3W9zbDDrbYU,1174 +pytz/zoneinfo/Asia/Aqtau,sha256=4n654FZtDssXSfhQszjZG5OmtbE2zo1KbiWcYrFJg00,969 +pytz/zoneinfo/Asia/Aqtobe,sha256=1oFHTb-ybcTqLXm0r1ZOVgdYMTHlGoNs-Pgvux50d3E,997 +pytz/zoneinfo/Asia/Ashgabat,sha256=-sfGnRumio7_Bs8w9YH4xRDWgjB3wBeW7c0C56Qqk64,605 +pytz/zoneinfo/Asia/Ashkhabad,sha256=-sfGnRumio7_Bs8w9YH4xRDWgjB3wBeW7c0C56Qqk64,605 +pytz/zoneinfo/Asia/Atyrau,sha256=_U8COUIE9nG_HKddZE1Q0sPuz3rMwfjwmfnVDY_vSmg,977 +pytz/zoneinfo/Asia/Baghdad,sha256=S-plKI4zCLqI0idGABEk3oRTazNyrIj2T98-EtWtZD8,969 +pytz/zoneinfo/Asia/Bahrain,sha256=wklGY3WPGp-z1OUwb_KOHzRTwBndt1RfDg9Uttt36G4,185 +pytz/zoneinfo/Asia/Baku,sha256=6_hq98SGG0j0JA8qYx96WcIMZSLW4w460QXh_OM_ccg,1213 +pytz/zoneinfo/Asia/Bangkok,sha256=hf_5PVegQcFSS60CjS80C7h-TGOrfQ4ncm83N8VmZkk,185 +pytz/zoneinfo/Asia/Barnaul,sha256=3zeUimLTMrIZE0vX6XHFvB3MoqExoVbE5CSm6GV0zf0,1207 +pytz/zoneinfo/Asia/Beirut,sha256=_Z_2ZAg_iL9vU51JDB8CB04uXBDrf1kLIis-JnXaS2o,2154 +pytz/zoneinfo/Asia/Bishkek,sha256=IOoUyjABILCkXu1rjCIqSwAufRYFklc5YAC4jdhVw6Q,969 +pytz/zoneinfo/Asia/Brunei,sha256=D5qtyWJ_SM8bTQeJJIYhqqojxlVKbrFC1EYMDU9GzXQ,469 +pytz/zoneinfo/Asia/Calcutta,sha256=6Qw0EDbLcgMgDik8s7UTJn4QSjmllPNeGVJU5rwKF88,285 +pytz/zoneinfo/Asia/Chita,sha256=LbSlS23swFkANUScg8zkNR0imANWNfOIaYd39HbLdIQ,1207 +pytz/zoneinfo/Asia/Choibalsan,sha256=qUkXRsTc_u7B90JxULSu7yzKbGtGfKcfEFIasGPC2ec,877 +pytz/zoneinfo/Asia/Chongqing,sha256=ZP_C5DqUQ1oEPAQNHTr36S0DGtx453N68YYbqk7u8-Y,561 +pytz/zoneinfo/Asia/Chungking,sha256=ZP_C5DqUQ1oEPAQNHTr36S0DGtx453N68YYbqk7u8-Y,561 +pytz/zoneinfo/Asia/Colombo,sha256=w52L7bgT4m5hcgRuevIPY83xytfkBmkLhnKMwp16KsY,358 +pytz/zoneinfo/Asia/Dacca,sha256=-xulJ2KVhvKp6rlZLMydpw7oXVirk-riEH-181xPE54,323 +pytz/zoneinfo/Asia/Damascus,sha256=EthGheaHWmy5IrLCc9NmM3jvASQFHt8TsBF07I1tgbg,1873 +pytz/zoneinfo/Asia/Dhaka,sha256=-xulJ2KVhvKp6rlZLMydpw7oXVirk-riEH-181xPE54,323 +pytz/zoneinfo/Asia/Dili,sha256=2A9uFmwSwoFA5o2ek1LA0ucohPnM42ghWvD9D5TdnJk,257 +pytz/zoneinfo/Asia/Dubai,sha256=pmdhPhaJRwKwONvxiZNGeFSICjlWzyY9JlFHv-H9upY,151 +pytz/zoneinfo/Asia/Dushanbe,sha256=koYnnYWuFsBXd1vJfZsGdpwnbFHEwvkGBmSrrx3KIss,577 +pytz/zoneinfo/Asia/Famagusta,sha256=CFrcygd8ude5x6OEtfM_Dw0KYHoxpPPzq46KoHVxjjc,2028 +pytz/zoneinfo/Asia/Gaza,sha256=t0YxcUQL53VNKnKbKijn0OE_MaryEynonabse-iTtzs,3844 +pytz/zoneinfo/Asia/Harbin,sha256=ZP_C5DqUQ1oEPAQNHTr36S0DGtx453N68YYbqk7u8-Y,561 +pytz/zoneinfo/Asia/Hebron,sha256=6Y0USHKx-xoCxCr_WpCuM3olP1vUGnzrcnGiyQFcqdQ,3872 +pytz/zoneinfo/Asia/Ho_Chi_Minh,sha256=EE-3MYV9Q51fP0LxqzoJxOZsx-0ZfIlK1w-lxuCAMHA,329 +pytz/zoneinfo/Asia/Hong_Kong,sha256=al_O4kPlq5JpgkLYjEaZzrcgiiLul9NC0R5B69JVWhc,1233 +pytz/zoneinfo/Asia/Hovd,sha256=Zn4PLGlD-URJDsbChor5bqWTzuAil2tbrGJW0j5TLbs,877 +pytz/zoneinfo/Asia/Irkutsk,sha256=IVuoXCwdeI-KIUfFkEt6yBjqYP3V9GTrF-_WLnffFzk,1229 +pytz/zoneinfo/Asia/Istanbul,sha256=Jk4wjndDta_uLWc8W1dWdjbavJJbsL5ROTmZboVnGKU,1933 +pytz/zoneinfo/Asia/Jakarta,sha256=TvEzBvSzfzFCdOsMAZ0QgR95JA5xf3kAZONhy5gEXRE,383 +pytz/zoneinfo/Asia/Jayapura,sha256=ihzUd-L8HUVqG-Na10MyPE-YYwjVFj-xerqjTN4EJZs,221 +pytz/zoneinfo/Asia/Jerusalem,sha256=JUuWQmW5Tha0pJjw61Q5aN7CX0z4D7ops9OOSnda6Dc,2388 +pytz/zoneinfo/Asia/Kabul,sha256=JZEbo8bSj_L7HnXUm2gAUlNlCvJlRJhFkSHCg5o3ggk,194 +pytz/zoneinfo/Asia/Kamchatka,sha256=KY1PlJvRSNkY_5hyJBxj5DDweeYVQaBK05ZgL3kdcCY,1152 +pytz/zoneinfo/Asia/Karachi,sha256=iB-mWMTXUyfBwAkZdz8_UmEw0xsgxIub-KNI7akzhkk,379 +pytz/zoneinfo/Asia/Kashgar,sha256=F1ZOdZZDsVHwDJinksR-hjcqPzqOljvdreZIWFulJxY,151 +pytz/zoneinfo/Asia/Kathmandu,sha256=_RsfeSWbCr8kM4YRJi7Xv6hAEiHW14IFhsXsfhbPjoM,198 +pytz/zoneinfo/Asia/Katmandu,sha256=_RsfeSWbCr8kM4YRJi7Xv6hAEiHW14IFhsXsfhbPjoM,198 +pytz/zoneinfo/Asia/Khandyga,sha256=bKfmw6k5qYDQsEHG3Mv-VYis3YhCeV7qijDxfxQNn_g,1257 +pytz/zoneinfo/Asia/Kolkata,sha256=6Qw0EDbLcgMgDik8s7UTJn4QSjmllPNeGVJU5rwKF88,285 +pytz/zoneinfo/Asia/Krasnoyarsk,sha256=D5KE_1wWSD2YdixDy8n3LBNaAlE1_y3TWXw6NrxFKKA,1193 +pytz/zoneinfo/Asia/Kuala_Lumpur,sha256=XmeVImeqcJ8hJzm7TjAti1nWJAxawOqq7jIzDnHX2hI,401 +pytz/zoneinfo/Asia/Kuching,sha256=D5qtyWJ_SM8bTQeJJIYhqqojxlVKbrFC1EYMDU9GzXQ,469 +pytz/zoneinfo/Asia/Kuwait,sha256=oCKH7uafN8R1o-ijXGoT5U1JZxwvoLzJu_2Cqyi2hUM,151 +pytz/zoneinfo/Asia/Macao,sha256=MvAkRyRsrA2r052ItlyF5bh2FheRjI0jPwg0uIiH2Yk,1227 +pytz/zoneinfo/Asia/Macau,sha256=MvAkRyRsrA2r052ItlyF5bh2FheRjI0jPwg0uIiH2Yk,1227 +pytz/zoneinfo/Asia/Magadan,sha256=HccEEXBQvMmLoC_JE-zP_MlLAZ1WmNLQLfM3tJt55M4,1208 +pytz/zoneinfo/Asia/Makassar,sha256=OhJtCqSTEU-u5n0opBVO5Bu-wQzcYPy9S_6aAhJXgOw,254 +pytz/zoneinfo/Asia/Manila,sha256=8xTSHFQuYVdW3ThdNqiWzVe6Fv75g_5rTQYURLvxrJ4,422 +pytz/zoneinfo/Asia/Muscat,sha256=pmdhPhaJRwKwONvxiZNGeFSICjlWzyY9JlFHv-H9upY,151 +pytz/zoneinfo/Asia/Nicosia,sha256=0Unm0IFT7HyGeQ7F3vTa_-klfysCgrulqFO6BD1plZU,2002 +pytz/zoneinfo/Asia/Novokuznetsk,sha256=pyxxtSUtYDeVmFk0Cg-F33laZS0iKtde9_GJnL9f0KM,1151 +pytz/zoneinfo/Asia/Novosibirsk,sha256=5K2-Gx15ThlHfolyW85S5zREtAcMjeHBYWK4E8x2LdY,1207 +pytz/zoneinfo/Asia/Omsk,sha256=HyXIWItJXBKVHUzWcQPi1Mmd6ZLmZk-QhRUo9Kv2XOI,1193 +pytz/zoneinfo/Asia/Oral,sha256=WQT4qRmC9RI_ll8zB9FvkAL8ezGb8qoqWd75GTlC7kQ,991 +pytz/zoneinfo/Asia/Phnom_Penh,sha256=hf_5PVegQcFSS60CjS80C7h-TGOrfQ4ncm83N8VmZkk,185 +pytz/zoneinfo/Asia/Pontianak,sha256=inOXwuKtjKv1z_eliPZSIqjSt6whtuxhPeG1YpjU_BQ,353 +pytz/zoneinfo/Asia/Pyongyang,sha256=_-g3GnDAtfDX4XAktXH9jFouLUDmOovnjoOfvRpUDsE,237 +pytz/zoneinfo/Asia/Qatar,sha256=wklGY3WPGp-z1OUwb_KOHzRTwBndt1RfDg9Uttt36G4,185 +pytz/zoneinfo/Asia/Qostanay,sha256=HIjln8QIPNRU6MkWzyPi6vDrjlmVZ4XzFxcUHtXMi7s,1025 +pytz/zoneinfo/Asia/Qyzylorda,sha256=JZLNN6NuLkqaWEeVaCZiW_gL6BrIFL9lr65iK7myVPg,1011 +pytz/zoneinfo/Asia/Rangoon,sha256=_YHASq4Z5YcUILIdhEzg27CGLzarUHPDHs1Dj0QgNGM,254 +pytz/zoneinfo/Asia/Riyadh,sha256=oCKH7uafN8R1o-ijXGoT5U1JZxwvoLzJu_2Cqyi2hUM,151 +pytz/zoneinfo/Asia/Saigon,sha256=EE-3MYV9Q51fP0LxqzoJxOZsx-0ZfIlK1w-lxuCAMHA,329 +pytz/zoneinfo/Asia/Sakhalin,sha256=xzAor82ihAe-yXEwC6OWiMzo9b6Z-oQl39NIkU5Hhbs,1188 +pytz/zoneinfo/Asia/Samarkand,sha256=zJKSRt3lEvd6Qvg9b49QAyO4cTJyVnTKyPYcyudpHxk,563 +pytz/zoneinfo/Asia/Seoul,sha256=LI9LsV3XcJC0l-KoQf8zI-y7rk-du57erS-N2Ptdi7Q,617 +pytz/zoneinfo/Asia/Shanghai,sha256=ZP_C5DqUQ1oEPAQNHTr36S0DGtx453N68YYbqk7u8-Y,561 +pytz/zoneinfo/Asia/Singapore,sha256=XmeVImeqcJ8hJzm7TjAti1nWJAxawOqq7jIzDnHX2hI,401 +pytz/zoneinfo/Asia/Srednekolymsk,sha256=efaaT8iFHrcccp-VZKNMvtTuPLNjG5V9JH5KKHhH3SI,1194 +pytz/zoneinfo/Asia/Taipei,sha256=DMmQwOpPql25ue3Nf8vAKKT4em06D1Z9rHbLIitxixk,761 +pytz/zoneinfo/Asia/Tashkent,sha256=apRPy251fSRy_ixsg3BOZNmUbHdO86P5-PdgC1Xws7U,577 +pytz/zoneinfo/Asia/Tbilisi,sha256=fT00i6obiTsjCJ01eBaWuRR9BwNPzKAqLMO1_WVX8Es,1007 +pytz/zoneinfo/Asia/Tehran,sha256=Lb2H9BCBXtz819FL6E3gBA7w2ROiIgPgx-f08XpqkVo,1248 +pytz/zoneinfo/Asia/Tel_Aviv,sha256=JUuWQmW5Tha0pJjw61Q5aN7CX0z4D7ops9OOSnda6Dc,2388 +pytz/zoneinfo/Asia/Thimbu,sha256=G2nTQVEMmKlWt0B74_fUAL7KQ3YAu__J6HciiYs2IyU,189 +pytz/zoneinfo/Asia/Thimphu,sha256=G2nTQVEMmKlWt0B74_fUAL7KQ3YAu__J6HciiYs2IyU,189 +pytz/zoneinfo/Asia/Tokyo,sha256=oCueZgRNxcNcX3ZGdif9y6Su4cyVhga4XHdwlcrYLOs,309 +pytz/zoneinfo/Asia/Tomsk,sha256=cr0ULZgWBnQfzDiJeYmqpA7Xo5QRzurvrHsrbZsnhOQ,1207 +pytz/zoneinfo/Asia/Ujung_Pandang,sha256=OhJtCqSTEU-u5n0opBVO5Bu-wQzcYPy9S_6aAhJXgOw,254 +pytz/zoneinfo/Asia/Ulaanbaatar,sha256=qUkXRsTc_u7B90JxULSu7yzKbGtGfKcfEFIasGPC2ec,877 +pytz/zoneinfo/Asia/Ulan_Bator,sha256=qUkXRsTc_u7B90JxULSu7yzKbGtGfKcfEFIasGPC2ec,877 +pytz/zoneinfo/Asia/Urumqi,sha256=F1ZOdZZDsVHwDJinksR-hjcqPzqOljvdreZIWFulJxY,151 +pytz/zoneinfo/Asia/Ust-Nera,sha256=zsG8kgnw0Fcs5N2WwNTVmvWkTlpwf7Oo8y68HcXjYyw,1238 +pytz/zoneinfo/Asia/Vientiane,sha256=hf_5PVegQcFSS60CjS80C7h-TGOrfQ4ncm83N8VmZkk,185 +pytz/zoneinfo/Asia/Vladivostok,sha256=XMQLMh5SPbI6C4R3UO4KhbnG4hWVkHNedzCQeqxFk6A,1194 +pytz/zoneinfo/Asia/Yakutsk,sha256=PPNrRGgg9jefOUM-6M8XqaIm-ElfmRZSWAtSGKLzNXQ,1193 +pytz/zoneinfo/Asia/Yangon,sha256=_YHASq4Z5YcUILIdhEzg27CGLzarUHPDHs1Dj0QgNGM,254 +pytz/zoneinfo/Asia/Yekaterinburg,sha256=4NyEW6Xjr4UsWPh63HIPI4G6GT_tVG1Xkgc2xbwGjzA,1229 +pytz/zoneinfo/Asia/Yerevan,sha256=FM0pUA4NbTWBb_CsJ5KCLVrLoNmad7njBKqFrJBDoxE,1137 +pytz/zoneinfo/Atlantic/Azores,sha256=66hDxaK8xFnktLMrpNxkD4r1gGkhS-PEpleuwzuGRA0,3442 +pytz/zoneinfo/Atlantic/Bermuda,sha256=LNGKfMsnYvwImjTyzXrLhMOHHDu7qI67RbYNKvvI15I,2396 +pytz/zoneinfo/Atlantic/Canary,sha256=ymK9ufqphvNjDK3hzikN4GfkcR3QeCBiPKyVc6FjlbA,1897 +pytz/zoneinfo/Atlantic/Cape_Verde,sha256=o92pLdLFX_b9vUiq3rNpca4tupIO3dx9rNrnPcA8474,256 +pytz/zoneinfo/Atlantic/Faeroe,sha256=NibdZPZtapnYR_myIZnMdTaSKGsOBGgujj0_T2NvAzs,1815 +pytz/zoneinfo/Atlantic/Faroe,sha256=NibdZPZtapnYR_myIZnMdTaSKGsOBGgujj0_T2NvAzs,1815 +pytz/zoneinfo/Atlantic/Jan_Mayen,sha256=XuR19xoPwaMvrrhJ-MOcbnqmbW1B7HQrl7OnQ2s7BwE,2298 +pytz/zoneinfo/Atlantic/Madeira,sha256=lYY85MC5-GUKExm353ixwtZDxasYavTTWELvv5RXLxE,3377 +pytz/zoneinfo/Atlantic/Reykjavik,sha256=0u-sTl8j2IyV1ywdtCgHFw9S9D3ZiiBa9akqkbny2Zc,148 +pytz/zoneinfo/Atlantic/South_Georgia,sha256=I9SAcPPumy6Xf9P7dg2aE16oxwDIqyKFqinJTC-XsgM,150 +pytz/zoneinfo/Atlantic/St_Helena,sha256=0u-sTl8j2IyV1ywdtCgHFw9S9D3ZiiBa9akqkbny2Zc,148 +pytz/zoneinfo/Atlantic/Stanley,sha256=siEjXTAuTum_4XGtS98MBE34XW_5xgXShEX5OMnSFjo,1200 +pytz/zoneinfo/Australia/ACT,sha256=QsOFdYWxbbL4_9R7oZ-qYPRzNA3o1P6TIOp76GFgWQY,2190 +pytz/zoneinfo/Australia/Adelaide,sha256=ld2EbxU75oVgmPe703z-I6aqLg0Kmv62ZcCGzkT5R20,2208 +pytz/zoneinfo/Australia/Brisbane,sha256=eW6Qzze2t0-speJmmvt1JMzbkSadIKdE84XHc7JUtGc,419 +pytz/zoneinfo/Australia/Broken_Hill,sha256=3k_3ljTvS5GSfo7Xh6w71UgR3aAwYPBsnCJ-mlEYCqQ,2229 +pytz/zoneinfo/Australia/Canberra,sha256=QsOFdYWxbbL4_9R7oZ-qYPRzNA3o1P6TIOp76GFgWQY,2190 +pytz/zoneinfo/Australia/Currie,sha256=GLQSzgIfsWxOvmKOrhpfofWqINQf6h36NYy3mcq6gcg,2358 +pytz/zoneinfo/Australia/Darwin,sha256=fn0IZhIW98FAnzLig-_GBtW5LA54jajdeeUzg4tCGvo,325 +pytz/zoneinfo/Australia/Eucla,sha256=i1-XGG8I6E0dXIdWGF4DlkfDLWhiAxJ_3gMpt-nm_u4,456 +pytz/zoneinfo/Australia/Hobart,sha256=GLQSzgIfsWxOvmKOrhpfofWqINQf6h36NYy3mcq6gcg,2358 +pytz/zoneinfo/Australia/LHI,sha256=oyPFQzmRqWPrSXt9pNHQmEi_PvX11k2clknziOS6ud8,1846 +pytz/zoneinfo/Australia/Lindeman,sha256=xM6Udx22oLNoLR1Y7GQhHOYov8nw3xQNqgc_NVQ2JK4,475 +pytz/zoneinfo/Australia/Lord_Howe,sha256=oyPFQzmRqWPrSXt9pNHQmEi_PvX11k2clknziOS6ud8,1846 +pytz/zoneinfo/Australia/Melbourne,sha256=lvx_MQcunMc6u2smIrl8X427bLsXvjkgpCSdjYCTNBM,2190 +pytz/zoneinfo/Australia/NSW,sha256=QsOFdYWxbbL4_9R7oZ-qYPRzNA3o1P6TIOp76GFgWQY,2190 +pytz/zoneinfo/Australia/North,sha256=fn0IZhIW98FAnzLig-_GBtW5LA54jajdeeUzg4tCGvo,325 +pytz/zoneinfo/Australia/Perth,sha256=Al1DOUh4U_ofMUQSeVlzSyD3x7SUjP9dchSaBUGmeWg,446 +pytz/zoneinfo/Australia/Queensland,sha256=eW6Qzze2t0-speJmmvt1JMzbkSadIKdE84XHc7JUtGc,419 +pytz/zoneinfo/Australia/South,sha256=ld2EbxU75oVgmPe703z-I6aqLg0Kmv62ZcCGzkT5R20,2208 +pytz/zoneinfo/Australia/Sydney,sha256=QsOFdYWxbbL4_9R7oZ-qYPRzNA3o1P6TIOp76GFgWQY,2190 +pytz/zoneinfo/Australia/Tasmania,sha256=GLQSzgIfsWxOvmKOrhpfofWqINQf6h36NYy3mcq6gcg,2358 +pytz/zoneinfo/Australia/Victoria,sha256=lvx_MQcunMc6u2smIrl8X427bLsXvjkgpCSdjYCTNBM,2190 +pytz/zoneinfo/Australia/West,sha256=Al1DOUh4U_ofMUQSeVlzSyD3x7SUjP9dchSaBUGmeWg,446 +pytz/zoneinfo/Australia/Yancowinna,sha256=3k_3ljTvS5GSfo7Xh6w71UgR3aAwYPBsnCJ-mlEYCqQ,2229 +pytz/zoneinfo/Brazil/Acre,sha256=0gpJUl46hQbp0P6Xj1S0NArIWeAryuuDXjsldvB5GHE,614 +pytz/zoneinfo/Brazil/DeNoronha,sha256=feeRAijQqKylZgqe84nKhsFLycT5zIBm7mLIvdyGw4w,702 +pytz/zoneinfo/Brazil/East,sha256=BMBnRO4_4HjvO4t3njjrMGZr-ZPmegkvyvL8KPY6ZM4,1430 +pytz/zoneinfo/Brazil/West,sha256=F6RLOOeOi9lymZiQmQ9pR8tFpPZ6EguNdPfOc6BhXDE,590 +pytz/zoneinfo/CET,sha256=gS9Vrrbozend9HhuFetCVrIegs9fXSjaG60X2UVwysA,2933 +pytz/zoneinfo/CST6CDT,sha256=_roybr6I6sIAF6cYdIxGxoRpoef153Fty48dQ6bm9oY,3592 +pytz/zoneinfo/Canada/Atlantic,sha256=TZpmc5PwWoLfTfQoQ_b3U17BE2iVKSeNkR0Ho8mbTn8,3424 +pytz/zoneinfo/Canada/Central,sha256=7P-_YQrneFcon7QKSTOnkiGjEppFDn3Z48MJ1qq8VBw,2868 +pytz/zoneinfo/Canada/Eastern,sha256=pYehoWB0Ofe6woPhgV8r26-5ZJpFPRjgbC5E5pltiI8,3494 +pytz/zoneinfo/Canada/Mountain,sha256=-TkIfc3QlvaCf0p8COZ43Y1HRBAl-nARUi-JdXeK1vE,2332 +pytz/zoneinfo/Canada/Newfoundland,sha256=r1-17uKv27eZ3JsVkw_DLZQbo6wvjuuVu7C2pDsmOgI,3655 +pytz/zoneinfo/Canada/Pacific,sha256=4YLiDsZ8RqfdCu2z1F-xOV7csU_yryrGlKV3azB77TE,2590 +pytz/zoneinfo/Canada/Saskatchewan,sha256=yjqT08pHbICYe83H8JmtaDBvCFqRv7Tfze3Y8xuXukw,980 +pytz/zoneinfo/Canada/Yukon,sha256=TrR6PCnYG-mSClBMohqlP8qnYhXMUsydI-L-quXFxyM,1614 +pytz/zoneinfo/Chile/Continental,sha256=0CDw13dCMUsoquMupoJgupkzAUNhDK6E0lVxURA7osA,2515 +pytz/zoneinfo/Chile/EasterIsland,sha256=QbubBs_xQlvKweAnurhyHjIK4ji77Gh4G-usXul6XVM,2219 +pytz/zoneinfo/Cuba,sha256=HUQeAuKBsEkI5SLZjqynXICOUVOajkKzKH5r-Ov5Odc,2416 +pytz/zoneinfo/EET,sha256=XDY-FBUddRyQHN8GxQLZ4awjuOlWlzlUdjv7OdXFNzA,2262 +pytz/zoneinfo/EST,sha256=kayA_pdpMcSQ0FjIzotdcf-m1JYfbKE-qcFT8LC8zqA,182 +pytz/zoneinfo/EST5EDT,sha256=6e0H177gx2qdRC0JHvHwFmj-58TyYBTAqGixn-bBipU,3552 +pytz/zoneinfo/Egypt,sha256=Lft-GCLQhaSJm9VqUmsEFoHIS1Vhfa7pFJn9GZCpifs,2399 +pytz/zoneinfo/Eire,sha256=QOjSocO1cihNo59vQkWxvIFPRSxE9apz0KARVx1czEM,3492 +pytz/zoneinfo/Etc/GMT,sha256=bZ83iIPAefhsA4elVHqSxEmGnYBuB94QCEqwTwJJAY0,114 +pytz/zoneinfo/Etc/GMT+0,sha256=bZ83iIPAefhsA4elVHqSxEmGnYBuB94QCEqwTwJJAY0,114 +pytz/zoneinfo/Etc/GMT+1,sha256=1Qzl2X9rQ_RXEf11yH09wQZCr_ph6UdFP7E0yu9s-IQ,116 +pytz/zoneinfo/Etc/GMT+10,sha256=JEQyQyQlkC0o6ZTdeVjZhCIOh6cK5TF7H00Pkls-sUI,117 +pytz/zoneinfo/Etc/GMT+11,sha256=tWvcvYMFCaE60nJVvDrrov7stJvs1KQYOyrhl3dzcUs,117 +pytz/zoneinfo/Etc/GMT+12,sha256=b70HEhErq8IJmq8x7cOZy4eR__3fq5uHHpjvPBEHqMA,117 +pytz/zoneinfo/Etc/GMT+2,sha256=T6Ep5zhslBKbYaECFUB6gUKh3iTZPyMoW1kjhonxrUo,116 +pytz/zoneinfo/Etc/GMT+3,sha256=QGoYrE04bUJ-OzL37dt2MZT5FxWNLpJDPVXgJbstYZA,116 +pytz/zoneinfo/Etc/GMT+4,sha256=RWrkNki-wV7X-coe0VvufBe6LrWVpkPJgia5QQYEnBo,116 +pytz/zoneinfo/Etc/GMT+5,sha256=oRmeC41dgYXT-zzyZIRKXN9IvdL2Da5nTuwmG2_prIA,116 +pytz/zoneinfo/Etc/GMT+6,sha256=d6dAnwiejyFI2n7AzFlFW0aFAT6zYNEjBIEG0uu0sbQ,116 +pytz/zoneinfo/Etc/GMT+7,sha256=TqjYbzd0YHpx1wisFg08J19wTpg6ztJLLongZY_lozs,116 +pytz/zoneinfo/Etc/GMT+8,sha256=th_8bIMmYgRPCesBrbmBhRr0jQO7whd70LiY9HfwJyk,116 +pytz/zoneinfo/Etc/GMT+9,sha256=Qq5E6iUS7JMJIymT7YoqlI8MtqtVy0mr9t6zWFtWc9Y,116 +pytz/zoneinfo/Etc/GMT-0,sha256=bZ83iIPAefhsA4elVHqSxEmGnYBuB94QCEqwTwJJAY0,114 +pytz/zoneinfo/Etc/GMT-1,sha256=73F1eU8uAQGP3mcoB2q99CjfManGFHk3fefljp9pYC4,117 +pytz/zoneinfo/Etc/GMT-10,sha256=fKWWNwLBOp1OkKjtc1w9LIXJR1mTTD-JdvYflRy1IrU,118 +pytz/zoneinfo/Etc/GMT-11,sha256=D2S79n6psa9t9_2vj5wIrFpHH2OJLcCKP6vtwzFZINY,118 +pytz/zoneinfo/Etc/GMT-12,sha256=me4V6lmWI8gSr8H7N41WAD0Eww1anh_EF34Qr9UoSnI,118 +pytz/zoneinfo/Etc/GMT-13,sha256=xbmbG1BQA6Dlpa_iUwEGyJxW4a3t6lmawdPKAE8vbR8,118 +pytz/zoneinfo/Etc/GMT-14,sha256=PpXoREBh02qFpvxVMj2pV9IAzSQvBE7XPvnN9qSZ-Kc,118 +pytz/zoneinfo/Etc/GMT-2,sha256=ve6hWLdeuiLhqagaWLqMD6HNybS1chRwjudfTZ2bYBE,117 +pytz/zoneinfo/Etc/GMT-3,sha256=N77jILanuLDVkLsdujXZSu-dsHiwN5MIpwh7fMUifso,117 +pytz/zoneinfo/Etc/GMT-4,sha256=LSko5fVHqPl5zfwjGqkbMa_OFnvtpT6o_4xYxNz9n5o,117 +pytz/zoneinfo/Etc/GMT-5,sha256=uLaSR5Mb18HRTsAA5SveY9PAJ97dO8QzIWqNXe3wZb4,117 +pytz/zoneinfo/Etc/GMT-6,sha256=JSN-RUAphJ50fpIv7cYC6unrtrz9S1Wma-piDHlGe7c,117 +pytz/zoneinfo/Etc/GMT-7,sha256=vVAOF8xU9T9ESnw68c0SFXpcvkoopaiwTR0zbefHHSU,117 +pytz/zoneinfo/Etc/GMT-8,sha256=S7xFQbFMpiDZy4v5L4D9fCrjRIzzoLC5p8Se23xi7us,117 +pytz/zoneinfo/Etc/GMT-9,sha256=I5vHNmUK-Yyg_S1skFN44VGVzBgktjFgVQiDIKO4aMI,117 +pytz/zoneinfo/Etc/GMT0,sha256=bZ83iIPAefhsA4elVHqSxEmGnYBuB94QCEqwTwJJAY0,114 +pytz/zoneinfo/Etc/Greenwich,sha256=bZ83iIPAefhsA4elVHqSxEmGnYBuB94QCEqwTwJJAY0,114 +pytz/zoneinfo/Etc/UCT,sha256=i4WEZ5GrLIpUY8g6W-PAQ-JXDXRIQ01BOYlp7Ufj5vI,114 +pytz/zoneinfo/Etc/UTC,sha256=i4WEZ5GrLIpUY8g6W-PAQ-JXDXRIQ01BOYlp7Ufj5vI,114 +pytz/zoneinfo/Etc/Universal,sha256=i4WEZ5GrLIpUY8g6W-PAQ-JXDXRIQ01BOYlp7Ufj5vI,114 +pytz/zoneinfo/Etc/Zulu,sha256=i4WEZ5GrLIpUY8g6W-PAQ-JXDXRIQ01BOYlp7Ufj5vI,114 +pytz/zoneinfo/Europe/Amsterdam,sha256=gS9Vrrbozend9HhuFetCVrIegs9fXSjaG60X2UVwysA,2933 +pytz/zoneinfo/Europe/Andorra,sha256=gTB5jCQmvIw3JJi1_vAcOYuhtzPBR6RXUx9gVV6p6ug,1742 +pytz/zoneinfo/Europe/Astrakhan,sha256=ZeGDZjwVVRoeR-J642zEnN26BPL58ViTJLbwnk7pLXk,1151 +pytz/zoneinfo/Europe/Athens,sha256=XDY-FBUddRyQHN8GxQLZ4awjuOlWlzlUdjv7OdXFNzA,2262 +pytz/zoneinfo/Europe/Belfast,sha256=yFSVBw3KQmh99qHD7ngKJ8vLgvGER1Dqb2QoM6RNKbQ,3664 +pytz/zoneinfo/Europe/Belgrade,sha256=OpWtsGFWBE_S-mYoQcAmjCta9HwbGQANnSmVY9OHCTo,1920 +pytz/zoneinfo/Europe/Berlin,sha256=XuR19xoPwaMvrrhJ-MOcbnqmbW1B7HQrl7OnQ2s7BwE,2298 +pytz/zoneinfo/Europe/Bratislava,sha256=G9fdhUXmzx651BnyZ6V7AOYIV9EV5aMJMm44eJaLLZw,2301 +pytz/zoneinfo/Europe/Brussels,sha256=gS9Vrrbozend9HhuFetCVrIegs9fXSjaG60X2UVwysA,2933 +pytz/zoneinfo/Europe/Bucharest,sha256=nfg6-bU2D6DMEWb9EMIBR5kxnNsbDSx0UKfHH_ZzqFc,2184 +pytz/zoneinfo/Europe/Budapest,sha256=lNwqxWciBvw9ei81VQwIKHbC_ZDJjpgHU6HFg4wCUkY,2368 +pytz/zoneinfo/Europe/Busingen,sha256=K5QY7Ujj2VUchKR4bhhb0hgdAJhmwED71ykXDQOGKe8,1909 +pytz/zoneinfo/Europe/Chisinau,sha256=e0lBroLteVj4iX0Zi_k358z1RgBlysonKOKkccs-nZM,2424 +pytz/zoneinfo/Europe/Copenhagen,sha256=XuR19xoPwaMvrrhJ-MOcbnqmbW1B7HQrl7OnQ2s7BwE,2298 +pytz/zoneinfo/Europe/Dublin,sha256=QOjSocO1cihNo59vQkWxvIFPRSxE9apz0KARVx1czEM,3492 +pytz/zoneinfo/Europe/Gibraltar,sha256=a87WpaBlvxI4gAU9OpQOkN8VUJbirVWYf-VfFLTIoS4,3068 +pytz/zoneinfo/Europe/Guernsey,sha256=yFSVBw3KQmh99qHD7ngKJ8vLgvGER1Dqb2QoM6RNKbQ,3664 +pytz/zoneinfo/Europe/Helsinki,sha256=GEkB7LsVhmegt7YuuWheCDvDGC7b7Nw9bTdDGS9qkJc,1900 +pytz/zoneinfo/Europe/Isle_of_Man,sha256=yFSVBw3KQmh99qHD7ngKJ8vLgvGER1Dqb2QoM6RNKbQ,3664 +pytz/zoneinfo/Europe/Istanbul,sha256=Jk4wjndDta_uLWc8W1dWdjbavJJbsL5ROTmZboVnGKU,1933 +pytz/zoneinfo/Europe/Jersey,sha256=yFSVBw3KQmh99qHD7ngKJ8vLgvGER1Dqb2QoM6RNKbQ,3664 +pytz/zoneinfo/Europe/Kaliningrad,sha256=s7GXSe1YvMcs7AiUhHNTA6I4nAOQn_Kmz_ZqJYO-LMM,1493 +pytz/zoneinfo/Europe/Kiev,sha256=-wrpG9jPuIKFP1NgBVvnxsMRf9L_h5z3J6Q3jj1AwNM,2120 +pytz/zoneinfo/Europe/Kirov,sha256=P7T2Zf5Eo6o4L4Dbg_BfiFjUgTj0dQXlrwY-QZ1eBVk,1185 +pytz/zoneinfo/Europe/Kyiv,sha256=-wrpG9jPuIKFP1NgBVvnxsMRf9L_h5z3J6Q3jj1AwNM,2120 +pytz/zoneinfo/Europe/Lisbon,sha256=krB8skaJImv5NDCNHxvTPDBqpNphDFLNW84lB3lgUCw,3527 +pytz/zoneinfo/Europe/Ljubljana,sha256=OpWtsGFWBE_S-mYoQcAmjCta9HwbGQANnSmVY9OHCTo,1920 +pytz/zoneinfo/Europe/London,sha256=yFSVBw3KQmh99qHD7ngKJ8vLgvGER1Dqb2QoM6RNKbQ,3664 +pytz/zoneinfo/Europe/Luxembourg,sha256=gS9Vrrbozend9HhuFetCVrIegs9fXSjaG60X2UVwysA,2933 +pytz/zoneinfo/Europe/Madrid,sha256=mkLX03rW3t0tmzKBIPe_noUvaFDErwC6_5ZPZZsWHOo,2614 +pytz/zoneinfo/Europe/Malta,sha256=EhKcbPL47765tWAiQ57cusaK2TaIQqZCgtJoEZs3Ud0,2620 +pytz/zoneinfo/Europe/Mariehamn,sha256=GEkB7LsVhmegt7YuuWheCDvDGC7b7Nw9bTdDGS9qkJc,1900 +pytz/zoneinfo/Europe/Minsk,sha256=KgPm0fHycntgd3xbTmmDl4O13Xh_9e2zUnd8XFSU29o,1307 +pytz/zoneinfo/Europe/Monaco,sha256=q3ehSIot1GZ6TyMHIjbg0oRf4ghAXuwbSDSYVim6evg,2962 +pytz/zoneinfo/Europe/Moscow,sha256=KmkofRcj6T8Ph28PJChm8JVp13uRvef6TZ0GuPzUiDw,1535 +pytz/zoneinfo/Europe/Nicosia,sha256=0Unm0IFT7HyGeQ7F3vTa_-klfysCgrulqFO6BD1plZU,2002 +pytz/zoneinfo/Europe/Oslo,sha256=XuR19xoPwaMvrrhJ-MOcbnqmbW1B7HQrl7OnQ2s7BwE,2298 +pytz/zoneinfo/Europe/Paris,sha256=q3ehSIot1GZ6TyMHIjbg0oRf4ghAXuwbSDSYVim6evg,2962 +pytz/zoneinfo/Europe/Podgorica,sha256=OpWtsGFWBE_S-mYoQcAmjCta9HwbGQANnSmVY9OHCTo,1920 +pytz/zoneinfo/Europe/Prague,sha256=G9fdhUXmzx651BnyZ6V7AOYIV9EV5aMJMm44eJaLLZw,2301 +pytz/zoneinfo/Europe/Riga,sha256=hJ2_0m1taW9IuA-hMyP5n-WX7YOrR0heKszJhgljRWk,2198 +pytz/zoneinfo/Europe/Rome,sha256=1a3oLMSiMpSbh9QxV8hLLDVbZqash89iUO1urYC1AY8,2641 +pytz/zoneinfo/Europe/Samara,sha256=nXL0IxbT6qu10CNuaDHxx4W1OaAnaaKTtIJ9N9URMoU,1201 +pytz/zoneinfo/Europe/San_Marino,sha256=1a3oLMSiMpSbh9QxV8hLLDVbZqash89iUO1urYC1AY8,2641 +pytz/zoneinfo/Europe/Sarajevo,sha256=OpWtsGFWBE_S-mYoQcAmjCta9HwbGQANnSmVY9OHCTo,1920 +pytz/zoneinfo/Europe/Saratov,sha256=ygwjvXN13TgaWxjg6ysWEnHWNxwrVtkEbrk8t9bzVVw,1169 +pytz/zoneinfo/Europe/Simferopol,sha256=tzl7xdNVSZprNCul4YE5LSpoR9JoujmOq8VbbB8wHic,1469 +pytz/zoneinfo/Europe/Skopje,sha256=OpWtsGFWBE_S-mYoQcAmjCta9HwbGQANnSmVY9OHCTo,1920 +pytz/zoneinfo/Europe/Sofia,sha256=hCQKXfMNrnA5xHNw_uzTjKzVw4-Bvsq5oGO4yUCv5tY,2077 +pytz/zoneinfo/Europe/Stockholm,sha256=XuR19xoPwaMvrrhJ-MOcbnqmbW1B7HQrl7OnQ2s7BwE,2298 +pytz/zoneinfo/Europe/Tallinn,sha256=4a6JC0aIpMzqIV7O35zoG0LLJwkQq5AoXZ2ivkic6-w,2148 +pytz/zoneinfo/Europe/Tirane,sha256=ztlZyCS9WCXeVW8nBun3Tyi5HUY0EtFbiBbEc1gucuw,2084 +pytz/zoneinfo/Europe/Tiraspol,sha256=e0lBroLteVj4iX0Zi_k358z1RgBlysonKOKkccs-nZM,2424 +pytz/zoneinfo/Europe/Ulyanovsk,sha256=c8Ad5p7CKj_1cCA7lVRpcPqbQXGYaX83cuu6uIFx-Bg,1253 +pytz/zoneinfo/Europe/Uzhgorod,sha256=-wrpG9jPuIKFP1NgBVvnxsMRf9L_h5z3J6Q3jj1AwNM,2120 +pytz/zoneinfo/Europe/Vaduz,sha256=K5QY7Ujj2VUchKR4bhhb0hgdAJhmwED71ykXDQOGKe8,1909 +pytz/zoneinfo/Europe/Vatican,sha256=1a3oLMSiMpSbh9QxV8hLLDVbZqash89iUO1urYC1AY8,2641 +pytz/zoneinfo/Europe/Vienna,sha256=ZmI3kADE6bnrJEccqh73XXBY36L1G4DkpiTQImtNrUk,2200 +pytz/zoneinfo/Europe/Vilnius,sha256=UFzRX3orCTB8d9IzlxJPy5eUA2oBPuCu1UJl-2D7C3U,2162 +pytz/zoneinfo/Europe/Volgograd,sha256=RgFvt7mzZ-TtIKL9BVHmoNZLIeLIuiDdXeY10g2_vks,1193 +pytz/zoneinfo/Europe/Warsaw,sha256=TiLDPbeVF0ckgLVEkaSeDaKZ8wctdJDOl_HE_Wd5rKs,2654 +pytz/zoneinfo/Europe/Zagreb,sha256=OpWtsGFWBE_S-mYoQcAmjCta9HwbGQANnSmVY9OHCTo,1920 +pytz/zoneinfo/Europe/Zaporozhye,sha256=-wrpG9jPuIKFP1NgBVvnxsMRf9L_h5z3J6Q3jj1AwNM,2120 +pytz/zoneinfo/Europe/Zurich,sha256=K5QY7Ujj2VUchKR4bhhb0hgdAJhmwED71ykXDQOGKe8,1909 +pytz/zoneinfo/Factory,sha256=aFFlKx93HXoJoF4SSuTlD8cZtJA-ne5oKzAa6eX2V4k,116 +pytz/zoneinfo/GB,sha256=yFSVBw3KQmh99qHD7ngKJ8vLgvGER1Dqb2QoM6RNKbQ,3664 +pytz/zoneinfo/GB-Eire,sha256=yFSVBw3KQmh99qHD7ngKJ8vLgvGER1Dqb2QoM6RNKbQ,3664 +pytz/zoneinfo/GMT,sha256=bZ83iIPAefhsA4elVHqSxEmGnYBuB94QCEqwTwJJAY0,114 +pytz/zoneinfo/GMT+0,sha256=bZ83iIPAefhsA4elVHqSxEmGnYBuB94QCEqwTwJJAY0,114 +pytz/zoneinfo/GMT-0,sha256=bZ83iIPAefhsA4elVHqSxEmGnYBuB94QCEqwTwJJAY0,114 +pytz/zoneinfo/GMT0,sha256=bZ83iIPAefhsA4elVHqSxEmGnYBuB94QCEqwTwJJAY0,114 +pytz/zoneinfo/Greenwich,sha256=bZ83iIPAefhsA4elVHqSxEmGnYBuB94QCEqwTwJJAY0,114 +pytz/zoneinfo/HST,sha256=fwPRv1Jk56sCOi75uZfd_Iy2k2aSQHx3B2K5xUlSPzM,329 +pytz/zoneinfo/Hongkong,sha256=al_O4kPlq5JpgkLYjEaZzrcgiiLul9NC0R5B69JVWhc,1233 +pytz/zoneinfo/Iceland,sha256=0u-sTl8j2IyV1ywdtCgHFw9S9D3ZiiBa9akqkbny2Zc,148 +pytz/zoneinfo/Indian/Antananarivo,sha256=yJsuJTqJJqbOz37_NOS_zbf-JNr_IthHGMMN7sDqSWg,265 +pytz/zoneinfo/Indian/Chagos,sha256=2errXzKdFIcpU0L-XRhSHxhNabIzbI5lXV3Pq6lt40Y,185 +pytz/zoneinfo/Indian/Christmas,sha256=hf_5PVegQcFSS60CjS80C7h-TGOrfQ4ncm83N8VmZkk,185 +pytz/zoneinfo/Indian/Cocos,sha256=_YHASq4Z5YcUILIdhEzg27CGLzarUHPDHs1Dj0QgNGM,254 +pytz/zoneinfo/Indian/Comoro,sha256=yJsuJTqJJqbOz37_NOS_zbf-JNr_IthHGMMN7sDqSWg,265 +pytz/zoneinfo/Indian/Kerguelen,sha256=F73ffVfBoUoHre0-DwsiQrYJcLpPOW-JJGk3n88lM5U,185 +pytz/zoneinfo/Indian/Mahe,sha256=pmdhPhaJRwKwONvxiZNGeFSICjlWzyY9JlFHv-H9upY,151 +pytz/zoneinfo/Indian/Maldives,sha256=F73ffVfBoUoHre0-DwsiQrYJcLpPOW-JJGk3n88lM5U,185 +pytz/zoneinfo/Indian/Mauritius,sha256=Znqrc1chimlciJsYBOl0NvIHnrNdCxncGxWczq1PBeI,227 +pytz/zoneinfo/Indian/Mayotte,sha256=yJsuJTqJJqbOz37_NOS_zbf-JNr_IthHGMMN7sDqSWg,265 +pytz/zoneinfo/Indian/Reunion,sha256=pmdhPhaJRwKwONvxiZNGeFSICjlWzyY9JlFHv-H9upY,151 +pytz/zoneinfo/Iran,sha256=Lb2H9BCBXtz819FL6E3gBA7w2ROiIgPgx-f08XpqkVo,1248 +pytz/zoneinfo/Israel,sha256=JUuWQmW5Tha0pJjw61Q5aN7CX0z4D7ops9OOSnda6Dc,2388 +pytz/zoneinfo/Jamaica,sha256=wlagieUPRf5-beie-h7QsONbNzjGsm8vMs8uf28pw28,482 +pytz/zoneinfo/Japan,sha256=oCueZgRNxcNcX3ZGdif9y6Su4cyVhga4XHdwlcrYLOs,309 +pytz/zoneinfo/Kwajalein,sha256=TmZ_0f-ySQ-saBAlRXV0f49Itwne51VBXn6rWcrWqHQ,302 +pytz/zoneinfo/Libya,sha256=W1dptGD70T7ppGoo0fczFQeDiIp0nultLNPV66MwB2c,625 +pytz/zoneinfo/MET,sha256=gS9Vrrbozend9HhuFetCVrIegs9fXSjaG60X2UVwysA,2933 +pytz/zoneinfo/MST,sha256=illz0sYuLL8lIPK0Tkou6dL0Vck_D0W_3rRTOvFYRmQ,360 +pytz/zoneinfo/MST7MDT,sha256=MugZwApDs8NI9TnXANQlUE8guNBowWQY0m-ptpPndck,2460 +pytz/zoneinfo/Mexico/BajaNorte,sha256=jC57Mhcm5jRZEto5Z5a9s2cr_EvRqRxl7lizjgBNTKU,2906 +pytz/zoneinfo/Mexico/BajaSur,sha256=BWH2NqVPA1PsyELPN_2BF8KllrsmQkqg1eujsQvnnx8,1060 +pytz/zoneinfo/Mexico/General,sha256=Uog2-FMWz2o12jR6sK9vemJamLeo6OEFMQR3s0xTxkc,1222 +pytz/zoneinfo/NZ,sha256=gADjoyPo_QISQU6UJrAgcHp3HDaMoOFRdH-d23uBSyc,2437 +pytz/zoneinfo/NZ-CHAT,sha256=xhexVc5lfJ_qAv2d3HrII6lfRSxKZYBAjY2zpYkCGE8,2054 +pytz/zoneinfo/Navajo,sha256=MugZwApDs8NI9TnXANQlUE8guNBowWQY0m-ptpPndck,2460 +pytz/zoneinfo/PRC,sha256=ZP_C5DqUQ1oEPAQNHTr36S0DGtx453N68YYbqk7u8-Y,561 +pytz/zoneinfo/PST8PDT,sha256=aJd7ua1tGG_vxser02AQpm4wAI3LLTdgh6QcSYYecmg,2852 +pytz/zoneinfo/Pacific/Apia,sha256=M3QKsp75Q7H1X3aeE_9ZqQli9aEkNCCQctZQ5sEKu00,598 +pytz/zoneinfo/Pacific/Auckland,sha256=gADjoyPo_QISQU6UJrAgcHp3HDaMoOFRdH-d23uBSyc,2437 +pytz/zoneinfo/Pacific/Bougainville,sha256=hWE86eXnNx-vABbp7-YSIqWyecHPMIWLftVloAoPhL8,254 +pytz/zoneinfo/Pacific/Chatham,sha256=xhexVc5lfJ_qAv2d3HrII6lfRSxKZYBAjY2zpYkCGE8,2054 +pytz/zoneinfo/Pacific/Chuuk,sha256=nB36HBWZTdh3TlP0DLFNz1KRQ0aHIfHbp7LC4Urp9fA,172 +pytz/zoneinfo/Pacific/Easter,sha256=QbubBs_xQlvKweAnurhyHjIK4ji77Gh4G-usXul6XVM,2219 +pytz/zoneinfo/Pacific/Efate,sha256=oSxNcQYx5-1FU2_yHzHI-hT-dMJcPxzy4XmdI1UxXAo,524 +pytz/zoneinfo/Pacific/Enderbury,sha256=HNTAKrsH_R2W3QRlKcmNld5KcXdP0ygXCjEovc1i-6Q,220 +pytz/zoneinfo/Pacific/Fakaofo,sha256=qOodpTMKjztvZIXVLe_f_kZ6WcHl9fCLE9ZsyvdFKLI,186 +pytz/zoneinfo/Pacific/Fiji,sha256=jB5FbOsCnHVQQ2ohPiWEQUPhG6JybB3Nog3qT6WJQ0I,564 +pytz/zoneinfo/Pacific/Funafuti,sha256=UyaKimsR8LjgL8Z2g65I0HTvr3tMZuA2wUeBB6_Zp9c,152 +pytz/zoneinfo/Pacific/Galapagos,sha256=_GJUYOjSiIjoNBO2qdq23isLMJ4NCVk3DKIRGeDc8BA,224 +pytz/zoneinfo/Pacific/Gambier,sha256=gAS7gr1HH_re0uYnL6eWo5KGJ-B5QaiM8mV2cY5mQxE,150 +pytz/zoneinfo/Pacific/Guadalcanal,sha256=M4kTWqaSQaV1AMhyLSvmwoBJF7X9icrILbvQJwp940g,152 +pytz/zoneinfo/Pacific/Guam,sha256=Ex9znmf6rNfGze6gNpZJCMr1TT4rkl2SnrhecrdJufI,494 +pytz/zoneinfo/Pacific/Honolulu,sha256=fwPRv1Jk56sCOi75uZfd_Iy2k2aSQHx3B2K5xUlSPzM,329 +pytz/zoneinfo/Pacific/Johnston,sha256=fwPRv1Jk56sCOi75uZfd_Iy2k2aSQHx3B2K5xUlSPzM,329 +pytz/zoneinfo/Pacific/Kanton,sha256=HNTAKrsH_R2W3QRlKcmNld5KcXdP0ygXCjEovc1i-6Q,220 +pytz/zoneinfo/Pacific/Kiritimati,sha256=hYk1Ooz-Lj1PuZCbNV2WJIvOLtCwSwq2u63cb1Z-3NQ,224 +pytz/zoneinfo/Pacific/Kosrae,sha256=Q0jrb4zeDrd61bU4V8TqjMc0Iep8rWZyZqJ0uqsunxs,337 +pytz/zoneinfo/Pacific/Kwajalein,sha256=TmZ_0f-ySQ-saBAlRXV0f49Itwne51VBXn6rWcrWqHQ,302 +pytz/zoneinfo/Pacific/Majuro,sha256=UyaKimsR8LjgL8Z2g65I0HTvr3tMZuA2wUeBB6_Zp9c,152 +pytz/zoneinfo/Pacific/Marquesas,sha256=FTxPJTWtk48LVb3N2U64KLpLsmvu0DQBubTCg-dvyGM,159 +pytz/zoneinfo/Pacific/Midway,sha256=fCYrYphYY6rUfxOw712y5cyRe104AC3pouqD3bCINFg,175 +pytz/zoneinfo/Pacific/Nauru,sha256=9ASKgLHB-8nsTEK1ApzfTH0yQtbNAmGX-JI7uHZiqnA,238 +pytz/zoneinfo/Pacific/Niue,sha256=OllXxukncR7a-SMmdFox5az1xpIPMhbahQhtObmpuDM,189 +pytz/zoneinfo/Pacific/Norfolk,sha256=DMdX1Bm18lzNuiCWzwfeHUMRGXPS8v5AWnh-_EX_AZw,866 +pytz/zoneinfo/Pacific/Noumea,sha256=tkHxxnxsXTOqz3YzWi0mkhTCIONzg-W7EpSRMdPjKdQ,290 +pytz/zoneinfo/Pacific/Pago_Pago,sha256=fCYrYphYY6rUfxOw712y5cyRe104AC3pouqD3bCINFg,175 +pytz/zoneinfo/Pacific/Palau,sha256=aN2HbT0reqwKrtLKDK9M2zb0d0ikdNlTrrntVxdH66o,166 +pytz/zoneinfo/Pacific/Pitcairn,sha256=U4jAUuvsRNoy8XrPa16YpcXCcqHJY0u6JvCNgPEWO1c,188 +pytz/zoneinfo/Pacific/Pohnpei,sha256=M4kTWqaSQaV1AMhyLSvmwoBJF7X9icrILbvQJwp940g,152 +pytz/zoneinfo/Pacific/Ponape,sha256=M4kTWqaSQaV1AMhyLSvmwoBJF7X9icrILbvQJwp940g,152 +pytz/zoneinfo/Pacific/Port_Moresby,sha256=nB36HBWZTdh3TlP0DLFNz1KRQ0aHIfHbp7LC4Urp9fA,172 +pytz/zoneinfo/Pacific/Rarotonga,sha256=wPEsoXbyDnuhfzkgLvUqhSzrMx_FD42uAPluSPMh3Bc,589 +pytz/zoneinfo/Pacific/Saipan,sha256=Ex9znmf6rNfGze6gNpZJCMr1TT4rkl2SnrhecrdJufI,494 +pytz/zoneinfo/Pacific/Samoa,sha256=fCYrYphYY6rUfxOw712y5cyRe104AC3pouqD3bCINFg,175 +pytz/zoneinfo/Pacific/Tahiti,sha256=BRff9G3E-iWKhOWR1Wu02Z0iMgjrwDXV-XNrqItXdTY,151 +pytz/zoneinfo/Pacific/Tarawa,sha256=UyaKimsR8LjgL8Z2g65I0HTvr3tMZuA2wUeBB6_Zp9c,152 +pytz/zoneinfo/Pacific/Tongatapu,sha256=OppBZqTAZib9HY7U9AC-JavO7m6NxPGUtUfPQAl9oBY,358 +pytz/zoneinfo/Pacific/Truk,sha256=nB36HBWZTdh3TlP0DLFNz1KRQ0aHIfHbp7LC4Urp9fA,172 +pytz/zoneinfo/Pacific/Wake,sha256=UyaKimsR8LjgL8Z2g65I0HTvr3tMZuA2wUeBB6_Zp9c,152 +pytz/zoneinfo/Pacific/Wallis,sha256=UyaKimsR8LjgL8Z2g65I0HTvr3tMZuA2wUeBB6_Zp9c,152 +pytz/zoneinfo/Pacific/Yap,sha256=nB36HBWZTdh3TlP0DLFNz1KRQ0aHIfHbp7LC4Urp9fA,172 +pytz/zoneinfo/Poland,sha256=TiLDPbeVF0ckgLVEkaSeDaKZ8wctdJDOl_HE_Wd5rKs,2654 +pytz/zoneinfo/Portugal,sha256=krB8skaJImv5NDCNHxvTPDBqpNphDFLNW84lB3lgUCw,3527 +pytz/zoneinfo/ROC,sha256=DMmQwOpPql25ue3Nf8vAKKT4em06D1Z9rHbLIitxixk,761 +pytz/zoneinfo/ROK,sha256=LI9LsV3XcJC0l-KoQf8zI-y7rk-du57erS-N2Ptdi7Q,617 +pytz/zoneinfo/Singapore,sha256=XmeVImeqcJ8hJzm7TjAti1nWJAxawOqq7jIzDnHX2hI,401 +pytz/zoneinfo/Turkey,sha256=Jk4wjndDta_uLWc8W1dWdjbavJJbsL5ROTmZboVnGKU,1933 +pytz/zoneinfo/UCT,sha256=i4WEZ5GrLIpUY8g6W-PAQ-JXDXRIQ01BOYlp7Ufj5vI,114 +pytz/zoneinfo/US/Alaska,sha256=oZA1NSPS2BWdymYpnCHFO8BlYVS-ll5KLg2Ez9CbETs,2371 +pytz/zoneinfo/US/Aleutian,sha256=IB1DhwJQAKbhPJ9jHLf8zW5Dad7HIkBS-dhv64E1OlM,2356 +pytz/zoneinfo/US/Arizona,sha256=illz0sYuLL8lIPK0Tkou6dL0Vck_D0W_3rRTOvFYRmQ,360 +pytz/zoneinfo/US/Central,sha256=_roybr6I6sIAF6cYdIxGxoRpoef153Fty48dQ6bm9oY,3592 +pytz/zoneinfo/US/East-Indiana,sha256=kNKy9Kj9ICsiYYfCCbAggzMA7exf-GpGPMxoXocHUyw,1682 +pytz/zoneinfo/US/Eastern,sha256=6e0H177gx2qdRC0JHvHwFmj-58TyYBTAqGixn-bBipU,3552 +pytz/zoneinfo/US/Hawaii,sha256=fwPRv1Jk56sCOi75uZfd_Iy2k2aSQHx3B2K5xUlSPzM,329 +pytz/zoneinfo/US/Indiana-Starke,sha256=CsvZ5BKw2qVav3x_F8CU9taJdDk7jX41Cfsqms6jXV8,2444 +pytz/zoneinfo/US/Michigan,sha256=hecz8yqY2Cj5B61G3gLZdAVZvRgK9l0P90c_gN-uD5g,2230 +pytz/zoneinfo/US/Mountain,sha256=MugZwApDs8NI9TnXANQlUE8guNBowWQY0m-ptpPndck,2460 +pytz/zoneinfo/US/Pacific,sha256=aJd7ua1tGG_vxser02AQpm4wAI3LLTdgh6QcSYYecmg,2852 +pytz/zoneinfo/US/Samoa,sha256=fCYrYphYY6rUfxOw712y5cyRe104AC3pouqD3bCINFg,175 +pytz/zoneinfo/UTC,sha256=i4WEZ5GrLIpUY8g6W-PAQ-JXDXRIQ01BOYlp7Ufj5vI,114 +pytz/zoneinfo/Universal,sha256=i4WEZ5GrLIpUY8g6W-PAQ-JXDXRIQ01BOYlp7Ufj5vI,114 +pytz/zoneinfo/W-SU,sha256=KmkofRcj6T8Ph28PJChm8JVp13uRvef6TZ0GuPzUiDw,1535 +pytz/zoneinfo/WET,sha256=krB8skaJImv5NDCNHxvTPDBqpNphDFLNW84lB3lgUCw,3527 +pytz/zoneinfo/Zulu,sha256=i4WEZ5GrLIpUY8g6W-PAQ-JXDXRIQ01BOYlp7Ufj5vI,114 +pytz/zoneinfo/iso3166.tab,sha256=g3yAeFCAyEM_2dTqh-ePFhrHpAOJMBxRU9T5AZi66yo,4841 +pytz/zoneinfo/leapseconds,sha256=1sjAMwUn2ZXeXlCvnAe5Kfqdadkucjz7YjtK5vrm07U,3698 +pytz/zoneinfo/tzdata.zi,sha256=Ao0jWLaDS9rV7pe_Rzm_sRKpK-5_vau_HP7J-c7o2yM,107520 +pytz/zoneinfo/zone.tab,sha256=TY44nl9LDsBGbVsU9C5d-wMIxDdhZfz0eDOa_Z3csAw,18818 +pytz/zoneinfo/zone1970.tab,sha256=QGVVVG5oWzTrRsJNgmtkndNenSAvTBOjxiH_Ie3cFYM,17601 +pytz/zoneinfo/zonenow.tab,sha256=bShki0W6r9LAH8O_pLqb-_o5MfcVwCN-qOq_yJiNSbs,8056 diff --git a/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/REQUESTED b/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/WHEEL b/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..4724c45738f6ac125bb3a21787855562e6870440 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.42.0) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/top_level.txt b/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..af44f198c687e245aada835efbab2f75ed2c9baf --- /dev/null +++ b/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/top_level.txt @@ -0,0 +1 @@ +pytz diff --git a/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/zip-safe b/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/zip-safe new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/.venv/lib/python3.11/site-packages/pytz-2026.2.dist-info/zip-safe @@ -0,0 +1 @@ + diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Bissau b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Bissau new file mode 100644 index 0000000000000000000000000000000000000000..82ea5aaf0c6ae2b3ec582013b6d16e6d6f29eb0a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Bissau differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Blantyre b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Blantyre new file mode 100644 index 0000000000000000000000000000000000000000..65f043f1d1833d26b5fe42a460f84b35d1dcba63 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Blantyre differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Brazzaville b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Brazzaville new file mode 100644 index 0000000000000000000000000000000000000000..afb6a4a8fb17b0d4670b8ea1b38f5cc6100244e4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Brazzaville differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Bujumbura b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Bujumbura new file mode 100644 index 0000000000000000000000000000000000000000..65f043f1d1833d26b5fe42a460f84b35d1dcba63 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Bujumbura differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Cairo b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Cairo new file mode 100644 index 0000000000000000000000000000000000000000..dd538c65db6ed0a0e47feb7b6001640516958e19 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Cairo differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Casablanca b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Casablanca new file mode 100644 index 0000000000000000000000000000000000000000..d39016b89d3c945030ce95f721ebb592c8105064 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Casablanca differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Ceuta b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Ceuta new file mode 100644 index 0000000000000000000000000000000000000000..b41ec4f81ab104943e84105246f0e5d0157e6e2e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Ceuta differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Conakry b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Conakry new file mode 100644 index 0000000000000000000000000000000000000000..28b32ab2e0b9053f39a91d9f28b6072e41423954 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Conakry differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Dakar b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Dakar new file mode 100644 index 0000000000000000000000000000000000000000..28b32ab2e0b9053f39a91d9f28b6072e41423954 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Dakar differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Dar_es_Salaam b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Dar_es_Salaam new file mode 100644 index 0000000000000000000000000000000000000000..9dcfc19c56e62b12b730f4335b34479695f273f5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Dar_es_Salaam differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Djibouti b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Djibouti new file mode 100644 index 0000000000000000000000000000000000000000..9dcfc19c56e62b12b730f4335b34479695f273f5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Djibouti differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Douala b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Douala new file mode 100644 index 0000000000000000000000000000000000000000..afb6a4a8fb17b0d4670b8ea1b38f5cc6100244e4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Douala differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/El_Aaiun b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/El_Aaiun new file mode 100644 index 0000000000000000000000000000000000000000..066fbed008cf662455eeca2c012ab8cb5bf1731a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/El_Aaiun differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Freetown b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Freetown new file mode 100644 index 0000000000000000000000000000000000000000..28b32ab2e0b9053f39a91d9f28b6072e41423954 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Freetown differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Gaborone b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Gaborone new file mode 100644 index 0000000000000000000000000000000000000000..65f043f1d1833d26b5fe42a460f84b35d1dcba63 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Gaborone differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Harare b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Harare new file mode 100644 index 0000000000000000000000000000000000000000..65f043f1d1833d26b5fe42a460f84b35d1dcba63 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Harare differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Johannesburg b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Johannesburg new file mode 100644 index 0000000000000000000000000000000000000000..b1c425daced454f53d7d18fea807bf8d081cf97e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Johannesburg differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Juba b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Juba new file mode 100644 index 0000000000000000000000000000000000000000..06482943a45a58a02a43b9e2b6a3f215b21b045f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Juba differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Kampala b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Kampala new file mode 100644 index 0000000000000000000000000000000000000000..9dcfc19c56e62b12b730f4335b34479695f273f5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Kampala differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Khartoum b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Khartoum new file mode 100644 index 0000000000000000000000000000000000000000..8ee8cb92e72d9c507ad0ee06dc6a38406ab06f34 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Khartoum differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Kigali b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Kigali new file mode 100644 index 0000000000000000000000000000000000000000..65f043f1d1833d26b5fe42a460f84b35d1dcba63 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Kigali differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Kinshasa b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Kinshasa new file mode 100644 index 0000000000000000000000000000000000000000..afb6a4a8fb17b0d4670b8ea1b38f5cc6100244e4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Kinshasa differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Lagos b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Lagos new file mode 100644 index 0000000000000000000000000000000000000000..afb6a4a8fb17b0d4670b8ea1b38f5cc6100244e4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Lagos differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Libreville b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Libreville new file mode 100644 index 0000000000000000000000000000000000000000..afb6a4a8fb17b0d4670b8ea1b38f5cc6100244e4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Libreville differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Lome b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Lome new file mode 100644 index 0000000000000000000000000000000000000000..28b32ab2e0b9053f39a91d9f28b6072e41423954 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Lome differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Luanda b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Luanda new file mode 100644 index 0000000000000000000000000000000000000000..afb6a4a8fb17b0d4670b8ea1b38f5cc6100244e4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Luanda differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Lubumbashi b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Lubumbashi new file mode 100644 index 0000000000000000000000000000000000000000..65f043f1d1833d26b5fe42a460f84b35d1dcba63 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Lubumbashi differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Lusaka b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Lusaka new file mode 100644 index 0000000000000000000000000000000000000000..65f043f1d1833d26b5fe42a460f84b35d1dcba63 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Lusaka differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Malabo b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Malabo new file mode 100644 index 0000000000000000000000000000000000000000..afb6a4a8fb17b0d4670b8ea1b38f5cc6100244e4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Malabo differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Maputo b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Maputo new file mode 100644 index 0000000000000000000000000000000000000000..65f043f1d1833d26b5fe42a460f84b35d1dcba63 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Maputo differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Maseru b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Maseru new file mode 100644 index 0000000000000000000000000000000000000000..b1c425daced454f53d7d18fea807bf8d081cf97e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Maseru differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Mbabane b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Mbabane new file mode 100644 index 0000000000000000000000000000000000000000..b1c425daced454f53d7d18fea807bf8d081cf97e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Mbabane differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Mogadishu b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Mogadishu new file mode 100644 index 0000000000000000000000000000000000000000..9dcfc19c56e62b12b730f4335b34479695f273f5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Mogadishu differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Monrovia b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Monrovia new file mode 100644 index 0000000000000000000000000000000000000000..6d688502a1ca80f2e57a6de2790ac3193879d248 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Monrovia differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Nairobi b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Nairobi new file mode 100644 index 0000000000000000000000000000000000000000..9dcfc19c56e62b12b730f4335b34479695f273f5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Nairobi differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Ndjamena b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Ndjamena new file mode 100644 index 0000000000000000000000000000000000000000..a968845e29b8b2b47d4a73f74ae04ef681d7d485 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Ndjamena differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Niamey b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Niamey new file mode 100644 index 0000000000000000000000000000000000000000..afb6a4a8fb17b0d4670b8ea1b38f5cc6100244e4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Niamey differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Nouakchott b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Nouakchott new file mode 100644 index 0000000000000000000000000000000000000000..28b32ab2e0b9053f39a91d9f28b6072e41423954 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Nouakchott differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Ouagadougou b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Ouagadougou new file mode 100644 index 0000000000000000000000000000000000000000..28b32ab2e0b9053f39a91d9f28b6072e41423954 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Ouagadougou differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Porto-Novo b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Porto-Novo new file mode 100644 index 0000000000000000000000000000000000000000..afb6a4a8fb17b0d4670b8ea1b38f5cc6100244e4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Porto-Novo differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Sao_Tome b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Sao_Tome new file mode 100644 index 0000000000000000000000000000000000000000..59f3759c409a1fb50e632ef5ef613d3fee7af7ef Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Sao_Tome differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Timbuktu b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Timbuktu new file mode 100644 index 0000000000000000000000000000000000000000..28b32ab2e0b9053f39a91d9f28b6072e41423954 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Timbuktu differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Tripoli b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Tripoli new file mode 100644 index 0000000000000000000000000000000000000000..07b393bb7db14cef1e906ebe63cfbbe8cddc79d5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Tripoli differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Tunis b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Tunis new file mode 100644 index 0000000000000000000000000000000000000000..427fa563033fdd8533ae56337fa20befe9719b42 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Tunis differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Windhoek b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Windhoek new file mode 100644 index 0000000000000000000000000000000000000000..abecd137b1fc3220637b22ffea0e7256a58e9377 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Africa/Windhoek differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Adak b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Adak new file mode 100644 index 0000000000000000000000000000000000000000..43236498f681cc06f64ca2afa613880331fe6fbb Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Adak differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Anchorage b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Anchorage new file mode 100644 index 0000000000000000000000000000000000000000..9bbb2fd3b361ea8aa4c126d14df5fa370343a63f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Anchorage differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Anguilla b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Anguilla new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Anguilla differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Antigua b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Antigua new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Antigua differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Araguaina b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Araguaina new file mode 100644 index 0000000000000000000000000000000000000000..919723dc2ca581b4f95154d4192d8f732cc55a45 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Araguaina differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Buenos_Aires b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Buenos_Aires new file mode 100644 index 0000000000000000000000000000000000000000..cc82e69898f161842b5001f10a84d3ac127c5c6f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Buenos_Aires differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Catamarca b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Catamarca new file mode 100644 index 0000000000000000000000000000000000000000..7268eb3738e5b3ba906f962533f48016cf3dcabc Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Catamarca differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/ComodRivadavia b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/ComodRivadavia new file mode 100644 index 0000000000000000000000000000000000000000..7268eb3738e5b3ba906f962533f48016cf3dcabc Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/ComodRivadavia differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Cordoba b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Cordoba new file mode 100644 index 0000000000000000000000000000000000000000..2ad6ea5db204d599266412558f5a1fae076fc41a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Cordoba differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Jujuy b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Jujuy new file mode 100644 index 0000000000000000000000000000000000000000..7ca0b46f680b80780981aab0699e64dd04070fe7 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Jujuy differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/La_Rioja b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/La_Rioja new file mode 100644 index 0000000000000000000000000000000000000000..a6a6694f3317a25e7bc4c66502e9f396fa106509 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/La_Rioja differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Mendoza b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Mendoza new file mode 100644 index 0000000000000000000000000000000000000000..3232c80e2003489921578f7acbabc73fb9148205 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Mendoza differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Rio_Gallegos b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Rio_Gallegos new file mode 100644 index 0000000000000000000000000000000000000000..8b1a2816ab136098d10a3d294993ea56b6c432dc Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Rio_Gallegos differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Salta b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Salta new file mode 100644 index 0000000000000000000000000000000000000000..7072dec22958ad5ac74ba3457a178a4faadaf570 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Salta differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/San_Juan b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/San_Juan new file mode 100644 index 0000000000000000000000000000000000000000..f3e185c3ab516065c52557aaceaaeacae1f8b221 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/San_Juan differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/San_Luis b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/San_Luis new file mode 100644 index 0000000000000000000000000000000000000000..2d1da3ae3d6f9c6f92a8ddf01c2ff51ae28e4de2 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/San_Luis differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Tucuman b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Tucuman new file mode 100644 index 0000000000000000000000000000000000000000..c6449f582a7804ecf66858a7fceda207c5d979be Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Tucuman differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Ushuaia b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Ushuaia new file mode 100644 index 0000000000000000000000000000000000000000..e74ce049c793c96e855ebb15779887a48622bf26 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Argentina/Ushuaia differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Aruba b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Aruba new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Aruba differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Asuncion b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Asuncion new file mode 100644 index 0000000000000000000000000000000000000000..9b07a80b85687b9e52f0c1cc713bad3621919edd Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Asuncion differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Atikokan b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Atikokan new file mode 100644 index 0000000000000000000000000000000000000000..9964b9a33452f4b636f43703b7cdec4891cbda5f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Atikokan differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Atka b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Atka new file mode 100644 index 0000000000000000000000000000000000000000..43236498f681cc06f64ca2afa613880331fe6fbb Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Atka differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Bahia b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Bahia new file mode 100644 index 0000000000000000000000000000000000000000..0b65e49fca21f1be92c3ab0dfa4cd05137f028b2 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Bahia differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Bahia_Banderas b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Bahia_Banderas new file mode 100644 index 0000000000000000000000000000000000000000..3a493e3d5a93f08201c41cb25616015963254589 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Bahia_Banderas differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Barbados b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Barbados new file mode 100644 index 0000000000000000000000000000000000000000..00cd045ac86d6060e9e8b8dc0460caa49d2479b5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Barbados differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Belem b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Belem new file mode 100644 index 0000000000000000000000000000000000000000..0ae1202682c94ee52f74a795099f6a11d34e0d1e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Belem differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Belize b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Belize new file mode 100644 index 0000000000000000000000000000000000000000..e6f5dfa6a8d82523e8cb12f17cf73560bc09a383 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Belize differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Blanc-Sablon b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Blanc-Sablon new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Blanc-Sablon differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Boa_Vista b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Boa_Vista new file mode 100644 index 0000000000000000000000000000000000000000..08d518b151425f85458727fbf69f6e601f12be86 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Boa_Vista differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Bogota b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Bogota new file mode 100644 index 0000000000000000000000000000000000000000..331a1b7c4c3af257841900fe076e5393e15db100 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Bogota differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Boise b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Boise new file mode 100644 index 0000000000000000000000000000000000000000..aad1d991c49c2e0fa28be3da4de447af33c8df1c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Boise differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Buenos_Aires b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Buenos_Aires new file mode 100644 index 0000000000000000000000000000000000000000..cc82e69898f161842b5001f10a84d3ac127c5c6f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Buenos_Aires differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Cambridge_Bay b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Cambridge_Bay new file mode 100644 index 0000000000000000000000000000000000000000..7e58a203c6bdc9045092f879f8f58aa960084cad Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Cambridge_Bay differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Campo_Grande b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Campo_Grande new file mode 100644 index 0000000000000000000000000000000000000000..53b3330fac2bd64c02d8dc171c722ec491bea41d Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Campo_Grande differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Cancun b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Cancun new file mode 100644 index 0000000000000000000000000000000000000000..0f7771e851b0e4242bcc65a9b838b8a1675541fb Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Cancun differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Caracas b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Caracas new file mode 100644 index 0000000000000000000000000000000000000000..3f3ebc9c410fe2a33ad26a5d02bb99b9503e1f72 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Caracas differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Catamarca b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Catamarca new file mode 100644 index 0000000000000000000000000000000000000000..7268eb3738e5b3ba906f962533f48016cf3dcabc Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Catamarca differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Cayenne b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Cayenne new file mode 100644 index 0000000000000000000000000000000000000000..e898594276143fa4cfd5d3815c4598b9e58dba07 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Cayenne differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Cayman b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Cayman new file mode 100644 index 0000000000000000000000000000000000000000..9964b9a33452f4b636f43703b7cdec4891cbda5f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Cayman differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Chicago b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Chicago new file mode 100644 index 0000000000000000000000000000000000000000..c6981a06b1d9c26f447518efe265a6454726eae7 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Chicago differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Chihuahua b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Chihuahua new file mode 100644 index 0000000000000000000000000000000000000000..667a2191145170e34271bb2200b93a2eec41d2ba Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Chihuahua differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Ciudad_Juarez b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Ciudad_Juarez new file mode 100644 index 0000000000000000000000000000000000000000..29af5982ac1c04357634570f15454d6f7fc30e04 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Ciudad_Juarez differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Coral_Harbour b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Coral_Harbour new file mode 100644 index 0000000000000000000000000000000000000000..9964b9a33452f4b636f43703b7cdec4891cbda5f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Coral_Harbour differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Cordoba b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Cordoba new file mode 100644 index 0000000000000000000000000000000000000000..2ad6ea5db204d599266412558f5a1fae076fc41a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Cordoba differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Costa_Rica b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Costa_Rica new file mode 100644 index 0000000000000000000000000000000000000000..37cb85e4dbfb7ac9c01eecf584a1a721ed251e93 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Costa_Rica differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Coyhaique b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Coyhaique new file mode 100644 index 0000000000000000000000000000000000000000..e0c6832bb36bde2468038a1e8fa25f89835a4e5f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Coyhaique differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Creston b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Creston new file mode 100644 index 0000000000000000000000000000000000000000..ab37e845566aa95659b7b85be0051d0c67a7e53a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Creston differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Cuiaba b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Cuiaba new file mode 100644 index 0000000000000000000000000000000000000000..26e97f6ebfc60c1517244840b77dbaedcbfabaaa Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Cuiaba differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Curacao b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Curacao new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Curacao differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Danmarkshavn b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Danmarkshavn new file mode 100644 index 0000000000000000000000000000000000000000..9549adcb657569ea304592a4070ceecb4550a4db Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Danmarkshavn differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Dawson b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Dawson new file mode 100644 index 0000000000000000000000000000000000000000..343b63227d2185863cd720bf449de000bbc794d0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Dawson differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Dawson_Creek b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Dawson_Creek new file mode 100644 index 0000000000000000000000000000000000000000..db9e3396557652707c0c2232e119e13a2dd172fb Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Dawson_Creek differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Denver b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Denver new file mode 100644 index 0000000000000000000000000000000000000000..abb2b974a47eb3e5c8b4f5d4370baf4898b239ab Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Denver differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Detroit b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Detroit new file mode 100644 index 0000000000000000000000000000000000000000..e104faa46545ee873295cde34e1d46bccad8647c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Detroit differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Dominica b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Dominica new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Dominica differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Edmonton b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Edmonton new file mode 100644 index 0000000000000000000000000000000000000000..cd78a6f8be1dd55ac5afd25bbae39bd5706e42d1 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Edmonton differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Eirunepe b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Eirunepe new file mode 100644 index 0000000000000000000000000000000000000000..d4c46e3091fe37b7db8d8b67d383302cd667987e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Eirunepe differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/El_Salvador b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/El_Salvador new file mode 100644 index 0000000000000000000000000000000000000000..e2f22304aad56062cfb66d23f3a8c296689286ed Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/El_Salvador differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Ensenada b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Ensenada new file mode 100644 index 0000000000000000000000000000000000000000..cf298188a2b0552addbb3b48d92513c304262922 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Ensenada differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Fort_Nelson b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Fort_Nelson new file mode 100644 index 0000000000000000000000000000000000000000..5a0b7f1ca032be1adf8ba91f863e727c5ec6a026 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Fort_Nelson differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Fort_Wayne b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Fort_Wayne new file mode 100644 index 0000000000000000000000000000000000000000..a84b6e99671f7305d1c459956e16a4dd772fc60e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Fort_Wayne differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Fortaleza b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Fortaleza new file mode 100644 index 0000000000000000000000000000000000000000..bee1a95152cbc37db7117d996d370ff9d3f11f79 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Fortaleza differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Glace_Bay b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Glace_Bay new file mode 100644 index 0000000000000000000000000000000000000000..48412a4cbf9241ea83887876b1b8b22c367ff4fd Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Glace_Bay differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Godthab b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Godthab new file mode 100644 index 0000000000000000000000000000000000000000..29958cf12a9df7cf775e3337b4894e4cf4b82f58 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Godthab differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Goose_Bay b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Goose_Bay new file mode 100644 index 0000000000000000000000000000000000000000..a3f299079aebb8524bf77e7f92e0a7e6d0a7b6fb Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Goose_Bay differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Grand_Turk b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Grand_Turk new file mode 100644 index 0000000000000000000000000000000000000000..06da1a6d7a3eeec0e82f7c5815a8fed4252e16b2 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Grand_Turk differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Grenada b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Grenada new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Grenada differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Guadeloupe b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Guadeloupe new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Guadeloupe differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Guatemala b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Guatemala new file mode 100644 index 0000000000000000000000000000000000000000..407138caf94e467b52d925f96ad80b506e16d9ec Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Guatemala differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Guayaquil b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Guayaquil new file mode 100644 index 0000000000000000000000000000000000000000..40831be11e0ab028ada80cecf5fdc0e2a233ee17 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Guayaquil differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Guyana b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Guyana new file mode 100644 index 0000000000000000000000000000000000000000..9b7036723053b8e8e3888b0497528fe714b5b735 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Guyana differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Halifax b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Halifax new file mode 100644 index 0000000000000000000000000000000000000000..756099abe6cee44295a5566ad6cd0c352fb82e64 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Halifax differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Havana b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Havana new file mode 100644 index 0000000000000000000000000000000000000000..b69ac4510784f23ee794cd6d11e62315a7318e5e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Havana differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Hermosillo b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Hermosillo new file mode 100644 index 0000000000000000000000000000000000000000..441fe3f2dd133fd081247131d860bac327107499 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Hermosillo differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Indianapolis b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Indianapolis new file mode 100644 index 0000000000000000000000000000000000000000..a84b6e99671f7305d1c459956e16a4dd772fc60e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Indianapolis differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Knox b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Knox new file mode 100644 index 0000000000000000000000000000000000000000..025d132dd48ba978c6fedf86d70173127be49d49 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Knox differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Marengo b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Marengo new file mode 100644 index 0000000000000000000000000000000000000000..677bbff6a9d89180cfd2dd3279a1c412610b6540 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Marengo differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Petersburg b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Petersburg new file mode 100644 index 0000000000000000000000000000000000000000..3082de00c2e0bfe89d4ac5e093d9e5921aa63610 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Petersburg differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Tell_City b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Tell_City new file mode 100644 index 0000000000000000000000000000000000000000..103c5cb31795edc15d548ef4ca71f1e04e2996b5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Tell_City differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Vevay b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Vevay new file mode 100644 index 0000000000000000000000000000000000000000..315b4c45aef1f241d025900544133c085fe4f69d Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Vevay differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Vincennes b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Vincennes new file mode 100644 index 0000000000000000000000000000000000000000..35a24133446b9ee1e8f8a7931519d93517130315 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Vincennes differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Winamac b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Winamac new file mode 100644 index 0000000000000000000000000000000000000000..6d4e19377e9f70c3782daa223753c640c60ab1eb Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indiana/Winamac differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indianapolis b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indianapolis new file mode 100644 index 0000000000000000000000000000000000000000..a84b6e99671f7305d1c459956e16a4dd772fc60e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Indianapolis differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Inuvik b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Inuvik new file mode 100644 index 0000000000000000000000000000000000000000..04c2df456ff0f879a419331dc3e9f4d4f68f0ecd Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Inuvik differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Iqaluit b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Iqaluit new file mode 100644 index 0000000000000000000000000000000000000000..0b47b9032f11d4ea7dd6ac51aa329f7a98253638 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Iqaluit differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Jamaica b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Jamaica new file mode 100644 index 0000000000000000000000000000000000000000..2a9b7fd52d37a1ffe9fc589daa04d88c6c71a6e0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Jamaica differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Jujuy b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Jujuy new file mode 100644 index 0000000000000000000000000000000000000000..7ca0b46f680b80780981aab0699e64dd04070fe7 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Jujuy differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Juneau b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Juneau new file mode 100644 index 0000000000000000000000000000000000000000..451f3490096338f40e601628ac70f04112ace51d Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Juneau differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Kentucky/Louisville b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Kentucky/Louisville new file mode 100644 index 0000000000000000000000000000000000000000..3a335b37165d2b3d6a09a1716cb158e272087c9f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Kentucky/Louisville differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Kentucky/Monticello b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Kentucky/Monticello new file mode 100644 index 0000000000000000000000000000000000000000..576f16bb2b21a7e0871a7222241ad65e533bb93a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Kentucky/Monticello differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Knox_IN b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Knox_IN new file mode 100644 index 0000000000000000000000000000000000000000..025d132dd48ba978c6fedf86d70173127be49d49 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Knox_IN differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Kralendijk b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Kralendijk new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Kralendijk differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/La_Paz b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/La_Paz new file mode 100644 index 0000000000000000000000000000000000000000..374586ea67601c4512b52859ff4e73bb30dbecc0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/La_Paz differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Lima b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Lima new file mode 100644 index 0000000000000000000000000000000000000000..c13bb6be45a3be6cc747d056c571623528dc1e34 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Lima differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Los_Angeles b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Los_Angeles new file mode 100644 index 0000000000000000000000000000000000000000..610e7af5fc13d9784de30d272c7c39d7938873a0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Los_Angeles differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Louisville b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Louisville new file mode 100644 index 0000000000000000000000000000000000000000..3a335b37165d2b3d6a09a1716cb158e272087c9f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Louisville differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Lower_Princes b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Lower_Princes new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Lower_Princes differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Maceio b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Maceio new file mode 100644 index 0000000000000000000000000000000000000000..437a47310ec174736b0efbbe9e7e9a8cc26c150c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Maceio differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Managua b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Managua new file mode 100644 index 0000000000000000000000000000000000000000..e0242bff6e5df45de8b235abb4a8405fbb8ba6a6 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Managua differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Manaus b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Manaus new file mode 100644 index 0000000000000000000000000000000000000000..2708baea5af1d5169a1cebd00c4762d6bb937fda Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Manaus differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Marigot b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Marigot new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Marigot differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Martinique b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Martinique new file mode 100644 index 0000000000000000000000000000000000000000..8df43dcf1c9f63d3ea9f056f062ea97e5c7c0b57 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Martinique differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Matamoros b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Matamoros new file mode 100644 index 0000000000000000000000000000000000000000..bbe04e86610a302d0a4deb36dd56f17673f2d3dc Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Matamoros differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Mazatlan b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Mazatlan new file mode 100644 index 0000000000000000000000000000000000000000..386616a59cdb2a21f1f24a6c72526bce0360cd24 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Mazatlan differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Mendoza b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Mendoza new file mode 100644 index 0000000000000000000000000000000000000000..3232c80e2003489921578f7acbabc73fb9148205 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Mendoza differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Menominee b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Menominee new file mode 100644 index 0000000000000000000000000000000000000000..314613866de53e1457f6cbf2fb617be7e4955edf Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Menominee differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Merida b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Merida new file mode 100644 index 0000000000000000000000000000000000000000..c4b9b4e8801cd9447d3bbb6ebff71e26df669c3c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Merida differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Metlakatla b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Metlakatla new file mode 100644 index 0000000000000000000000000000000000000000..1e94be3d552ea0d6e29824469866c2a51a187be9 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Metlakatla differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Mexico_City b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Mexico_City new file mode 100644 index 0000000000000000000000000000000000000000..ad70cf3e08498845aaafc38e4eccca7461c8d337 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Mexico_City differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Miquelon b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Miquelon new file mode 100644 index 0000000000000000000000000000000000000000..f780ea990ff22675b5cc4e4dbe2c8690ae4e41b4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Miquelon differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Moncton b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Moncton new file mode 100644 index 0000000000000000000000000000000000000000..9df8d0f2ec9fc8f1974d83cdd8155c79340007ed Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Moncton differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Monterrey b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Monterrey new file mode 100644 index 0000000000000000000000000000000000000000..2d7993a0d71720493175bd05b2b0b7409d67407a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Monterrey differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Montevideo b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Montevideo new file mode 100644 index 0000000000000000000000000000000000000000..e7bbfbb8c332fee14ae1b4aa07cc4d128e6a3ca7 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Montevideo differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Montreal b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Montreal new file mode 100644 index 0000000000000000000000000000000000000000..170137333f9f6dba10ac3ad7f32876aaf518c4fd Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Montreal differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Montserrat b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Montserrat new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Montserrat differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Nassau b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Nassau new file mode 100644 index 0000000000000000000000000000000000000000..170137333f9f6dba10ac3ad7f32876aaf518c4fd Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Nassau differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/New_York b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/New_York new file mode 100644 index 0000000000000000000000000000000000000000..a8b9ab1992257d721ad627b14f535c3d4b020888 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/New_York differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Nipigon b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Nipigon new file mode 100644 index 0000000000000000000000000000000000000000..170137333f9f6dba10ac3ad7f32876aaf518c4fd Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Nipigon differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Nome b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Nome new file mode 100644 index 0000000000000000000000000000000000000000..10998df3bbe67aa8a02602301d10ec2b2c33006b Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Nome differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Noronha b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Noronha new file mode 100644 index 0000000000000000000000000000000000000000..73b4b336ab55544e6061409261c16cacc39aff61 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Noronha differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/North_Dakota/Beulah b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/North_Dakota/Beulah new file mode 100644 index 0000000000000000000000000000000000000000..33e317e25b127335de68052f57d33ceed620524e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/North_Dakota/Beulah differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/North_Dakota/Center b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/North_Dakota/Center new file mode 100644 index 0000000000000000000000000000000000000000..17fe13bcc1c73ecbc6738499667816f54c164e39 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/North_Dakota/Center differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/North_Dakota/New_Salem b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/North_Dakota/New_Salem new file mode 100644 index 0000000000000000000000000000000000000000..12dbe801aeb06823483525727f6f6695dec1bc24 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/North_Dakota/New_Salem differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Nuuk b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Nuuk new file mode 100644 index 0000000000000000000000000000000000000000..29958cf12a9df7cf775e3337b4894e4cf4b82f58 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Nuuk differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Ojinaga b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Ojinaga new file mode 100644 index 0000000000000000000000000000000000000000..45118a4f4b73e55879aaa81a8536e7214538f0e8 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Ojinaga differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Panama b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Panama new file mode 100644 index 0000000000000000000000000000000000000000..9964b9a33452f4b636f43703b7cdec4891cbda5f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Panama differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Pangnirtung b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Pangnirtung new file mode 100644 index 0000000000000000000000000000000000000000..0b47b9032f11d4ea7dd6ac51aa329f7a98253638 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Pangnirtung differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Paramaribo b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Paramaribo new file mode 100644 index 0000000000000000000000000000000000000000..f1b82b4f9a546fd067cf043ba5d35da6fbe41fbe Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Paramaribo differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Phoenix b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Phoenix new file mode 100644 index 0000000000000000000000000000000000000000..ab37e845566aa95659b7b85be0051d0c67a7e53a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Phoenix differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Port-au-Prince b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Port-au-Prince new file mode 100644 index 0000000000000000000000000000000000000000..287f1439266639f9564149d6e162feaba3fbed86 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Port-au-Prince differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Port_of_Spain b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Port_of_Spain new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Port_of_Spain differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Porto_Acre b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Porto_Acre new file mode 100644 index 0000000000000000000000000000000000000000..cdda168cb97b1d45a4f1b7791a74523b74c88e6d Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Porto_Acre differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Porto_Velho b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Porto_Velho new file mode 100644 index 0000000000000000000000000000000000000000..e00398602c634e6f1e9c0bc0e12b064f343ffe0c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Porto_Velho differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Puerto_Rico b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Puerto_Rico new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Puerto_Rico differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Punta_Arenas b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Punta_Arenas new file mode 100644 index 0000000000000000000000000000000000000000..411a839b84d3c629bf7eb623024077efe614ed07 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Punta_Arenas differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Rainy_River b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Rainy_River new file mode 100644 index 0000000000000000000000000000000000000000..ac40299f6b27043e8f2454ac594b0ec184c1a237 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Rainy_River differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Rankin_Inlet b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Rankin_Inlet new file mode 100644 index 0000000000000000000000000000000000000000..e2714921a8f3c05189d20a9151931fb6eadf493b Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Rankin_Inlet differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Recife b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Recife new file mode 100644 index 0000000000000000000000000000000000000000..5bf6c211c19b7cd37107a2e3b317c2a7195c2947 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Recife differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Regina b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Regina new file mode 100644 index 0000000000000000000000000000000000000000..20c9c84df491e4072ec4c5d2c931a7433d9fd394 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Regina differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Resolute b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Resolute new file mode 100644 index 0000000000000000000000000000000000000000..19668900dea9249c1714b8838e44ac2070069138 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Resolute differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Rio_Branco b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Rio_Branco new file mode 100644 index 0000000000000000000000000000000000000000..cdda168cb97b1d45a4f1b7791a74523b74c88e6d Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Rio_Branco differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Rosario b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Rosario new file mode 100644 index 0000000000000000000000000000000000000000..2ad6ea5db204d599266412558f5a1fae076fc41a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Rosario differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Santa_Isabel b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Santa_Isabel new file mode 100644 index 0000000000000000000000000000000000000000..cf298188a2b0552addbb3b48d92513c304262922 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Santa_Isabel differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Santarem b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Santarem new file mode 100644 index 0000000000000000000000000000000000000000..001638c2f2a964eb2cc75660f66a1bcc447d7a57 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Santarem differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Santiago b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Santiago new file mode 100644 index 0000000000000000000000000000000000000000..010c6bd04cae79078540da560ce38400bfe0ade6 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Santiago differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Santo_Domingo b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Santo_Domingo new file mode 100644 index 0000000000000000000000000000000000000000..4fe36fd4c11f998ba3f626c5e23d97bd82d12791 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Santo_Domingo differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Sao_Paulo b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Sao_Paulo new file mode 100644 index 0000000000000000000000000000000000000000..67935ff4da8f527e03cd05ed2aa20e601e10f921 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Sao_Paulo differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Scoresbysund b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Scoresbysund new file mode 100644 index 0000000000000000000000000000000000000000..9bf411ef5ad5f81f3863223ea75117404d5d1421 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Scoresbysund differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Shiprock b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Shiprock new file mode 100644 index 0000000000000000000000000000000000000000..abb2b974a47eb3e5c8b4f5d4370baf4898b239ab Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Shiprock differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Sitka b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Sitka new file mode 100644 index 0000000000000000000000000000000000000000..31f7061371910ad42e4310b7a646ba1a98b6cba4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Sitka differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/St_Barthelemy b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/St_Barthelemy new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/St_Barthelemy differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/St_Johns b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/St_Johns new file mode 100644 index 0000000000000000000000000000000000000000..65a5b0c720dad151ffdcba3dbe91c8bd638845c6 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/St_Johns differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/St_Kitts b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/St_Kitts new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/St_Kitts differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/St_Lucia b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/St_Lucia new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/St_Lucia differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/St_Thomas b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/St_Thomas new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/St_Thomas differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/St_Vincent b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/St_Vincent new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/St_Vincent differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Swift_Current b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Swift_Current new file mode 100644 index 0000000000000000000000000000000000000000..8e9ef255eeb11515b84126d9ee5c0c6b3c72f2a0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Swift_Current differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Tegucigalpa b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Tegucigalpa new file mode 100644 index 0000000000000000000000000000000000000000..2adacb2e500e2f9621b2debc1756d308747b67f6 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Tegucigalpa differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Thule b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Thule new file mode 100644 index 0000000000000000000000000000000000000000..6f802f1c2acf9cc73481ae86c9e099fcfc28cf25 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Thule differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Thunder_Bay b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Thunder_Bay new file mode 100644 index 0000000000000000000000000000000000000000..170137333f9f6dba10ac3ad7f32876aaf518c4fd Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Thunder_Bay differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Tijuana b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Tijuana new file mode 100644 index 0000000000000000000000000000000000000000..cf298188a2b0552addbb3b48d92513c304262922 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Tijuana differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Toronto b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Toronto new file mode 100644 index 0000000000000000000000000000000000000000..170137333f9f6dba10ac3ad7f32876aaf518c4fd Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Toronto differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Tortola b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Tortola new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Tortola differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Vancouver b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Vancouver new file mode 100644 index 0000000000000000000000000000000000000000..b229820a75b7882b7014507a0104072ffa9e8d3c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Vancouver differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Virgin b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Virgin new file mode 100644 index 0000000000000000000000000000000000000000..a662a57137b69e8ba445e899566222cdd422a764 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Virgin differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Whitehorse b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Whitehorse new file mode 100644 index 0000000000000000000000000000000000000000..318c4a8e4003bcff00f9a21e294e7815f170235b Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Whitehorse differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Winnipeg b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Winnipeg new file mode 100644 index 0000000000000000000000000000000000000000..ac40299f6b27043e8f2454ac594b0ec184c1a237 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Winnipeg differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Yakutat b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Yakutat new file mode 100644 index 0000000000000000000000000000000000000000..da209f9f0a07625ec83d4ec84917216347f5687f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Yakutat differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Yellowknife b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Yellowknife new file mode 100644 index 0000000000000000000000000000000000000000..cd78a6f8be1dd55ac5afd25bbae39bd5706e42d1 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/America/Yellowknife differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Casey b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Casey new file mode 100644 index 0000000000000000000000000000000000000000..586a7653ef20d04440bfcb6645454ef215a1bba4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Casey differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Davis b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Davis new file mode 100644 index 0000000000000000000000000000000000000000..d4d47b24647bcabc981440f0859e94228428798d Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Davis differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/DumontDUrville b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/DumontDUrville new file mode 100644 index 0000000000000000000000000000000000000000..7be2474dd91c8a7da181fcda09d838254b890d75 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/DumontDUrville differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Macquarie b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Macquarie new file mode 100644 index 0000000000000000000000000000000000000000..9e7cc687d76b00d8f112245d5c5d2f20a2a61814 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Macquarie differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Mawson b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Mawson new file mode 100644 index 0000000000000000000000000000000000000000..6d93f6e1d3f76bcb6325f503958d19798b098fa0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Mawson differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/McMurdo b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/McMurdo new file mode 100644 index 0000000000000000000000000000000000000000..6575fdce31183d8238b18f2f30ab5b9227c7071c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/McMurdo differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Palmer b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Palmer new file mode 100644 index 0000000000000000000000000000000000000000..9c8fd317e0537afd4066001f6700cd6490fbe5a8 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Palmer differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Rothera b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Rothera new file mode 100644 index 0000000000000000000000000000000000000000..241cc44d507c50777f7225df197765e522f22313 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Rothera differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/South_Pole b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/South_Pole new file mode 100644 index 0000000000000000000000000000000000000000..6575fdce31183d8238b18f2f30ab5b9227c7071c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/South_Pole differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Syowa b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Syowa new file mode 100644 index 0000000000000000000000000000000000000000..8c8062471dce91a5be827d6908795ee7391a4afc Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Syowa differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Troll b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Troll new file mode 100644 index 0000000000000000000000000000000000000000..a1dcea14de9cfb95311ebe94e8a1096c27800941 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Troll differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Vostok b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Vostok new file mode 100644 index 0000000000000000000000000000000000000000..016e06b1bbc8d2e73d07265ef12e2be0c77982f4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Antarctica/Vostok differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Arctic/Longyearbyen b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Arctic/Longyearbyen new file mode 100644 index 0000000000000000000000000000000000000000..7f6d958f8630cba512d8e58ca8edfbd516291522 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Arctic/Longyearbyen differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Aden b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Aden new file mode 100644 index 0000000000000000000000000000000000000000..8c8062471dce91a5be827d6908795ee7391a4afc Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Aden differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Almaty b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Almaty new file mode 100644 index 0000000000000000000000000000000000000000..855abbd6e3ee338c3d46bee7b592f3d645d2c273 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Almaty differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Amman b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Amman new file mode 100644 index 0000000000000000000000000000000000000000..0a8e350a334bbee29b155fb9944c6831460d973e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Amman differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Anadyr b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Anadyr new file mode 100644 index 0000000000000000000000000000000000000000..35c531c0709fabace97e52cacef74b4cfbbcd820 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Anadyr differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Aqtau b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Aqtau new file mode 100644 index 0000000000000000000000000000000000000000..0e1c16d32eca7652aa7e1392e606cf5b54e26bc5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Aqtau differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Aqtobe b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Aqtobe new file mode 100644 index 0000000000000000000000000000000000000000..3b5d6eb41883a7b32819f387b4c03c93e9d489cf Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Aqtobe differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ashgabat b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ashgabat new file mode 100644 index 0000000000000000000000000000000000000000..2bd1cb3da0f5a11024f1609b09ae68645e6ae75c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ashgabat differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ashkhabad b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ashkhabad new file mode 100644 index 0000000000000000000000000000000000000000..2bd1cb3da0f5a11024f1609b09ae68645e6ae75c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ashkhabad differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Atyrau b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Atyrau new file mode 100644 index 0000000000000000000000000000000000000000..e7ea9c545a56ae0b0fabd02e47748004a6d87c3b Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Atyrau differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Baghdad b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Baghdad new file mode 100644 index 0000000000000000000000000000000000000000..c0e607234a07cd2650aabc1fd022bd5d4f99b175 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Baghdad differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Bahrain b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Bahrain new file mode 100644 index 0000000000000000000000000000000000000000..098997e7dd972ca4acedc535c4a645ee966baa33 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Bahrain differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Baku b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Baku new file mode 100644 index 0000000000000000000000000000000000000000..ae0ce4e7c3d273b537fa3653784e73be68ebb441 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Baku differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Bangkok b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Bangkok new file mode 100644 index 0000000000000000000000000000000000000000..fa799db39e7625dd74bd9caa5c29b4819a7cbd3f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Bangkok differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Barnaul b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Barnaul new file mode 100644 index 0000000000000000000000000000000000000000..2f6b8101d474642410af84260fb2b88c493945b1 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Barnaul differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Beirut b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Beirut new file mode 100644 index 0000000000000000000000000000000000000000..fb266ede2279b6aff913538d9d5aae3935e53aeb Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Beirut differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Bishkek b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Bishkek new file mode 100644 index 0000000000000000000000000000000000000000..547fd5e1bd16152073f7237ad265b5c643e7cbfe Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Bishkek differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Brunei b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Brunei new file mode 100644 index 0000000000000000000000000000000000000000..098c6a0b0afc2c3ef3a6937104d270074ff2a9b6 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Brunei differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Calcutta b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Calcutta new file mode 100644 index 0000000000000000000000000000000000000000..0014046d29a38e9b8006f746fea794d7f71eb479 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Calcutta differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Chita b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Chita new file mode 100644 index 0000000000000000000000000000000000000000..75b3d7b3a6b3bf2b7459532fce1142912b897efd Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Chita differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Choibalsan b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Choibalsan new file mode 100644 index 0000000000000000000000000000000000000000..2aa5cc4b84d369b03684c30cdb66b7abf087f467 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Choibalsan differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Chongqing b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Chongqing new file mode 100644 index 0000000000000000000000000000000000000000..91f6f8bc2e234bafd484146986bdb289082c3588 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Chongqing differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Chungking b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Chungking new file mode 100644 index 0000000000000000000000000000000000000000..91f6f8bc2e234bafd484146986bdb289082c3588 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Chungking differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Colombo b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Colombo new file mode 100644 index 0000000000000000000000000000000000000000..353fe2aa3564c79809e05e12711f90669e5e2388 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Colombo differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Dacca b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Dacca new file mode 100644 index 0000000000000000000000000000000000000000..3cf597d83f90403132573d78247e6d02e66d0583 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Dacca differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Damascus b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Damascus new file mode 100644 index 0000000000000000000000000000000000000000..afd956c878856e0e239887963b32ddb8327ccf38 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Damascus differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Dhaka b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Dhaka new file mode 100644 index 0000000000000000000000000000000000000000..3cf597d83f90403132573d78247e6d02e66d0583 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Dhaka differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Dili b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Dili new file mode 100644 index 0000000000000000000000000000000000000000..4614e4fc4353a7094ba5173a0bd09158ef49d9a9 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Dili differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Dubai b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Dubai new file mode 100644 index 0000000000000000000000000000000000000000..b3ac791aef4e73d6d644c40c614f37f15d462cdd Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Dubai differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Dushanbe b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Dushanbe new file mode 100644 index 0000000000000000000000000000000000000000..89e875beaef780b2ceae08ee4703cd813f33d6a4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Dushanbe differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Famagusta b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Famagusta new file mode 100644 index 0000000000000000000000000000000000000000..653b146a60e5e5641a07bfc82f9590dad1ed69f5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Famagusta differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Gaza b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Gaza new file mode 100644 index 0000000000000000000000000000000000000000..dd5781e8ae26ed66aa2650c580c561fae50df4b5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Gaza differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Harbin b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Harbin new file mode 100644 index 0000000000000000000000000000000000000000..91f6f8bc2e234bafd484146986bdb289082c3588 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Harbin differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Hebron b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Hebron new file mode 100644 index 0000000000000000000000000000000000000000..a64fc9e7b2599e8f60d228ef3cf1743b5296cd4f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Hebron differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ho_Chi_Minh b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ho_Chi_Minh new file mode 100644 index 0000000000000000000000000000000000000000..4ec038b6883617d8ebd2200dcd33530067255830 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ho_Chi_Minh differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Hong_Kong b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Hong_Kong new file mode 100644 index 0000000000000000000000000000000000000000..f9f7b134dd5c35e4718c6fa3697024cb95442c53 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Hong_Kong differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Hovd b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Hovd new file mode 100644 index 0000000000000000000000000000000000000000..8b9abca344daf199b554888fc4fff562a7a18e4a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Hovd differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Irkutsk b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Irkutsk new file mode 100644 index 0000000000000000000000000000000000000000..e74a4d3f6b3a799b0fd12272c3aae0f084fb2783 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Irkutsk differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Istanbul b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Istanbul new file mode 100644 index 0000000000000000000000000000000000000000..7c2336dd80c3c9cbf71cb53d2b2c1f89a65a8ba5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Istanbul differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Jakarta b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Jakarta new file mode 100644 index 0000000000000000000000000000000000000000..ec4bd5747a8c9c528dfd22c8e3171851784ade59 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Jakarta differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Jayapura b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Jayapura new file mode 100644 index 0000000000000000000000000000000000000000..3002c82022f7e9a4ff1ad545ea617106a13b24e0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Jayapura differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Jerusalem b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Jerusalem new file mode 100644 index 0000000000000000000000000000000000000000..1ebd0664aa29c0abd722661f761031ec0304631c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Jerusalem differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kabul b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kabul new file mode 100644 index 0000000000000000000000000000000000000000..661efc83294edbe5f615daa87a1f4a5590aae167 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kabul differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kamchatka b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kamchatka new file mode 100644 index 0000000000000000000000000000000000000000..99776f515fd53a86a6886311d91ff33b401396c3 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kamchatka differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Karachi b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Karachi new file mode 100644 index 0000000000000000000000000000000000000000..ba65c0e8d31cb1d59935878af29baac722f45936 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Karachi differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kashgar b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kashgar new file mode 100644 index 0000000000000000000000000000000000000000..62bdcac14db3f464ff561e32db2c6b55c0cb1866 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kashgar differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kathmandu b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kathmandu new file mode 100644 index 0000000000000000000000000000000000000000..751cf4a8939e898f8abe4358a98b2a5f59f65e21 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kathmandu differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Katmandu b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Katmandu new file mode 100644 index 0000000000000000000000000000000000000000..751cf4a8939e898f8abe4358a98b2a5f59f65e21 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Katmandu differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Khandyga b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Khandyga new file mode 100644 index 0000000000000000000000000000000000000000..7cdc99a9803f4e1ce3c5f7f9e6cd761f178536ae Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Khandyga differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kolkata b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kolkata new file mode 100644 index 0000000000000000000000000000000000000000..0014046d29a38e9b8006f746fea794d7f71eb479 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kolkata differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Krasnoyarsk b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Krasnoyarsk new file mode 100644 index 0000000000000000000000000000000000000000..4c27b2decd1159cf47285f0b18b5ed5178641e82 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Krasnoyarsk differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kuala_Lumpur b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kuala_Lumpur new file mode 100644 index 0000000000000000000000000000000000000000..3d9f191e3acad56fb4f73ab44a74c50cdf91c12e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kuala_Lumpur differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kuching b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kuching new file mode 100644 index 0000000000000000000000000000000000000000..098c6a0b0afc2c3ef3a6937104d270074ff2a9b6 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kuching differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kuwait b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kuwait new file mode 100644 index 0000000000000000000000000000000000000000..8c8062471dce91a5be827d6908795ee7391a4afc Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Kuwait differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Macao b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Macao new file mode 100644 index 0000000000000000000000000000000000000000..cac65063d0dbf48e37c547fba3b67f34110d5a90 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Macao differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Macau b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Macau new file mode 100644 index 0000000000000000000000000000000000000000..cac65063d0dbf48e37c547fba3b67f34110d5a90 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Macau differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Magadan b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Magadan new file mode 100644 index 0000000000000000000000000000000000000000..70c198baf743457c58a8fe4d1f28186d8541a505 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Magadan differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Makassar b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Makassar new file mode 100644 index 0000000000000000000000000000000000000000..556ba866933d37f3cfcf8042045d64e209bae30f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Makassar differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Manila b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Manila new file mode 100644 index 0000000000000000000000000000000000000000..168f72096d96e9f8a6dc4c6073086c19dbc20d0d Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Manila differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Muscat b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Muscat new file mode 100644 index 0000000000000000000000000000000000000000..b3ac791aef4e73d6d644c40c614f37f15d462cdd Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Muscat differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Nicosia b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Nicosia new file mode 100644 index 0000000000000000000000000000000000000000..f7f10ab7665e94ca44fd8cd98a362cd4b304eff1 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Nicosia differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Novokuznetsk b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Novokuznetsk new file mode 100644 index 0000000000000000000000000000000000000000..a5e1b79600d21019a3b93f3e2d796129d9b1d655 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Novokuznetsk differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Novosibirsk b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Novosibirsk new file mode 100644 index 0000000000000000000000000000000000000000..4ac7582ad5ee895bf9b354436142fd7ece1996ee Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Novosibirsk differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Omsk b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Omsk new file mode 100644 index 0000000000000000000000000000000000000000..16c5f3cfed75151d50ffc2dc483ac6279816a01f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Omsk differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Oral b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Oral new file mode 100644 index 0000000000000000000000000000000000000000..3b9ecacf6eef72366df1cc847c0d4666efc8a48c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Oral differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Phnom_Penh b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Phnom_Penh new file mode 100644 index 0000000000000000000000000000000000000000..fa799db39e7625dd74bd9caa5c29b4819a7cbd3f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Phnom_Penh differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Pontianak b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Pontianak new file mode 100644 index 0000000000000000000000000000000000000000..12ce24cbeae404efe6921081d21289be452ff88d Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Pontianak differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Pyongyang b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Pyongyang new file mode 100644 index 0000000000000000000000000000000000000000..7ad7e0b2cf8fa4fc844fe8cc9c58e4f3018cead1 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Pyongyang differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Qatar b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Qatar new file mode 100644 index 0000000000000000000000000000000000000000..098997e7dd972ca4acedc535c4a645ee966baa33 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Qatar differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Qostanay b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Qostanay new file mode 100644 index 0000000000000000000000000000000000000000..2ee9ef7e9859bc95da2450e3b282fd4275cbbb39 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Qostanay differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Qyzylorda b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Qyzylorda new file mode 100644 index 0000000000000000000000000000000000000000..27b522a7d5e24eafdf29dd541ebbca69ce4db7b8 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Qyzylorda differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Rangoon b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Rangoon new file mode 100644 index 0000000000000000000000000000000000000000..eef37b42e8a0e7179f8113bea01f4a71d668e8ef Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Rangoon differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Riyadh b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Riyadh new file mode 100644 index 0000000000000000000000000000000000000000..8c8062471dce91a5be827d6908795ee7391a4afc Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Riyadh differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Saigon b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Saigon new file mode 100644 index 0000000000000000000000000000000000000000..4ec038b6883617d8ebd2200dcd33530067255830 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Saigon differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Sakhalin b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Sakhalin new file mode 100644 index 0000000000000000000000000000000000000000..beb77b449654decdfec1f6bfc16fafc8bc8ceeaa Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Sakhalin differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Samarkand b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Samarkand new file mode 100644 index 0000000000000000000000000000000000000000..8a93767bfef2cecbeee15db413a0a7c277e761a1 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Samarkand differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Seoul b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Seoul new file mode 100644 index 0000000000000000000000000000000000000000..96199e73e73aafacd89e48cb2855a96d7a134e1d Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Seoul differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Shanghai b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Shanghai new file mode 100644 index 0000000000000000000000000000000000000000..91f6f8bc2e234bafd484146986bdb289082c3588 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Shanghai differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Singapore b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Singapore new file mode 100644 index 0000000000000000000000000000000000000000..3d9f191e3acad56fb4f73ab44a74c50cdf91c12e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Singapore differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Srednekolymsk b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Srednekolymsk new file mode 100644 index 0000000000000000000000000000000000000000..d21e7eeed5408affb3277bde903d6f5f2090ea61 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Srednekolymsk differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Taipei b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Taipei new file mode 100644 index 0000000000000000000000000000000000000000..24c43444b6751343d2915843d03e55753d4d7359 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Taipei differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Tashkent b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Tashkent new file mode 100644 index 0000000000000000000000000000000000000000..a9f6cd93c849c8b1677bc54f8bb836c91b40d63d Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Tashkent differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Tbilisi b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Tbilisi new file mode 100644 index 0000000000000000000000000000000000000000..229ce6958e152fa356530780516222481dcce483 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Tbilisi differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Tehran b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Tehran new file mode 100644 index 0000000000000000000000000000000000000000..78f28cc402027bd337cf67b937bf8c31a75ee8b3 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Tehran differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Tel_Aviv b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Tel_Aviv new file mode 100644 index 0000000000000000000000000000000000000000..1ebd0664aa29c0abd722661f761031ec0304631c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Tel_Aviv differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Thimbu b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Thimbu new file mode 100644 index 0000000000000000000000000000000000000000..95a9de9657f5b8b15aacb6e05c085c7e11bb8f55 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Thimbu differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Thimphu b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Thimphu new file mode 100644 index 0000000000000000000000000000000000000000..95a9de9657f5b8b15aacb6e05c085c7e11bb8f55 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Thimphu differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Tokyo b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Tokyo new file mode 100644 index 0000000000000000000000000000000000000000..26f4d34d67b46513491f26c2e661c6e653cc130d Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Tokyo differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Tomsk b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Tomsk new file mode 100644 index 0000000000000000000000000000000000000000..a6e494a78cef4baaa30d06cea4186391788eb30b Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Tomsk differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ujung_Pandang b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ujung_Pandang new file mode 100644 index 0000000000000000000000000000000000000000..556ba866933d37f3cfcf8042045d64e209bae30f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ujung_Pandang differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ulaanbaatar b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ulaanbaatar new file mode 100644 index 0000000000000000000000000000000000000000..2aa5cc4b84d369b03684c30cdb66b7abf087f467 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ulaanbaatar differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ulan_Bator b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ulan_Bator new file mode 100644 index 0000000000000000000000000000000000000000..2aa5cc4b84d369b03684c30cdb66b7abf087f467 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ulan_Bator differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Urumqi b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Urumqi new file mode 100644 index 0000000000000000000000000000000000000000..62bdcac14db3f464ff561e32db2c6b55c0cb1866 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Urumqi differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ust-Nera b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ust-Nera new file mode 100644 index 0000000000000000000000000000000000000000..d05726aba9fd67bf230290b3e2d74b75e46ee214 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Ust-Nera differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Vientiane b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Vientiane new file mode 100644 index 0000000000000000000000000000000000000000..fa799db39e7625dd74bd9caa5c29b4819a7cbd3f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Vientiane differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Vladivostok b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Vladivostok new file mode 100644 index 0000000000000000000000000000000000000000..274a10b43d8089ed522daf9ded5b6aa3653075c9 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Vladivostok differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Yakutsk b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Yakutsk new file mode 100644 index 0000000000000000000000000000000000000000..ae65a5f9b94d196b29b68ddec7862e098d829b59 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Yakutsk differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Yangon b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Yangon new file mode 100644 index 0000000000000000000000000000000000000000..eef37b42e8a0e7179f8113bea01f4a71d668e8ef Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Yangon differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Yekaterinburg b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Yekaterinburg new file mode 100644 index 0000000000000000000000000000000000000000..d4d19ccf1e91121589a7cabff444bc916f67f9ce Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Yekaterinburg differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Yerevan b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Yerevan new file mode 100644 index 0000000000000000000000000000000000000000..0d5f6853ac79c41fcb445a23062a27aca9e471f3 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Asia/Yerevan differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Azores b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Azores new file mode 100644 index 0000000000000000000000000000000000000000..dd2c235bf9c4cc3c2ec4c2725a7cd5deac128fea Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Azores differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Bermuda b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Bermuda new file mode 100644 index 0000000000000000000000000000000000000000..527524ed295aba41b9a0448ffd7993c489a2cb99 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Bermuda differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Canary b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Canary new file mode 100644 index 0000000000000000000000000000000000000000..f3192156ff043a529461aa9004a8de9dda326f7d Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Canary differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Cape_Verde b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Cape_Verde new file mode 100644 index 0000000000000000000000000000000000000000..0d0d31a2f092d03f8512ed9c34f36a3f3f21209b Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Cape_Verde differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Faeroe b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Faeroe new file mode 100644 index 0000000000000000000000000000000000000000..4dab7ef0859c244b916d61b7489d7371881e0ca2 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Faeroe differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Faroe b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Faroe new file mode 100644 index 0000000000000000000000000000000000000000..4dab7ef0859c244b916d61b7489d7371881e0ca2 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Faroe differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Jan_Mayen b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Jan_Mayen new file mode 100644 index 0000000000000000000000000000000000000000..7f6d958f8630cba512d8e58ca8edfbd516291522 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Jan_Mayen differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Madeira b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Madeira new file mode 100644 index 0000000000000000000000000000000000000000..6725a0ffc1f7cfc6cad54361ca91500e087e2b64 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Madeira differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Reykjavik b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Reykjavik new file mode 100644 index 0000000000000000000000000000000000000000..28b32ab2e0b9053f39a91d9f28b6072e41423954 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Reykjavik differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/South_Georgia b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/South_Georgia new file mode 100644 index 0000000000000000000000000000000000000000..a2b59a9d1088690cb2f9ad9011bfa59e6cb5c658 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/South_Georgia differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/St_Helena b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/St_Helena new file mode 100644 index 0000000000000000000000000000000000000000..28b32ab2e0b9053f39a91d9f28b6072e41423954 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/St_Helena differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Stanley b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Stanley new file mode 100644 index 0000000000000000000000000000000000000000..1527d0e1a762674748735a95b6c3034d162f4240 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Atlantic/Stanley differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/ACT b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/ACT new file mode 100644 index 0000000000000000000000000000000000000000..0aea4c3d43e504dafabc031d7ca9cbe8db46163c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/ACT differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Adelaide b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Adelaide new file mode 100644 index 0000000000000000000000000000000000000000..f5dedca59e2b220f7395c73f60ff26e610373e8b Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Adelaide differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Brisbane b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Brisbane new file mode 100644 index 0000000000000000000000000000000000000000..7ff9949ffa93e44835ab133998b89e440094f909 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Brisbane differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Broken_Hill b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Broken_Hill new file mode 100644 index 0000000000000000000000000000000000000000..698c76e30e91f568a29daca12993cfacbfdbf83e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Broken_Hill differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Canberra b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Canberra new file mode 100644 index 0000000000000000000000000000000000000000..0aea4c3d43e504dafabc031d7ca9cbe8db46163c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Canberra differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Currie b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Currie new file mode 100644 index 0000000000000000000000000000000000000000..3adb8e1bf7c6ec51f1c100538799271d7d7a6e6f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Currie differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Darwin b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Darwin new file mode 100644 index 0000000000000000000000000000000000000000..74a30879bc6180d588a706451226cb4c95faf79d Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Darwin differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Eucla b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Eucla new file mode 100644 index 0000000000000000000000000000000000000000..1551e96cbc3de5565356954b61aac3c4388e90db Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Eucla differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Hobart b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Hobart new file mode 100644 index 0000000000000000000000000000000000000000..3adb8e1bf7c6ec51f1c100538799271d7d7a6e6f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Hobart differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/LHI b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/LHI new file mode 100644 index 0000000000000000000000000000000000000000..069a95ad686c1139e2ff2b9ce94dc5ef5bc98c67 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/LHI differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Lindeman b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Lindeman new file mode 100644 index 0000000000000000000000000000000000000000..4ee1825abfe65887069dcbd10bcf786d50ba0702 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Lindeman differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Lord_Howe b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Lord_Howe new file mode 100644 index 0000000000000000000000000000000000000000..069a95ad686c1139e2ff2b9ce94dc5ef5bc98c67 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Lord_Howe differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Melbourne b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Melbourne new file mode 100644 index 0000000000000000000000000000000000000000..ee903f4b1fc292bc9cbec7b501a266030ef3510e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Melbourne differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/NSW b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/NSW new file mode 100644 index 0000000000000000000000000000000000000000..0aea4c3d43e504dafabc031d7ca9cbe8db46163c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/NSW differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/North b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/North new file mode 100644 index 0000000000000000000000000000000000000000..74a30879bc6180d588a706451226cb4c95faf79d Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/North differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Perth b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Perth new file mode 100644 index 0000000000000000000000000000000000000000..f8ddbdf215d34b022af11c3d1930dd6ea4dca87e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Perth differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Queensland b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Queensland new file mode 100644 index 0000000000000000000000000000000000000000..7ff9949ffa93e44835ab133998b89e440094f909 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Queensland differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/South b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/South new file mode 100644 index 0000000000000000000000000000000000000000..f5dedca59e2b220f7395c73f60ff26e610373e8b Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/South differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Sydney b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Sydney new file mode 100644 index 0000000000000000000000000000000000000000..0aea4c3d43e504dafabc031d7ca9cbe8db46163c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Sydney differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Tasmania b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Tasmania new file mode 100644 index 0000000000000000000000000000000000000000..3adb8e1bf7c6ec51f1c100538799271d7d7a6e6f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Tasmania differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Victoria b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Victoria new file mode 100644 index 0000000000000000000000000000000000000000..ee903f4b1fc292bc9cbec7b501a266030ef3510e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Victoria differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/West b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/West new file mode 100644 index 0000000000000000000000000000000000000000..f8ddbdf215d34b022af11c3d1930dd6ea4dca87e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/West differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Yancowinna b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Yancowinna new file mode 100644 index 0000000000000000000000000000000000000000..698c76e30e91f568a29daca12993cfacbfdbf83e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Australia/Yancowinna differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Brazil/Acre b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Brazil/Acre new file mode 100644 index 0000000000000000000000000000000000000000..cdda168cb97b1d45a4f1b7791a74523b74c88e6d Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Brazil/Acre differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Brazil/DeNoronha b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Brazil/DeNoronha new file mode 100644 index 0000000000000000000000000000000000000000..73b4b336ab55544e6061409261c16cacc39aff61 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Brazil/DeNoronha differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Brazil/East b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Brazil/East new file mode 100644 index 0000000000000000000000000000000000000000..67935ff4da8f527e03cd05ed2aa20e601e10f921 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Brazil/East differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Brazil/West b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Brazil/West new file mode 100644 index 0000000000000000000000000000000000000000..2708baea5af1d5169a1cebd00c4762d6bb937fda Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Brazil/West differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/CET b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/CET new file mode 100644 index 0000000000000000000000000000000000000000..40d7124e5346af056c75e2f7012a51d94e8154b7 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/CET differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/CST6CDT b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/CST6CDT new file mode 100644 index 0000000000000000000000000000000000000000..c6981a06b1d9c26f447518efe265a6454726eae7 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/CST6CDT differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Atlantic b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Atlantic new file mode 100644 index 0000000000000000000000000000000000000000..756099abe6cee44295a5566ad6cd0c352fb82e64 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Atlantic differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Central b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Central new file mode 100644 index 0000000000000000000000000000000000000000..ac40299f6b27043e8f2454ac594b0ec184c1a237 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Central differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Eastern b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Eastern new file mode 100644 index 0000000000000000000000000000000000000000..170137333f9f6dba10ac3ad7f32876aaf518c4fd Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Eastern differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Mountain b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Mountain new file mode 100644 index 0000000000000000000000000000000000000000..cd78a6f8be1dd55ac5afd25bbae39bd5706e42d1 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Mountain differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Newfoundland b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Newfoundland new file mode 100644 index 0000000000000000000000000000000000000000..65a5b0c720dad151ffdcba3dbe91c8bd638845c6 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Newfoundland differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Pacific b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Pacific new file mode 100644 index 0000000000000000000000000000000000000000..b229820a75b7882b7014507a0104072ffa9e8d3c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Pacific differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Saskatchewan b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Saskatchewan new file mode 100644 index 0000000000000000000000000000000000000000..20c9c84df491e4072ec4c5d2c931a7433d9fd394 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Saskatchewan differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Yukon b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Yukon new file mode 100644 index 0000000000000000000000000000000000000000..318c4a8e4003bcff00f9a21e294e7815f170235b Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Canada/Yukon differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Chile/Continental b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Chile/Continental new file mode 100644 index 0000000000000000000000000000000000000000..010c6bd04cae79078540da560ce38400bfe0ade6 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Chile/Continental differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Chile/EasterIsland b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Chile/EasterIsland new file mode 100644 index 0000000000000000000000000000000000000000..184cb6a83b3392d0492c42297531c85e7e38c4f5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Chile/EasterIsland differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Cuba b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Cuba new file mode 100644 index 0000000000000000000000000000000000000000..b69ac4510784f23ee794cd6d11e62315a7318e5e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Cuba differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/EET b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/EET new file mode 100644 index 0000000000000000000000000000000000000000..9f3a0678d766881389e129c93def7fffd74f14f1 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/EET differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/EST b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/EST new file mode 100644 index 0000000000000000000000000000000000000000..9964b9a33452f4b636f43703b7cdec4891cbda5f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/EST differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/EST5EDT b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/EST5EDT new file mode 100644 index 0000000000000000000000000000000000000000..a8b9ab1992257d721ad627b14f535c3d4b020888 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/EST5EDT differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Egypt b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Egypt new file mode 100644 index 0000000000000000000000000000000000000000..dd538c65db6ed0a0e47feb7b6001640516958e19 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Egypt differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Eire b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Eire new file mode 100644 index 0000000000000000000000000000000000000000..c729def42fc0822e6b24f7bf503c67cc7e6965f0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Eire differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT new file mode 100644 index 0000000000000000000000000000000000000000..c63474664a289aa3c3c0d8b2ce06d484679754c0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+0 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+0 new file mode 100644 index 0000000000000000000000000000000000000000..c63474664a289aa3c3c0d8b2ce06d484679754c0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+0 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+1 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+1 new file mode 100644 index 0000000000000000000000000000000000000000..4dab6f9005bea50a065c685ec8260b0da2bff921 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+1 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+10 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+10 new file mode 100644 index 0000000000000000000000000000000000000000..c749290af2f6b5fe22770c34eb1e8fc87cd85aff Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+10 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+11 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+11 new file mode 100644 index 0000000000000000000000000000000000000000..d969982309e5ca7d32979a7dad814ca307d2cd8d Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+11 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+12 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+12 new file mode 100644 index 0000000000000000000000000000000000000000..cdeec90973be28ee4075eadd22b8b574db2d7a5f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+12 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+2 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+2 new file mode 100644 index 0000000000000000000000000000000000000000..fbd2a941fda996f4abc1f0e09cdf99c271f5a1e2 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+2 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+3 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+3 new file mode 100644 index 0000000000000000000000000000000000000000..ee246ef56f18de61105af0c14d201fd090f74905 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+3 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+4 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+4 new file mode 100644 index 0000000000000000000000000000000000000000..5a25ff2a6afda2cb09b9e147ad20610bc1923444 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+4 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+5 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+5 new file mode 100644 index 0000000000000000000000000000000000000000..c0b745f1cc44d03a00f8bdf127c154392e3baf27 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+5 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+6 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+6 new file mode 100644 index 0000000000000000000000000000000000000000..06e777d57e0267a0635b6b284729fddcfe6221dd Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+6 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+7 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+7 new file mode 100644 index 0000000000000000000000000000000000000000..4e0b53a082f11f9b9debf5e110b97b1b0473c9a6 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+7 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+8 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+8 new file mode 100644 index 0000000000000000000000000000000000000000..714b0c562889a8a774d9aa27810d8400164d00e6 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+8 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+9 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+9 new file mode 100644 index 0000000000000000000000000000000000000000..78b9daa373d2aa2856eafcc92ebc6d899cafde5c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT+9 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-0 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-0 new file mode 100644 index 0000000000000000000000000000000000000000..c63474664a289aa3c3c0d8b2ce06d484679754c0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-0 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-1 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-1 new file mode 100644 index 0000000000000000000000000000000000000000..a838bebf5e7bac3e1257eeb0a61c1b83feb1324c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-1 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-10 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-10 new file mode 100644 index 0000000000000000000000000000000000000000..68ff77db0d95c7d054ef33c05e05ba71bcbbbdd8 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-10 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-11 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-11 new file mode 100644 index 0000000000000000000000000000000000000000..66af5a42be440f1fb8fec3b915afb49b356f63a5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-11 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-12 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-12 new file mode 100644 index 0000000000000000000000000000000000000000..17ba5057727dd73bd5f6234cc5b239b71a861945 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-12 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-13 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-13 new file mode 100644 index 0000000000000000000000000000000000000000..5f3706ce64cadf081a6c56abd7ba423575a4abb2 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-13 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-14 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-14 new file mode 100644 index 0000000000000000000000000000000000000000..7e9f9c465ce6211c65d617f60472c9b55b5052c5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-14 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-2 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-2 new file mode 100644 index 0000000000000000000000000000000000000000..fcef6d9acb247deb539fcc4b30149802572ea642 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-2 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-3 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-3 new file mode 100644 index 0000000000000000000000000000000000000000..27973bc857b4e618218ca2790acacb81f7c7bb82 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-3 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-4 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-4 new file mode 100644 index 0000000000000000000000000000000000000000..1efd841261a977ae218d408f9cc308c3e312a5e8 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-4 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-5 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-5 new file mode 100644 index 0000000000000000000000000000000000000000..1f761844fc44f8228bb748235bfd30be6c389cd1 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-5 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-6 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-6 new file mode 100644 index 0000000000000000000000000000000000000000..952681ed46cb60e59baf76a2c43b49d5f67255d1 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-6 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-7 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-7 new file mode 100644 index 0000000000000000000000000000000000000000..cefc9126c691060225ff2eee1241b1e5e9825fcd Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-7 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-8 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-8 new file mode 100644 index 0000000000000000000000000000000000000000..afb093da00685297cb11347c4840acf3a8e2e2bf Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-8 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-9 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-9 new file mode 100644 index 0000000000000000000000000000000000000000..9265fb7c2071ec0e66c657ad2ae42d5dd525fe97 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT-9 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT0 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT0 new file mode 100644 index 0000000000000000000000000000000000000000..c63474664a289aa3c3c0d8b2ce06d484679754c0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/GMT0 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/Greenwich b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/Greenwich new file mode 100644 index 0000000000000000000000000000000000000000..c63474664a289aa3c3c0d8b2ce06d484679754c0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/Greenwich differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/UCT b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/UCT new file mode 100644 index 0000000000000000000000000000000000000000..91558be0c2bf903b2364215ba26d5227d6126508 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/UCT differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/UTC b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/UTC new file mode 100644 index 0000000000000000000000000000000000000000..91558be0c2bf903b2364215ba26d5227d6126508 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/UTC differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/Universal b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/Universal new file mode 100644 index 0000000000000000000000000000000000000000..91558be0c2bf903b2364215ba26d5227d6126508 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/Universal differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/Zulu b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/Zulu new file mode 100644 index 0000000000000000000000000000000000000000..91558be0c2bf903b2364215ba26d5227d6126508 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Etc/Zulu differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Amsterdam b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Amsterdam new file mode 100644 index 0000000000000000000000000000000000000000..40d7124e5346af056c75e2f7012a51d94e8154b7 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Amsterdam differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Andorra b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Andorra new file mode 100644 index 0000000000000000000000000000000000000000..5962550392fa78514061582e9371c32b9f1d929b Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Andorra differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Astrakhan b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Astrakhan new file mode 100644 index 0000000000000000000000000000000000000000..a41624f5df9698d78049008a2bd8a77395c0480a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Astrakhan differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Athens b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Athens new file mode 100644 index 0000000000000000000000000000000000000000..9f3a0678d766881389e129c93def7fffd74f14f1 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Athens differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Belfast b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Belfast new file mode 100644 index 0000000000000000000000000000000000000000..5ad74220e83e6c30a0aeefda4a00271b8ebdfcad Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Belfast differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Belgrade b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Belgrade new file mode 100644 index 0000000000000000000000000000000000000000..27de456f16ab549627b284a39e2265cbdb4ad8e9 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Belgrade differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Berlin b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Berlin new file mode 100644 index 0000000000000000000000000000000000000000..7f6d958f8630cba512d8e58ca8edfbd516291522 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Berlin differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Bratislava b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Bratislava new file mode 100644 index 0000000000000000000000000000000000000000..ce8f433ece44f0b96b18d3b5780730e7f9cad9f5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Bratislava differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Brussels b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Brussels new file mode 100644 index 0000000000000000000000000000000000000000..40d7124e5346af056c75e2f7012a51d94e8154b7 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Brussels differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Bucharest b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Bucharest new file mode 100644 index 0000000000000000000000000000000000000000..4303b903e5e007484c0d8e1eea43a35e9b53f38b Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Bucharest differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Budapest b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Budapest new file mode 100644 index 0000000000000000000000000000000000000000..b76c873d9256e1d73c2ea672140b813f15657bc7 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Budapest differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Busingen b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Busingen new file mode 100644 index 0000000000000000000000000000000000000000..ad6cf59281a1046d9dcd045fda521585e3e33e06 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Busingen differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Chisinau b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Chisinau new file mode 100644 index 0000000000000000000000000000000000000000..062ca3792a7458579c6acb3fd1d23f0620c2895a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Chisinau differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Copenhagen b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Copenhagen new file mode 100644 index 0000000000000000000000000000000000000000..7f6d958f8630cba512d8e58ca8edfbd516291522 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Copenhagen differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Dublin b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Dublin new file mode 100644 index 0000000000000000000000000000000000000000..c729def42fc0822e6b24f7bf503c67cc7e6965f0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Dublin differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Gibraltar b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Gibraltar new file mode 100644 index 0000000000000000000000000000000000000000..a38f11ffdf2a9e538695ea8cae20b1d2c04164af Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Gibraltar differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Guernsey b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Guernsey new file mode 100644 index 0000000000000000000000000000000000000000..5ad74220e83e6c30a0aeefda4a00271b8ebdfcad Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Guernsey differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Helsinki b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Helsinki new file mode 100644 index 0000000000000000000000000000000000000000..b4f8f9cbb57450549933f83ac90dd56a2ca75344 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Helsinki differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Isle_of_Man b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Isle_of_Man new file mode 100644 index 0000000000000000000000000000000000000000..5ad74220e83e6c30a0aeefda4a00271b8ebdfcad Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Isle_of_Man differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Istanbul b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Istanbul new file mode 100644 index 0000000000000000000000000000000000000000..7c2336dd80c3c9cbf71cb53d2b2c1f89a65a8ba5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Istanbul differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Jersey b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Jersey new file mode 100644 index 0000000000000000000000000000000000000000..5ad74220e83e6c30a0aeefda4a00271b8ebdfcad Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Jersey differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Kaliningrad b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Kaliningrad new file mode 100644 index 0000000000000000000000000000000000000000..cc99beabe4ffc5107c4719d1201d6583b9ead03a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Kaliningrad differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Kiev b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Kiev new file mode 100644 index 0000000000000000000000000000000000000000..52efea88065b220e44fd876de3bf3090fe62cc79 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Kiev differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Kirov b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Kirov new file mode 100644 index 0000000000000000000000000000000000000000..0cfb956be223ab593ca27569ad554dae9b106117 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Kirov differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Kyiv b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Kyiv new file mode 100644 index 0000000000000000000000000000000000000000..52efea88065b220e44fd876de3bf3090fe62cc79 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Kyiv differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Lisbon b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Lisbon new file mode 100644 index 0000000000000000000000000000000000000000..616de167b8a1c534c1c526e7bbff82f7d5da94f2 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Lisbon differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Ljubljana b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Ljubljana new file mode 100644 index 0000000000000000000000000000000000000000..27de456f16ab549627b284a39e2265cbdb4ad8e9 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Ljubljana differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/London b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/London new file mode 100644 index 0000000000000000000000000000000000000000..5ad74220e83e6c30a0aeefda4a00271b8ebdfcad Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/London differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Luxembourg b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Luxembourg new file mode 100644 index 0000000000000000000000000000000000000000..40d7124e5346af056c75e2f7012a51d94e8154b7 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Luxembourg differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Madrid b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Madrid new file mode 100644 index 0000000000000000000000000000000000000000..53f4cd101c18058a1ee4d4b9d1184ada11d74232 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Madrid differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Malta b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Malta new file mode 100644 index 0000000000000000000000000000000000000000..1d1a7bcfcbbf37ff1ad7025f9843d0d2d8cb06d4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Malta differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Mariehamn b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Mariehamn new file mode 100644 index 0000000000000000000000000000000000000000..b4f8f9cbb57450549933f83ac90dd56a2ca75344 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Mariehamn differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Minsk b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Minsk new file mode 100644 index 0000000000000000000000000000000000000000..3731e40d8634c4d58ddb3378d828c92948f06d67 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Minsk differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Monaco b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Monaco new file mode 100644 index 0000000000000000000000000000000000000000..7d366c6098c49ecd546e1cc1538919e1414a3aee Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Monaco differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Moscow b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Moscow new file mode 100644 index 0000000000000000000000000000000000000000..ddb3f4e99a1030f33b56fad986c8d9c16e59eb32 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Moscow differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Nicosia b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Nicosia new file mode 100644 index 0000000000000000000000000000000000000000..f7f10ab7665e94ca44fd8cd98a362cd4b304eff1 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Nicosia differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Oslo b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Oslo new file mode 100644 index 0000000000000000000000000000000000000000..7f6d958f8630cba512d8e58ca8edfbd516291522 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Oslo differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Paris b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Paris new file mode 100644 index 0000000000000000000000000000000000000000..7d366c6098c49ecd546e1cc1538919e1414a3aee Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Paris differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Podgorica b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Podgorica new file mode 100644 index 0000000000000000000000000000000000000000..27de456f16ab549627b284a39e2265cbdb4ad8e9 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Podgorica differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Prague b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Prague new file mode 100644 index 0000000000000000000000000000000000000000..ce8f433ece44f0b96b18d3b5780730e7f9cad9f5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Prague differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Riga b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Riga new file mode 100644 index 0000000000000000000000000000000000000000..8db477d01736445cafce8af7a7085d226d81f546 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Riga differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Rome b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Rome new file mode 100644 index 0000000000000000000000000000000000000000..32b2899a306dde401fa2e3952d06f5f4d9952bed Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Rome differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Samara b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Samara new file mode 100644 index 0000000000000000000000000000000000000000..d0ea2f25e9b4acaf3167a09a1c647943425e51b1 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Samara differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/San_Marino b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/San_Marino new file mode 100644 index 0000000000000000000000000000000000000000..32b2899a306dde401fa2e3952d06f5f4d9952bed Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/San_Marino differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Sarajevo b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Sarajevo new file mode 100644 index 0000000000000000000000000000000000000000..27de456f16ab549627b284a39e2265cbdb4ad8e9 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Sarajevo differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Saratov b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Saratov new file mode 100644 index 0000000000000000000000000000000000000000..a863913358adb7a9118fcc48a6a5dcfba5f9914b Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Saratov differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Simferopol b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Simferopol new file mode 100644 index 0000000000000000000000000000000000000000..4bf24de1d9f8ebc410f120aa83d98b7e41d1e6c4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Simferopol differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Skopje b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Skopje new file mode 100644 index 0000000000000000000000000000000000000000..27de456f16ab549627b284a39e2265cbdb4ad8e9 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Skopje differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Sofia b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Sofia new file mode 100644 index 0000000000000000000000000000000000000000..0e4d879332d21c93c229fc25587205020eeb3127 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Sofia differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Stockholm b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Stockholm new file mode 100644 index 0000000000000000000000000000000000000000..7f6d958f8630cba512d8e58ca8edfbd516291522 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Stockholm differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Tallinn b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Tallinn new file mode 100644 index 0000000000000000000000000000000000000000..b5acca3cf51e7f7b3176965748688ff41720246f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Tallinn differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Tirane b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Tirane new file mode 100644 index 0000000000000000000000000000000000000000..0b86017d243f1b7bbb41d6b4feefcb2b7edfc7d8 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Tirane differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Tiraspol b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Tiraspol new file mode 100644 index 0000000000000000000000000000000000000000..062ca3792a7458579c6acb3fd1d23f0620c2895a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Tiraspol differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Ulyanovsk b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Ulyanovsk new file mode 100644 index 0000000000000000000000000000000000000000..d668233b37f268a745e995177195b06ffab1e69b Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Ulyanovsk differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Uzhgorod b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Uzhgorod new file mode 100644 index 0000000000000000000000000000000000000000..52efea88065b220e44fd876de3bf3090fe62cc79 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Uzhgorod differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Vaduz b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Vaduz new file mode 100644 index 0000000000000000000000000000000000000000..ad6cf59281a1046d9dcd045fda521585e3e33e06 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Vaduz differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Vatican b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Vatican new file mode 100644 index 0000000000000000000000000000000000000000..32b2899a306dde401fa2e3952d06f5f4d9952bed Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Vatican differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Vienna b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Vienna new file mode 100644 index 0000000000000000000000000000000000000000..3582bb15cd7322088839b0134987ad10e717b6b5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Vienna differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Vilnius b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Vilnius new file mode 100644 index 0000000000000000000000000000000000000000..7abd63fa608e0186b9f154d9fcc32472c28f6759 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Vilnius differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Volgograd b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Volgograd new file mode 100644 index 0000000000000000000000000000000000000000..9d51a38c0ea719c5042837d47e0f427057b845f0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Volgograd differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Warsaw b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Warsaw new file mode 100644 index 0000000000000000000000000000000000000000..e33cf67171da78aa9e6eb02e50f9b9603da4c3f4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Warsaw differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Zagreb b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Zagreb new file mode 100644 index 0000000000000000000000000000000000000000..27de456f16ab549627b284a39e2265cbdb4ad8e9 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Zagreb differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Zaporozhye b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Zaporozhye new file mode 100644 index 0000000000000000000000000000000000000000..52efea88065b220e44fd876de3bf3090fe62cc79 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Zaporozhye differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Zurich b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Zurich new file mode 100644 index 0000000000000000000000000000000000000000..ad6cf59281a1046d9dcd045fda521585e3e33e06 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Europe/Zurich differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Factory b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Factory new file mode 100644 index 0000000000000000000000000000000000000000..60aa2a0d695ba577ff87624d479f1eb25c8f1caf Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Factory differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/GB b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/GB new file mode 100644 index 0000000000000000000000000000000000000000..5ad74220e83e6c30a0aeefda4a00271b8ebdfcad Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/GB differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/GB-Eire b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/GB-Eire new file mode 100644 index 0000000000000000000000000000000000000000..5ad74220e83e6c30a0aeefda4a00271b8ebdfcad Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/GB-Eire differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/GMT b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/GMT new file mode 100644 index 0000000000000000000000000000000000000000..c63474664a289aa3c3c0d8b2ce06d484679754c0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/GMT differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/GMT+0 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/GMT+0 new file mode 100644 index 0000000000000000000000000000000000000000..c63474664a289aa3c3c0d8b2ce06d484679754c0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/GMT+0 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/GMT-0 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/GMT-0 new file mode 100644 index 0000000000000000000000000000000000000000..c63474664a289aa3c3c0d8b2ce06d484679754c0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/GMT-0 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/GMT0 b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/GMT0 new file mode 100644 index 0000000000000000000000000000000000000000..c63474664a289aa3c3c0d8b2ce06d484679754c0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/GMT0 differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Greenwich b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Greenwich new file mode 100644 index 0000000000000000000000000000000000000000..c63474664a289aa3c3c0d8b2ce06d484679754c0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Greenwich differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/HST b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/HST new file mode 100644 index 0000000000000000000000000000000000000000..c7cd060159bd22fc5e6f10ac5a2089afb2c19c6a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/HST differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Hongkong b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Hongkong new file mode 100644 index 0000000000000000000000000000000000000000..f9f7b134dd5c35e4718c6fa3697024cb95442c53 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Hongkong differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Iceland b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Iceland new file mode 100644 index 0000000000000000000000000000000000000000..28b32ab2e0b9053f39a91d9f28b6072e41423954 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Iceland differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Antananarivo b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Antananarivo new file mode 100644 index 0000000000000000000000000000000000000000..9dcfc19c56e62b12b730f4335b34479695f273f5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Antananarivo differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Chagos b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Chagos new file mode 100644 index 0000000000000000000000000000000000000000..a5554816e2928c2bd5d02e032bbeb1e1cb101009 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Chagos differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Christmas b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Christmas new file mode 100644 index 0000000000000000000000000000000000000000..fa799db39e7625dd74bd9caa5c29b4819a7cbd3f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Christmas differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Cocos b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Cocos new file mode 100644 index 0000000000000000000000000000000000000000..eef37b42e8a0e7179f8113bea01f4a71d668e8ef Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Cocos differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Comoro b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Comoro new file mode 100644 index 0000000000000000000000000000000000000000..9dcfc19c56e62b12b730f4335b34479695f273f5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Comoro differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Kerguelen b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Kerguelen new file mode 100644 index 0000000000000000000000000000000000000000..555728b1a0187cc0ac63b8fe45c44bd1e0957918 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Kerguelen differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Mahe b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Mahe new file mode 100644 index 0000000000000000000000000000000000000000..b3ac791aef4e73d6d644c40c614f37f15d462cdd Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Mahe differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Maldives b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Maldives new file mode 100644 index 0000000000000000000000000000000000000000..555728b1a0187cc0ac63b8fe45c44bd1e0957918 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Maldives differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Mauritius b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Mauritius new file mode 100644 index 0000000000000000000000000000000000000000..212d4b2e2afaed06110a1acff4fdb6bd6103b4ff Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Mauritius differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Mayotte b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Mayotte new file mode 100644 index 0000000000000000000000000000000000000000..9dcfc19c56e62b12b730f4335b34479695f273f5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Mayotte differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Reunion b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Reunion new file mode 100644 index 0000000000000000000000000000000000000000..b3ac791aef4e73d6d644c40c614f37f15d462cdd Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Indian/Reunion differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Iran b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Iran new file mode 100644 index 0000000000000000000000000000000000000000..78f28cc402027bd337cf67b937bf8c31a75ee8b3 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Iran differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Israel b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Israel new file mode 100644 index 0000000000000000000000000000000000000000..1ebd0664aa29c0abd722661f761031ec0304631c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Israel differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Jamaica b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Jamaica new file mode 100644 index 0000000000000000000000000000000000000000..2a9b7fd52d37a1ffe9fc589daa04d88c6c71a6e0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Jamaica differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Japan b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Japan new file mode 100644 index 0000000000000000000000000000000000000000..26f4d34d67b46513491f26c2e661c6e653cc130d Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Japan differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Kwajalein b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Kwajalein new file mode 100644 index 0000000000000000000000000000000000000000..1887a607422edd499fdf24afe91a04294f1caf6f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Kwajalein differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Libya b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Libya new file mode 100644 index 0000000000000000000000000000000000000000..07b393bb7db14cef1e906ebe63cfbbe8cddc79d5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Libya differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/MET b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/MET new file mode 100644 index 0000000000000000000000000000000000000000..40d7124e5346af056c75e2f7012a51d94e8154b7 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/MET differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/MST b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/MST new file mode 100644 index 0000000000000000000000000000000000000000..ab37e845566aa95659b7b85be0051d0c67a7e53a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/MST differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/MST7MDT b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/MST7MDT new file mode 100644 index 0000000000000000000000000000000000000000..abb2b974a47eb3e5c8b4f5d4370baf4898b239ab Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/MST7MDT differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Mexico/BajaNorte b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Mexico/BajaNorte new file mode 100644 index 0000000000000000000000000000000000000000..cf298188a2b0552addbb3b48d92513c304262922 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Mexico/BajaNorte differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Mexico/BajaSur b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Mexico/BajaSur new file mode 100644 index 0000000000000000000000000000000000000000..386616a59cdb2a21f1f24a6c72526bce0360cd24 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Mexico/BajaSur differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Mexico/General b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Mexico/General new file mode 100644 index 0000000000000000000000000000000000000000..ad70cf3e08498845aaafc38e4eccca7461c8d337 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Mexico/General differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/NZ b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/NZ new file mode 100644 index 0000000000000000000000000000000000000000..6575fdce31183d8238b18f2f30ab5b9227c7071c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/NZ differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/NZ-CHAT b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/NZ-CHAT new file mode 100644 index 0000000000000000000000000000000000000000..bde46cf7e4b7909714b93cee39a5d953387d756c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/NZ-CHAT differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Navajo b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Navajo new file mode 100644 index 0000000000000000000000000000000000000000..abb2b974a47eb3e5c8b4f5d4370baf4898b239ab Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Navajo differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/PRC b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/PRC new file mode 100644 index 0000000000000000000000000000000000000000..91f6f8bc2e234bafd484146986bdb289082c3588 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/PRC differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/PST8PDT b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/PST8PDT new file mode 100644 index 0000000000000000000000000000000000000000..610e7af5fc13d9784de30d272c7c39d7938873a0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/PST8PDT differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Apia b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Apia new file mode 100644 index 0000000000000000000000000000000000000000..e592a68e53f6215de9e1d1ac61ce062a333283c0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Apia differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Auckland b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Auckland new file mode 100644 index 0000000000000000000000000000000000000000..6575fdce31183d8238b18f2f30ab5b9227c7071c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Auckland differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Bougainville b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Bougainville new file mode 100644 index 0000000000000000000000000000000000000000..c535acdabda1b6ed96420e589d4f6868d23d8933 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Bougainville differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Chatham b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Chatham new file mode 100644 index 0000000000000000000000000000000000000000..bde46cf7e4b7909714b93cee39a5d953387d756c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Chatham differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Chuuk b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Chuuk new file mode 100644 index 0000000000000000000000000000000000000000..7be2474dd91c8a7da181fcda09d838254b890d75 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Chuuk differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Easter b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Easter new file mode 100644 index 0000000000000000000000000000000000000000..184cb6a83b3392d0492c42297531c85e7e38c4f5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Easter differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Efate b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Efate new file mode 100644 index 0000000000000000000000000000000000000000..777325fc6c6da8795d89aed5206c60f8bf80b0ab Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Efate differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Enderbury b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Enderbury new file mode 100644 index 0000000000000000000000000000000000000000..b1c4b0734483033722c28c8b2884ac9dddd2ab45 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Enderbury differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Fakaofo b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Fakaofo new file mode 100644 index 0000000000000000000000000000000000000000..4905ea72b1640ca67e35b06395e1c700dffa8d21 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Fakaofo differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Fiji b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Fiji new file mode 100644 index 0000000000000000000000000000000000000000..acf8091ac85151a6c887e2b5049ee40754b7b0d3 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Fiji differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Funafuti b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Funafuti new file mode 100644 index 0000000000000000000000000000000000000000..47661d40a4188eb39e8d52e5af8ab23ef7f23766 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Funafuti differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Galapagos b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Galapagos new file mode 100644 index 0000000000000000000000000000000000000000..40051ddf63a32d79e5233b916e3911d6dc3ef759 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Galapagos differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Gambier b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Gambier new file mode 100644 index 0000000000000000000000000000000000000000..84acaf41520d2d302f75e300e2b47c5527218df4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Gambier differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Guadalcanal b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Guadalcanal new file mode 100644 index 0000000000000000000000000000000000000000..1ab8353464ddb93947f871f07cfd12540373269c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Guadalcanal differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Guam b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Guam new file mode 100644 index 0000000000000000000000000000000000000000..66490d25dff9bcc8f710b0141f1a02e64aeb32f3 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Guam differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Honolulu b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Honolulu new file mode 100644 index 0000000000000000000000000000000000000000..c7cd060159bd22fc5e6f10ac5a2089afb2c19c6a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Honolulu differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Johnston b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Johnston new file mode 100644 index 0000000000000000000000000000000000000000..c7cd060159bd22fc5e6f10ac5a2089afb2c19c6a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Johnston differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Kanton b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Kanton new file mode 100644 index 0000000000000000000000000000000000000000..b1c4b0734483033722c28c8b2884ac9dddd2ab45 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Kanton differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Kiritimati b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Kiritimati new file mode 100644 index 0000000000000000000000000000000000000000..b4c6037a2d2a8f89539c3df05c32b6f52b1b1e92 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Kiritimati differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Kosrae b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Kosrae new file mode 100644 index 0000000000000000000000000000000000000000..0666fb0dd161cb732b29f84d49c49cc985a3559a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Kosrae differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Kwajalein b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Kwajalein new file mode 100644 index 0000000000000000000000000000000000000000..1887a607422edd499fdf24afe91a04294f1caf6f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Kwajalein differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Majuro b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Majuro new file mode 100644 index 0000000000000000000000000000000000000000..47661d40a4188eb39e8d52e5af8ab23ef7f23766 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Majuro differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Marquesas b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Marquesas new file mode 100644 index 0000000000000000000000000000000000000000..f546c03f96b24859521aab5b9997bfc5dd124ead Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Marquesas differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Midway b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Midway new file mode 100644 index 0000000000000000000000000000000000000000..cb56709a77dedb471150f4907771bf38f1879ba4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Midway differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Nauru b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Nauru new file mode 100644 index 0000000000000000000000000000000000000000..3339b6cf86d6e98ba70c9bd6cab3dbf50588acd2 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Nauru differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Niue b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Niue new file mode 100644 index 0000000000000000000000000000000000000000..f76972f8849a7d6ed8c3e2649b02f44de891094f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Niue differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Norfolk b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Norfolk new file mode 100644 index 0000000000000000000000000000000000000000..3b4186d61152629b764efc4222e41647b65f7fbb Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Norfolk differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Noumea b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Noumea new file mode 100644 index 0000000000000000000000000000000000000000..959cc8cd26f8a7b10a70e0f93bf3b3c9bbc680d2 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Noumea differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Pago_Pago b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Pago_Pago new file mode 100644 index 0000000000000000000000000000000000000000..cb56709a77dedb471150f4907771bf38f1879ba4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Pago_Pago differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Palau b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Palau new file mode 100644 index 0000000000000000000000000000000000000000..1cbebe28afd90c0a2e02786655e18a157284a412 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Palau differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Pitcairn b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Pitcairn new file mode 100644 index 0000000000000000000000000000000000000000..5ee90e70203d72484b8752475c86139671bcb102 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Pitcairn differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Pohnpei b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Pohnpei new file mode 100644 index 0000000000000000000000000000000000000000..1ab8353464ddb93947f871f07cfd12540373269c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Pohnpei differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Ponape b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Ponape new file mode 100644 index 0000000000000000000000000000000000000000..1ab8353464ddb93947f871f07cfd12540373269c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Ponape differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Port_Moresby b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Port_Moresby new file mode 100644 index 0000000000000000000000000000000000000000..7be2474dd91c8a7da181fcda09d838254b890d75 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Port_Moresby differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Rarotonga b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Rarotonga new file mode 100644 index 0000000000000000000000000000000000000000..184a87c112b99536acbafa1ec7aba52c7a94b549 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Rarotonga differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Saipan b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Saipan new file mode 100644 index 0000000000000000000000000000000000000000..66490d25dff9bcc8f710b0141f1a02e64aeb32f3 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Saipan differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Samoa b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Samoa new file mode 100644 index 0000000000000000000000000000000000000000..cb56709a77dedb471150f4907771bf38f1879ba4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Samoa differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Tahiti b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Tahiti new file mode 100644 index 0000000000000000000000000000000000000000..481edd30580f00eccf69de4f1c332fc048210011 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Tahiti differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Tarawa b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Tarawa new file mode 100644 index 0000000000000000000000000000000000000000..47661d40a4188eb39e8d52e5af8ab23ef7f23766 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Tarawa differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Tongatapu b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Tongatapu new file mode 100644 index 0000000000000000000000000000000000000000..c8824ab5434985650f33e12eace1981f8b116207 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Tongatapu differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Truk b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Truk new file mode 100644 index 0000000000000000000000000000000000000000..7be2474dd91c8a7da181fcda09d838254b890d75 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Truk differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Wake b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Wake new file mode 100644 index 0000000000000000000000000000000000000000..47661d40a4188eb39e8d52e5af8ab23ef7f23766 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Wake differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Wallis b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Wallis new file mode 100644 index 0000000000000000000000000000000000000000..47661d40a4188eb39e8d52e5af8ab23ef7f23766 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Wallis differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Yap b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Yap new file mode 100644 index 0000000000000000000000000000000000000000..7be2474dd91c8a7da181fcda09d838254b890d75 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Pacific/Yap differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Poland b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Poland new file mode 100644 index 0000000000000000000000000000000000000000..e33cf67171da78aa9e6eb02e50f9b9603da4c3f4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Poland differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Portugal b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Portugal new file mode 100644 index 0000000000000000000000000000000000000000..616de167b8a1c534c1c526e7bbff82f7d5da94f2 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Portugal differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/ROC b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/ROC new file mode 100644 index 0000000000000000000000000000000000000000..24c43444b6751343d2915843d03e55753d4d7359 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/ROC differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/ROK b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/ROK new file mode 100644 index 0000000000000000000000000000000000000000..96199e73e73aafacd89e48cb2855a96d7a134e1d Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/ROK differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Singapore b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Singapore new file mode 100644 index 0000000000000000000000000000000000000000..3d9f191e3acad56fb4f73ab44a74c50cdf91c12e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Singapore differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Turkey b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Turkey new file mode 100644 index 0000000000000000000000000000000000000000..7c2336dd80c3c9cbf71cb53d2b2c1f89a65a8ba5 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Turkey differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/UCT b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/UCT new file mode 100644 index 0000000000000000000000000000000000000000..91558be0c2bf903b2364215ba26d5227d6126508 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/UCT differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Alaska b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Alaska new file mode 100644 index 0000000000000000000000000000000000000000..9bbb2fd3b361ea8aa4c126d14df5fa370343a63f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Alaska differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Aleutian b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Aleutian new file mode 100644 index 0000000000000000000000000000000000000000..43236498f681cc06f64ca2afa613880331fe6fbb Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Aleutian differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Arizona b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Arizona new file mode 100644 index 0000000000000000000000000000000000000000..ab37e845566aa95659b7b85be0051d0c67a7e53a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Arizona differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Central b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Central new file mode 100644 index 0000000000000000000000000000000000000000..c6981a06b1d9c26f447518efe265a6454726eae7 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Central differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/East-Indiana b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/East-Indiana new file mode 100644 index 0000000000000000000000000000000000000000..a84b6e99671f7305d1c459956e16a4dd772fc60e Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/East-Indiana differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Eastern b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Eastern new file mode 100644 index 0000000000000000000000000000000000000000..a8b9ab1992257d721ad627b14f535c3d4b020888 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Eastern differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Hawaii b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Hawaii new file mode 100644 index 0000000000000000000000000000000000000000..c7cd060159bd22fc5e6f10ac5a2089afb2c19c6a Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Hawaii differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Indiana-Starke b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Indiana-Starke new file mode 100644 index 0000000000000000000000000000000000000000..025d132dd48ba978c6fedf86d70173127be49d49 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Indiana-Starke differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Michigan b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Michigan new file mode 100644 index 0000000000000000000000000000000000000000..e104faa46545ee873295cde34e1d46bccad8647c Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Michigan differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Mountain b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Mountain new file mode 100644 index 0000000000000000000000000000000000000000..abb2b974a47eb3e5c8b4f5d4370baf4898b239ab Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Mountain differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Pacific b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Pacific new file mode 100644 index 0000000000000000000000000000000000000000..610e7af5fc13d9784de30d272c7c39d7938873a0 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Pacific differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Samoa b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Samoa new file mode 100644 index 0000000000000000000000000000000000000000..cb56709a77dedb471150f4907771bf38f1879ba4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/US/Samoa differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/UTC b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/UTC new file mode 100644 index 0000000000000000000000000000000000000000..91558be0c2bf903b2364215ba26d5227d6126508 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/UTC differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Universal b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Universal new file mode 100644 index 0000000000000000000000000000000000000000..91558be0c2bf903b2364215ba26d5227d6126508 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Universal differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/W-SU b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/W-SU new file mode 100644 index 0000000000000000000000000000000000000000..ddb3f4e99a1030f33b56fad986c8d9c16e59eb32 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/W-SU differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/WET b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/WET new file mode 100644 index 0000000000000000000000000000000000000000..616de167b8a1c534c1c526e7bbff82f7d5da94f2 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/WET differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Zulu b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Zulu new file mode 100644 index 0000000000000000000000000000000000000000..91558be0c2bf903b2364215ba26d5227d6126508 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/Zulu differ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/iso3166.tab b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/iso3166.tab new file mode 100644 index 0000000000000000000000000000000000000000..4ae35234b9722a8b673823e67106479eae6d31a6 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/iso3166.tab @@ -0,0 +1,279 @@ +# ISO 3166 alpha-2 country codes +# +# This file is in the public domain, so clarified as of +# 2009-05-17 by Arthur David Olson. +# +# From Paul Eggert (2025-07-01): +# This file contains a table of two-letter country codes. Columns are +# separated by a single tab. Lines beginning with ‘#’ are comments. +# All text uses UTF-8 encoding. The columns of the table are as follows: +# +# 1. ISO 3166-1 alpha-2 country code, current as of +# ISO/TC 46 N1127 (2024-02-29). See: ISO/TC 46 Documents +# https://www.iso.org/committee/48750.html?view=documents +# 2. The usual English name for the coded region. This sometimes +# departs from ISO-listed names, sometimes so that sorted subsets +# of names are useful (e.g., “Samoa (American)” and “Samoa +# (western)” rather than “American Samoa” and “Samoa”), +# sometimes to avoid confusion among non-experts (e.g., +# “Czech Republic” and “Turkey” rather than “Czechia” and “Türkiye”), +# and sometimes to omit needless detail or churn (e.g., “Netherlands” +# rather than “Netherlands (the)” or “Netherlands (Kingdom of the)”). +# +# The table is sorted by country code. +# +# This table is intended as an aid for users, to help them select time +# zone data appropriate for their practical needs. It is not intended +# to take or endorse any position on legal or territorial claims. +# +#country- +#code name of country, territory, area, or subdivision +AD Andorra +AE United Arab Emirates +AF Afghanistan +AG Antigua & Barbuda +AI Anguilla +AL Albania +AM Armenia +AO Angola +AQ Antarctica +AR Argentina +AS Samoa (American) +AT Austria +AU Australia +AW Aruba +AX Åland Islands +AZ Azerbaijan +BA Bosnia & Herzegovina +BB Barbados +BD Bangladesh +BE Belgium +BF Burkina Faso +BG Bulgaria +BH Bahrain +BI Burundi +BJ Benin +BL St Barthelemy +BM Bermuda +BN Brunei +BO Bolivia +BQ Caribbean NL +BR Brazil +BS Bahamas +BT Bhutan +BV Bouvet Island +BW Botswana +BY Belarus +BZ Belize +CA Canada +CC Cocos (Keeling) Islands +CD Congo (Dem. Rep.) +CF Central African Rep. +CG Congo (Rep.) +CH Switzerland +CI Côte d’Ivoire +CK Cook Islands +CL Chile +CM Cameroon +CN China +CO Colombia +CR Costa Rica +CU Cuba +CV Cape Verde +CW Curaçao +CX Christmas Island +CY Cyprus +CZ Czech Republic +DE Germany +DJ Djibouti +DK Denmark +DM Dominica +DO Dominican Republic +DZ Algeria +EC Ecuador +EE Estonia +EG Egypt +EH Western Sahara +ER Eritrea +ES Spain +ET Ethiopia +FI Finland +FJ Fiji +FK Falkland Islands +FM Micronesia +FO Faroe Islands +FR France +GA Gabon +GB Britain (UK) +GD Grenada +GE Georgia +GF French Guiana +GG Guernsey +GH Ghana +GI Gibraltar +GL Greenland +GM Gambia +GN Guinea +GP Guadeloupe +GQ Equatorial Guinea +GR Greece +GS South Georgia & the South Sandwich Islands +GT Guatemala +GU Guam +GW Guinea-Bissau +GY Guyana +HK Hong Kong +HM Heard Island & McDonald Islands +HN Honduras +HR Croatia +HT Haiti +HU Hungary +ID Indonesia +IE Ireland +IL Israel +IM Isle of Man +IN India +IO British Indian Ocean Territory +IQ Iraq +IR Iran +IS Iceland +IT Italy +JE Jersey +JM Jamaica +JO Jordan +JP Japan +KE Kenya +KG Kyrgyzstan +KH Cambodia +KI Kiribati +KM Comoros +KN St Kitts & Nevis +KP Korea (North) +KR Korea (South) +KW Kuwait +KY Cayman Islands +KZ Kazakhstan +LA Laos +LB Lebanon +LC St Lucia +LI Liechtenstein +LK Sri Lanka +LR Liberia +LS Lesotho +LT Lithuania +LU Luxembourg +LV Latvia +LY Libya +MA Morocco +MC Monaco +MD Moldova +ME Montenegro +MF St Martin (French) +MG Madagascar +MH Marshall Islands +MK North Macedonia +ML Mali +MM Myanmar (Burma) +MN Mongolia +MO Macau +MP Northern Mariana Islands +MQ Martinique +MR Mauritania +MS Montserrat +MT Malta +MU Mauritius +MV Maldives +MW Malawi +MX Mexico +MY Malaysia +MZ Mozambique +NA Namibia +NC New Caledonia +NE Niger +NF Norfolk Island +NG Nigeria +NI Nicaragua +NL Netherlands +NO Norway +NP Nepal +NR Nauru +NU Niue +NZ New Zealand +OM Oman +PA Panama +PE Peru +PF French Polynesia +PG Papua New Guinea +PH Philippines +PK Pakistan +PL Poland +PM St Pierre & Miquelon +PN Pitcairn +PR Puerto Rico +PS Palestine +PT Portugal +PW Palau +PY Paraguay +QA Qatar +RE Réunion +RO Romania +RS Serbia +RU Russia +RW Rwanda +SA Saudi Arabia +SB Solomon Islands +SC Seychelles +SD Sudan +SE Sweden +SG Singapore +SH St Helena +SI Slovenia +SJ Svalbard & Jan Mayen +SK Slovakia +SL Sierra Leone +SM San Marino +SN Senegal +SO Somalia +SR Suriname +SS South Sudan +ST Sao Tome & Principe +SV El Salvador +SX St Maarten (Dutch) +SY Syria +SZ Eswatini (Swaziland) +TC Turks & Caicos Is +TD Chad +TF French S. Terr. +TG Togo +TH Thailand +TJ Tajikistan +TK Tokelau +TL East Timor +TM Turkmenistan +TN Tunisia +TO Tonga +TR Turkey +TT Trinidad & Tobago +TV Tuvalu +TW Taiwan +TZ Tanzania +UA Ukraine +UG Uganda +UM US minor outlying islands +US United States +UY Uruguay +UZ Uzbekistan +VA Vatican City +VC St Vincent +VE Venezuela +VG Virgin Islands (UK) +VI Virgin Islands (US) +VN Vietnam +VU Vanuatu +WF Wallis & Futuna +WS Samoa (western) +YE Yemen +YT Mayotte +ZA South Africa +ZM Zambia +ZW Zimbabwe diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/leapseconds b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/leapseconds new file mode 100644 index 0000000000000000000000000000000000000000..a796e4b4e22e2d4bd28c1763bced1a154172409f --- /dev/null +++ b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/leapseconds @@ -0,0 +1,86 @@ +# Allowance for leap seconds added to each time zone file. + +# This file is in the public domain. + +# This file is generated automatically from the data in the public-domain +# NIST/IERS format leap-seconds.list file, which can be copied from +# +# or via a less-secure protocol and with different comments and +# less volatile last-modified and expiration timestamps, from +# . +# For more about leap-seconds.list, please see +# The NTP Timescale and Leap Seconds +# . + +# The rules for leap seconds are specified in Annex 1 (Time scales) of: +# Standard-frequency and time-signal emissions. +# International Telecommunication Union - Radiocommunication Sector +# (ITU-R) Recommendation TF.460-6 (02/2002) +# . +# The International Earth Rotation and Reference Systems Service (IERS) +# periodically uses leap seconds to keep UTC to within 0.9 s of UT1 +# (a proxy for Earth's angle in space as measured by astronomers) +# and publishes leap second data in a copyrighted file +# . +# See: Levine J. Coordinated Universal Time and the leap second. +# URSI Radio Sci Bull. 2016;89(4):30-6. doi:10.23919/URSIRSB.2016.7909995 +# . + +# There were no leap seconds before 1972, as no official mechanism +# accounted for the discrepancy between atomic time (TAI) and the earth's +# rotation. The first ("1 Jan 1972") data line in leap-seconds.list +# does not denote a leap second; it denotes the start of the current definition +# of UTC. + +# All leap-seconds are Stationary (S) at the given UTC time. +# The correction (+ or -) is made at the given time, so in the unlikely +# event of a negative leap second, a line would look like this: +# Leap YEAR MON DAY 23:59:59 - S +# Typical lines look like this: +# Leap YEAR MON DAY 23:59:60 + S +Leap 1972 Jun 30 23:59:60 + S +Leap 1972 Dec 31 23:59:60 + S +Leap 1973 Dec 31 23:59:60 + S +Leap 1974 Dec 31 23:59:60 + S +Leap 1975 Dec 31 23:59:60 + S +Leap 1976 Dec 31 23:59:60 + S +Leap 1977 Dec 31 23:59:60 + S +Leap 1978 Dec 31 23:59:60 + S +Leap 1979 Dec 31 23:59:60 + S +Leap 1981 Jun 30 23:59:60 + S +Leap 1982 Jun 30 23:59:60 + S +Leap 1983 Jun 30 23:59:60 + S +Leap 1985 Jun 30 23:59:60 + S +Leap 1987 Dec 31 23:59:60 + S +Leap 1989 Dec 31 23:59:60 + S +Leap 1990 Dec 31 23:59:60 + S +Leap 1992 Jun 30 23:59:60 + S +Leap 1993 Jun 30 23:59:60 + S +Leap 1994 Jun 30 23:59:60 + S +Leap 1995 Dec 31 23:59:60 + S +Leap 1997 Jun 30 23:59:60 + S +Leap 1998 Dec 31 23:59:60 + S +Leap 2005 Dec 31 23:59:60 + S +Leap 2008 Dec 31 23:59:60 + S +Leap 2012 Jun 30 23:59:60 + S +Leap 2015 Jun 30 23:59:60 + S +Leap 2016 Dec 31 23:59:60 + S + +# UTC timestamp when this leap second list expires. +# Any additional leap seconds will come after this. +# This Expires line is commented out for now, +# so that pre-2020a zic implementations do not reject this file. +#Expires 2026 Dec 28 00:00:00 + +# Here are POSIX timestamps for the data in this file. +# "#updated" gives the last time the leap seconds data changed +# or, if this file was derived from the IERS leap-seconds.list, +# the last time that file changed in any way. +# "#expires" gives the first time this file might be wrong; +# if this file was derived from the IERS leap-seconds.list, +# this is typically a bit less than one year after "updated". +#updated 1767698058 (2026-01-06 11:14:18 UTC) +#expires 1798416000 (2026-12-28 00:00:00 UTC) + +# Updated through IERS Bulletin C (https://hpiers.obspm.fr/iers/bul/bulc/bulletinc.dat) +# File expires on 28 December 2026 diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/tzdata.zi b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/tzdata.zi new file mode 100644 index 0000000000000000000000000000000000000000..0c69c8450c241b808af481682f40d949a4b691fe --- /dev/null +++ b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/tzdata.zi @@ -0,0 +1,4302 @@ +# version unknown +# redo posix_only +# This zic input file is in the public domain. +R d 1916 o - Jun 14 23s 1 S +R d 1916 1919 - O Su>=1 23s 0 - +R d 1917 o - Mar 24 23s 1 S +R d 1918 o - Mar 9 23s 1 S +R d 1919 o - Mar 1 23s 1 S +R d 1920 o - F 14 23s 1 S +R d 1920 o - O 23 23s 0 - +R d 1921 o - Mar 14 23s 1 S +R d 1921 o - Jun 21 23s 0 - +R d 1939 o - S 11 23s 1 S +R d 1939 o - N 19 1 0 - +R d 1944 1945 - Ap M>=1 2 1 S +R d 1944 o - O 8 2 0 - +R d 1945 o - S 16 1 0 - +R d 1971 o - Ap 25 23s 1 S +R d 1971 o - S 26 23s 0 - +R d 1977 o - May 6 0 1 S +R d 1977 o - O 21 0 0 - +R d 1978 o - Mar 24 1 1 S +R d 1978 o - S 22 3 0 - +R d 1980 o - Ap 25 0 1 S +R d 1980 o - O 31 2 0 - +R K 1940 o - Jul 15 0 1 S +R K 1940 o - O 1 0 0 - +R K 1941 o - Ap 15 0 1 S +R K 1941 o - S 16 0 0 - +R K 1942 1944 - Ap 1 0 1 S +R K 1942 o - O 27 0 0 - +R K 1943 1945 - N 1 0 0 - +R K 1945 o - Ap 16 0 1 S +R K 1957 o - May 10 0 1 S +R K 1957 1958 - O 1 0 0 - +R K 1958 o - May 1 0 1 S +R K 1959 1981 - May 1 1 1 S +R K 1959 1965 - S 30 3 0 - +R K 1966 1994 - O 1 3 0 - +R K 1982 o - Jul 25 1 1 S +R K 1983 o - Jul 12 1 1 S +R K 1984 1988 - May 1 1 1 S +R K 1989 o - May 6 1 1 S +R K 1990 1994 - May 1 1 1 S +R K 1995 2010 - Ap lastF 0s 1 S +R K 1995 2005 - S lastTh 24 0 - +R K 2006 o - S 21 24 0 - +R K 2007 o - S Th>=1 24 0 - +R K 2008 o - Au lastTh 24 0 - +R K 2009 o - Au 20 24 0 - +R K 2010 o - Au 10 24 0 - +R K 2010 o - S 9 24 1 S +R K 2010 o - S lastTh 24 0 - +R K 2014 o - May 15 24 1 S +R K 2014 o - Jun 26 24 0 - +R K 2014 o - Jul 31 24 1 S +R K 2014 o - S lastTh 24 0 - +R K 2023 ma - Ap lastF 0 1 S +R K 2023 ma - O lastTh 24 0 - +R L 1951 o - O 14 2 1 S +R L 1952 o - Ja 1 0 0 - +R L 1953 o - O 9 2 1 S +R L 1954 o - Ja 1 0 0 - +R L 1955 o - S 30 0 1 S +R L 1956 o - Ja 1 0 0 - +R L 1982 1984 - Ap 1 0 1 S +R L 1982 1985 - O 1 0 0 - +R L 1985 o - Ap 6 0 1 S +R L 1986 o - Ap 4 0 1 S +R L 1986 o - O 3 0 0 - +R L 1987 1989 - Ap 1 0 1 S +R L 1987 1989 - O 1 0 0 - +R L 1997 o - Ap 4 0 1 S +R L 1997 o - O 4 0 0 - +R L 2013 o - Mar lastF 1 1 S +R L 2013 o - O lastF 2 0 - +R MU 1982 o - O 10 0 1 - +R MU 1983 o - Mar 21 0 0 - +R MU 2008 o - O lastSu 2 1 - +R MU 2009 o - Mar lastSu 2 0 - +R M 1939 o - S 12 0 1 - +R M 1939 o - N 19 0 0 - +R M 1940 o - F 25 0 1 - +R M 1945 o - N 18 0 0 - +R M 1950 o - Jun 11 0 1 - +R M 1950 o - O 29 0 0 - +R M 1967 o - Jun 3 12 1 - +R M 1967 o - O 1 0 0 - +R M 1974 o - Jun 24 0 1 - +R M 1974 o - S 1 0 0 - +R M 1976 1977 - May 1 0 1 - +R M 1976 o - Au 1 0 0 - +R M 1977 o - S 28 0 0 - +R M 1978 o - Jun 1 0 1 - +R M 1978 o - Au 4 0 0 - +R M 2008 o - Jun 1 0 1 - +R M 2008 o - S 1 0 0 - +R M 2009 o - Jun 1 0 1 - +R M 2009 o - Au 21 0 0 - +R M 2010 o - May 2 0 1 - +R M 2010 o - Au 8 0 0 - +R M 2011 o - Ap 3 0 1 - +R M 2011 o - Jul 31 0 0 - +R M 2012 2013 - Ap lastSu 2 1 - +R M 2012 o - Jul 20 3 0 - +R M 2012 o - Au 20 2 1 - +R M 2012 o - S 30 3 0 - +R M 2013 o - Jul 7 3 0 - +R M 2013 o - Au 10 2 1 - +R M 2013 2018 - O lastSu 3 0 - +R M 2014 2018 - Mar lastSu 2 1 - +R M 2014 o - Jun 28 3 0 - +R M 2014 o - Au 2 2 1 - +R M 2015 o - Jun 14 3 0 - +R M 2015 o - Jul 19 2 1 - +R M 2016 o - Jun 5 3 0 - +R M 2016 o - Jul 10 2 1 - +R M 2017 o - May 21 3 0 - +R M 2017 o - Jul 2 2 1 - +R M 2018 o - May 13 3 0 - +R M 2018 o - Jun 17 2 1 - +R M 2019 o - May 5 3 -1 - +R M 2019 o - Jun 9 2 0 - +R M 2020 o - Ap 19 3 -1 - +R M 2020 o - May 31 2 0 - +R M 2021 o - Ap 11 3 -1 - +R M 2021 o - May 16 2 0 - +R M 2022 o - Mar 27 3 -1 - +R M 2022 o - May 8 2 0 - +R M 2023 o - Mar 19 3 -1 - +R M 2023 o - Ap 23 2 0 - +R M 2024 o - Mar 10 3 -1 - +R M 2024 o - Ap 14 2 0 - +R M 2025 o - F 23 3 -1 - +R M 2025 o - Ap 6 2 0 - +R M 2026 o - F 15 3 -1 - +R M 2026 o - Mar 22 2 0 - +R M 2027 o - F 7 3 -1 - +R M 2027 o - Mar 14 2 0 - +R M 2028 o - Ja 23 3 -1 - +R M 2028 o - Mar 5 2 0 - +R M 2029 o - Ja 14 3 -1 - +R M 2029 o - F 18 2 0 - +R M 2029 o - D 30 3 -1 - +R M 2030 o - F 10 2 0 - +R M 2030 o - D 22 3 -1 - +R M 2031 o - Ja 26 2 0 - +R M 2031 o - D 14 3 -1 - +R M 2032 o - Ja 18 2 0 - +R M 2032 o - N 28 3 -1 - +R M 2033 o - Ja 9 2 0 - +R M 2033 o - N 20 3 -1 - +R M 2033 o - D 25 2 0 - +R M 2034 o - N 5 3 -1 - +R M 2034 o - D 17 2 0 - +R M 2035 o - O 28 3 -1 - +R M 2035 o - D 9 2 0 - +R M 2036 o - O 19 3 -1 - +R M 2036 o - N 23 2 0 - +R M 2037 o - O 4 3 -1 - +R M 2037 o - N 15 2 0 - +R M 2038 o - S 26 3 -1 - +R M 2038 o - O 31 2 0 - +R M 2039 o - S 18 3 -1 - +R M 2039 o - O 23 2 0 - +R M 2040 o - S 2 3 -1 - +R M 2040 o - O 14 2 0 - +R M 2041 o - Au 25 3 -1 - +R M 2041 o - S 29 2 0 - +R M 2042 o - Au 10 3 -1 - +R M 2042 o - S 21 2 0 - +R M 2043 o - Au 2 3 -1 - +R M 2043 o - S 13 2 0 - +R M 2044 o - Jul 24 3 -1 - +R M 2044 o - Au 28 2 0 - +R M 2045 o - Jul 9 3 -1 - +R M 2045 o - Au 20 2 0 - +R M 2046 o - Jul 1 3 -1 - +R M 2046 o - Au 5 2 0 - +R M 2047 o - Jun 23 3 -1 - +R M 2047 o - Jul 28 2 0 - +R M 2048 o - Jun 7 3 -1 - +R M 2048 o - Jul 19 2 0 - +R M 2049 o - May 30 3 -1 - +R M 2049 o - Jul 4 2 0 - +R M 2050 o - May 15 3 -1 - +R M 2050 o - Jun 26 2 0 - +R M 2051 o - May 7 3 -1 - +R M 2051 o - Jun 18 2 0 - +R M 2052 o - Ap 28 3 -1 - +R M 2052 o - Jun 2 2 0 - +R M 2053 o - Ap 13 3 -1 - +R M 2053 o - May 25 2 0 - +R M 2054 o - Ap 5 3 -1 - +R M 2054 o - May 10 2 0 - +R M 2055 o - Mar 28 3 -1 - +R M 2055 o - May 2 2 0 - +R M 2056 o - Mar 12 3 -1 - +R M 2056 o - Ap 23 2 0 - +R M 2057 o - Mar 4 3 -1 - +R M 2057 o - Ap 8 2 0 - +R M 2058 o - F 17 3 -1 - +R M 2058 o - Mar 31 2 0 - +R M 2059 o - F 9 3 -1 - +R M 2059 o - Mar 23 2 0 - +R M 2060 o - F 1 3 -1 - +R M 2060 o - Mar 7 2 0 - +R M 2061 o - Ja 16 3 -1 - +R M 2061 o - F 27 2 0 - +R M 2062 o - Ja 8 3 -1 - +R M 2062 o - F 12 2 0 - +R M 2062 o - D 31 3 -1 - +R M 2063 o - F 4 2 0 - +R M 2063 o - D 16 3 -1 - +R M 2064 o - Ja 27 2 0 - +R M 2064 o - D 7 3 -1 - +R M 2065 o - Ja 11 2 0 - +R M 2065 o - N 22 3 -1 - +R M 2066 o - Ja 3 2 0 - +R M 2066 o - N 14 3 -1 - +R M 2066 o - D 26 2 0 - +R M 2067 o - N 6 3 -1 - +R M 2067 o - D 11 2 0 - +R M 2068 o - O 21 3 -1 - +R M 2068 o - D 2 2 0 - +R M 2069 o - O 13 3 -1 - +R M 2069 o - N 17 2 0 - +R M 2070 o - O 5 3 -1 - +R M 2070 o - N 9 2 0 - +R M 2071 o - S 20 3 -1 - +R M 2071 o - N 1 2 0 - +R M 2072 o - S 11 3 -1 - +R M 2072 o - O 16 2 0 - +R M 2073 o - Au 27 3 -1 - +R M 2073 o - O 8 2 0 - +R M 2074 o - Au 19 3 -1 - +R M 2074 o - S 30 2 0 - +R M 2075 o - Au 11 3 -1 - +R M 2075 o - S 15 2 0 - +R M 2076 o - Jul 26 3 -1 - +R M 2076 o - S 6 2 0 - +R M 2077 o - Jul 18 3 -1 - +R M 2077 o - Au 22 2 0 - +R M 2078 o - Jul 10 3 -1 - +R M 2078 o - Au 14 2 0 - +R M 2079 o - Jun 25 3 -1 - +R M 2079 o - Au 6 2 0 - +R M 2080 o - Jun 16 3 -1 - +R M 2080 o - Jul 21 2 0 - +R M 2081 o - Jun 1 3 -1 - +R M 2081 o - Jul 13 2 0 - +R M 2082 o - May 24 3 -1 - +R M 2082 o - Jun 28 2 0 - +R M 2083 o - May 16 3 -1 - +R M 2083 o - Jun 20 2 0 - +R M 2084 o - Ap 30 3 -1 - +R M 2084 o - Jun 11 2 0 - +R M 2085 o - Ap 22 3 -1 - +R M 2085 o - May 27 2 0 - +R M 2086 o - Ap 14 3 -1 - +R M 2086 o - May 19 2 0 - +R M 2087 o - Mar 30 3 -1 - +R M 2087 o - May 11 2 0 - +R NA 1994 o - Mar 21 0 -1 WAT +R NA 1994 2017 - S Su>=1 2 0 CAT +R NA 1995 2017 - Ap Su>=1 2 -1 WAT +R SA 1942 1943 - S Su>=15 2 1 - +R SA 1943 1944 - Mar Su>=15 2 0 - +R SD 1970 o - May 1 0 1 S +R SD 1970 1985 - O 15 0 0 - +R SD 1971 o - Ap 30 0 1 S +R SD 1972 1985 - Ap lastSu 0 1 S +R n 1939 o - Ap 15 23s 1 S +R n 1939 o - N 18 23s 0 - +R n 1940 o - F 25 23s 1 S +R n 1941 o - O 6 0 0 - +R n 1942 o - Mar 9 0 1 S +R n 1942 o - N 2 3 0 - +R n 1943 o - Mar 29 2 1 S +R n 1943 o - Ap 17 2 0 - +R n 1943 o - Ap 25 2 1 S +R n 1943 o - O 4 2 0 - +R n 1944 1945 - Ap M>=1 2 1 S +R n 1944 o - O 8 0 0 - +R n 1945 o - S 16 0 0 - +R n 1977 o - Ap 30 0s 1 S +R n 1977 o - S 24 0s 0 - +R n 1978 o - May 1 0s 1 S +R n 1978 o - O 1 0s 0 - +R n 1988 o - Jun 1 0s 1 S +R n 1988 1990 - S lastSu 0s 0 - +R n 1989 o - Mar 26 0s 1 S +R n 1990 o - May 1 0s 1 S +R n 2005 o - May 1 0s 1 S +R n 2005 o - S 30 1s 0 - +R n 2006 2008 - Mar lastSu 2s 1 S +R n 2006 2008 - O lastSu 2s 0 - +R Tr 2005 ma - Mar lastSu 1u 2 +02 +R Tr 2004 ma - O lastSu 1u 0 +00 +R AM 2011 o - Mar lastSu 2s 1 - +R AM 2011 o - O lastSu 2s 0 - +R AZ 1997 2015 - Mar lastSu 4 1 - +R AZ 1997 2015 - O lastSu 5 0 - +R BD 2009 o - Jun 19 23 1 - +R BD 2009 o - D 31 24 0 - +R Sh 1919 o - Ap 12 24 1 D +R Sh 1919 o - S 30 24 0 S +R Sh 1940 o - Jun 1 0 1 D +R Sh 1940 o - O 12 24 0 S +R Sh 1941 o - Mar 15 0 1 D +R Sh 1941 o - N 1 24 0 S +R Sh 1942 o - Ja 31 0 1 D +R Sh 1945 o - S 1 24 0 S +R Sh 1946 o - May 15 0 1 D +R Sh 1946 o - S 30 24 0 S +R Sh 1947 o - Ap 15 0 1 D +R Sh 1947 o - O 31 24 0 S +R Sh 1948 1949 - May 1 0 1 D +R Sh 1948 1949 - S 30 24 0 S +R CN 1986 o - May 4 2 1 D +R CN 1986 1991 - S Su>=11 2 0 S +R CN 1987 1991 - Ap Su>=11 2 1 D +R HK 1946 o - Ap 21 0 1 S +R HK 1946 o - D 1 3:30s 0 - +R HK 1947 o - Ap 13 3:30s 1 S +R HK 1947 o - N 30 3:30s 0 - +R HK 1948 o - May 2 3:30s 1 S +R HK 1948 1952 - O Su>=28 3:30s 0 - +R HK 1949 1953 - Ap Su>=1 3:30 1 S +R HK 1953 1964 - O Su>=31 3:30 0 - +R HK 1954 1964 - Mar Su>=18 3:30 1 S +R HK 1965 1976 - Ap Su>=16 3:30 1 S +R HK 1965 1976 - O Su>=16 3:30 0 - +R HK 1973 o - D 30 3:30 1 S +R HK 1979 o - May 13 3:30 1 S +R HK 1979 o - O 21 3:30 0 - +R f 1946 o - May 15 0 1 D +R f 1946 o - O 1 0 0 S +R f 1947 o - Ap 15 0 1 D +R f 1947 o - N 1 0 0 S +R f 1948 1951 - May 1 0 1 D +R f 1948 1951 - O 1 0 0 S +R f 1952 o - Mar 1 0 1 D +R f 1952 1954 - N 1 0 0 S +R f 1953 1959 - Ap 1 0 1 D +R f 1955 1961 - O 1 0 0 S +R f 1960 1961 - Jun 1 0 1 D +R f 1974 1975 - Ap 1 0 1 D +R f 1974 1975 - O 1 0 0 S +R f 1979 o - Jul 1 0 1 D +R f 1979 o - O 1 0 0 S +R _ 1942 1943 - Ap 30 23 1 - +R _ 1942 o - N 17 23 0 - +R _ 1943 o - S 30 23 0 S +R _ 1946 o - Ap 30 23s 1 D +R _ 1946 o - S 30 23s 0 S +R _ 1947 o - Ap 19 23s 1 D +R _ 1947 o - N 30 23s 0 S +R _ 1948 o - May 2 23s 1 D +R _ 1948 o - O 31 23s 0 S +R _ 1949 1950 - Ap Sa>=1 23s 1 D +R _ 1949 1950 - O lastSa 23s 0 S +R _ 1951 o - Mar 31 23s 1 D +R _ 1951 o - O 28 23s 0 S +R _ 1952 1953 - Ap Sa>=1 23s 1 D +R _ 1952 o - N 1 23s 0 S +R _ 1953 1954 - O lastSa 23s 0 S +R _ 1954 1956 - Mar Sa>=17 23s 1 D +R _ 1955 o - N 5 23s 0 S +R _ 1956 1964 - N Su>=1 3:30 0 S +R _ 1957 1964 - Mar Su>=18 3:30 1 D +R _ 1965 1973 - Ap Su>=16 3:30 1 D +R _ 1965 1966 - O Su>=16 2:30 0 S +R _ 1967 1976 - O Su>=16 3:30 0 S +R _ 1973 o - D 30 3:30 1 D +R _ 1975 1976 - Ap Su>=16 3:30 1 D +R _ 1979 o - May 13 3:30 1 D +R _ 1979 o - O Su>=16 3:30 0 S +R CY 1975 o - Ap 13 0 1 S +R CY 1975 o - O 12 0 0 - +R CY 1976 o - May 15 0 1 S +R CY 1976 o - O 11 0 0 - +R CY 1977 1980 - Ap Su>=1 0 1 S +R CY 1977 o - S 25 0 0 - +R CY 1978 o - O 2 0 0 - +R CY 1979 1997 - S lastSu 0 0 - +R CY 1981 1998 - Mar lastSu 0 1 S +R i 1910 o - Ja 1 0 0 - +R i 1977 o - Mar 21 23 1 - +R i 1977 o - O 20 24 0 - +R i 1978 o - Mar 24 24 1 - +R i 1978 o - Au 5 1 0 - +R i 1979 o - May 26 24 1 - +R i 1979 o - S 18 24 0 - +R i 1980 o - Mar 20 24 1 - +R i 1980 o - S 22 24 0 - +R i 1991 o - May 2 24 1 - +R i 1992 1995 - Mar 21 24 1 - +R i 1991 1995 - S 21 24 0 - +R i 1996 o - Mar 20 24 1 - +R i 1996 o - S 20 24 0 - +R i 1997 1999 - Mar 21 24 1 - +R i 1997 1999 - S 21 24 0 - +R i 2000 o - Mar 20 24 1 - +R i 2000 o - S 20 24 0 - +R i 2001 2003 - Mar 21 24 1 - +R i 2001 2003 - S 21 24 0 - +R i 2004 o - Mar 20 24 1 - +R i 2004 o - S 20 24 0 - +R i 2005 o - Mar 21 24 1 - +R i 2005 o - S 21 24 0 - +R i 2008 o - Mar 20 24 1 - +R i 2008 o - S 20 24 0 - +R i 2009 2011 - Mar 21 24 1 - +R i 2009 2011 - S 21 24 0 - +R i 2012 o - Mar 20 24 1 - +R i 2012 o - S 20 24 0 - +R i 2013 2015 - Mar 21 24 1 - +R i 2013 2015 - S 21 24 0 - +R i 2016 o - Mar 20 24 1 - +R i 2016 o - S 20 24 0 - +R i 2017 2019 - Mar 21 24 1 - +R i 2017 2019 - S 21 24 0 - +R i 2020 o - Mar 20 24 1 - +R i 2020 o - S 20 24 0 - +R i 2021 2022 - Mar 21 24 1 - +R i 2021 2022 - S 21 24 0 - +R IQ 1982 o - May 1 0 1 - +R IQ 1982 1984 - O 1 0 0 - +R IQ 1983 o - Mar 31 0 1 - +R IQ 1984 1985 - Ap 1 0 1 - +R IQ 1985 1990 - S lastSu 1s 0 - +R IQ 1986 1990 - Mar lastSu 1s 1 - +R IQ 1991 2007 - Ap 1 3s 1 - +R IQ 1991 2007 - O 1 3s 0 - +R Z 1940 o - May 31 24u 1 D +R Z 1940 o - S 30 24u 0 S +R Z 1940 o - N 16 24u 1 D +R Z 1942 1946 - O 31 24u 0 S +R Z 1943 1944 - Mar 31 24u 1 D +R Z 1945 1946 - Ap 15 24u 1 D +R Z 1948 o - May 22 24u 2 DD +R Z 1948 o - Au 31 24u 1 D +R Z 1948 1949 - O 31 24u 0 S +R Z 1949 o - Ap 30 24u 1 D +R Z 1950 o - Ap 15 24u 1 D +R Z 1950 o - S 14 24u 0 S +R Z 1951 o - Mar 31 24u 1 D +R Z 1951 o - N 10 24u 0 S +R Z 1952 o - Ap 19 24u 1 D +R Z 1952 o - O 18 24u 0 S +R Z 1953 o - Ap 11 24u 1 D +R Z 1953 o - S 12 24u 0 S +R Z 1954 o - Jun 12 24u 1 D +R Z 1954 o - S 11 24u 0 S +R Z 1955 o - Jun 11 24u 1 D +R Z 1955 o - S 10 24u 0 S +R Z 1956 o - Jun 2 24u 1 D +R Z 1956 o - S 29 24u 0 S +R Z 1957 o - Ap 27 24u 1 D +R Z 1957 o - S 21 24u 0 S +R Z 1974 o - Jul 6 24 1 D +R Z 1974 o - O 12 24 0 S +R Z 1975 o - Ap 19 24 1 D +R Z 1975 o - Au 30 24 0 S +R Z 1980 o - Au 2 24s 1 D +R Z 1980 o - S 13 24s 0 S +R Z 1984 o - May 5 24s 1 D +R Z 1984 o - Au 25 24s 0 S +R Z 1985 o - Ap 13 24 1 D +R Z 1985 o - Au 31 24 0 S +R Z 1986 o - May 17 24 1 D +R Z 1986 o - S 6 24 0 S +R Z 1987 o - Ap 14 24 1 D +R Z 1987 o - S 12 24 0 S +R Z 1988 o - Ap 9 24 1 D +R Z 1988 o - S 3 24 0 S +R Z 1989 o - Ap 29 24 1 D +R Z 1989 o - S 2 24 0 S +R Z 1990 o - Mar 24 24 1 D +R Z 1990 o - Au 25 24 0 S +R Z 1991 o - Mar 23 24 1 D +R Z 1991 o - Au 31 24 0 S +R Z 1992 o - Mar 28 24 1 D +R Z 1992 o - S 5 24 0 S +R Z 1993 o - Ap 2 0 1 D +R Z 1993 o - S 5 0 0 S +R Z 1994 o - Ap 1 0 1 D +R Z 1994 o - Au 28 0 0 S +R Z 1995 o - Mar 31 0 1 D +R Z 1995 o - S 3 0 0 S +R Z 1996 o - Mar 14 24 1 D +R Z 1996 o - S 15 24 0 S +R Z 1997 o - Mar 20 24 1 D +R Z 1997 o - S 13 24 0 S +R Z 1998 o - Mar 20 0 1 D +R Z 1998 o - S 6 0 0 S +R Z 1999 o - Ap 2 2 1 D +R Z 1999 o - S 3 2 0 S +R Z 2000 o - Ap 14 2 1 D +R Z 2000 o - O 6 1 0 S +R Z 2001 o - Ap 9 1 1 D +R Z 2001 o - S 24 1 0 S +R Z 2002 o - Mar 29 1 1 D +R Z 2002 o - O 7 1 0 S +R Z 2003 o - Mar 28 1 1 D +R Z 2003 o - O 3 1 0 S +R Z 2004 o - Ap 7 1 1 D +R Z 2004 o - S 22 1 0 S +R Z 2005 2012 - Ap F<=1 2 1 D +R Z 2005 o - O 9 2 0 S +R Z 2006 o - O 1 2 0 S +R Z 2007 o - S 16 2 0 S +R Z 2008 o - O 5 2 0 S +R Z 2009 o - S 27 2 0 S +R Z 2010 o - S 12 2 0 S +R Z 2011 o - O 2 2 0 S +R Z 2012 o - S 23 2 0 S +R Z 2013 ma - Mar F>=23 2 1 D +R Z 2013 ma - O lastSu 2 0 S +R JP 1948 o - May Sa>=1 24 1 D +R JP 1948 1951 - S Sa>=8 25 0 S +R JP 1949 o - Ap Sa>=1 24 1 D +R JP 1950 1951 - May Sa>=1 24 1 D +R J 1973 o - Jun 6 0 1 S +R J 1973 1975 - O 1 0 0 - +R J 1974 1977 - May 1 0 1 S +R J 1976 o - N 1 0 0 - +R J 1977 o - O 1 0 0 - +R J 1978 o - Ap 30 0 1 S +R J 1978 o - S 30 0 0 - +R J 1985 o - Ap 1 0 1 S +R J 1985 o - O 1 0 0 - +R J 1986 1988 - Ap F>=1 0 1 S +R J 1986 1990 - O F>=1 0 0 - +R J 1989 o - May 8 0 1 S +R J 1990 o - Ap 27 0 1 S +R J 1991 o - Ap 17 0 1 S +R J 1991 o - S 27 0 0 - +R J 1992 o - Ap 10 0 1 S +R J 1992 1993 - O F>=1 0 0 - +R J 1993 1998 - Ap F>=1 0 1 S +R J 1994 o - S F>=15 0 0 - +R J 1995 1998 - S F>=15 0s 0 - +R J 1999 o - Jul 1 0s 1 S +R J 1999 2002 - S lastF 0s 0 - +R J 2000 2001 - Mar lastTh 0s 1 S +R J 2002 2012 - Mar lastTh 24 1 S +R J 2003 o - O 24 0s 0 - +R J 2004 o - O 15 0s 0 - +R J 2005 o - S lastF 0s 0 - +R J 2006 2011 - O lastF 0s 0 - +R J 2013 o - D 20 0 0 - +R J 2014 2021 - Mar lastTh 24 1 S +R J 2014 2022 - O lastF 0s 0 - +R J 2022 o - F lastTh 24 1 S +R KG 1992 1996 - Ap Su>=7 0s 1 - +R KG 1992 1996 - S lastSu 0 0 - +R KG 1997 2005 - Mar lastSu 2:30 1 - +R KG 1997 2004 - O lastSu 2:30 0 - +R KR 1948 o - Jun 1 0 1 D +R KR 1948 o - S 12 24 0 S +R KR 1949 o - Ap 3 0 1 D +R KR 1949 1951 - S Sa>=7 24 0 S +R KR 1950 o - Ap 1 0 1 D +R KR 1951 o - May 6 0 1 D +R KR 1955 o - May 5 0 1 D +R KR 1955 o - S 8 24 0 S +R KR 1956 o - May 20 0 1 D +R KR 1956 o - S 29 24 0 S +R KR 1957 1960 - May Su>=1 0 1 D +R KR 1957 1960 - S Sa>=17 24 0 S +R KR 1987 1988 - May Su>=8 2 1 D +R KR 1987 1988 - O Su>=8 3 0 S +R l 1920 o - Mar 28 0 1 S +R l 1920 o - O 25 0 0 - +R l 1921 o - Ap 3 0 1 S +R l 1921 o - O 3 0 0 - +R l 1922 o - Mar 26 0 1 S +R l 1922 o - O 8 0 0 - +R l 1923 o - Ap 22 0 1 S +R l 1923 o - S 16 0 0 - +R l 1957 1961 - May 1 0 1 S +R l 1957 1961 - O 1 0 0 - +R l 1972 o - Jun 22 0 1 S +R l 1972 1977 - O 1 0 0 - +R l 1973 1977 - May 1 0 1 S +R l 1978 o - Ap 30 0 1 S +R l 1978 o - S 30 0 0 - +R l 1984 1987 - May 1 0 1 S +R l 1984 1991 - O 16 0 0 - +R l 1988 o - Jun 1 0 1 S +R l 1989 o - May 10 0 1 S +R l 1990 1992 - May 1 0 1 S +R l 1992 o - O 4 0 0 - +R l 1993 ma - Mar lastSu 0 1 S +R l 1993 1998 - S lastSu 0 0 - +R l 1999 ma - O lastSu 0 0 - +R NB 1935 1941 - S 14 0 0:20 - +R NB 1935 1941 - D 14 0 0 - +R X 1983 1984 - Ap 1 0 1 - +R X 1983 o - O 1 0 0 - +R X 1985 1998 - Mar lastSu 0 1 - +R X 1984 1998 - S lastSu 0 0 - +R X 2001 o - Ap lastSa 2 1 - +R X 2001 2006 - S lastSa 2 0 - +R X 2002 2006 - Mar lastSa 2 1 - +R X 2015 2016 - Mar lastSa 2 1 - +R X 2015 2016 - S lastSa 0 0 - +R PK 2002 o - Ap Su>=2 0 1 S +R PK 2002 o - O Su>=2 0 0 - +R PK 2008 o - Jun 1 0 1 S +R PK 2008 2009 - N 1 0 0 - +R PK 2009 o - Ap 15 0 1 S +R P 1999 2005 - Ap F>=15 0 1 S +R P 1999 2003 - O F>=15 0 0 - +R P 2004 o - O 1 1 0 - +R P 2005 o - O 4 2 0 - +R P 2006 2007 - Ap 1 0 1 S +R P 2006 o - S 22 0 0 - +R P 2007 o - S 13 2 0 - +R P 2008 2009 - Mar lastF 0 1 S +R P 2008 o - S 1 0 0 - +R P 2009 o - S 4 1 0 - +R P 2010 o - Mar 26 0 1 S +R P 2010 o - Au 11 0 0 - +R P 2011 o - Ap 1 0:1 1 S +R P 2011 o - Au 1 0 0 - +R P 2011 o - Au 30 0 1 S +R P 2011 o - S 30 0 0 - +R P 2012 2014 - Mar lastTh 24 1 S +R P 2012 o - S 21 1 0 - +R P 2013 o - S 27 0 0 - +R P 2014 o - O 24 0 0 - +R P 2015 o - Mar 28 0 1 S +R P 2015 o - O 23 1 0 - +R P 2016 2018 - Mar Sa<=30 1 1 S +R P 2016 2018 - O Sa<=30 1 0 - +R P 2019 o - Mar 29 0 1 S +R P 2019 o - O Sa<=30 0 0 - +R P 2020 2021 - Mar Sa<=30 0 1 S +R P 2020 o - O 24 1 0 - +R P 2021 o - O 29 1 0 - +R P 2022 o - Mar 27 0 1 S +R P 2022 2035 - O Sa<=30 2 0 - +R P 2023 o - Ap 29 2 1 S +R P 2024 o - Ap 20 2 1 S +R P 2025 o - Ap 12 2 1 S +R P 2026 2054 - Mar Sa<=30 2 1 S +R P 2036 o - O 18 2 0 - +R P 2037 o - O 10 2 0 - +R P 2038 o - S 25 2 0 - +R P 2039 o - S 17 2 0 - +R P 2040 o - S 1 2 0 - +R P 2040 o - O 20 2 1 S +R P 2040 2067 - O Sa<=30 2 0 - +R P 2041 o - Au 24 2 0 - +R P 2041 o - O 5 2 1 S +R P 2042 o - Au 16 2 0 - +R P 2042 o - S 27 2 1 S +R P 2043 o - Au 1 2 0 - +R P 2043 o - S 19 2 1 S +R P 2044 o - Jul 23 2 0 - +R P 2044 o - S 3 2 1 S +R P 2045 o - Jul 15 2 0 - +R P 2045 o - Au 26 2 1 S +R P 2046 o - Jun 30 2 0 - +R P 2046 o - Au 18 2 1 S +R P 2047 o - Jun 22 2 0 - +R P 2047 o - Au 3 2 1 S +R P 2048 o - Jun 6 2 0 - +R P 2048 o - Jul 25 2 1 S +R P 2049 o - May 29 2 0 - +R P 2049 o - Jul 10 2 1 S +R P 2050 o - May 21 2 0 - +R P 2050 o - Jul 2 2 1 S +R P 2051 o - May 6 2 0 - +R P 2051 o - Jun 24 2 1 S +R P 2052 o - Ap 27 2 0 - +R P 2052 o - Jun 8 2 1 S +R P 2053 o - Ap 12 2 0 - +R P 2053 o - May 31 2 1 S +R P 2054 o - Ap 4 2 0 - +R P 2054 o - May 23 2 1 S +R P 2055 o - May 8 2 1 S +R P 2056 o - Ap 29 2 1 S +R P 2057 o - Ap 14 2 1 S +R P 2058 o - Ap 6 2 1 S +R P 2059 ma - Mar Sa<=30 2 1 S +R P 2068 o - O 20 2 0 - +R P 2069 o - O 12 2 0 - +R P 2070 o - O 4 2 0 - +R P 2071 o - S 19 2 0 - +R P 2072 o - S 10 2 0 - +R P 2072 o - O 22 2 1 S +R P 2072 ma - O Sa<=30 2 0 - +R P 2073 o - S 2 2 0 - +R P 2073 o - O 14 2 1 S +R P 2074 o - Au 18 2 0 - +R P 2074 o - O 6 2 1 S +R P 2075 o - Au 10 2 0 - +R P 2075 o - S 21 2 1 S +R P 2076 o - Jul 25 2 0 - +R P 2076 o - S 12 2 1 S +R P 2077 o - Jul 17 2 0 - +R P 2077 o - S 4 2 1 S +R P 2078 o - Jul 9 2 0 - +R P 2078 o - Au 20 2 1 S +R P 2079 o - Jun 24 2 0 - +R P 2079 o - Au 12 2 1 S +R P 2080 o - Jun 15 2 0 - +R P 2080 o - Jul 27 2 1 S +R P 2081 o - Jun 7 2 0 - +R P 2081 o - Jul 19 2 1 S +R P 2082 o - May 23 2 0 - +R P 2082 o - Jul 11 2 1 S +R P 2083 o - May 15 2 0 - +R P 2083 o - Jun 26 2 1 S +R P 2084 o - Ap 29 2 0 - +R P 2084 o - Jun 17 2 1 S +R P 2085 o - Ap 21 2 0 - +R P 2085 o - Jun 9 2 1 S +R P 2086 o - Ap 13 2 0 - +R P 2086 o - May 25 2 1 S +R PH 1936 o - O 31 24 1 D +R PH 1937 o - Ja 15 24 0 S +R PH 1941 o - D 15 24 1 D +R PH 1945 o - N 30 24 0 S +R PH 1954 o - Ap 11 24 1 D +R PH 1954 o - Jun 4 24 0 S +R PH 1977 o - Mar 27 24 1 D +R PH 1977 o - S 21 24 0 S +R PH 1990 o - May 21 0 1 D +R PH 1990 o - Jul 28 24 0 S +R S 1920 1923 - Ap Su>=15 2 1 S +R S 1920 1923 - O Su>=1 2 0 - +R S 1962 o - Ap 29 2 1 S +R S 1962 o - O 1 2 0 - +R S 1963 1965 - May 1 2 1 S +R S 1963 o - S 30 2 0 - +R S 1964 o - O 1 2 0 - +R S 1965 o - S 30 2 0 - +R S 1966 o - Ap 24 2 1 S +R S 1966 1976 - O 1 2 0 - +R S 1967 1978 - May 1 2 1 S +R S 1977 1978 - S 1 2 0 - +R S 1983 1984 - Ap 9 2 1 S +R S 1983 1984 - O 1 2 0 - +R S 1986 o - F 16 2 1 S +R S 1986 o - O 9 2 0 - +R S 1987 o - Mar 1 2 1 S +R S 1987 1988 - O 31 2 0 - +R S 1988 o - Mar 15 2 1 S +R S 1989 o - Mar 31 2 1 S +R S 1989 o - O 1 2 0 - +R S 1990 o - Ap 1 2 1 S +R S 1990 o - S 30 2 0 - +R S 1991 o - Ap 1 0 1 S +R S 1991 1992 - O 1 0 0 - +R S 1992 o - Ap 8 0 1 S +R S 1993 o - Mar 26 0 1 S +R S 1993 o - S 25 0 0 - +R S 1994 1996 - Ap 1 0 1 S +R S 1994 2005 - O 1 0 0 - +R S 1997 1998 - Mar lastM 0 1 S +R S 1999 2006 - Ap 1 0 1 S +R S 2006 o - S 22 0 0 - +R S 2007 o - Mar lastF 0 1 S +R S 2007 o - N F>=1 0 0 - +R S 2008 o - Ap F>=1 0 1 S +R S 2008 o - N 1 0 0 - +R S 2009 o - Mar lastF 0 1 S +R S 2010 2011 - Ap F>=1 0 1 S +R S 2012 2022 - Mar lastF 0 1 S +R S 2009 2022 - O lastF 0 0 - +R AU 1917 o - Ja 1 2s 1 D +R AU 1917 o - Mar lastSu 2s 0 S +R AU 1942 o - Ja 1 2s 1 D +R AU 1942 o - Mar lastSu 2s 0 S +R AU 1942 o - S 27 2s 1 D +R AU 1943 1944 - Mar lastSu 2s 0 S +R AU 1943 o - O 3 2s 1 D +R AW 1974 o - O lastSu 2s 1 D +R AW 1975 o - Mar Su>=1 2s 0 S +R AW 1983 o - O lastSu 2s 1 D +R AW 1984 o - Mar Su>=1 2s 0 S +R AW 1991 o - N 17 2s 1 D +R AW 1992 o - Mar Su>=1 2s 0 S +R AW 2006 o - D 3 2s 1 D +R AW 2007 2009 - Mar lastSu 2s 0 S +R AW 2007 2008 - O lastSu 2s 1 D +R AQ 1971 o - O lastSu 2s 1 D +R AQ 1972 o - F lastSu 2s 0 S +R AQ 1989 1991 - O lastSu 2s 1 D +R AQ 1990 1992 - Mar Su>=1 2s 0 S +R Ho 1992 1993 - O lastSu 2s 1 D +R Ho 1993 1994 - Mar Su>=1 2s 0 S +R AS 1971 1985 - O lastSu 2s 1 D +R AS 1986 o - O 19 2s 1 D +R AS 1987 2007 - O lastSu 2s 1 D +R AS 1972 o - F 27 2s 0 S +R AS 1973 1985 - Mar Su>=1 2s 0 S +R AS 1986 1990 - Mar Su>=15 2s 0 S +R AS 1991 o - Mar 3 2s 0 S +R AS 1992 o - Mar 22 2s 0 S +R AS 1993 o - Mar 7 2s 0 S +R AS 1994 o - Mar 20 2s 0 S +R AS 1995 2005 - Mar lastSu 2s 0 S +R AS 2006 o - Ap 2 2s 0 S +R AS 2007 o - Mar lastSu 2s 0 S +R AS 2008 ma - Ap Su>=1 2s 0 S +R AS 2008 ma - O Su>=1 2s 1 D +R AT 1916 o - O Su>=1 2s 1 D +R AT 1917 o - Mar lastSu 2s 0 S +R AT 1917 1918 - O Su>=22 2s 1 D +R AT 1918 1919 - Mar Su>=1 2s 0 S +R AT 1967 o - O Su>=1 2s 1 D +R AT 1968 o - Mar Su>=29 2s 0 S +R AT 1968 1985 - O lastSu 2s 1 D +R AT 1969 1971 - Mar Su>=8 2s 0 S +R AT 1972 o - F lastSu 2s 0 S +R AT 1973 1981 - Mar Su>=1 2s 0 S +R AT 1982 1983 - Mar lastSu 2s 0 S +R AT 1984 1986 - Mar Su>=1 2s 0 S +R AT 1986 o - O Su>=15 2s 1 D +R AT 1987 1990 - Mar Su>=15 2s 0 S +R AT 1987 o - O Su>=22 2s 1 D +R AT 1988 1990 - O lastSu 2s 1 D +R AT 1991 1999 - O Su>=1 2s 1 D +R AT 1991 2005 - Mar lastSu 2s 0 S +R AT 2000 o - Au lastSu 2s 1 D +R AT 2001 ma - O Su>=1 2s 1 D +R AT 2006 o - Ap Su>=1 2s 0 S +R AT 2007 o - Mar lastSu 2s 0 S +R AT 2008 ma - Ap Su>=1 2s 0 S +R AV 1971 1985 - O lastSu 2s 1 D +R AV 1972 o - F lastSu 2s 0 S +R AV 1973 1985 - Mar Su>=1 2s 0 S +R AV 1986 1990 - Mar Su>=15 2s 0 S +R AV 1986 1987 - O Su>=15 2s 1 D +R AV 1988 1999 - O lastSu 2s 1 D +R AV 1991 1994 - Mar Su>=1 2s 0 S +R AV 1995 2005 - Mar lastSu 2s 0 S +R AV 2000 o - Au lastSu 2s 1 D +R AV 2001 2007 - O lastSu 2s 1 D +R AV 2006 o - Ap Su>=1 2s 0 S +R AV 2007 o - Mar lastSu 2s 0 S +R AV 2008 ma - Ap Su>=1 2s 0 S +R AV 2008 ma - O Su>=1 2s 1 D +R AN 1971 1985 - O lastSu 2s 1 D +R AN 1972 o - F 27 2s 0 S +R AN 1973 1981 - Mar Su>=1 2s 0 S +R AN 1982 o - Ap Su>=1 2s 0 S +R AN 1983 1985 - Mar Su>=1 2s 0 S +R AN 1986 1989 - Mar Su>=15 2s 0 S +R AN 1986 o - O 19 2s 1 D +R AN 1987 1999 - O lastSu 2s 1 D +R AN 1990 1995 - Mar Su>=1 2s 0 S +R AN 1996 2005 - Mar lastSu 2s 0 S +R AN 2000 o - Au lastSu 2s 1 D +R AN 2001 2007 - O lastSu 2s 1 D +R AN 2006 o - Ap Su>=1 2s 0 S +R AN 2007 o - Mar lastSu 2s 0 S +R AN 2008 ma - Ap Su>=1 2s 0 S +R AN 2008 ma - O Su>=1 2s 1 D +R LH 1981 1984 - O lastSu 2 1 - +R LH 1982 1985 - Mar Su>=1 2 0 - +R LH 1985 o - O lastSu 2 0:30 - +R LH 1986 1989 - Mar Su>=15 2 0 - +R LH 1986 o - O 19 2 0:30 - +R LH 1987 1999 - O lastSu 2 0:30 - +R LH 1990 1995 - Mar Su>=1 2 0 - +R LH 1996 2005 - Mar lastSu 2 0 - +R LH 2000 o - Au lastSu 2 0:30 - +R LH 2001 2007 - O lastSu 2 0:30 - +R LH 2006 o - Ap Su>=1 2 0 - +R LH 2007 o - Mar lastSu 2 0 - +R LH 2008 ma - Ap Su>=1 2 0 - +R LH 2008 ma - O Su>=1 2 0:30 - +R FJ 1998 1999 - N Su>=1 2 1 - +R FJ 1999 2000 - F lastSu 3 0 - +R FJ 2009 o - N 29 2 1 - +R FJ 2010 o - Mar lastSu 3 0 - +R FJ 2010 2013 - O Su>=21 2 1 - +R FJ 2011 o - Mar Su>=1 3 0 - +R FJ 2012 2013 - Ja Su>=18 3 0 - +R FJ 2014 o - Ja Su>=18 2 0 - +R FJ 2014 2018 - N Su>=1 2 1 - +R FJ 2015 2021 - Ja Su>=12 3 0 - +R FJ 2019 o - N Su>=8 2 1 - +R FJ 2020 o - D 20 2 1 - +R Gu 1959 o - Jun 27 2 1 D +R Gu 1961 o - Ja 29 2 0 S +R Gu 1967 o - S 1 2 1 D +R Gu 1969 o - Ja 26 0:1 0 S +R Gu 1969 o - Jun 22 2 1 D +R Gu 1969 o - Au 31 2 0 S +R Gu 1970 1971 - Ap lastSu 2 1 D +R Gu 1970 1971 - S Su>=1 2 0 S +R Gu 1973 o - D 16 2 1 D +R Gu 1974 o - F 24 2 0 S +R Gu 1976 o - May 26 2 1 D +R Gu 1976 o - Au 22 2:1 0 S +R Gu 1977 o - Ap 24 2 1 D +R Gu 1977 o - Au 28 2 0 S +R NC 1977 1978 - D Su>=1 0 1 - +R NC 1978 1979 - F 27 0 0 - +R NC 1996 o - D 1 2s 1 - +R NC 1997 o - Mar 2 2s 0 - +R NZ 1927 o - N 6 2 1 S +R NZ 1928 o - Mar 4 2 0 M +R NZ 1928 1933 - O Su>=8 2 0:30 S +R NZ 1929 1933 - Mar Su>=15 2 0 M +R NZ 1934 1940 - Ap lastSu 2 0 M +R NZ 1934 1940 - S lastSu 2 0:30 S +R NZ 1946 o - Ja 1 0 0 S +R NZ 1974 o - N Su>=1 2s 1 D +R k 1974 o - N Su>=1 2:45s 1 - +R NZ 1975 o - F lastSu 2s 0 S +R k 1975 o - F lastSu 2:45s 0 - +R NZ 1975 1988 - O lastSu 2s 1 D +R k 1975 1988 - O lastSu 2:45s 1 - +R NZ 1976 1989 - Mar Su>=1 2s 0 S +R k 1976 1989 - Mar Su>=1 2:45s 0 - +R NZ 1989 o - O Su>=8 2s 1 D +R k 1989 o - O Su>=8 2:45s 1 - +R NZ 1990 2006 - O Su>=1 2s 1 D +R k 1990 2006 - O Su>=1 2:45s 1 - +R NZ 1990 2007 - Mar Su>=15 2s 0 S +R k 1990 2007 - Mar Su>=15 2:45s 0 - +R NZ 2007 ma - S lastSu 2s 1 D +R k 2007 ma - S lastSu 2:45s 1 - +R NZ 2008 ma - Ap Su>=1 2s 0 S +R k 2008 ma - Ap Su>=1 2:45s 0 - +R CK 1978 o - N 12 0 0:30 - +R CK 1979 1991 - Mar Su>=1 0 0 - +R CK 1979 1990 - O lastSu 0 0:30 - +R WS 2010 o - S lastSu 0 1 - +R WS 2011 o - Ap Sa>=1 4 0 - +R WS 2011 o - S lastSa 3 1 - +R WS 2012 2021 - Ap Su>=1 4 0 - +R WS 2012 2020 - S lastSu 3 1 - +R TO 1999 o - O 7 2s 1 - +R TO 2000 o - Mar 19 2s 0 - +R TO 2000 2001 - N Su>=1 2 1 - +R TO 2001 2002 - Ja lastSu 2 0 - +R TO 2016 o - N Su>=1 2 1 - +R TO 2017 o - Ja Su>=15 3 0 - +R VU 1973 o - D 22 12u 1 - +R VU 1974 o - Mar 30 12u 0 - +R VU 1983 1991 - S Sa>=22 24 1 - +R VU 1984 1991 - Mar Sa>=22 24 0 - +R VU 1992 1993 - Ja Sa>=22 24 0 - +R VU 1992 o - O Sa>=22 24 1 - +R G 1916 o - May 21 2s 1 BST +R G 1916 o - O 1 2s 0 GMT +R G 1917 o - Ap 8 2s 1 BST +R G 1917 o - S 17 2s 0 GMT +R G 1918 o - Mar 24 2s 1 BST +R G 1918 o - S 30 2s 0 GMT +R G 1919 o - Mar 30 2s 1 BST +R G 1919 o - S 29 2s 0 GMT +R G 1920 o - Mar 28 2s 1 BST +R G 1920 o - O 25 2s 0 GMT +R G 1921 o - Ap 3 2s 1 BST +R G 1921 o - O 3 2s 0 GMT +R G 1922 o - Mar 26 2s 1 BST +R G 1922 o - O 8 2s 0 GMT +R G 1923 o - Ap Su>=16 2s 1 BST +R G 1923 1924 - S Su>=16 2s 0 GMT +R G 1924 o - Ap Su>=9 2s 1 BST +R G 1925 1926 - Ap Su>=16 2s 1 BST +R G 1925 1938 - O Su>=2 2s 0 GMT +R G 1927 o - Ap Su>=9 2s 1 BST +R G 1928 1929 - Ap Su>=16 2s 1 BST +R G 1930 o - Ap Su>=9 2s 1 BST +R G 1931 1932 - Ap Su>=16 2s 1 BST +R G 1933 o - Ap Su>=9 2s 1 BST +R G 1934 o - Ap Su>=16 2s 1 BST +R G 1935 o - Ap Su>=9 2s 1 BST +R G 1936 1937 - Ap Su>=16 2s 1 BST +R G 1938 o - Ap Su>=9 2s 1 BST +R G 1939 o - Ap Su>=16 2s 1 BST +R G 1939 o - N Su>=16 2s 0 GMT +R G 1940 o - F Su>=23 2s 1 BST +R G 1941 o - May Su>=2 1s 2 BDST +R G 1941 1943 - Au Su>=9 1s 1 BST +R G 1942 1944 - Ap Su>=2 1s 2 BDST +R G 1944 o - S Su>=16 1s 1 BST +R G 1945 o - Ap M>=2 1s 2 BDST +R G 1945 o - Jul Su>=9 1s 1 BST +R G 1945 1946 - O Su>=2 2s 0 GMT +R G 1946 o - Ap Su>=9 2s 1 BST +R G 1947 o - Mar 16 2s 1 BST +R G 1947 o - Ap 13 1s 2 BDST +R G 1947 o - Au 10 1s 1 BST +R G 1947 o - N 2 2s 0 GMT +R G 1948 o - Mar 14 2s 1 BST +R G 1948 o - O 31 2s 0 GMT +R G 1949 o - Ap 3 2s 1 BST +R G 1949 o - O 30 2s 0 GMT +R G 1950 1952 - Ap Su>=14 2s 1 BST +R G 1950 1952 - O Su>=21 2s 0 GMT +R G 1953 o - Ap Su>=16 2s 1 BST +R G 1953 1960 - O Su>=2 2s 0 GMT +R G 1954 o - Ap Su>=9 2s 1 BST +R G 1955 1956 - Ap Su>=16 2s 1 BST +R G 1957 o - Ap Su>=9 2s 1 BST +R G 1958 1959 - Ap Su>=16 2s 1 BST +R G 1960 o - Ap Su>=9 2s 1 BST +R G 1961 1963 - Mar lastSu 2s 1 BST +R G 1961 1968 - O Su>=23 2s 0 GMT +R G 1964 1967 - Mar Su>=19 2s 1 BST +R G 1968 o - F 18 2s 1 BST +R G 1972 1980 - Mar Su>=16 2s 1 BST +R G 1972 1980 - O Su>=23 2s 0 GMT +R G 1981 1995 - Mar lastSu 1u 1 BST +R G 1981 1989 - O Su>=23 1u 0 GMT +R G 1990 1995 - O Su>=22 1u 0 GMT +R IE 1971 o - O 31 2u -1 - +R IE 1972 1980 - Mar Su>=16 2u 0 - +R IE 1972 1980 - O Su>=23 2u -1 - +R IE 1981 ma - Mar lastSu 1u 0 - +R IE 1981 1989 - O Su>=23 1u -1 - +R IE 1990 1995 - O Su>=22 1u -1 - +R IE 1996 ma - O lastSu 1u -1 - +R E 1977 1980 - Ap Su>=1 1u 1 S +R E 1977 o - S lastSu 1u 0 - +R E 1978 o - O 1 1u 0 - +R E 1979 1995 - S lastSu 1u 0 - +R E 1981 ma - Mar lastSu 1u 1 S +R E 1996 ma - O lastSu 1u 0 - +R W- 1977 1980 - Ap Su>=1 1s 1 S +R W- 1977 o - S lastSu 1s 0 - +R W- 1978 o - O 1 1s 0 - +R W- 1979 1995 - S lastSu 1s 0 - +R W- 1981 ma - Mar lastSu 1s 1 S +R W- 1996 ma - O lastSu 1s 0 - +R c 1916 o - Ap 30 23 1 S +R c 1916 o - O 1 1 0 - +R c 1917 1918 - Ap M>=15 2s 1 S +R c 1917 1918 - S M>=15 2s 0 - +R c 1940 o - Ap 1 2s 1 S +R c 1942 o - N 2 2s 0 - +R c 1943 o - Mar 29 2s 1 S +R c 1943 o - O 4 2s 0 - +R c 1944 1945 - Ap M>=1 2s 1 S +R c 1944 o - O 2 2s 0 - +R c 1945 o - S 16 2s 0 - +R c 1977 1980 - Ap Su>=1 2s 1 S +R c 1977 o - S lastSu 2s 0 - +R c 1978 o - O 1 2s 0 - +R c 1979 1995 - S lastSu 2s 0 - +R c 1981 ma - Mar lastSu 2s 1 S +R c 1996 ma - O lastSu 2s 0 - +R e 1977 1980 - Ap Su>=1 0 1 S +R e 1977 o - S lastSu 0 0 - +R e 1978 o - O 1 0 0 - +R e 1979 1995 - S lastSu 0 0 - +R e 1981 ma - Mar lastSu 0 1 S +R e 1996 ma - O lastSu 0 0 - +R R 1917 o - Jul 1 23 1 MST +R R 1917 o - D 28 0 0 MMT +R R 1918 o - May 31 22 2 MDST +R R 1918 o - S 16 1 1 MST +R R 1919 o - May 31 23 2 MDST +R R 1919 o - Jul 1 0u 1 MSD +R R 1919 o - Au 16 0 0 MSK +R R 1921 o - F 14 23 1 MSD +R R 1921 o - Mar 20 23 2 +05 +R R 1921 o - S 1 0 1 MSD +R R 1921 o - O 1 0 0 - +R R 1981 1984 - Ap 1 0 1 S +R R 1981 1983 - O 1 0 0 - +R R 1984 1995 - S lastSu 2s 0 - +R R 1985 2010 - Mar lastSu 2s 1 S +R R 1996 2010 - O lastSu 2s 0 - +R q 1940 o - Jun 16 0 1 S +R q 1942 o - N 2 3 0 - +R q 1943 o - Mar 29 2 1 S +R q 1943 o - Ap 10 3 0 - +R q 1974 o - May 4 0 1 S +R q 1974 o - O 2 0 0 - +R q 1975 o - May 1 0 1 S +R q 1975 o - O 2 0 0 - +R q 1976 o - May 2 0 1 S +R q 1976 o - O 3 0 0 - +R q 1977 o - May 8 0 1 S +R q 1977 o - O 2 0 0 - +R q 1978 o - May 6 0 1 S +R q 1978 o - O 1 0 0 - +R q 1979 o - May 5 0 1 S +R q 1979 o - S 30 0 0 - +R q 1980 o - May 3 0 1 S +R q 1980 o - O 4 0 0 - +R q 1981 o - Ap 26 0 1 S +R q 1981 o - S 27 0 0 - +R q 1982 o - May 2 0 1 S +R q 1982 o - O 3 0 0 - +R q 1983 o - Ap 18 0 1 S +R q 1983 o - O 1 0 0 - +R q 1984 o - Ap 1 0 1 S +R a 1920 o - Ap 5 2s 1 S +R a 1920 o - S 13 2s 0 - +R a 1946 o - Ap 14 2s 1 S +R a 1946 o - O 7 2s 0 - +R a 1947 1948 - O Su>=1 2s 0 - +R a 1947 o - Ap 6 2s 1 S +R a 1948 o - Ap 18 2s 1 S +R a 1980 o - Ap 6 0 1 S +R a 1980 o - S 28 0 0 - +R b 1918 o - Mar 9 0s 1 S +R b 1918 1919 - O Sa>=1 23s 0 - +R b 1919 o - Mar 1 23s 1 S +R b 1920 o - F 14 23s 1 S +R b 1920 o - O 23 23s 0 - +R b 1921 o - Mar 14 23s 1 S +R b 1921 o - O 25 23s 0 - +R b 1922 o - Mar 25 23s 1 S +R b 1922 1927 - O Sa>=1 23s 0 - +R b 1923 o - Ap 21 23s 1 S +R b 1924 o - Mar 29 23s 1 S +R b 1925 o - Ap 4 23s 1 S +R b 1926 o - Ap 17 23s 1 S +R b 1927 o - Ap 9 23s 1 S +R b 1928 o - Ap 14 23s 1 S +R b 1928 1938 - O Su>=2 2s 0 - +R b 1929 o - Ap 21 2s 1 S +R b 1930 o - Ap 13 2s 1 S +R b 1931 o - Ap 19 2s 1 S +R b 1932 o - Ap 3 2s 1 S +R b 1933 o - Mar 26 2s 1 S +R b 1934 o - Ap 8 2s 1 S +R b 1935 o - Mar 31 2s 1 S +R b 1936 o - Ap 19 2s 1 S +R b 1937 o - Ap 4 2s 1 S +R b 1938 o - Mar 27 2s 1 S +R b 1939 o - Ap 16 2s 1 S +R b 1939 o - N 19 2s 0 - +R b 1940 o - F 25 2s 1 S +R b 1944 o - S 17 2s 0 - +R b 1945 o - Ap 2 2s 1 S +R b 1945 o - S 16 2s 0 - +R b 1946 o - May 19 2s 1 S +R b 1946 o - O 7 2s 0 - +R BG 1979 o - Mar 31 23 1 S +R BG 1979 o - O 1 1 0 - +R BG 1980 1982 - Ap Sa>=1 23 1 S +R BG 1980 o - S 29 1 0 - +R BG 1981 o - S 27 2 0 - +R CZ 1945 o - Ap M>=1 2s 1 S +R CZ 1945 o - O 1 2s 0 - +R CZ 1946 o - May 6 2s 1 S +R CZ 1946 1949 - O Su>=1 2s 0 - +R CZ 1947 1948 - Ap Su>=15 2s 1 S +R CZ 1949 o - Ap 9 2s 1 S +R Th 1991 1992 - Mar lastSu 2 1 D +R Th 1991 1992 - S lastSu 2 0 S +R Th 1993 2006 - Ap Su>=1 2 1 D +R Th 1993 2006 - O lastSu 2 0 S +R Th 2007 ma - Mar Su>=8 2 1 D +R Th 2007 ma - N Su>=1 2 0 S +R FI 1942 o - Ap 2 24 1 S +R FI 1942 o - O 4 1 0 - +R FI 1981 1982 - Mar lastSu 2 1 S +R FI 1981 1982 - S lastSu 3 0 - +R F 1916 o - Jun 14 23s 1 S +R F 1916 1919 - O Su>=1 23s 0 - +R F 1917 o - Mar 24 23s 1 S +R F 1918 o - Mar 9 23s 1 S +R F 1919 o - Mar 1 23s 1 S +R F 1920 o - F 14 23s 1 S +R F 1920 o - O 23 23s 0 - +R F 1921 o - Mar 14 23s 1 S +R F 1921 o - O 25 23s 0 - +R F 1922 o - Mar 25 23s 1 S +R F 1922 1938 - O Sa>=1 23s 0 - +R F 1923 o - May 26 23s 1 S +R F 1924 o - Mar 29 23s 1 S +R F 1925 o - Ap 4 23s 1 S +R F 1926 o - Ap 17 23s 1 S +R F 1927 o - Ap 9 23s 1 S +R F 1928 o - Ap 14 23s 1 S +R F 1929 o - Ap 20 23s 1 S +R F 1930 o - Ap 12 23s 1 S +R F 1931 o - Ap 18 23s 1 S +R F 1932 o - Ap 2 23s 1 S +R F 1933 o - Mar 25 23s 1 S +R F 1934 o - Ap 7 23s 1 S +R F 1935 o - Mar 30 23s 1 S +R F 1936 o - Ap 18 23s 1 S +R F 1937 o - Ap 3 23s 1 S +R F 1938 o - Mar 26 23s 1 S +R F 1939 o - Ap 15 23s 1 S +R F 1939 o - N 18 23s 0 - +R F 1940 o - F 25 2 1 S +R F 1941 o - May 5 0 2 M +R F 1941 o - O 6 0 1 S +R F 1942 o - Mar 9 0 2 M +R F 1942 o - N 2 3 1 S +R F 1943 o - Mar 29 2 2 M +R F 1943 o - O 4 3 1 S +R F 1944 o - Ap 3 2 2 M +R F 1944 o - O 8 1 1 S +R F 1945 o - Ap 2 2 2 M +R F 1945 o - S 16 3 0 - +R F 1976 o - Mar 28 1 1 S +R F 1976 o - S 26 1 0 - +R DE 1946 o - Ap 14 2s 1 S +R DE 1946 o - O 7 2s 0 - +R DE 1947 1949 - O Su>=1 2s 0 - +R DE 1947 o - Ap 6 3s 1 S +R DE 1947 o - May 11 2s 2 M +R DE 1947 o - Jun 29 3 1 S +R DE 1948 o - Ap 18 2s 1 S +R DE 1949 o - Ap 10 2s 1 S +R So 1945 o - May 24 2 2 M +R So 1945 o - S 24 3 1 S +R So 1945 o - N 18 2s 0 - +R g 1932 o - Jul 7 0 1 S +R g 1932 o - S 1 0 0 - +R g 1941 o - Ap 7 0 1 S +R g 1942 o - N 2 3 0 - +R g 1943 o - Mar 30 0 1 S +R g 1943 o - O 4 0 0 - +R g 1952 o - Jul 1 0 1 S +R g 1952 o - N 2 0 0 - +R g 1975 o - Ap 12 0s 1 S +R g 1975 o - N 26 0s 0 - +R g 1976 o - Ap 11 2s 1 S +R g 1976 o - O 10 2s 0 - +R g 1977 1978 - Ap Su>=1 2s 1 S +R g 1977 o - S 26 2s 0 - +R g 1978 o - S 24 4 0 - +R g 1979 o - Ap 1 9 1 S +R g 1979 o - S 29 2 0 - +R g 1980 o - Ap 1 0 1 S +R g 1980 o - S 28 0 0 - +R h 1918 1919 - Ap 15 2 1 S +R h 1918 1920 - S M>=15 3 0 - +R h 1920 o - Ap 5 2 1 S +R h 1945 o - May 1 23 1 S +R h 1945 o - N 1 1 0 - +R h 1946 o - Mar 31 2s 1 S +R h 1946 o - O 7 2 0 - +R h 1947 1949 - Ap Su>=4 2s 1 S +R h 1947 1949 - O Su>=1 2s 0 - +R h 1954 o - May 23 0 1 S +R h 1954 o - O 3 0 0 - +R h 1955 o - May 22 2 1 S +R h 1955 o - O 2 3 0 - +R h 1956 1957 - Jun Su>=1 2 1 S +R h 1956 1957 - S lastSu 3 0 - +R h 1980 o - Ap 6 0 1 S +R h 1980 o - S 28 1 0 - +R h 1981 1983 - Mar lastSu 0 1 S +R h 1981 1983 - S lastSu 1 0 - +R I 1916 o - Jun 3 24 1 S +R I 1916 1917 - S 30 24 0 - +R I 1917 o - Mar 31 24 1 S +R I 1918 o - Mar 9 24 1 S +R I 1918 o - O 6 24 0 - +R I 1919 o - Mar 1 24 1 S +R I 1919 o - O 4 24 0 - +R I 1920 o - Mar 20 24 1 S +R I 1920 o - S 18 24 0 - +R I 1940 o - Jun 14 24 1 S +R I 1942 o - N 2 2s 0 - +R I 1943 o - Mar 29 2s 1 S +R I 1943 o - O 4 2s 0 - +R I 1944 o - Ap 2 2s 1 S +R I 1944 o - S 17 2s 0 - +R I 1945 o - Ap 2 2 1 S +R I 1945 o - S 15 1 0 - +R I 1946 o - Mar 17 2s 1 S +R I 1946 o - O 6 2s 0 - +R I 1947 o - Mar 16 0s 1 S +R I 1947 o - O 5 0s 0 - +R I 1948 o - F 29 2s 1 S +R I 1948 o - O 3 2s 0 - +R I 1966 1968 - May Su>=22 0s 1 S +R I 1966 o - S 24 24 0 - +R I 1967 1969 - S Su>=22 0s 0 - +R I 1969 o - Jun 1 0s 1 S +R I 1970 o - May 31 0s 1 S +R I 1970 o - S lastSu 0s 0 - +R I 1971 1972 - May Su>=22 0s 1 S +R I 1971 o - S lastSu 0s 0 - +R I 1972 o - O 1 0s 0 - +R I 1973 o - Jun 3 0s 1 S +R I 1973 1974 - S lastSu 0s 0 - +R I 1974 o - May 26 0s 1 S +R I 1975 o - Jun 1 0s 1 S +R I 1975 1977 - S lastSu 0s 0 - +R I 1976 o - May 30 0s 1 S +R I 1977 1979 - May Su>=22 0s 1 S +R I 1978 o - O 1 0s 0 - +R I 1979 o - S 30 0s 0 - +R LV 1989 1996 - Mar lastSu 2s 1 S +R LV 1989 1996 - S lastSu 2s 0 - +R MT 1973 o - Mar 31 0s 1 S +R MT 1973 o - S 29 0s 0 - +R MT 1974 o - Ap 21 0s 1 S +R MT 1974 o - S 16 0s 0 - +R MT 1975 1979 - Ap Su>=15 2 1 S +R MT 1975 1980 - S Su>=15 2 0 - +R MT 1980 o - Mar 31 2 1 S +R MD 1997 2021 - Mar lastSu 2 1 S +R MD 1997 2021 - O lastSu 3 0 - +R O 1918 1919 - S 16 2s 0 - +R O 1919 o - Ap 15 2s 1 S +R O 1944 o - Ap 3 2s 1 S +R O 1944 o - O 4 2 0 - +R O 1945 o - Ap 29 0 1 S +R O 1945 o - N 1 0 0 - +R O 1946 o - Ap 14 0s 1 S +R O 1946 o - O 7 2s 0 - +R O 1947 o - May 4 2s 1 S +R O 1947 1949 - O Su>=1 2s 0 - +R O 1948 o - Ap 18 2s 1 S +R O 1949 o - Ap 10 2s 1 S +R O 1957 o - Jun 2 1s 1 S +R O 1957 1958 - S lastSu 1s 0 - +R O 1958 o - Mar 30 1s 1 S +R O 1959 o - May 31 1s 1 S +R O 1959 1961 - O Su>=1 1s 0 - +R O 1960 o - Ap 3 1s 1 S +R O 1961 1964 - May lastSu 1s 1 S +R O 1962 1964 - S lastSu 1s 0 - +R p 1916 o - Jun 17 23 1 S +R p 1916 o - N 1 1 0 - +R p 1917 1921 - Mar 1 0 1 S +R p 1917 1921 - O 14 24 0 - +R p 1924 o - Ap 16 23s 1 S +R p 1924 o - O 4 23s 0 - +R p 1926 o - Ap 17 23s 1 S +R p 1926 1929 - O Sa>=1 23s 0 - +R p 1927 o - Ap 9 23s 1 S +R p 1928 o - Ap 14 23s 1 S +R p 1929 o - Ap 20 23s 1 S +R p 1931 o - Ap 18 23s 1 S +R p 1931 1932 - O Sa>=1 23s 0 - +R p 1932 o - Ap 2 23s 1 S +R p 1934 o - Ap 7 23s 1 S +R p 1934 1938 - O Sa>=1 23s 0 - +R p 1935 o - Mar 30 23s 1 S +R p 1936 o - Ap 18 23s 1 S +R p 1937 o - Ap 3 23s 1 S +R p 1938 o - Mar 26 23s 1 S +R p 1939 o - Ap 15 23s 1 S +R p 1939 o - N 18 23s 0 - +R p 1940 o - F 24 23s 1 S +R p 1940 o - O 7 23s 0 - +R p 1941 o - Ap 5 23s 1 S +R p 1941 o - O 5 23s 0 - +R p 1942 1945 - Mar Sa>=8 23s 1 S +R p 1942 o - Ap 25 22s 2 M +R p 1942 o - Au 15 22s 1 S +R p 1942 1945 - O Sa>=24 23s 0 - +R p 1943 o - Ap 17 22s 2 M +R p 1943 1945 - Au Sa>=25 22s 1 S +R p 1944 1945 - Ap Sa>=21 22s 2 M +R p 1946 o - Ap Sa>=1 23s 1 S +R p 1946 o - O Sa>=1 23s 0 - +R p 1947 1966 - Ap Su>=1 2s 1 S +R p 1947 1965 - O Su>=1 2s 0 - +R p 1976 o - S lastSu 1 0 - +R p 1977 o - Mar lastSu 0s 1 S +R p 1977 o - S lastSu 0s 0 - +R p 1978 1980 - Ap Su>=1 1s 1 S +R p 1978 o - O 1 1s 0 - +R p 1979 1980 - S lastSu 1s 0 - +R p 1981 1986 - Mar lastSu 0s 1 S +R p 1981 1985 - S lastSu 0s 0 - +R z 1932 o - May 21 0s 1 S +R z 1932 1939 - O Su>=1 0s 0 - +R z 1933 1939 - Ap Su>=2 0s 1 S +R z 1979 o - May 27 0 1 S +R z 1979 o - S lastSu 0 0 - +R z 1980 o - Ap 5 23 1 S +R z 1980 o - S lastSu 1 0 - +R z 1991 1993 - Mar lastSu 0s 1 S +R z 1991 1993 - S lastSu 0s 0 - +R s 1918 o - Ap 15 23 1 S +R s 1918 1919 - O 6 24s 0 - +R s 1919 o - Ap 6 23 1 S +R s 1924 o - Ap 16 23 1 S +R s 1924 o - O 4 24s 0 - +R s 1926 o - Ap 17 23 1 S +R s 1926 1929 - O Sa>=1 24s 0 - +R s 1927 o - Ap 9 23 1 S +R s 1928 o - Ap 15 0 1 S +R s 1929 o - Ap 20 23 1 S +R s 1937 o - Jun 16 23 1 S +R s 1937 o - O 2 24s 0 - +R s 1938 o - Ap 2 23 1 S +R s 1938 o - Ap 30 23 2 M +R s 1938 o - O 2 24 1 S +R s 1939 o - O 7 24s 0 - +R s 1942 o - May 2 23 1 S +R s 1942 o - S 1 1 0 - +R s 1943 1946 - Ap Sa>=13 23 1 S +R s 1943 1944 - O Su>=1 1 0 - +R s 1945 1946 - S lastSu 1 0 - +R s 1949 o - Ap 30 23 1 S +R s 1949 o - O 2 1 0 - +R s 1974 1975 - Ap Sa>=12 23 1 S +R s 1974 1975 - O Su>=1 1 0 - +R s 1976 o - Mar 27 23 1 S +R s 1976 1977 - S lastSu 1 0 - +R s 1977 o - Ap 2 23 1 S +R s 1978 o - Ap 2 2s 1 S +R s 1978 o - O 1 2s 0 - +R Sp 1967 o - Jun 3 12 1 S +R Sp 1967 o - O 1 0 0 - +R Sp 1974 o - Jun 24 0 1 S +R Sp 1974 o - S 1 0 0 - +R Sp 1976 1977 - May 1 0 1 S +R Sp 1976 o - Au 1 0 0 - +R Sp 1977 o - S 28 0 0 - +R Sp 1978 o - Jun 1 0 1 S +R Sp 1978 o - Au 4 0 0 - +R CH 1941 1942 - May M>=1 1 1 S +R CH 1941 1942 - O M>=1 2 0 - +R T 1916 o - May 1 0 1 S +R T 1916 o - O 1 0 0 - +R T 1920 o - Mar 28 0 1 S +R T 1920 o - O 25 0 0 - +R T 1921 o - Ap 3 0 1 S +R T 1921 o - O 3 0 0 - +R T 1922 o - Mar 26 0 1 S +R T 1922 o - O 8 0 0 - +R T 1924 o - May 13 0 1 S +R T 1924 1925 - O 1 0 0 - +R T 1925 o - May 1 0 1 S +R T 1940 o - Jul 1 0 1 S +R T 1940 o - O 6 0 0 - +R T 1940 o - D 1 0 1 S +R T 1941 o - S 21 0 0 - +R T 1942 o - Ap 1 0 1 S +R T 1945 o - O 8 0 0 - +R T 1946 o - Jun 1 0 1 S +R T 1946 o - O 1 0 0 - +R T 1947 1948 - Ap Su>=16 0 1 S +R T 1947 1951 - O Su>=2 0 0 - +R T 1949 o - Ap 10 0 1 S +R T 1950 o - Ap 16 0 1 S +R T 1951 o - Ap 22 0 1 S +R T 1962 o - Jul 15 0 1 S +R T 1963 o - O 30 0 0 - +R T 1964 o - May 15 0 1 S +R T 1964 o - O 1 0 0 - +R T 1973 o - Jun 3 1 1 S +R T 1973 1976 - O Su>=31 2 0 - +R T 1974 o - Mar 31 2 1 S +R T 1975 o - Mar 22 2 1 S +R T 1976 o - Mar 21 2 1 S +R T 1977 1978 - Ap Su>=1 2 1 S +R T 1977 1978 - O Su>=15 2 0 - +R T 1978 o - Jun 29 0 0 - +R T 1983 o - Jul 31 2 1 S +R T 1983 o - O 2 2 0 - +R T 1985 o - Ap 20 1s 1 S +R T 1985 o - S 28 1s 0 - +R T 1986 1993 - Mar lastSu 1s 1 S +R T 1986 1995 - S lastSu 1s 0 - +R T 1994 o - Mar 20 1s 1 S +R T 1995 2006 - Mar lastSu 1s 1 S +R T 1996 2006 - O lastSu 1s 0 - +R u 1918 1919 - Mar lastSu 2 1 D +R u 1918 1919 - O lastSu 2 0 S +R u 1942 o - F 9 2 1 W +R u 1945 o - Au 14 23u 1 P +R u 1945 o - S 30 2 0 S +R u 1967 2006 - O lastSu 2 0 S +R u 1967 1973 - Ap lastSu 2 1 D +R u 1974 o - Ja 6 2 1 D +R u 1975 o - F lastSu 2 1 D +R u 1976 1986 - Ap lastSu 2 1 D +R u 1987 2006 - Ap Su>=1 2 1 D +R u 2007 ma - Mar Su>=8 2 1 D +R u 2007 ma - N Su>=1 2 0 S +R NY 1920 o - Mar lastSu 2 1 D +R NY 1920 o - O lastSu 2 0 S +R NY 1921 1966 - Ap lastSu 2 1 D +R NY 1921 1954 - S lastSu 2 0 S +R NY 1955 1966 - O lastSu 2 0 S +R Ch 1920 o - Jun 13 2 1 D +R Ch 1920 1921 - O lastSu 2 0 S +R Ch 1921 o - Mar lastSu 2 1 D +R Ch 1922 1966 - Ap lastSu 2 1 D +R Ch 1922 1954 - S lastSu 2 0 S +R Ch 1955 1966 - O lastSu 2 0 S +R De 1920 1921 - Mar lastSu 2 1 D +R De 1920 o - O lastSu 2 0 S +R De 1921 o - May 22 2 0 S +R De 1965 1966 - Ap lastSu 2 1 D +R De 1965 1966 - O lastSu 2 0 S +R CA 1948 o - Mar 14 2:1 1 D +R CA 1949 o - Ja 1 2 0 S +R CA 1950 1966 - Ap lastSu 1 1 D +R CA 1950 1961 - S lastSu 2 0 S +R CA 1962 1966 - O lastSu 2 0 S +R In 1941 o - Jun 22 2 1 D +R In 1941 1954 - S lastSu 2 0 S +R In 1946 1954 - Ap lastSu 2 1 D +R Ma 1951 o - Ap lastSu 2 1 D +R Ma 1951 o - S lastSu 2 0 S +R Ma 1954 1960 - Ap lastSu 2 1 D +R Ma 1954 1960 - S lastSu 2 0 S +R V 1946 o - Ap lastSu 2 1 D +R V 1946 o - S lastSu 2 0 S +R V 1953 1954 - Ap lastSu 2 1 D +R V 1953 1959 - S lastSu 2 0 S +R V 1955 o - May 1 0 1 D +R V 1956 1963 - Ap lastSu 2 1 D +R V 1960 o - O lastSu 2 0 S +R V 1961 o - S lastSu 2 0 S +R V 1962 1963 - O lastSu 2 0 S +R Pe 1955 o - May 1 0 1 D +R Pe 1955 1960 - S lastSu 2 0 S +R Pe 1956 1963 - Ap lastSu 2 1 D +R Pe 1961 1963 - O lastSu 2 0 S +R Pi 1955 o - May 1 0 1 D +R Pi 1955 1960 - S lastSu 2 0 S +R Pi 1956 1964 - Ap lastSu 2 1 D +R Pi 1961 1964 - O lastSu 2 0 S +R St 1947 1961 - Ap lastSu 2 1 D +R St 1947 1954 - S lastSu 2 0 S +R St 1955 1956 - O lastSu 2 0 S +R St 1957 1958 - S lastSu 2 0 S +R St 1959 1961 - O lastSu 2 0 S +R Pu 1946 1960 - Ap lastSu 2 1 D +R Pu 1946 1954 - S lastSu 2 0 S +R Pu 1955 1956 - O lastSu 2 0 S +R Pu 1957 1960 - S lastSu 2 0 S +R v 1921 o - May 1 2 1 D +R v 1921 o - S 1 2 0 S +R v 1941 o - Ap lastSu 2 1 D +R v 1941 o - S lastSu 2 0 S +R v 1946 o - Ap lastSu 0:1 1 D +R v 1946 o - Jun 2 2 0 S +R v 1950 1961 - Ap lastSu 2 1 D +R v 1950 1955 - S lastSu 2 0 S +R v 1956 1961 - O lastSu 2 0 S +R Dt 1948 o - Ap lastSu 2 1 D +R Dt 1948 o - S lastSu 2 0 S +R Me 1946 o - Ap lastSu 2 1 D +R Me 1946 o - S lastSu 2 0 S +R Me 1966 o - Ap lastSu 2 1 D +R Me 1966 o - O lastSu 2 0 S +R C 1918 o - Ap 14 2 1 D +R C 1918 o - O 27 2 0 S +R C 1942 o - F 9 2 1 W +R C 1945 o - Au 14 23u 1 P +R C 1945 o - S 30 2 0 S +R C 1974 1986 - Ap lastSu 2 1 D +R C 1974 2006 - O lastSu 2 0 S +R C 1987 2006 - Ap Su>=1 2 1 D +R C 2007 ma - Mar Su>=8 2 1 D +R C 2007 ma - N Su>=1 2 0 S +R j 1917 o - Ap 8 2 1 D +R j 1917 o - S 17 2 0 S +R j 1919 o - May 5 23 1 D +R j 1919 o - Au 12 23 0 S +R j 1920 1935 - May Su>=1 23 1 D +R j 1920 1935 - O lastSu 23 0 S +R j 1936 1941 - May M>=9 0 1 D +R j 1936 1941 - O M>=2 0 0 S +R j 1946 1950 - May Su>=8 2 1 D +R j 1946 1950 - O Su>=2 2 0 S +R j 1951 1986 - Ap lastSu 2 1 D +R j 1951 1959 - S lastSu 2 0 S +R j 1960 1986 - O lastSu 2 0 S +R j 1987 o - Ap Su>=1 0:1 1 D +R j 1987 2006 - O lastSu 0:1 0 S +R j 1988 o - Ap Su>=1 0:1 2 DD +R j 1989 2006 - Ap Su>=1 0:1 1 D +R j 2007 2011 - Mar Su>=8 0:1 1 D +R j 2007 2010 - N Su>=1 0:1 0 S +R H 1916 o - Ap 1 0 1 D +R H 1916 o - O 1 0 0 S +R H 1920 o - May 9 0 1 D +R H 1920 o - Au 29 0 0 S +R H 1921 o - May 6 0 1 D +R H 1921 1922 - S 5 0 0 S +R H 1922 o - Ap 30 0 1 D +R H 1923 1925 - May Su>=1 0 1 D +R H 1923 o - S 4 0 0 S +R H 1924 o - S 15 0 0 S +R H 1925 o - S 28 0 0 S +R H 1926 o - May 16 0 1 D +R H 1926 o - S 13 0 0 S +R H 1927 o - May 1 0 1 D +R H 1927 o - S 26 0 0 S +R H 1928 1931 - May Su>=8 0 1 D +R H 1928 o - S 9 0 0 S +R H 1929 o - S 3 0 0 S +R H 1930 o - S 15 0 0 S +R H 1931 1932 - S M>=24 0 0 S +R H 1932 o - May 1 0 1 D +R H 1933 o - Ap 30 0 1 D +R H 1933 o - O 2 0 0 S +R H 1934 o - May 20 0 1 D +R H 1934 o - S 16 0 0 S +R H 1935 o - Jun 2 0 1 D +R H 1935 o - S 30 0 0 S +R H 1936 o - Jun 1 0 1 D +R H 1936 o - S 14 0 0 S +R H 1937 1938 - May Su>=1 0 1 D +R H 1937 1941 - S M>=24 0 0 S +R H 1939 o - May 28 0 1 D +R H 1940 1941 - May Su>=1 0 1 D +R H 1946 1949 - Ap lastSu 2 1 D +R H 1946 1949 - S lastSu 2 0 S +R H 1951 1954 - Ap lastSu 2 1 D +R H 1951 1954 - S lastSu 2 0 S +R H 1956 1959 - Ap lastSu 2 1 D +R H 1956 1959 - S lastSu 2 0 S +R H 1962 1973 - Ap lastSu 2 1 D +R H 1962 1973 - O lastSu 2 0 S +R o 1933 1935 - Jun Su>=8 1 1 D +R o 1933 1935 - S Su>=8 1 0 S +R o 1936 1938 - Jun Su>=1 1 1 D +R o 1936 1938 - S Su>=1 1 0 S +R o 1939 o - May 27 1 1 D +R o 1939 1941 - S Sa>=21 1 0 S +R o 1940 o - May 19 1 1 D +R o 1941 o - May 4 1 1 D +R o 1946 1972 - Ap lastSu 2 1 D +R o 1946 1956 - S lastSu 2 0 S +R o 1957 1972 - O lastSu 2 0 S +R o 1993 2006 - Ap Su>=1 0:1 1 D +R o 1993 2006 - O lastSu 0:1 0 S +R t 1919 o - Mar 30 23:30 1 D +R t 1919 o - O 26 0 0 S +R t 1920 o - May 2 2 1 D +R t 1920 o - S 26 0 0 S +R t 1921 o - May 15 2 1 D +R t 1921 o - S 15 2 0 S +R t 1922 1923 - May Su>=8 2 1 D +R t 1922 1926 - S Su>=15 2 0 S +R t 1924 1927 - May Su>=1 2 1 D +R t 1927 1937 - S Su>=25 2 0 S +R t 1928 1937 - Ap Su>=25 2 1 D +R t 1938 1940 - Ap lastSu 2 1 D +R t 1938 1939 - S lastSu 2 0 S +R t 1945 1948 - S lastSu 2 0 S +R t 1946 1973 - Ap lastSu 2 1 D +R t 1949 1950 - N lastSu 2 0 S +R t 1951 1956 - S lastSu 2 0 S +R t 1957 1973 - O lastSu 2 0 S +R W 1916 o - Ap 23 0 1 D +R W 1916 o - S 17 0 0 S +R W 1918 o - Ap 14 2 1 D +R W 1918 o - O 27 2 0 S +R W 1937 o - May 16 2 1 D +R W 1937 o - S 26 2 0 S +R W 1942 o - F 9 2 1 W +R W 1945 o - Au 14 23u 1 P +R W 1945 o - S lastSu 2 0 S +R W 1946 o - May 12 2 1 D +R W 1946 o - O 13 2 0 S +R W 1947 1949 - Ap lastSu 2 1 D +R W 1947 1949 - S lastSu 2 0 S +R W 1950 o - May 1 2 1 D +R W 1950 o - S 30 2 0 S +R W 1951 1960 - Ap lastSu 2 1 D +R W 1951 1958 - S lastSu 2 0 S +R W 1959 o - O lastSu 2 0 S +R W 1960 o - S lastSu 2 0 S +R W 1963 o - Ap lastSu 2 1 D +R W 1963 o - S 22 2 0 S +R W 1966 1986 - Ap lastSu 2s 1 D +R W 1966 2005 - O lastSu 2s 0 S +R W 1987 2005 - Ap Su>=1 2s 1 D +R r 1918 o - Ap 14 2 1 D +R r 1918 o - O 27 2 0 S +R r 1930 1934 - May Su>=1 0 1 D +R r 1930 1934 - O Su>=1 0 0 S +R r 1937 1941 - Ap Su>=8 0 1 D +R r 1937 o - O Su>=8 0 0 S +R r 1938 o - O Su>=1 0 0 S +R r 1939 1941 - O Su>=8 0 0 S +R r 1942 o - F 9 2 1 W +R r 1945 o - Au 14 23u 1 P +R r 1945 o - S lastSu 2 0 S +R r 1946 o - Ap Su>=8 2 1 D +R r 1946 o - O Su>=8 2 0 S +R r 1947 1957 - Ap lastSu 2 1 D +R r 1947 1957 - S lastSu 2 0 S +R r 1959 o - Ap lastSu 2 1 D +R r 1959 o - O lastSu 2 0 S +R Sw 1957 o - Ap lastSu 2 1 D +R Sw 1957 o - O lastSu 2 0 S +R Sw 1959 1961 - Ap lastSu 2 1 D +R Sw 1959 o - O lastSu 2 0 S +R Sw 1960 1961 - S lastSu 2 0 S +R Ed 1918 1919 - Ap Su>=8 2 1 D +R Ed 1918 o - O 27 2 0 S +R Ed 1919 o - May 27 2 0 S +R Ed 1920 1923 - Ap lastSu 2 1 D +R Ed 1920 o - O lastSu 2 0 S +R Ed 1921 1923 - S lastSu 2 0 S +R Ed 1942 o - F 9 2 1 W +R Ed 1945 o - Au 14 23u 1 P +R Ed 1945 o - S lastSu 2 0 S +R Ed 1947 o - Ap lastSu 2 1 D +R Ed 1947 o - S lastSu 2 0 S +R Ed 1972 1986 - Ap lastSu 2 1 D +R Ed 1972 2006 - O lastSu 2 0 S +R Va 1918 o - Ap 14 2 1 D +R Va 1918 o - O 27 2 0 S +R Va 1942 o - F 9 2 1 W +R Va 1945 o - Au 14 23u 1 P +R Va 1945 o - S 30 2 0 S +R Va 1946 1986 - Ap lastSu 2 1 D +R Va 1946 o - S 29 2 0 S +R Va 1947 1961 - S lastSu 2 0 S +R Va 1962 2006 - O lastSu 2 0 S +R Y 1918 o - Ap 14 2 1 D +R Y 1918 o - O 27 2 0 S +R Y 1919 o - May 25 2 1 D +R Y 1919 o - N 1 0 0 S +R Y 1942 o - F 9 2 1 W +R Y 1945 o - Au 14 23u 1 P +R Y 1945 o - S 30 2 0 S +R Y 1972 1986 - Ap lastSu 2 1 D +R Y 1972 2006 - O lastSu 2 0 S +R Y 1987 2006 - Ap Su>=1 2 1 D +R Yu 1965 o - Ap lastSu 0 2 DD +R Yu 1965 o - O lastSu 2 0 S +R m 1931 o - Ap 30 0 1 D +R m 1931 o - O 1 0 0 S +R m 1939 o - F 5 0 1 D +R m 1939 o - Jun 25 0 0 S +R m 1940 o - D 9 0 1 D +R m 1941 o - Ap 1 0 0 S +R m 1943 o - D 16 0 1 W +R m 1944 o - May 1 0 0 S +R m 1950 o - F 12 0 1 D +R m 1950 o - Jul 30 0 0 S +R m 1996 2000 - Ap Su>=1 2 1 D +R m 1996 2000 - O lastSu 2 0 S +R m 2001 o - May Su>=1 2 1 D +R m 2001 o - S lastSu 2 0 S +R m 2002 2022 - Ap Su>=1 2 1 D +R m 2002 2022 - O lastSu 2 0 S +R BB 1942 o - Ap 19 5u 1 D +R BB 1942 o - Au 31 6u 0 S +R BB 1943 o - May 2 5u 1 D +R BB 1943 o - S 5 6u 0 S +R BB 1944 o - Ap 10 5u 0:30 - +R BB 1944 o - S 10 6u 0 S +R BB 1977 o - Jun 12 2 1 D +R BB 1977 1978 - O Su>=1 2 0 S +R BB 1978 1980 - Ap Su>=15 2 1 D +R BB 1979 o - S 30 2 0 S +R BB 1980 o - S 25 2 0 S +R BZ 1918 1941 - O Sa>=1 24 0:30 -0530 +R BZ 1919 1942 - F Sa>=8 24 0 CST +R BZ 1942 o - Jun 27 24 1 CWT +R BZ 1945 o - Au 14 23u 1 CPT +R BZ 1945 o - D 15 24 0 CST +R BZ 1947 1967 - O Sa>=1 24 0:30 -0530 +R BZ 1948 1968 - F Sa>=8 24 0 CST +R BZ 1973 o - D 5 0 1 CDT +R BZ 1974 o - F 9 0 0 CST +R BZ 1982 o - D 18 0 1 CDT +R BZ 1983 o - F 12 0 0 CST +R Be 1917 o - Ap 5 24 1 - +R Be 1917 o - S 30 24 0 - +R Be 1918 o - Ap 13 24 1 - +R Be 1918 o - S 15 24 0 S +R Be 1942 o - Ja 11 2 1 D +R Be 1942 o - O 18 2 0 S +R Be 1943 o - Mar 21 2 1 D +R Be 1943 o - O 31 2 0 S +R Be 1944 1945 - Mar Su>=8 2 1 D +R Be 1944 1945 - N Su>=1 2 0 S +R Be 1947 o - May Su>=15 2 1 D +R Be 1947 o - S Su>=8 2 0 S +R Be 1948 1952 - May Su>=22 2 1 D +R Be 1948 1952 - S Su>=1 2 0 S +R Be 1956 o - May Su>=22 2 1 D +R Be 1956 o - O lastSu 2 0 S +R CR 1979 1980 - F lastSu 0 1 D +R CR 1979 1980 - Jun Su>=1 0 0 S +R CR 1991 1992 - Ja Sa>=15 0 1 D +R CR 1991 o - Jul 1 0 0 S +R CR 1992 o - Mar 15 0 0 S +R Q 1928 o - Jun 10 0 1 D +R Q 1928 o - O 10 0 0 S +R Q 1940 1942 - Jun Su>=1 0 1 D +R Q 1940 1942 - S Su>=1 0 0 S +R Q 1945 1946 - Jun Su>=1 0 1 D +R Q 1945 1946 - S Su>=1 0 0 S +R Q 1965 o - Jun 1 0 1 D +R Q 1965 o - S 30 0 0 S +R Q 1966 o - May 29 0 1 D +R Q 1966 o - O 2 0 0 S +R Q 1967 o - Ap 8 0 1 D +R Q 1967 1968 - S Su>=8 0 0 S +R Q 1968 o - Ap 14 0 1 D +R Q 1969 1977 - Ap lastSu 0 1 D +R Q 1969 1971 - O lastSu 0 0 S +R Q 1972 1974 - O 8 0 0 S +R Q 1975 1977 - O lastSu 0 0 S +R Q 1978 o - May 7 0 1 D +R Q 1978 1990 - O Su>=8 0 0 S +R Q 1979 1980 - Mar Su>=15 0 1 D +R Q 1981 1985 - May Su>=5 0 1 D +R Q 1986 1989 - Mar Su>=14 0 1 D +R Q 1990 1997 - Ap Su>=1 0 1 D +R Q 1991 1995 - O Su>=8 0s 0 S +R Q 1996 o - O 6 0s 0 S +R Q 1997 o - O 12 0s 0 S +R Q 1998 1999 - Mar lastSu 0s 1 D +R Q 1998 2003 - O lastSu 0s 0 S +R Q 2000 2003 - Ap Su>=1 0s 1 D +R Q 2004 o - Mar lastSu 0s 1 D +R Q 2006 2010 - O lastSu 0s 0 S +R Q 2007 o - Mar Su>=8 0s 1 D +R Q 2008 o - Mar Su>=15 0s 1 D +R Q 2009 2010 - Mar Su>=8 0s 1 D +R Q 2011 o - Mar Su>=15 0s 1 D +R Q 2011 o - N 13 0s 0 S +R Q 2012 o - Ap 1 0s 1 D +R Q 2012 ma - N Su>=1 0s 0 S +R Q 2013 ma - Mar Su>=8 0s 1 D +R DO 1966 o - O 30 0 1 EDT +R DO 1967 o - F 28 0 0 EST +R DO 1969 1973 - O lastSu 0 0:30 -0430 +R DO 1970 o - F 21 0 0 EST +R DO 1971 o - Ja 20 0 0 EST +R DO 1972 1974 - Ja 21 0 0 EST +R SV 1987 1988 - May Su>=1 0 1 D +R SV 1987 1988 - S lastSu 0 0 S +R GT 1973 o - N 25 0 1 D +R GT 1974 o - F 24 0 0 S +R GT 1983 o - May 21 0 1 D +R GT 1983 o - S 22 0 0 S +R GT 1991 o - Mar 23 0 1 D +R GT 1991 o - S 7 0 0 S +R GT 2006 o - Ap 30 0 1 D +R GT 2006 o - O 1 0 0 S +R HT 1983 o - May 8 0 1 D +R HT 1984 1987 - Ap lastSu 0 1 D +R HT 1983 1987 - O lastSu 0 0 S +R HT 1988 1997 - Ap Su>=1 1s 1 D +R HT 1988 1997 - O lastSu 1s 0 S +R HT 2005 2006 - Ap Su>=1 0 1 D +R HT 2005 2006 - O lastSu 0 0 S +R HT 2012 2015 - Mar Su>=8 2 1 D +R HT 2012 2015 - N Su>=1 2 0 S +R HT 2017 ma - Mar Su>=8 2 1 D +R HT 2017 ma - N Su>=1 2 0 S +R HN 1987 1988 - May Su>=1 0 1 D +R HN 1987 1988 - S lastSu 0 0 S +R HN 2006 o - May Su>=1 0 1 D +R HN 2006 o - Au M>=1 0 0 S +R NI 1979 1980 - Mar Su>=16 0 1 D +R NI 1979 1980 - Jun M>=23 0 0 S +R NI 2005 o - Ap 10 0 1 D +R NI 2005 o - O Su>=1 0 0 S +R NI 2006 o - Ap 30 2 1 D +R NI 2006 o - O Su>=1 1 0 S +R A 1930 o - D 1 0 1 - +R A 1931 o - Ap 1 0 0 - +R A 1931 o - O 15 0 1 - +R A 1932 1940 - Mar 1 0 0 - +R A 1932 1939 - N 1 0 1 - +R A 1940 o - Jul 1 0 1 - +R A 1941 o - Jun 15 0 0 - +R A 1941 o - O 15 0 1 - +R A 1943 o - Au 1 0 0 - +R A 1943 o - O 15 0 1 - +R A 1946 o - Mar 1 0 0 - +R A 1946 o - O 1 0 1 - +R A 1963 o - O 1 0 0 - +R A 1963 o - D 15 0 1 - +R A 1964 1966 - Mar 1 0 0 - +R A 1964 1966 - O 15 0 1 - +R A 1967 o - Ap 2 0 0 - +R A 1967 1968 - O Su>=1 0 1 - +R A 1968 1969 - Ap Su>=1 0 0 - +R A 1974 o - Ja 23 0 1 - +R A 1974 o - May 1 0 0 - +R A 1988 o - D 1 0 1 - +R A 1989 1993 - Mar Su>=1 0 0 - +R A 1989 1992 - O Su>=15 0 1 - +R A 1999 o - O Su>=1 0 1 - +R A 2000 o - Mar 3 0 0 - +R A 2007 o - D 30 0 1 - +R A 2008 2009 - Mar Su>=15 0 0 - +R A 2008 o - O Su>=15 0 1 - +R Sa 2008 2009 - Mar Su>=8 0 0 - +R Sa 2007 2008 - O Su>=8 0 1 - +R B 1931 o - O 3 11 1 - +R B 1932 1933 - Ap 1 0 0 - +R B 1932 o - O 3 0 1 - +R B 1949 1952 - D 1 0 1 - +R B 1950 o - Ap 16 1 0 - +R B 1951 1952 - Ap 1 0 0 - +R B 1953 o - Mar 1 0 0 - +R B 1963 o - D 9 0 1 - +R B 1964 o - Mar 1 0 0 - +R B 1965 o - Ja 31 0 1 - +R B 1965 o - Mar 31 0 0 - +R B 1965 o - D 1 0 1 - +R B 1966 1968 - Mar 1 0 0 - +R B 1966 1967 - N 1 0 1 - +R B 1985 o - N 2 0 1 - +R B 1986 o - Mar 15 0 0 - +R B 1986 o - O 25 0 1 - +R B 1987 o - F 14 0 0 - +R B 1987 o - O 25 0 1 - +R B 1988 o - F 7 0 0 - +R B 1988 o - O 16 0 1 - +R B 1989 o - Ja 29 0 0 - +R B 1989 o - O 15 0 1 - +R B 1990 o - F 11 0 0 - +R B 1990 o - O 21 0 1 - +R B 1991 o - F 17 0 0 - +R B 1991 o - O 20 0 1 - +R B 1992 o - F 9 0 0 - +R B 1992 o - O 25 0 1 - +R B 1993 o - Ja 31 0 0 - +R B 1993 1995 - O Su>=11 0 1 - +R B 1994 1995 - F Su>=15 0 0 - +R B 1996 o - F 11 0 0 - +R B 1996 o - O 6 0 1 - +R B 1997 o - F 16 0 0 - +R B 1997 o - O 6 0 1 - +R B 1998 o - Mar 1 0 0 - +R B 1998 o - O 11 0 1 - +R B 1999 o - F 21 0 0 - +R B 1999 o - O 3 0 1 - +R B 2000 o - F 27 0 0 - +R B 2000 2001 - O Su>=8 0 1 - +R B 2001 2006 - F Su>=15 0 0 - +R B 2002 o - N 3 0 1 - +R B 2003 o - O 19 0 1 - +R B 2004 o - N 2 0 1 - +R B 2005 o - O 16 0 1 - +R B 2006 o - N 5 0 1 - +R B 2007 o - F 25 0 0 - +R B 2007 o - O Su>=8 0 1 - +R B 2008 2017 - O Su>=15 0 1 - +R B 2008 2011 - F Su>=15 0 0 - +R B 2012 o - F Su>=22 0 0 - +R B 2013 2014 - F Su>=15 0 0 - +R B 2015 o - F Su>=22 0 0 - +R B 2016 2019 - F Su>=15 0 0 - +R B 2018 o - N Su>=1 0 1 - +R x 1927 1931 - S 1 0 1 - +R x 1928 1932 - Ap 1 0 0 - +R x 1968 o - N 3 4u 1 - +R x 1969 o - Mar 30 3u 0 - +R x 1969 o - N 23 4u 1 - +R x 1970 o - Mar 29 3u 0 - +R x 1971 o - Mar 14 3u 0 - +R x 1970 1972 - O Su>=9 4u 1 - +R x 1972 1986 - Mar Su>=9 3u 0 - +R x 1973 o - S 30 4u 1 - +R x 1974 1987 - O Su>=9 4u 1 - +R x 1987 o - Ap 12 3u 0 - +R x 1988 1990 - Mar Su>=9 3u 0 - +R x 1988 1989 - O Su>=9 4u 1 - +R x 1990 o - S 16 4u 1 - +R x 1991 1996 - Mar Su>=9 3u 0 - +R x 1991 1997 - O Su>=9 4u 1 - +R x 1997 o - Mar 30 3u 0 - +R x 1998 o - Mar Su>=9 3u 0 - +R x 1998 o - S 27 4u 1 - +R x 1999 o - Ap 4 3u 0 - +R x 1999 2010 - O Su>=9 4u 1 - +R x 2000 2007 - Mar Su>=9 3u 0 - +R x 2008 o - Mar 30 3u 0 - +R x 2009 o - Mar Su>=9 3u 0 - +R x 2010 o - Ap Su>=1 3u 0 - +R x 2011 o - May Su>=2 3u 0 - +R x 2011 o - Au Su>=16 4u 1 - +R x 2012 2014 - Ap Su>=23 3u 0 - +R x 2012 2014 - S Su>=2 4u 1 - +R x 2016 2018 - May Su>=9 3u 0 - +R x 2016 2018 - Au Su>=9 4u 1 - +R x 2019 ma - Ap Su>=2 3u 0 - +R x 2019 2021 - S Su>=2 4u 1 - +R x 2022 o - S Su>=9 4u 1 - +R x 2023 ma - S Su>=2 4u 1 - +R CO 1992 o - May 3 0 1 - +R CO 1993 o - F 6 24 0 - +R EC 1992 o - N 28 0 1 - +R EC 1993 o - F 5 0 0 - +R FK 1937 1938 - S lastSu 0 1 - +R FK 1938 1942 - Mar Su>=19 0 0 - +R FK 1939 o - O 1 0 1 - +R FK 1940 1942 - S lastSu 0 1 - +R FK 1943 o - Ja 1 0 0 - +R FK 1983 o - S lastSu 0 1 - +R FK 1984 1985 - Ap lastSu 0 0 - +R FK 1984 o - S 16 0 1 - +R FK 1985 2000 - S Su>=9 0 1 - +R FK 1986 2000 - Ap Su>=16 0 0 - +R FK 2001 2010 - Ap Su>=15 2 0 - +R FK 2001 2010 - S Su>=1 2 1 - +R y 1975 1988 - O 1 0 1 - +R y 1975 1978 - Mar 1 0 0 - +R y 1979 1991 - Ap 1 0 0 - +R y 1989 o - O 22 0 1 - +R y 1990 o - O 1 0 1 - +R y 1991 o - O 6 0 1 - +R y 1992 o - Mar 1 0 0 - +R y 1992 o - O 5 0 1 - +R y 1993 o - Mar 31 0 0 - +R y 1993 1995 - O 1 0 1 - +R y 1994 1995 - F lastSu 0 0 - +R y 1996 o - Mar 1 0 0 - +R y 1996 2001 - O Su>=1 0 1 - +R y 1997 o - F lastSu 0 0 - +R y 1998 2001 - Mar Su>=1 0 0 - +R y 2002 2004 - Ap Su>=1 0 0 - +R y 2002 2003 - S Su>=1 0 1 - +R y 2004 2009 - O Su>=15 0 1 - +R y 2005 2009 - Mar Su>=8 0 0 - +R y 2010 2024 - O Su>=1 0 1 - +R y 2010 2012 - Ap Su>=8 0 0 - +R y 2013 2024 - Mar Su>=22 0 0 - +R PE 1938 o - Ja 1 0 1 - +R PE 1938 o - Ap 1 0 0 - +R PE 1938 1939 - S lastSu 0 1 - +R PE 1939 1940 - Mar Su>=24 0 0 - +R PE 1986 1987 - Ja 1 0 1 - +R PE 1986 1987 - Ap 1 0 0 - +R PE 1990 o - Ja 1 0 1 - +R PE 1990 o - Ap 1 0 0 - +R PE 1994 o - Ja 1 0 1 - +R PE 1994 o - Ap 1 0 0 - +R U 1923 1925 - O 1 0 0:30 - +R U 1924 1926 - Ap 1 0 0 - +R U 1933 1938 - O lastSu 0 0:30 - +R U 1934 1941 - Mar lastSa 24 0 - +R U 1939 o - O 1 0 0:30 - +R U 1940 o - O 27 0 0:30 - +R U 1941 o - Au 1 0 0:30 - +R U 1942 o - D 14 0 0:30 - +R U 1943 o - Mar 14 0 0 - +R U 1959 o - May 24 0 0:30 - +R U 1959 o - N 15 0 0 - +R U 1960 o - Ja 17 0 1 - +R U 1960 o - Mar 6 0 0 - +R U 1965 o - Ap 4 0 1 - +R U 1965 o - S 26 0 0 - +R U 1968 o - May 27 0 0:30 - +R U 1968 o - D 1 0 0 - +R U 1970 o - Ap 25 0 1 - +R U 1970 o - Jun 14 0 0 - +R U 1972 o - Ap 23 0 1 - +R U 1972 o - Jul 16 0 0 - +R U 1974 o - Ja 13 0 1:30 - +R U 1974 o - Mar 10 0 0:30 - +R U 1974 o - S 1 0 0 - +R U 1974 o - D 22 0 1 - +R U 1975 o - Mar 30 0 0 - +R U 1976 o - D 19 0 1 - +R U 1977 o - Mar 6 0 0 - +R U 1977 o - D 4 0 1 - +R U 1978 1979 - Mar Su>=1 0 0 - +R U 1978 o - D 17 0 1 - +R U 1979 o - Ap 29 0 1 - +R U 1980 o - Mar 16 0 0 - +R U 1987 o - D 14 0 1 - +R U 1988 o - F 28 0 0 - +R U 1988 o - D 11 0 1 - +R U 1989 o - Mar 5 0 0 - +R U 1989 o - O 29 0 1 - +R U 1990 o - F 25 0 0 - +R U 1990 1991 - O Su>=21 0 1 - +R U 1991 1992 - Mar Su>=1 0 0 - +R U 1992 o - O 18 0 1 - +R U 1993 o - F 28 0 0 - +R U 2004 o - S 19 0 1 - +R U 2005 o - Mar 27 2 0 - +R U 2005 o - O 9 2 1 - +R U 2006 2015 - Mar Su>=8 2 0 - +R U 2006 2014 - O Su>=1 2 1 - +Z Africa/Abidjan -0:16:8 - LMT 1912 +0 - GMT +Z Africa/Algiers 0:12:12 - LMT 1891 Mar 16 +0:9:21 - PMT 1911 Mar 11 +0 d WE%sT 1940 F 25 2 +1 d CE%sT 1946 O 7 +0 - WET 1956 Ja 29 +1 - CET 1963 Ap 14 +0 d WE%sT 1977 O 21 +1 d CE%sT 1979 O 26 +0 d WE%sT 1981 May +1 - CET +Z Africa/Bissau -1:2:20 - LMT 1912 Ja 1 1u +-1 - %z 1975 +0 - GMT +Z Africa/Cairo 2:5:9 - LMT 1900 O +2 K EE%sT +Z Africa/Casablanca -0:30:20 - LMT 1913 O 26 +0 M %z 1984 Mar 16 +1 - %z 1986 +0 M %z 2018 O 28 3 +1 M %z +Z Africa/Ceuta -0:21:16 - LMT 1901 Ja 1 0u +0 - WET 1918 May 6 23 +0 1 WEST 1918 O 7 23 +0 - WET 1924 +0 s WE%sT 1929 +0 - WET 1967 +0 Sp WE%sT 1984 Mar 16 +1 - CET 1986 +1 E CE%sT +Z Africa/El_Aaiun -0:52:48 - LMT 1934 +-1 - %z 1976 Ap 14 +0 M %z 2018 O 28 3 +1 M %z +Z Africa/Johannesburg 1:52 - LMT 1892 F 8 +1:30 - SAST 1903 Mar +2 SA SAST +Z Africa/Juba 2:6:28 - LMT 1931 +2 SD CA%sT 2000 Ja 15 12 +3 - EAT 2021 F +2 - CAT +Z Africa/Khartoum 2:10:8 - LMT 1931 +2 SD CA%sT 2000 Ja 15 12 +3 - EAT 2017 N +2 - CAT +Z Africa/Lagos 0:13:35 - LMT 1905 Jul +0 - GMT 1908 Jul +0:13:35 - LMT 1914 +0:30 - %z 1919 S +1 - WAT +Z Africa/Maputo 2:10:18 - LMT 1909 +2 - CAT +Z Africa/Monrovia -0:43:8 - LMT 1882 +-0:43:8 - MMT 1919 Mar +-0:44:30 - MMT 1972 Ja 7 +0 - GMT +Z Africa/Nairobi 2:27:16 - LMT 1908 May +2:30 - %z 1928 Jun 30 24 +3 - EAT 1930 Ja 4 24 +2:30 - %z 1936 D 31 24 +2:45 - %z 1942 Jul 31 24 +3 - EAT +Z Africa/Ndjamena 1:0:12 - LMT 1912 +1 - WAT 1979 O 14 +1 1 WAST 1980 Mar 8 +1 - WAT +Z Africa/Sao_Tome 0:26:56 - LMT 1884 +-0:36:45 - LMT 1912 Ja 1 0u +0 - GMT 2018 Ja 1 1 +1 - WAT 2019 Ja 1 2 +0 - GMT +Z Africa/Tripoli 0:52:44 - LMT 1920 +1 L CE%sT 1959 +2 - EET 1982 +1 L CE%sT 1990 May 4 +2 - EET 1996 S 30 +1 L CE%sT 1997 O 4 +2 - EET 2012 N 10 2 +1 L CE%sT 2013 O 25 2 +2 - EET +Z Africa/Tunis 0:40:44 - LMT 1881 May 12 +0:9:21 - PMT 1911 Mar 11 +1 n CE%sT +Z Africa/Windhoek 1:8:24 - LMT 1892 F 8 +1:30 - %z 1903 Mar +2 - SAST 1942 S 20 2 +2 1 SAST 1943 Mar 21 2 +2 - SAST 1990 Mar 21 +2 NA %s +Z America/Adak 12:13:22 - LMT 1867 O 19 12:44:35 +-11:46:38 - LMT 1900 Au 20 12 +-11 - NST 1942 +-11 u N%sT 1946 +-11 - NST 1967 Ap +-11 - BST 1969 +-11 u B%sT 1983 O 30 2 +-10 u AH%sT 1983 N 30 +-10 u H%sT +Z America/Anchorage 14:0:24 - LMT 1867 O 19 14:31:37 +-9:59:36 - LMT 1900 Au 20 12 +-10 - AST 1942 +-10 u A%sT 1967 Ap +-10 - AHST 1969 +-10 u AH%sT 1983 O 30 2 +-9 u Y%sT 1983 N 30 +-9 u AK%sT +Z America/Araguaina -3:12:48 - LMT 1914 +-3 B %z 1990 S 17 +-3 - %z 1995 S 14 +-3 B %z 2003 S 24 +-3 - %z 2012 O 21 +-3 B %z 2013 S +-3 - %z +Z America/Argentina/Buenos_Aires -3:53:48 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 A %z +Z America/Argentina/Catamarca -4:23:8 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1991 Mar 3 +-4 - %z 1991 O 20 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 - %z 2004 Jun +-4 - %z 2004 Jun 20 +-3 A %z 2008 O 18 +-3 - %z +Z America/Argentina/Cordoba -4:16:48 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1991 Mar 3 +-4 - %z 1991 O 20 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 A %z +Z America/Argentina/Jujuy -4:21:12 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1990 Mar 4 +-4 - %z 1990 O 28 +-4 1 %z 1991 Mar 17 +-4 - %z 1991 O 6 +-3 1 %z 1992 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 A %z 2008 O 18 +-3 - %z +Z America/Argentina/La_Rioja -4:27:24 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1991 Mar +-4 - %z 1991 May 7 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 - %z 2004 Jun +-4 - %z 2004 Jun 20 +-3 A %z 2008 O 18 +-3 - %z +Z America/Argentina/Mendoza -4:35:16 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1990 Mar 4 +-4 - %z 1990 O 15 +-4 1 %z 1991 Mar +-4 - %z 1991 O 15 +-4 1 %z 1992 Mar +-4 - %z 1992 O 18 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 - %z 2004 May 23 +-4 - %z 2004 S 26 +-3 A %z 2008 O 18 +-3 - %z +Z America/Argentina/Rio_Gallegos -4:36:52 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 - %z 2004 Jun +-4 - %z 2004 Jun 20 +-3 A %z 2008 O 18 +-3 - %z +Z America/Argentina/Salta -4:21:40 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1991 Mar 3 +-4 - %z 1991 O 20 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 A %z 2008 O 18 +-3 - %z +Z America/Argentina/San_Juan -4:34:4 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1991 Mar +-4 - %z 1991 May 7 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 - %z 2004 May 31 +-4 - %z 2004 Jul 25 +-3 A %z 2008 O 18 +-3 - %z +Z America/Argentina/San_Luis -4:25:24 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1990 +-3 1 %z 1990 Mar 14 +-4 - %z 1990 O 15 +-4 1 %z 1991 Mar +-4 - %z 1991 Jun +-3 - %z 1999 O 3 +-4 1 %z 2000 Mar 3 +-3 - %z 2004 May 31 +-4 - %z 2004 Jul 25 +-3 A %z 2008 Ja 21 +-4 Sa %z 2009 O 11 +-3 - %z +Z America/Argentina/Tucuman -4:20:52 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1991 Mar 3 +-4 - %z 1991 O 20 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 - %z 2004 Jun +-4 - %z 2004 Jun 13 +-3 A %z +Z America/Argentina/Ushuaia -4:33:12 - LMT 1894 O 31 +-4:16:48 - CMT 1920 May +-4 - %z 1930 D +-4 A %z 1969 O 5 +-3 A %z 1999 O 3 +-4 A %z 2000 Mar 3 +-3 - %z 2004 May 30 +-4 - %z 2004 Jun 20 +-3 A %z 2008 O 18 +-3 - %z +Z America/Asuncion -3:50:40 - LMT 1890 +-3:50:40 - AMT 1931 O 10 +-4 - %z 1972 O +-3 - %z 1974 Ap +-4 y %z 2024 O 15 +-3 - %z +Z America/Bahia -2:34:4 - LMT 1914 +-3 B %z 2003 S 24 +-3 - %z 2011 O 16 +-3 B %z 2012 O 21 +-3 - %z +Z America/Bahia_Banderas -7:1 - LMT 1922 Ja 1 7u +-7 - MST 1927 Jun 10 +-6 - CST 1930 N 15 +-7 m M%sT 1932 Ap +-6 - CST 1942 Ap 24 +-7 - MST 1970 +-7 m M%sT 2010 Ap 4 2 +-6 m C%sT +Z America/Barbados -3:58:29 - LMT 1911 Au 28 +-4 BB A%sT 1944 +-4 BB AST/-0330 1945 +-4 BB A%sT +Z America/Belem -3:13:56 - LMT 1914 +-3 B %z 1988 S 12 +-3 - %z +Z America/Belize -5:52:48 - LMT 1912 Ap +-6 BZ %s +Z America/Boa_Vista -4:2:40 - LMT 1914 +-4 B %z 1988 S 12 +-4 - %z 1999 S 30 +-4 B %z 2000 O 15 +-4 - %z +Z America/Bogota -4:56:16 - LMT 1884 Mar 13 +-4:56:16 - BMT 1914 N 23 +-5 CO %z +Z America/Boise -7:44:49 - LMT 1883 N 18 20u +-8 u P%sT 1923 May 13 2 +-7 u M%sT 1974 +-7 - MST 1974 F 3 2 +-7 u M%sT +Z America/Cambridge_Bay 0 - -00 1920 +-7 Y M%sT 1999 O 31 2 +-6 C C%sT 2000 O 29 2 +-5 - EST 2000 N 5 +-6 - CST 2001 Ap 1 3 +-7 C M%sT +Z America/Campo_Grande -3:38:28 - LMT 1914 +-4 B %z +Z America/Cancun -5:47:4 - LMT 1922 Ja 1 6u +-6 - CST 1981 D 26 2 +-5 - EST 1983 Ja 4 +-6 m C%sT 1997 O 26 2 +-5 m E%sT 1998 Au 2 2 +-6 m C%sT 2015 F 1 2 +-5 - EST +Z America/Caracas -4:27:44 - LMT 1890 +-4:27:40 - CMT 1912 F 12 +-4:30 - %z 1965 +-4 - %z 2007 D 9 3 +-4:30 - %z 2016 May 1 2:30 +-4 - %z +Z America/Cayenne -3:29:20 - LMT 1911 Jul +-4 - %z 1967 O +-3 - %z +Z America/Chicago -5:50:36 - LMT 1883 N 18 18u +-6 u C%sT 1920 +-6 Ch C%sT 1936 Mar 1 2 +-5 - EST 1936 N 15 2 +-6 Ch C%sT 1942 +-6 u C%sT 1946 +-6 Ch C%sT 1967 +-6 u C%sT +Z America/Chihuahua -7:4:20 - LMT 1922 Ja 1 7u +-7 - MST 1927 Jun 10 +-6 - CST 1930 N 15 +-7 m M%sT 1932 Ap +-6 - CST 1996 +-6 m C%sT 1998 +-6 - CST 1998 Ap Su>=1 3 +-7 m M%sT 2022 O 30 2 +-6 - CST +Z America/Ciudad_Juarez -7:5:56 - LMT 1922 Ja 1 7u +-7 - MST 1927 Jun 10 +-6 - CST 1930 N 15 +-7 m M%sT 1932 Ap +-6 - CST 1996 +-6 m C%sT 1998 +-6 - CST 1998 Ap Su>=1 3 +-7 m M%sT 2010 +-7 u M%sT 2022 O 30 2 +-6 - CST 2022 N 30 +-7 u M%sT +Z America/Costa_Rica -5:36:13 - LMT 1890 +-5:36:13 - SJMT 1921 Ja 15 +-6 CR C%sT +Z America/Coyhaique -4:48:16 - LMT 1890 +-4:42:45 - SMT 1910 Ja 10 +-5 - %z 1916 Jul +-4:42:45 - SMT 1918 S 10 +-4 - %z 1919 Jul +-4:42:45 - SMT 1927 S +-5 x %z 1932 S +-4 - %z 1942 Jun +-5 - %z 1942 Au +-4 - %z 1946 Au 28 24 +-5 1 %z 1947 Mar 31 24 +-5 - %z 1947 May 21 23 +-4 x %z 2025 Mar 20 +-3 - %z +Z America/Cuiaba -3:44:20 - LMT 1914 +-4 B %z 2003 S 24 +-4 - %z 2004 O +-4 B %z +Z America/Danmarkshavn -1:14:40 - LMT 1916 Jul 28 +-3 - %z 1980 Ap 6 2 +-3 E %z 1996 +0 - GMT +Z America/Dawson -9:17:40 - LMT 1900 Au 20 +-9 Y Y%sT 1965 +-9 Yu Y%sT 1973 O 28 +-8 - PST 1980 +-8 C P%sT 2020 N +-7 - MST +Z America/Dawson_Creek -8:0:56 - LMT 1884 +-8 C P%sT 1947 +-8 Va P%sT 1972 Au 30 2 +-7 - MST +Z America/Denver -6:59:56 - LMT 1883 N 18 19u +-7 u M%sT 1920 +-7 De M%sT 1942 +-7 u M%sT 1946 +-7 De M%sT 1967 +-7 u M%sT +Z America/Detroit -5:32:11 - LMT 1905 +-6 - CST 1915 May 15 2 +-5 - EST 1942 +-5 u E%sT 1946 +-5 Dt E%sT 1967 Jun 14 0:1 +-5 u E%sT 1969 +-5 - EST 1973 +-5 u E%sT 1975 +-5 - EST 1975 Ap 27 2 +-5 u E%sT +Z America/Edmonton -7:33:52 - LMT 1906 S +-7 Ed M%sT 1987 +-7 C M%sT +Z America/Eirunepe -4:39:28 - LMT 1914 +-5 B %z 1988 S 12 +-5 - %z 1993 S 28 +-5 B %z 1994 S 22 +-5 - %z 2008 Jun 24 +-4 - %z 2013 N 10 +-5 - %z +Z America/El_Salvador -5:56:48 - LMT 1921 +-6 SV C%sT +Z America/Fort_Nelson -8:10:47 - LMT 1884 +-8 Va P%sT 1946 +-8 - PST 1947 +-8 Va P%sT 1987 +-8 C P%sT 2015 Mar 8 2 +-7 - MST +Z America/Fortaleza -2:34 - LMT 1914 +-3 B %z 1990 S 17 +-3 - %z 1999 S 30 +-3 B %z 2000 O 22 +-3 - %z 2001 S 13 +-3 B %z 2002 O +-3 - %z +Z America/Glace_Bay -3:59:48 - LMT 1902 Jun 15 +-4 C A%sT 1953 +-4 H A%sT 1954 +-4 - AST 1972 +-4 H A%sT 1974 +-4 C A%sT +Z America/Goose_Bay -4:1:40 - LMT 1884 +-3:30:52 - NST 1918 +-3:30:52 C N%sT 1919 +-3:30:52 - NST 1935 Mar 30 +-3:30 - NST 1936 +-3:30 j N%sT 1942 May 11 +-3:30 C N%sT 1946 +-3:30 j N%sT 1966 Mar 15 2 +-4 j A%sT 2011 N +-4 C A%sT +Z America/Grand_Turk -4:44:32 - LMT 1890 +-5:7:10 - KMT 1912 F +-5 - EST 1979 +-5 u E%sT 2015 Mar 8 2 +-4 - AST 2018 Mar 11 3 +-5 u E%sT +Z America/Guatemala -6:2:4 - LMT 1918 O 5 +-6 GT C%sT +Z America/Guayaquil -5:19:20 - LMT 1890 +-5:14 - QMT 1931 +-5 EC %z +Z America/Guyana -3:52:39 - LMT 1911 Au +-4 - %z 1915 Mar +-3:45 - %z 1975 Au +-3 - %z 1992 Mar 29 1 +-4 - %z +Z America/Halifax -4:14:24 - LMT 1902 Jun 15 +-4 H A%sT 1918 +-4 C A%sT 1919 +-4 H A%sT 1942 F 9 2s +-4 C A%sT 1946 +-4 H A%sT 1974 +-4 C A%sT +Z America/Havana -5:29:28 - LMT 1890 +-5:29:36 - HMT 1925 Jul 19 12 +-5 Q C%sT +Z America/Hermosillo -7:23:52 - LMT 1922 Ja 1 7u +-7 - MST 1927 Jun 10 +-6 - CST 1930 N 15 +-7 m M%sT 1932 Ap +-6 - CST 1942 Ap 24 +-7 - MST 1996 +-7 m M%sT 1999 +-7 - MST +Z America/Indiana/Indianapolis -5:44:38 - LMT 1883 N 18 18u +-6 u C%sT 1920 +-6 In C%sT 1942 +-6 u C%sT 1946 +-6 In C%sT 1955 Ap 24 2 +-5 - EST 1957 S 29 2 +-6 - CST 1958 Ap 27 2 +-5 - EST 1969 +-5 u E%sT 1971 +-5 - EST 2006 +-5 u E%sT +Z America/Indiana/Knox -5:46:30 - LMT 1883 N 18 18u +-6 u C%sT 1947 +-6 St C%sT 1962 Ap 29 2 +-5 - EST 1963 O 27 2 +-6 u C%sT 1991 O 27 2 +-5 - EST 2006 Ap 2 2 +-6 u C%sT +Z America/Indiana/Marengo -5:45:23 - LMT 1883 N 18 18u +-6 u C%sT 1951 +-6 Ma C%sT 1961 Ap 30 2 +-5 - EST 1969 +-5 u E%sT 1974 Ja 6 2 +-6 1 CDT 1974 O 27 2 +-5 u E%sT 1976 +-5 - EST 2006 +-5 u E%sT +Z America/Indiana/Petersburg -5:49:7 - LMT 1883 N 18 18u +-6 u C%sT 1955 +-6 Pi C%sT 1965 Ap 25 2 +-5 - EST 1966 O 30 2 +-6 u C%sT 1977 O 30 2 +-5 - EST 2006 Ap 2 2 +-6 u C%sT 2007 N 4 2 +-5 u E%sT +Z America/Indiana/Tell_City -5:47:3 - LMT 1883 N 18 18u +-6 u C%sT 1946 +-6 Pe C%sT 1964 Ap 26 2 +-5 - EST 1967 O 29 2 +-6 u C%sT 1969 Ap 27 2 +-5 u E%sT 1971 +-5 - EST 2006 Ap 2 2 +-6 u C%sT +Z America/Indiana/Vevay -5:40:16 - LMT 1883 N 18 18u +-6 u C%sT 1954 Ap 25 2 +-5 - EST 1969 +-5 u E%sT 1973 +-5 - EST 2006 +-5 u E%sT +Z America/Indiana/Vincennes -5:50:7 - LMT 1883 N 18 18u +-6 u C%sT 1946 +-6 V C%sT 1964 Ap 26 2 +-5 - EST 1969 +-5 u E%sT 1971 +-5 - EST 2006 Ap 2 2 +-6 u C%sT 2007 N 4 2 +-5 u E%sT +Z America/Indiana/Winamac -5:46:25 - LMT 1883 N 18 18u +-6 u C%sT 1946 +-6 Pu C%sT 1961 Ap 30 2 +-5 - EST 1969 +-5 u E%sT 1971 +-5 - EST 2006 Ap 2 2 +-6 u C%sT 2007 Mar 11 2 +-5 u E%sT +Z America/Inuvik 0 - -00 1953 +-8 Y P%sT 1979 Ap lastSu 2 +-7 Y M%sT 1980 +-7 C M%sT +Z America/Iqaluit 0 - -00 1942 Au +-5 Y E%sT 1999 O 31 2 +-6 C C%sT 2000 O 29 2 +-5 C E%sT +Z America/Jamaica -5:7:10 - LMT 1890 +-5:7:10 - KMT 1912 F +-5 - EST 1974 +-5 u E%sT 1984 +-5 - EST +Z America/Juneau 15:2:19 - LMT 1867 O 19 15:33:32 +-8:57:41 - LMT 1900 Au 20 12 +-8 - PST 1942 +-8 u P%sT 1946 +-8 - PST 1969 +-8 u P%sT 1980 Ap 27 2 +-9 u Y%sT 1980 O 26 2 +-8 u P%sT 1983 O 30 2 +-9 u Y%sT 1983 N 30 +-9 u AK%sT +Z America/Kentucky/Louisville -5:43:2 - LMT 1883 N 18 18u +-6 u C%sT 1921 +-6 v C%sT 1942 +-6 u C%sT 1946 +-6 v C%sT 1961 Jul 23 2 +-5 - EST 1968 +-5 u E%sT 1974 Ja 6 2 +-6 1 CDT 1974 O 27 2 +-5 u E%sT +Z America/Kentucky/Monticello -5:39:24 - LMT 1883 N 18 18u +-6 u C%sT 1946 +-6 - CST 1968 +-6 u C%sT 2000 O 29 2 +-5 u E%sT +Z America/La_Paz -4:32:36 - LMT 1890 +-4:32:36 - CMT 1931 O 15 +-4:32:36 1 BST 1932 Mar 21 +-4 - %z +Z America/Lima -5:8:12 - LMT 1890 +-5:8:36 - LMT 1908 Jul 28 +-5 PE %z +Z America/Los_Angeles -7:52:58 - LMT 1883 N 18 20u +-8 u P%sT 1946 +-8 CA P%sT 1967 +-8 u P%sT +Z America/Maceio -2:22:52 - LMT 1914 +-3 B %z 1990 S 17 +-3 - %z 1995 O 13 +-3 B %z 1996 S 4 +-3 - %z 1999 S 30 +-3 B %z 2000 O 22 +-3 - %z 2001 S 13 +-3 B %z 2002 O +-3 - %z +Z America/Managua -5:45:8 - LMT 1890 +-5:45:12 - MMT 1934 Jun 23 +-6 - CST 1973 May +-5 - EST 1975 F 16 +-6 NI C%sT 1992 Ja 1 4 +-5 - EST 1992 S 24 +-6 - CST 1993 +-5 - EST 1997 +-6 NI C%sT +Z America/Manaus -4:0:4 - LMT 1914 +-4 B %z 1988 S 12 +-4 - %z 1993 S 28 +-4 B %z 1994 S 22 +-4 - %z +Z America/Martinique -4:4:20 - LMT 1890 +-4:4:20 - FFMT 1911 May +-4 - AST 1980 Ap 6 +-4 1 ADT 1980 S 28 +-4 - AST +Z America/Matamoros -6:30 - LMT 1922 Ja 1 6u +-6 - CST 1988 +-6 u C%sT 1989 +-6 m C%sT 2010 +-6 u C%sT +Z America/Mazatlan -7:5:40 - LMT 1922 Ja 1 7u +-7 - MST 1927 Jun 10 +-6 - CST 1930 N 15 +-7 m M%sT 1932 Ap +-6 - CST 1942 Ap 24 +-7 - MST 1970 +-7 m M%sT +Z America/Menominee -5:50:27 - LMT 1885 S 18 12 +-6 u C%sT 1946 +-6 Me C%sT 1969 Ap 27 2 +-5 - EST 1973 Ap 29 2 +-6 u C%sT +Z America/Merida -5:58:28 - LMT 1922 Ja 1 6u +-6 - CST 1981 D 26 2 +-5 - EST 1982 N 2 2 +-6 m C%sT +Z America/Metlakatla 15:13:42 - LMT 1867 O 19 15:44:55 +-8:46:18 - LMT 1900 Au 20 12 +-8 - PST 1942 +-8 u P%sT 1946 +-8 - PST 1969 +-8 u P%sT 1983 O 30 2 +-8 - PST 2015 N 1 2 +-9 u AK%sT 2018 N 4 2 +-8 - PST 2019 Ja 20 2 +-9 u AK%sT +Z America/Mexico_City -6:36:36 - LMT 1922 Ja 1 7u +-7 - MST 1927 Jun 10 +-6 - CST 1930 N 15 +-7 m M%sT 1932 Ap +-6 m C%sT 2001 S 30 2 +-6 - CST 2002 F 20 +-6 m C%sT +Z America/Miquelon -3:44:40 - LMT 1911 Jun 15 +-4 - AST 1980 May +-3 - %z 1987 +-3 C %z +Z America/Moncton -4:19:8 - LMT 1883 D 9 +-5 - EST 1902 Jun 15 +-4 C A%sT 1933 +-4 o A%sT 1942 +-4 C A%sT 1946 +-4 o A%sT 1973 +-4 C A%sT 1993 +-4 o A%sT 2007 +-4 C A%sT +Z America/Monterrey -6:41:16 - LMT 1922 Ja 1 6u +-7 - MST 1927 Jun 10 +-6 - CST 1930 N 15 +-7 m M%sT 1932 Ap +-6 - CST 1988 +-6 u C%sT 1989 +-6 m C%sT +Z America/Montevideo -3:44:51 - LMT 1908 Jun 10 +-3:44:51 - MMT 1920 May +-4 - %z 1923 O +-3:30 U %z 1942 D 14 +-3 U %z 1960 +-3 U %z 1968 +-3 U %z 1970 +-3 U %z 1974 +-3 U %z 1974 Mar 10 +-3 U %z 1974 D 22 +-3 U %z +Z America/New_York -4:56:2 - LMT 1883 N 18 17u +-5 u E%sT 1920 +-5 NY E%sT 1942 +-5 u E%sT 1946 +-5 NY E%sT 1967 +-5 u E%sT +Z America/Nome 12:58:22 - LMT 1867 O 19 13:29:35 +-11:1:38 - LMT 1900 Au 20 12 +-11 - NST 1942 +-11 u N%sT 1946 +-11 - NST 1967 Ap +-11 - BST 1969 +-11 u B%sT 1983 O 30 2 +-9 u Y%sT 1983 N 30 +-9 u AK%sT +Z America/Noronha -2:9:40 - LMT 1914 +-2 B %z 1990 S 17 +-2 - %z 1999 S 30 +-2 B %z 2000 O 15 +-2 - %z 2001 S 13 +-2 B %z 2002 O +-2 - %z +Z America/North_Dakota/Beulah -6:47:7 - LMT 1883 N 18 19u +-7 u M%sT 2010 N 7 2 +-6 u C%sT +Z America/North_Dakota/Center -6:45:12 - LMT 1883 N 18 19u +-7 u M%sT 1992 O 25 2 +-6 u C%sT +Z America/North_Dakota/New_Salem -6:45:39 - LMT 1883 N 18 19u +-7 u M%sT 2003 O 26 2 +-6 u C%sT +Z America/Nuuk -3:26:56 - LMT 1916 Jul 28 +-3 - %z 1980 Ap 6 2 +-3 E %z 2023 Mar 26 1u +-2 - %z 2023 O 29 1u +-2 E %z +Z America/Ojinaga -6:57:40 - LMT 1922 Ja 1 7u +-7 - MST 1927 Jun 10 +-6 - CST 1930 N 15 +-7 m M%sT 1932 Ap +-6 - CST 1996 +-6 m C%sT 1998 +-6 - CST 1998 Ap Su>=1 3 +-7 m M%sT 2010 +-7 u M%sT 2022 O 30 2 +-6 - CST 2022 N 30 +-6 u C%sT +Z America/Panama -5:18:8 - LMT 1890 +-5:19:36 - CMT 1908 Ap 22 +-5 - EST +Z America/Paramaribo -3:40:40 - LMT 1911 +-3:40:52 - PMT 1935 +-3:40:36 - PMT 1945 O +-3:30 - %z 1984 O +-3 - %z +Z America/Phoenix -7:28:18 - LMT 1883 N 18 19u +-7 u M%sT 1944 Ja 1 0:1 +-7 - MST 1944 Ap 1 0:1 +-7 u M%sT 1944 O 1 0:1 +-7 - MST 1967 +-7 u M%sT 1968 Mar 21 +-7 - MST +Z America/Port-au-Prince -4:49:20 - LMT 1890 +-4:49 - PPMT 1917 Ja 24 12 +-5 HT E%sT +Z America/Porto_Velho -4:15:36 - LMT 1914 +-4 B %z 1988 S 12 +-4 - %z +Z America/Puerto_Rico -4:24:25 - LMT 1899 Mar 28 12 +-4 - AST 1942 May 3 +-4 u A%sT 1946 +-4 - AST +Z America/Punta_Arenas -4:43:40 - LMT 1890 +-4:42:45 - SMT 1910 Ja 10 +-5 - %z 1916 Jul +-4:42:45 - SMT 1918 S 10 +-4 - %z 1919 Jul +-4:42:45 - SMT 1927 S +-5 x %z 1932 S +-4 - %z 1942 Jun +-5 - %z 1942 Au +-4 - %z 1946 Au 28 24 +-5 1 %z 1947 Mar 31 24 +-5 - %z 1947 May 21 23 +-4 x %z 2016 D 4 +-3 - %z +Z America/Rankin_Inlet 0 - -00 1957 +-6 Y C%sT 2000 O 29 2 +-5 - EST 2001 Ap 1 3 +-6 C C%sT +Z America/Recife -2:19:36 - LMT 1914 +-3 B %z 1990 S 17 +-3 - %z 1999 S 30 +-3 B %z 2000 O 15 +-3 - %z 2001 S 13 +-3 B %z 2002 O +-3 - %z +Z America/Regina -6:58:36 - LMT 1905 S +-7 r M%sT 1960 Ap lastSu 2 +-6 - CST +Z America/Resolute 0 - -00 1947 Au 31 +-6 Y C%sT 2000 O 29 2 +-5 - EST 2001 Ap 1 3 +-6 C C%sT 2006 O 29 2 +-5 - EST 2007 Mar 11 3 +-6 C C%sT +Z America/Rio_Branco -4:31:12 - LMT 1914 +-5 B %z 1988 S 12 +-5 - %z 2008 Jun 24 +-4 - %z 2013 N 10 +-5 - %z +Z America/Santarem -3:38:48 - LMT 1914 +-4 B %z 1988 S 12 +-4 - %z 2008 Jun 24 +-3 - %z +Z America/Santiago -4:42:45 - LMT 1890 +-4:42:45 - SMT 1910 Ja 10 +-5 - %z 1916 Jul +-4:42:45 - SMT 1918 S 10 +-4 - %z 1919 Jul +-4:42:45 - SMT 1927 S +-5 x %z 1932 S +-4 - %z 1942 Jun +-5 - %z 1942 Au +-4 - %z 1946 Jul 14 24 +-4 1 %z 1946 Au 28 24 +-5 1 %z 1947 Mar 31 24 +-5 - %z 1947 May 21 23 +-4 x %z +Z America/Santo_Domingo -4:39:36 - LMT 1890 +-4:40 - SDMT 1933 Ap 1 12 +-5 DO %s 1974 O 27 +-4 - AST 2000 O 29 2 +-5 u E%sT 2000 D 3 1 +-4 - AST +Z America/Sao_Paulo -3:6:28 - LMT 1914 +-3 B %z 1963 O 23 +-3 1 %z 1964 +-3 B %z +Z America/Scoresbysund -1:27:52 - LMT 1916 Jul 28 +-2 - %z 1980 Ap 6 2 +-2 c %z 1981 Mar 29 +-1 E %z 2024 Mar 31 +-2 E %z +Z America/Sitka 14:58:47 - LMT 1867 O 19 15:30 +-9:1:13 - LMT 1900 Au 20 12 +-8 - PST 1942 +-8 u P%sT 1946 +-8 - PST 1969 +-8 u P%sT 1983 O 30 2 +-9 u Y%sT 1983 N 30 +-9 u AK%sT +Z America/St_Johns -3:30:52 - LMT 1884 +-3:30:52 j N%sT 1918 +-3:30:52 C N%sT 1919 +-3:30:52 j N%sT 1935 Mar 30 +-3:30 j N%sT 1942 May 11 +-3:30 C N%sT 1946 +-3:30 j N%sT 2011 N +-3:30 C N%sT +Z America/Swift_Current -7:11:20 - LMT 1905 S +-7 C M%sT 1946 Ap lastSu 2 +-7 r M%sT 1950 +-7 Sw M%sT 1972 Ap lastSu 2 +-6 - CST +Z America/Tegucigalpa -5:48:52 - LMT 1921 Ap +-6 HN C%sT +Z America/Thule -4:35:8 - LMT 1916 Jul 28 +-4 Th A%sT +Z America/Tijuana -7:48:4 - LMT 1922 Ja 1 7u +-7 - MST 1924 +-8 - PST 1927 Jun 10 +-7 - MST 1930 N 15 +-8 - PST 1931 Ap +-8 1 PDT 1931 S 30 +-8 - PST 1942 Ap 24 +-8 1 PWT 1945 Au 14 23u +-8 1 PPT 1945 N 15 +-8 - PST 1948 Ap 5 +-8 1 PDT 1949 Ja 14 +-8 - PST 1950 May +-8 1 PDT 1950 S 24 +-8 - PST 1951 Ap 29 2 +-8 1 PDT 1951 S 30 2 +-8 - PST 1952 Ap 27 2 +-8 1 PDT 1952 S 28 2 +-8 CA P%sT 1967 +-8 u P%sT 1996 +-8 m P%sT 2001 +-8 u P%sT 2002 F 20 +-8 m P%sT 2010 +-8 u P%sT +Z America/Toronto -5:17:32 - LMT 1895 +-5 C E%sT 1919 +-5 t E%sT 1942 F 9 2s +-5 C E%sT 1946 +-5 t E%sT 1974 +-5 C E%sT +Z America/Vancouver -8:12:28 - LMT 1884 +-8 Va P%sT 1987 +-8 C P%sT 2026 Mar 9 +-8 1 PDT 2026 N 1 2 +-7 - MST +Z America/Whitehorse -9:0:12 - LMT 1900 Au 20 +-9 Y Y%sT 1965 +-9 Yu Y%sT 1966 F 27 +-8 - PST 1980 +-8 C P%sT 2020 N +-7 - MST +Z America/Winnipeg -6:28:36 - LMT 1887 Jul 16 +-6 W C%sT 2006 +-6 C C%sT +Z America/Yakutat 14:41:5 - LMT 1867 O 19 15:12:18 +-9:18:55 - LMT 1900 Au 20 12 +-9 - YST 1942 +-9 u Y%sT 1946 +-9 - YST 1969 +-9 u Y%sT 1983 N 30 +-9 u AK%sT +Z Antarctica/Casey 0 - -00 1969 +8 - %z 2009 O 18 2 +11 - %z 2010 Mar 5 2 +8 - %z 2011 O 28 2 +11 - %z 2012 F 21 17u +8 - %z 2016 O 22 +11 - %z 2018 Mar 11 4 +8 - %z 2018 O 7 4 +11 - %z 2019 Mar 17 3 +8 - %z 2019 O 4 3 +11 - %z 2020 Mar 8 3 +8 - %z 2020 O 4 0:1 +11 - %z 2021 Mar 14 +8 - %z 2021 O 3 0:1 +11 - %z 2022 Mar 13 +8 - %z 2022 O 2 0:1 +11 - %z 2023 Mar 9 3 +8 - %z +Z Antarctica/Davis 0 - -00 1957 Ja 13 +7 - %z 1964 N +0 - -00 1969 F +7 - %z 2009 O 18 2 +5 - %z 2010 Mar 10 20u +7 - %z 2011 O 28 2 +5 - %z 2012 F 21 20u +7 - %z +Z Antarctica/Macquarie 0 - -00 1899 N +10 - AEST 1916 O 1 2 +10 1 AEDT 1917 F +10 AU AE%sT 1919 Ap 1 0s +0 - -00 1948 Mar 25 +10 AU AE%sT 1967 +10 AT AE%sT 2010 +10 1 AEDT 2011 +10 AT AE%sT +Z Antarctica/Mawson 0 - -00 1954 F 13 +6 - %z 2009 O 18 2 +5 - %z +Z Antarctica/Palmer 0 - -00 1965 +-4 A %z 1969 O 5 +-3 A %z 1982 May +-4 x %z 2016 D 4 +-3 - %z +Z Antarctica/Rothera 0 - -00 1976 D +-3 - %z +Z Antarctica/Troll 0 - -00 2005 F 12 +0 Tr %s +Z Antarctica/Vostok 0 - -00 1957 D 16 +7 - %z 1994 F +0 - -00 1994 N +7 - %z 2023 D 18 2 +5 - %z +Z Asia/Almaty 5:7:48 - LMT 1924 May 2 +5 - %z 1930 Jun 21 +6 R %z 1991 Mar 31 2s +5 R %z 1992 Ja 19 2s +6 R %z 2004 O 31 2s +6 - %z 2024 Mar +5 - %z +Z Asia/Amman 2:23:44 - LMT 1931 +2 J EE%sT 2022 O 28 0s +3 - %z +Z Asia/Anadyr 11:49:56 - LMT 1924 May 2 +12 - %z 1930 Jun 21 +13 R %z 1982 Ap 1 0s +12 R %z 1991 Mar 31 2s +11 R %z 1992 Ja 19 2s +12 R %z 2010 Mar 28 2s +11 R %z 2011 Mar 27 2s +12 - %z +Z Asia/Aqtau 3:21:4 - LMT 1924 May 2 +4 - %z 1930 Jun 21 +5 - %z 1981 O +6 - %z 1982 Ap +5 R %z 1991 Mar 31 2s +4 R %z 1992 Ja 19 2s +5 R %z 1994 S 25 2s +4 R %z 2004 O 31 2s +5 - %z +Z Asia/Aqtobe 3:48:40 - LMT 1924 May 2 +4 - %z 1930 Jun 21 +5 - %z 1981 Ap +5 1 %z 1981 O +6 - %z 1982 Ap +5 R %z 1991 Mar 31 2s +4 R %z 1992 Ja 19 2s +5 R %z 2004 O 31 2s +5 - %z +Z Asia/Ashgabat 3:53:32 - LMT 1924 May 2 +4 - %z 1930 Jun 21 +5 R %z 1991 Mar 31 2 +4 R %z 1992 Ja 19 2 +5 - %z +Z Asia/Atyrau 3:27:44 - LMT 1924 May 2 +3 - %z 1930 Jun 21 +5 - %z 1981 O +6 - %z 1982 Ap +5 R %z 1991 Mar 31 2s +4 R %z 1992 Ja 19 2s +5 R %z 1999 Mar 28 2s +4 R %z 2004 O 31 2s +5 - %z +Z Asia/Baghdad 2:57:40 - LMT 1890 +2:57:36 - BMT 1918 +3 - %z 1982 May +3 IQ %z +Z Asia/Baku 3:19:24 - LMT 1924 May 2 +3 - %z 1957 Mar +4 R %z 1991 Mar 31 2s +3 R %z 1992 S lastSu 2s +4 - %z 1996 +4 E %z 1997 +4 AZ %z +Z Asia/Bangkok 6:42:4 - LMT 1880 +6:42:4 - BMT 1920 Ap +7 - %z +Z Asia/Barnaul 5:35 - LMT 1919 D 10 +6 - %z 1930 Jun 21 +7 R %z 1991 Mar 31 2s +6 R %z 1992 Ja 19 2s +7 R %z 1995 May 28 +6 R %z 2011 Mar 27 2s +7 - %z 2014 O 26 2s +6 - %z 2016 Mar 27 2s +7 - %z +Z Asia/Beirut 2:22 - LMT 1880 +2 l EE%sT +Z Asia/Bishkek 4:58:24 - LMT 1924 May 2 +5 - %z 1930 Jun 21 +6 R %z 1991 Mar 31 2s +5 R %z 1991 Au 31 2 +5 KG %z 2005 Au 12 +6 - %z +Z Asia/Chita 7:33:52 - LMT 1919 D 15 +8 - %z 1930 Jun 21 +9 R %z 1991 Mar 31 2s +8 R %z 1992 Ja 19 2s +9 R %z 2011 Mar 27 2s +10 - %z 2014 O 26 2s +8 - %z 2016 Mar 27 2 +9 - %z +Z Asia/Colombo 5:19:24 - LMT 1880 +5:19:32 - MMT 1906 +5:30 - %z 1942 Ja 5 +5:30 0:30 %z 1942 S +5:30 1 %z 1945 O 16 2 +5:30 - %z 1996 May 25 +6:30 - %z 1996 O 26 0:30 +6 - %z 2006 Ap 15 0:30 +5:30 - %z +Z Asia/Damascus 2:25:12 - LMT 1920 +2 S EE%sT 2022 O 28 +3 - %z +Z Asia/Dhaka 6:1:40 - LMT 1890 +5:53:20 - HMT 1941 O +6:30 - %z 1942 May 15 +5:30 - %z 1942 S +6:30 - %z 1951 S 30 +6 - %z 2009 +6 BD %z +Z Asia/Dili 8:22:20 - LMT 1911 D 31 16u +8 - %z 1942 F 21 23 +9 - %z 1976 May 3 +8 - %z 2000 S 17 +9 - %z +Z Asia/Dubai 3:41:12 - LMT 1920 +4 - %z +Z Asia/Dushanbe 4:35:12 - LMT 1924 May 2 +5 - %z 1930 Jun 21 +6 R %z 1991 Mar 31 2s +5 1 %z 1991 S 9 2s +5 - %z +Z Asia/Famagusta 2:15:48 - LMT 1921 N 14 +2 CY EE%sT 1998 S +2 E EE%sT 2016 S 8 +3 - %z 2017 O 29 1u +2 E EE%sT +Z Asia/Gaza 2:17:52 - LMT 1900 O +2 Z EET/EEST 1948 May 15 +2 K EE%sT 1967 Jun 5 +2 Z I%sT 1996 +2 J EE%sT 1999 +2 P EE%sT 2008 Au 29 +2 - EET 2008 S +2 P EE%sT 2010 +2 - EET 2010 Mar 27 0:1 +2 P EE%sT 2011 Au +2 - EET 2012 +2 P EE%sT +Z Asia/Hebron 2:20:23 - LMT 1900 O +2 Z EET/EEST 1948 May 15 +2 K EE%sT 1967 Jun 5 +2 Z I%sT 1996 +2 J EE%sT 1999 +2 P EE%sT +Z Asia/Ho_Chi_Minh 7:6:30 - LMT 1906 Jul +7:6:30 - PLMT 1911 May +7 - %z 1942 D 31 23 +8 - %z 1945 Mar 14 23 +9 - %z 1945 S 1 24 +7 - %z 1947 Ap +8 - %z 1955 Jul 1 1 +7 - %z 1959 D 31 23 +8 - %z 1975 Jun 13 +7 - %z +Z Asia/Hong_Kong 7:36:42 - LMT 1904 O 29 17u +8 - HKT 1941 Jun 15 3 +8 1 HKST 1941 O 1 4 +8 0:30 HKWT 1941 D 25 +9 - JST 1945 N 18 2 +8 HK HK%sT +Z Asia/Hovd 6:6:36 - LMT 1905 Au +6 - %z 1978 +7 X %z +Z Asia/Irkutsk 6:57:5 - LMT 1880 +6:57:5 - IMT 1920 Ja 25 +7 - %z 1930 Jun 21 +8 R %z 1991 Mar 31 2s +7 R %z 1992 Ja 19 2s +8 R %z 2011 Mar 27 2s +9 - %z 2014 O 26 2s +8 - %z +Z Asia/Jakarta 7:7:12 - LMT 1867 Au 10 +7:7:12 - BMT 1923 D 31 16:40u +7:20 - %z 1932 N +7:30 - %z 1942 Mar 23 +9 - %z 1945 S 23 +7:30 - %z 1948 May +8 - %z 1950 May +7:30 - %z 1964 +7 - WIB +Z Asia/Jayapura 9:22:48 - LMT 1932 N +9 - %z 1944 S +9:30 - %z 1964 +9 - WIT +Z Asia/Jerusalem 2:20:54 - LMT 1880 +2:20:40 - JMT 1918 +2 Z I%sT +Z Asia/Kabul 4:36:48 - LMT 1890 +4 - %z 1945 +4:30 - %z +Z Asia/Kamchatka 10:34:36 - LMT 1922 N 10 +11 - %z 1930 Jun 21 +12 R %z 1991 Mar 31 2s +11 R %z 1992 Ja 19 2s +12 R %z 2010 Mar 28 2s +11 R %z 2011 Mar 27 2s +12 - %z +Z Asia/Karachi 4:28:12 - LMT 1907 +5:30 - %z 1942 S +5:30 1 %z 1945 O 15 +5:30 - %z 1951 S 30 +5 - %z 1971 Mar 26 +5 PK PK%sT +Z Asia/Kathmandu 5:41:16 - LMT 1920 +5:30 - %z 1986 +5:45 - %z +Z Asia/Khandyga 9:2:13 - LMT 1919 D 15 +8 - %z 1930 Jun 21 +9 R %z 1991 Mar 31 2s +8 R %z 1992 Ja 19 2s +9 R %z 2004 +10 R %z 2011 Mar 27 2s +11 - %z 2011 S 13 0s +10 - %z 2014 O 26 2s +9 - %z +Z Asia/Kolkata 5:53:28 - LMT 1854 Jun 28 +5:53:20 - HMT 1870 +5:21:10 - MMT 1906 +5:30 - IST 1941 O +5:30 1 %z 1942 May 15 +5:30 - IST 1942 S +5:30 1 %z 1945 O 15 +5:30 - IST +Z Asia/Krasnoyarsk 6:11:26 - LMT 1920 Ja 6 +6 - %z 1930 Jun 21 +7 R %z 1991 Mar 31 2s +6 R %z 1992 Ja 19 2s +7 R %z 2011 Mar 27 2s +8 - %z 2014 O 26 2s +7 - %z +Z Asia/Kuching 7:21:20 - LMT 1926 Mar +7:30 - %z 1933 +8 NB %z 1942 F 16 +9 - %z 1945 S 12 +8 - %z +Z Asia/Macau 7:34:10 - LMT 1904 O 30 +8 - CST 1941 D 21 23 +9 _ %z 1945 S 30 24 +8 _ C%sT +Z Asia/Magadan 10:3:12 - LMT 1924 May 2 +10 - %z 1930 Jun 21 +11 R %z 1991 Mar 31 2s +10 R %z 1992 Ja 19 2s +11 R %z 2011 Mar 27 2s +12 - %z 2014 O 26 2s +10 - %z 2016 Ap 24 2s +11 - %z +Z Asia/Makassar 7:57:36 - LMT 1920 +7:57:36 - MMT 1932 N +8 - %z 1942 F 9 +9 - %z 1945 S 23 +8 - WITA +Z Asia/Manila -15:56:8 - LMT 1844 D 31 +8:3:52 - LMT 1899 S 6 4u +8 PH P%sT 1942 F 11 24 +9 - JST 1945 Mar 4 +8 PH P%sT +Z Asia/Nicosia 2:13:28 - LMT 1921 N 14 +2 CY EE%sT 1998 S +2 E EE%sT +Z Asia/Novokuznetsk 5:48:48 - LMT 1924 May +6 - %z 1930 Jun 21 +7 R %z 1991 Mar 31 2s +6 R %z 1992 Ja 19 2s +7 R %z 2010 Mar 28 2s +6 R %z 2011 Mar 27 2s +7 - %z +Z Asia/Novosibirsk 5:31:40 - LMT 1919 D 14 6 +6 - %z 1930 Jun 21 +7 R %z 1991 Mar 31 2s +6 R %z 1992 Ja 19 2s +7 R %z 1993 May 23 +6 R %z 2011 Mar 27 2s +7 - %z 2014 O 26 2s +6 - %z 2016 Jul 24 2s +7 - %z +Z Asia/Omsk 4:53:30 - LMT 1919 N 14 +5 - %z 1930 Jun 21 +6 R %z 1991 Mar 31 2s +5 R %z 1992 Ja 19 2s +6 R %z 2011 Mar 27 2s +7 - %z 2014 O 26 2s +6 - %z +Z Asia/Oral 3:25:24 - LMT 1924 May 2 +3 - %z 1930 Jun 21 +5 - %z 1981 Ap +5 1 %z 1981 O +6 - %z 1982 Ap +5 R %z 1989 Mar 26 2s +4 R %z 1992 Ja 19 2s +5 R %z 1992 Mar 29 2s +4 R %z 2004 O 31 2s +5 - %z +Z Asia/Pontianak 7:17:20 - LMT 1908 May +7:17:20 - PMT 1932 N +7:30 - %z 1942 Ja 29 +9 - %z 1945 S 23 +7:30 - %z 1948 May +8 - %z 1950 May +7:30 - %z 1964 +8 - WITA 1988 +7 - WIB +Z Asia/Pyongyang 8:23 - LMT 1908 Ap +8:30 - KST 1912 +9 - JST 1945 Au 24 +9 - KST 2015 Au 15 +8:30 - KST 2018 May 4 23:30 +9 - KST +Z Asia/Qatar 3:26:8 - LMT 1920 +4 - %z 1972 Jun +3 - %z +Z Asia/Qostanay 4:14:28 - LMT 1924 May 2 +4 - %z 1930 Jun 21 +5 - %z 1981 Ap +5 1 %z 1981 O +6 - %z 1982 Ap +5 R %z 1991 Mar 31 2s +4 R %z 1992 Ja 19 2s +5 R %z 2004 O 31 2s +6 - %z 2024 Mar +5 - %z +Z Asia/Qyzylorda 4:21:52 - LMT 1924 May 2 +4 - %z 1930 Jun 21 +5 - %z 1981 Ap +5 1 %z 1981 O +6 - %z 1982 Ap +5 R %z 1991 Mar 31 2s +4 R %z 1991 S 29 2s +5 R %z 1992 Ja 19 2s +6 R %z 1992 Mar 29 2s +5 R %z 2004 O 31 2s +6 - %z 2018 D 21 +5 - %z +Z Asia/Riyadh 3:6:52 - LMT 1947 Mar 14 +3 - %z +Z Asia/Sakhalin 9:30:48 - LMT 1905 Au 23 +9 - %z 1945 Au 25 +11 R %z 1991 Mar 31 2s +10 R %z 1992 Ja 19 2s +11 R %z 1997 Mar lastSu 2s +10 R %z 2011 Mar 27 2s +11 - %z 2014 O 26 2s +10 - %z 2016 Mar 27 2s +11 - %z +Z Asia/Samarkand 4:27:53 - LMT 1924 May 2 +4 - %z 1930 Jun 21 +5 - %z 1981 Ap +5 1 %z 1981 O +6 - %z 1982 Ap +5 R %z 1992 +5 - %z +Z Asia/Seoul 8:27:52 - LMT 1908 Ap +8:30 - KST 1912 +9 - JST 1945 S 8 +9 KR K%sT 1954 Mar 21 +8:30 KR K%sT 1961 Au 10 +9 KR K%sT +Z Asia/Shanghai 8:5:43 - LMT 1901 +8 Sh C%sT 1949 May 28 +8 CN C%sT +Z Asia/Singapore 6:55:25 - LMT 1901 +6:55:25 - SMT 1905 Jun +7 - %z 1933 +7 0:20 %z 1936 +7:20 - %z 1941 S +7:30 - %z 1942 F 16 +9 - %z 1945 S 12 +7:30 - %z 1981 D 31 16u +8 - %z +Z Asia/Srednekolymsk 10:14:52 - LMT 1924 May 2 +10 - %z 1930 Jun 21 +11 R %z 1991 Mar 31 2s +10 R %z 1992 Ja 19 2s +11 R %z 2011 Mar 27 2s +12 - %z 2014 O 26 2s +11 - %z +Z Asia/Taipei 8:6 - LMT 1896 +8 - CST 1937 O +9 - JST 1945 S 21 1 +8 f C%sT +Z Asia/Tashkent 4:37:11 - LMT 1924 May 2 +5 - %z 1930 Jun 21 +6 R %z 1991 Mar 31 2 +5 R %z 1992 +5 - %z +Z Asia/Tbilisi 2:59:11 - LMT 1880 +2:59:11 - TBMT 1924 May 2 +3 - %z 1957 Mar +4 R %z 1991 Mar 31 2s +3 R %z 1992 +3 e %z 1994 S lastSu +4 e %z 1996 O lastSu +4 1 %z 1997 Mar lastSu +4 e %z 2004 Jun 27 +3 R %z 2005 Mar lastSu 2 +4 - %z +Z Asia/Tehran 3:25:44 - LMT 1916 +3:25:44 - TMT 1935 Jun 13 +3:30 i %z 1977 O 20 24 +4 i %z 1978 N 10 24 +3:30 i %z +Z Asia/Thimphu 5:58:36 - LMT 1947 Au 15 +5:30 - %z 1987 O +6 - %z +Z Asia/Tokyo 9:18:59 - LMT 1887 D 31 15u +9 JP J%sT +Z Asia/Tomsk 5:39:51 - LMT 1919 D 22 +6 - %z 1930 Jun 21 +7 R %z 1991 Mar 31 2s +6 R %z 1992 Ja 19 2s +7 R %z 2002 May 1 3 +6 R %z 2011 Mar 27 2s +7 - %z 2014 O 26 2s +6 - %z 2016 May 29 2s +7 - %z +Z Asia/Ulaanbaatar 7:7:32 - LMT 1905 Au +7 - %z 1978 +8 X %z +Z Asia/Urumqi 5:50:20 - LMT 1928 +6 - %z +Z Asia/Ust-Nera 9:32:54 - LMT 1919 D 15 +8 - %z 1930 Jun 21 +9 R %z 1981 Ap +11 R %z 1991 Mar 31 2s +10 R %z 1992 Ja 19 2s +11 R %z 2011 Mar 27 2s +12 - %z 2011 S 13 0s +11 - %z 2014 O 26 2s +10 - %z +Z Asia/Vladivostok 8:47:31 - LMT 1922 N 15 +9 - %z 1930 Jun 21 +10 R %z 1991 Mar 31 2s +9 R %z 1992 Ja 19 2s +10 R %z 2011 Mar 27 2s +11 - %z 2014 O 26 2s +10 - %z +Z Asia/Yakutsk 8:38:58 - LMT 1919 D 15 +8 - %z 1930 Jun 21 +9 R %z 1991 Mar 31 2s +8 R %z 1992 Ja 19 2s +9 R %z 2011 Mar 27 2s +10 - %z 2014 O 26 2s +9 - %z +Z Asia/Yangon 6:24:47 - LMT 1880 +6:24:47 - RMT 1920 +6:30 - %z 1942 May +9 - %z 1945 May 3 +6:30 - %z +Z Asia/Yekaterinburg 4:2:33 - LMT 1916 Jul 3 +3:45:5 - PMT 1919 Jul 15 4 +4 - %z 1930 Jun 21 +5 R %z 1991 Mar 31 2s +4 R %z 1992 Ja 19 2s +5 R %z 2011 Mar 27 2s +6 - %z 2014 O 26 2s +5 - %z +Z Asia/Yerevan 2:58 - LMT 1924 May 2 +3 - %z 1957 Mar +4 R %z 1991 Mar 31 2s +3 R %z 1995 S 24 2s +4 - %z 1997 +4 R %z 2011 +4 AM %z +Z Atlantic/Azores -1:42:40 - LMT 1884 +-1:54:32 - HMT 1912 Ja 1 2u +-2 p %z 1966 O 2 2s +-1 - %z 1982 Mar 28 0s +-1 p %z 1986 +-1 E %z 1992 D 27 1s +0 E WE%sT 1993 Jun 17 1u +-1 E %z +Z Atlantic/Bermuda -4:19:18 - LMT 1890 +-4:19:18 Be BMT/BST 1930 Ja 1 2 +-4 Be A%sT 1974 Ap 28 2 +-4 C A%sT 1976 +-4 u A%sT +Z Atlantic/Canary -1:1:36 - LMT 1922 Mar +-1 - %z 1946 S 30 1 +0 - WET 1980 Ap 6 0s +0 1 WEST 1980 S 28 1u +0 E WE%sT +Z Atlantic/Cape_Verde -1:34:4 - LMT 1912 Ja 1 2u +-2 - %z 1942 S +-2 1 %z 1945 O 15 +-2 - %z 1975 N 25 2 +-1 - %z +Z Atlantic/Faroe -0:27:4 - LMT 1908 Ja 11 +0 - WET 1981 +0 E WE%sT +Z Atlantic/Madeira -1:7:36 - LMT 1884 +-1:7:36 - FMT 1912 Ja 1 1u +-1 p %z 1966 O 2 2s +0 - WET 1982 Ap 4 +0 p WE%sT 1986 Jul 31 +0 E WE%sT +Z Atlantic/South_Georgia -2:26:8 - LMT 1890 +-2 - %z +Z Atlantic/Stanley -3:51:24 - LMT 1890 +-3:51:24 - SMT 1912 Mar 12 +-4 FK %z 1983 May +-3 FK %z 1985 S 15 +-4 FK %z 2010 S 5 2 +-3 - %z +Z Australia/Adelaide 9:14:20 - LMT 1895 F +9 - ACST 1899 May +9:30 AU AC%sT 1971 +9:30 AS AC%sT +Z Australia/Brisbane 10:12:8 - LMT 1895 +10 AU AE%sT 1971 +10 AQ AE%sT +Z Australia/Broken_Hill 9:25:48 - LMT 1895 F +10 - AEST 1896 Au 23 +9 - ACST 1899 May +9:30 AU AC%sT 1971 +9:30 AN AC%sT 2000 +9:30 AS AC%sT +Z Australia/Darwin 8:43:20 - LMT 1895 F +9 - ACST 1899 May +9:30 AU AC%sT +Z Australia/Eucla 8:35:28 - LMT 1895 D +8:45 AU %z 1943 Jul +8:45 AW %z +Z Australia/Hobart 9:49:16 - LMT 1895 S +10 AT AE%sT 1919 O 24 +10 AU AE%sT 1967 +10 AT AE%sT +Z Australia/Lindeman 9:55:56 - LMT 1895 +10 AU AE%sT 1971 +10 AQ AE%sT 1992 Jul +10 Ho AE%sT +Z Australia/Lord_Howe 10:36:20 - LMT 1895 F +10 - AEST 1981 Mar +10:30 LH %z 1985 Jul +10:30 LH %z +Z Australia/Melbourne 9:39:52 - LMT 1895 F +10 AU AE%sT 1971 +10 AV AE%sT +Z Australia/Perth 7:43:24 - LMT 1895 D +8 AU AW%sT 1943 Jul +8 AW AW%sT +Z Australia/Sydney 10:4:52 - LMT 1895 F +10 AU AE%sT 1971 +10 AN AE%sT +Z Etc/GMT 0 - GMT +Z Etc/GMT+1 -1 - %z +Z Etc/GMT+10 -10 - %z +Z Etc/GMT+11 -11 - %z +Z Etc/GMT+12 -12 - %z +Z Etc/GMT+2 -2 - %z +Z Etc/GMT+3 -3 - %z +Z Etc/GMT+4 -4 - %z +Z Etc/GMT+5 -5 - %z +Z Etc/GMT+6 -6 - %z +Z Etc/GMT+7 -7 - %z +Z Etc/GMT+8 -8 - %z +Z Etc/GMT+9 -9 - %z +Z Etc/GMT-1 1 - %z +Z Etc/GMT-10 10 - %z +Z Etc/GMT-11 11 - %z +Z Etc/GMT-12 12 - %z +Z Etc/GMT-13 13 - %z +Z Etc/GMT-14 14 - %z +Z Etc/GMT-2 2 - %z +Z Etc/GMT-3 3 - %z +Z Etc/GMT-4 4 - %z +Z Etc/GMT-5 5 - %z +Z Etc/GMT-6 6 - %z +Z Etc/GMT-7 7 - %z +Z Etc/GMT-8 8 - %z +Z Etc/GMT-9 9 - %z +Z Etc/UTC 0 - UTC +Z Europe/Andorra 0:6:4 - LMT 1901 +0 - WET 1946 S 30 +1 - CET 1985 Mar 31 2 +1 E CE%sT +Z Europe/Astrakhan 3:12:12 - LMT 1924 May +3 - %z 1930 Jun 21 +4 R %z 1989 Mar 26 2s +3 R %z 1991 Mar 31 2s +4 - %z 1992 Mar 29 2s +3 R %z 2011 Mar 27 2s +4 - %z 2014 O 26 2s +3 - %z 2016 Mar 27 2s +4 - %z +Z Europe/Athens 1:34:52 - LMT 1895 S 14 +1:34:52 - AMT 1916 Jul 28 0:1 +2 g EE%sT 1941 Ap 30 +1 g CE%sT 1944 Ap 4 +2 g EE%sT 1981 +2 E EE%sT +Z Europe/Belgrade 1:22 - LMT 1884 +1 - CET 1941 Ap 18 23 +1 c CE%sT 1945 +1 - CET 1945 May 8 2s +1 1 CEST 1945 S 16 2s +1 - CET 1982 N 27 +1 E CE%sT +Z Europe/Berlin 0:53:28 - LMT 1893 Ap +1 c CE%sT 1945 May 24 2 +1 So CE%sT 1946 +1 DE CE%sT 1980 +1 E CE%sT +Z Europe/Brussels 0:17:30 - LMT 1880 +0:17:30 - BMT 1892 May 1 0:17:30 +0 - WET 1914 N 8 +1 - CET 1916 May +1 c CE%sT 1918 N 11 11u +0 b WE%sT 1940 May 20 2s +1 c CE%sT 1944 S 3 +1 b CE%sT 1977 +1 E CE%sT +Z Europe/Bucharest 1:44:24 - LMT 1891 O +1:44:24 - BMT 1931 Jul 24 +2 z EE%sT 1981 Mar 29 2s +2 c EE%sT 1991 +2 z EE%sT 1994 +2 e EE%sT 1997 +2 E EE%sT +Z Europe/Budapest 1:16:20 - LMT 1890 N +1 c CE%sT 1918 +1 h CE%sT 1941 Ap 7 23 +1 c CE%sT 1945 +1 h CE%sT 1984 +1 E CE%sT +Z Europe/Chisinau 1:55:20 - LMT 1880 +1:55 - CMT 1918 F 15 +1:44:24 - BMT 1931 Jul 24 +2 z EE%sT 1940 Au 15 +2 1 EEST 1941 Jul 17 +1 c CE%sT 1944 Au 24 +3 R MSK/MSD 1990 May 6 2 +2 R EE%sT 1992 +2 e EE%sT 1997 +2 MD EE%sT 2022 +2 E EE%sT +Z Europe/Dublin -0:25:21 - LMT 1880 Au 2 +-0:25:21 - DMT 1916 May 21 2s +-0:25:21 1 IST 1916 O 1 2s +0 G %s 1921 D 6 +0 G GMT/IST 1940 F 25 2s +0 1 IST 1946 O 6 2s +0 - GMT 1947 Mar 16 2s +0 1 IST 1947 N 2 2s +0 - GMT 1948 Ap 18 2s +0 G GMT/IST 1968 O 27 +1 IE IST/GMT +Z Europe/Gibraltar -0:21:24 - LMT 1880 Au 2 +0 G %s 1957 Ap 14 2 +1 - CET 1982 +1 E CE%sT +Z Europe/Helsinki 1:39:49 - LMT 1878 May 31 +1:39:49 - HMT 1921 May +2 FI EE%sT 1983 +2 E EE%sT +Z Europe/Istanbul 1:55:52 - LMT 1880 +1:56:56 - IMT 1910 O +2 T EE%sT 1978 Jun 29 +3 T %z 1984 N 1 2 +2 T EE%sT 2007 +2 E EE%sT 2011 Mar 27 1u +2 - EET 2011 Mar 28 1u +2 E EE%sT 2014 Mar 30 1u +2 - EET 2014 Mar 31 1u +2 E EE%sT 2015 O 25 1u +2 1 EEST 2015 N 8 1u +2 E EE%sT 2016 S 7 +3 - %z +Z Europe/Kaliningrad 1:22 - LMT 1893 Ap +1 c CE%sT 1945 Ap 10 +2 O EE%sT 1946 Ap 7 +3 R MSK/MSD 1989 Mar 26 2s +2 R EE%sT 2011 Mar 27 2s +3 - %z 2014 O 26 2s +2 - EET +Z Europe/Kirov 3:18:48 - LMT 1919 Jul 1 0u +3 - %z 1930 Jun 21 +4 R %z 1989 Mar 26 2s +3 R MSK/MSD 1991 Mar 31 2s +4 - %z 1992 Mar 29 2s +3 R MSK/MSD 2011 Mar 27 2s +4 - MSK 2014 O 26 2s +3 - MSK +Z Europe/Kyiv 2:2:4 - LMT 1880 +2:2:4 - KMT 1924 May 2 +2 - EET 1930 Jun 21 +3 - MSK 1941 S 20 +1 c CE%sT 1943 N 6 +3 R MSK/MSD 1990 Jul 1 2 +2 1 EEST 1991 S 29 3 +2 c EE%sT 1996 May 13 +2 E EE%sT +Z Europe/Lisbon -0:36:45 - LMT 1884 +-0:36:45 - LMT 1912 Ja 1 0u +0 p WE%sT 1966 O 2 2s +1 - CET 1976 S 26 1 +0 p WE%sT 1986 +0 E WE%sT 1992 S 27 1u +1 E CE%sT 1996 Mar 31 1u +0 E WE%sT +Z Europe/London -0:1:15 - LMT 1847 D +0 G %s 1968 O 27 +1 - BST 1971 O 31 2u +0 G %s 1996 +0 E GMT/BST +Z Europe/Madrid -0:14:44 - LMT 1901 Ja 1 0u +0 s WE%sT 1940 Mar 16 23 +1 s CE%sT 1979 +1 E CE%sT +Z Europe/Malta 0:58:4 - LMT 1893 N 2 +1 I CE%sT 1973 Mar 31 +1 MT CE%sT 1981 +1 E CE%sT +Z Europe/Minsk 1:50:16 - LMT 1880 +1:50 - MMT 1924 May 2 +2 - EET 1930 Jun 21 +3 - MSK 1941 Jun 28 +1 c CE%sT 1944 Jul 3 +3 R MSK/MSD 1990 +3 - MSK 1991 Mar 31 2s +2 R EE%sT 2011 Mar 27 2s +3 - %z +Z Europe/Moscow 2:30:17 - LMT 1880 +2:30:17 - MMT 1916 Jul 3 +2:31:19 R %s 1919 Jul 1 0u +3 R %s 1921 O +3 R MSK/MSD 1922 O +2 - EET 1930 Jun 21 +3 R MSK/MSD 1991 Mar 31 2s +2 R EE%sT 1992 Ja 19 2s +3 R MSK/MSD 2011 Mar 27 2s +4 - MSK 2014 O 26 2s +3 - MSK +Z Europe/Paris 0:9:21 - LMT 1891 Mar 16 +0:9:21 - PMT 1911 Mar 11 +0 F WE%sT 1940 Jun 14 23 +1 c CE%sT 1944 Au 25 +0 F WE%sT 1945 S 16 3 +1 F CE%sT 1977 +1 E CE%sT +Z Europe/Prague 0:57:44 - LMT 1850 +0:57:44 - PMT 1891 O +1 c CE%sT 1945 May 9 +1 CZ CE%sT 1946 D 1 3 +1 -1 GMT 1947 F 23 2 +1 CZ CE%sT 1979 +1 E CE%sT +Z Europe/Riga 1:36:34 - LMT 1880 +1:36:34 - RMT 1918 Ap 15 2 +1:36:34 1 LST 1918 S 16 3 +1:36:34 - RMT 1919 Ap 1 2 +1:36:34 1 LST 1919 May 22 3 +1:36:34 - RMT 1926 May 11 +2 - EET 1940 Au 5 +3 - MSK 1941 Jul +1 c CE%sT 1944 O 13 +3 R MSK/MSD 1989 Mar lastSu 2s +2 1 EEST 1989 S lastSu 2s +2 LV EE%sT 1997 Ja 21 +2 E EE%sT 2000 F 29 +2 - EET 2001 Ja 2 +2 E EE%sT +Z Europe/Rome 0:49:56 - LMT 1866 D 12 +0:49:56 - RMT 1893 O 31 23u +1 I CE%sT 1943 S 10 +1 c CE%sT 1944 Jun 4 +1 I CE%sT 1980 +1 E CE%sT +Z Europe/Samara 3:20:20 - LMT 1919 Jul 1 0u +3 - %z 1930 Jun 21 +4 - %z 1935 Ja 27 +4 R %z 1989 Mar 26 2s +3 R %z 1991 Mar 31 2s +2 R %z 1991 S 29 2s +3 - %z 1991 O 20 3 +4 R %z 2010 Mar 28 2s +3 R %z 2011 Mar 27 2s +4 - %z +Z Europe/Saratov 3:4:18 - LMT 1919 Jul 1 0u +3 - %z 1930 Jun 21 +4 R %z 1988 Mar 27 2s +3 R %z 1991 Mar 31 2s +4 - %z 1992 Mar 29 2s +3 R %z 2011 Mar 27 2s +4 - %z 2014 O 26 2s +3 - %z 2016 D 4 2s +4 - %z +Z Europe/Simferopol 2:16:24 - LMT 1880 +2:16 - SMT 1924 May 2 +2 - EET 1930 Jun 21 +3 - MSK 1941 N +1 c CE%sT 1944 Ap 13 +3 R MSK/MSD 1990 +3 - MSK 1990 Jul 1 2 +2 - EET 1992 Mar 20 +2 c EE%sT 1994 May +3 c MSK/MSD 1996 Mar 31 0s +3 1 MSD 1996 O 27 3s +3 - MSK 1997 Mar lastSu 1u +2 E EE%sT 2014 Mar 30 2 +4 - MSK 2014 O 26 2s +3 - MSK +Z Europe/Sofia 1:33:16 - LMT 1880 +1:56:56 - IMT 1894 N 30 +2 - EET 1942 N 2 3 +1 c CE%sT 1945 +1 - CET 1945 Ap 2 3 +2 - EET 1979 Mar 31 23 +2 BG EE%sT 1982 S 26 3 +2 c EE%sT 1991 +2 e EE%sT 1997 +2 E EE%sT +Z Europe/Tallinn 1:39 - LMT 1880 +1:39 - TMT 1918 F +1 c CE%sT 1919 Jul +1:39 - TMT 1921 May +2 - EET 1940 Au 6 +3 - MSK 1941 S 15 +1 c CE%sT 1944 S 22 +3 R MSK/MSD 1989 Mar 26 2s +2 1 EEST 1989 S 24 2s +2 c EE%sT 1998 S 22 +2 E EE%sT 1999 O 31 4 +2 - EET 2002 F 21 +2 E EE%sT +Z Europe/Tirane 1:19:20 - LMT 1914 +1 - CET 1940 Jun 16 +1 q CE%sT 1984 Jul +1 E CE%sT +Z Europe/Ulyanovsk 3:13:36 - LMT 1919 Jul 1 0u +3 - %z 1930 Jun 21 +4 R %z 1989 Mar 26 2s +3 R %z 1991 Mar 31 2s +2 R %z 1992 Ja 19 2s +3 R %z 2011 Mar 27 2s +4 - %z 2014 O 26 2s +3 - %z 2016 Mar 27 2s +4 - %z +Z Europe/Vienna 1:5:21 - LMT 1893 Ap +1 c CE%sT 1920 +1 a CE%sT 1940 Ap 1 2s +1 c CE%sT 1945 Ap 2 2s +1 1 CEST 1945 Ap 12 2s +1 - CET 1946 +1 a CE%sT 1981 +1 E CE%sT +Z Europe/Vilnius 1:41:16 - LMT 1880 +1:24 - WMT 1917 +1:35:36 - KMT 1919 O 10 +1 - CET 1920 Jul 12 +2 - EET 1920 O 9 +1 - CET 1940 Au 3 +3 - MSK 1941 Jun 24 +1 c CE%sT 1944 Au +3 R MSK/MSD 1989 Mar 26 2s +2 R EE%sT 1991 S 29 2s +2 c EE%sT 1998 +2 - EET 1998 Mar 29 1u +1 E CE%sT 1999 O 31 1u +2 - EET 2003 +2 E EE%sT +Z Europe/Volgograd 2:57:40 - LMT 1920 Ja 3 +3 - %z 1930 Jun 21 +4 - %z 1961 N 11 +4 R %z 1988 Mar 27 2s +3 R MSK/MSD 1991 Mar 31 2s +4 - %z 1992 Mar 29 2s +3 R MSK/MSD 2011 Mar 27 2s +4 - MSK 2014 O 26 2s +3 - MSK 2018 O 28 2s +4 - %z 2020 D 27 2s +3 - MSK +Z Europe/Warsaw 1:24 - LMT 1880 +1:24 - WMT 1915 Au 5 +1 c CE%sT 1918 S 16 3 +2 O EE%sT 1922 Jun +1 O CE%sT 1940 Jun 23 2 +1 c CE%sT 1944 O +1 O CE%sT 1977 +1 W- CE%sT 1988 +1 E CE%sT +Z Europe/Zurich 0:34:8 - LMT 1853 Jul 16 +0:29:46 - BMT 1894 Jun +1 CH CE%sT 1981 +1 E CE%sT +Z Factory 0 - -00 +Z Indian/Chagos 4:49:40 - LMT 1907 +5 - %z 1996 +6 - %z +Z Indian/Maldives 4:54 - LMT 1880 +4:54 - MMT 1960 +5 - %z +Z Indian/Mauritius 3:50 - LMT 1907 +4 MU %z +Z Pacific/Apia 12:33:4 - LMT 1892 Jul 5 +-11:26:56 - LMT 1911 +-11:30 - %z 1950 +-11 WS %z 2011 D 29 24 +13 WS %z +Z Pacific/Auckland 11:39:4 - LMT 1868 N 2 +11:30 NZ NZ%sT 1946 +12 NZ NZ%sT +Z Pacific/Bougainville 10:22:16 - LMT 1880 +9:48:32 - PMMT 1895 +10 - %z 1942 Jul +9 - %z 1945 Au 21 +10 - %z 2014 D 28 2 +11 - %z +Z Pacific/Chatham 12:13:48 - LMT 1868 N 2 +12:15 - %z 1946 +12:45 k %z +Z Pacific/Easter -7:17:28 - LMT 1890 +-7:17:28 - EMT 1932 S +-7 x %z 1982 Mar 14 3u +-6 x %z +Z Pacific/Efate 11:13:16 - LMT 1912 Ja 13 +11 VU %z +Z Pacific/Fakaofo -11:24:56 - LMT 1901 +-11 - %z 2011 D 30 +13 - %z +Z Pacific/Fiji 11:55:44 - LMT 1915 O 26 +12 FJ %z +Z Pacific/Galapagos -5:58:24 - LMT 1931 +-5 - %z 1986 +-6 EC %z +Z Pacific/Gambier -8:59:48 - LMT 1912 O +-9 - %z +Z Pacific/Guadalcanal 10:39:48 - LMT 1912 O +11 - %z +Z Pacific/Guam -14:21 - LMT 1844 D 31 +9:39 - LMT 1901 +10 - GST 1941 D 10 +9 - %z 1944 Jul 31 +10 Gu G%sT 2000 D 23 +10 - ChST +Z Pacific/Honolulu -10:31:26 - LMT 1896 Ja 13 12 +-10:30 - HST 1933 Ap 30 2 +-10:30 1 HDT 1933 May 21 12 +-10:30 u H%sT 1947 Jun 8 2 +-10 - HST +Z Pacific/Kanton 0 - -00 1937 Au 31 +-12 - %z 1979 O +-11 - %z 1994 D 31 +13 - %z +Z Pacific/Kiritimati -10:29:20 - LMT 1901 +-10:40 - %z 1979 O +-10 - %z 1994 D 31 +14 - %z +Z Pacific/Kosrae -13:8:4 - LMT 1844 D 31 +10:51:56 - LMT 1901 +11 - %z 1914 O +9 - %z 1919 F +11 - %z 1937 +10 - %z 1941 Ap +9 - %z 1945 Au +11 - %z 1969 O +12 - %z 1999 +11 - %z +Z Pacific/Kwajalein 11:9:20 - LMT 1901 +11 - %z 1937 +10 - %z 1941 Ap +9 - %z 1944 F 6 +11 - %z 1969 O +-12 - %z 1993 Au 20 24 +12 - %z +Z Pacific/Marquesas -9:18 - LMT 1912 O +-9:30 - %z +Z Pacific/Nauru 11:7:40 - LMT 1921 Ja 15 +11:30 - %z 1942 Au 29 +9 - %z 1945 S 8 +11:30 - %z 1979 F 10 2 +12 - %z +Z Pacific/Niue -11:19:40 - LMT 1952 O 16 +-11:20 - %z 1964 Jul +-11 - %z +Z Pacific/Norfolk 11:11:52 - LMT 1901 +11:12 - %z 1951 +11:30 - %z 1974 O 27 2s +11:30 1 %z 1975 Mar 2 2s +11:30 - %z 2015 O 4 2s +11 - %z 2019 Jul +11 AN %z +Z Pacific/Noumea 11:5:48 - LMT 1912 Ja 13 +11 NC %z +Z Pacific/Pago_Pago 12:37:12 - LMT 1892 Jul 5 +-11:22:48 - LMT 1911 +-11 - SST +Z Pacific/Palau -15:2:4 - LMT 1844 D 31 +8:57:56 - LMT 1901 +9 - %z +Z Pacific/Pitcairn -8:40:20 - LMT 1901 +-8:30 - %z 1998 Ap 27 +-8 - %z +Z Pacific/Port_Moresby 9:48:40 - LMT 1880 +9:48:32 - PMMT 1895 +10 - %z +Z Pacific/Rarotonga 13:20:56 - LMT 1899 D 26 +-10:39:4 - LMT 1952 O 16 +-10:30 - %z 1978 N 12 +-10 CK %z +Z Pacific/Tahiti -9:58:16 - LMT 1912 O +-10 - %z +Z Pacific/Tarawa 11:32:4 - LMT 1901 +12 - %z +Z Pacific/Tongatapu 12:19:12 - LMT 1945 S 10 +12:20 - %z 1961 +13 - %z 1999 +13 TO %z +L Etc/GMT GMT +L Australia/Sydney Australia/ACT +L Australia/Lord_Howe Australia/LHI +L Australia/Sydney Australia/NSW +L Australia/Darwin Australia/North +L Australia/Brisbane Australia/Queensland +L Australia/Adelaide Australia/South +L Australia/Hobart Australia/Tasmania +L Australia/Melbourne Australia/Victoria +L Australia/Perth Australia/West +L Australia/Broken_Hill Australia/Yancowinna +L America/Rio_Branco Brazil/Acre +L America/Noronha Brazil/DeNoronha +L America/Sao_Paulo Brazil/East +L America/Manaus Brazil/West +L Europe/Brussels CET +L America/Chicago CST6CDT +L America/Halifax Canada/Atlantic +L America/Winnipeg Canada/Central +L America/Toronto Canada/Eastern +L America/Edmonton Canada/Mountain +L America/St_Johns Canada/Newfoundland +L America/Vancouver Canada/Pacific +L America/Regina Canada/Saskatchewan +L America/Whitehorse Canada/Yukon +L America/Santiago Chile/Continental +L Pacific/Easter Chile/EasterIsland +L America/Havana Cuba +L Europe/Athens EET +L America/Panama EST +L America/New_York EST5EDT +L Africa/Cairo Egypt +L Europe/Dublin Eire +L Etc/GMT Etc/GMT+0 +L Etc/GMT Etc/GMT-0 +L Etc/GMT Etc/GMT0 +L Etc/GMT Etc/Greenwich +L Etc/UTC Etc/UCT +L Etc/UTC Etc/Universal +L Etc/UTC Etc/Zulu +L Europe/London GB +L Europe/London GB-Eire +L Etc/GMT GMT+0 +L Etc/GMT GMT-0 +L Etc/GMT GMT0 +L Etc/GMT Greenwich +L Asia/Hong_Kong Hongkong +L Africa/Abidjan Iceland +L Asia/Tehran Iran +L Asia/Jerusalem Israel +L America/Jamaica Jamaica +L Asia/Tokyo Japan +L Pacific/Kwajalein Kwajalein +L Africa/Tripoli Libya +L Europe/Brussels MET +L America/Phoenix MST +L America/Denver MST7MDT +L America/Tijuana Mexico/BajaNorte +L America/Mazatlan Mexico/BajaSur +L America/Mexico_City Mexico/General +L Pacific/Auckland NZ +L Pacific/Chatham NZ-CHAT +L America/Denver Navajo +L Asia/Shanghai PRC +L Europe/Warsaw Poland +L Europe/Lisbon Portugal +L Asia/Taipei ROC +L Asia/Seoul ROK +L Asia/Singapore Singapore +L Europe/Istanbul Turkey +L Etc/UTC UCT +L America/Anchorage US/Alaska +L America/Adak US/Aleutian +L America/Phoenix US/Arizona +L America/Chicago US/Central +L America/Indiana/Indianapolis US/East-Indiana +L America/New_York US/Eastern +L Pacific/Honolulu US/Hawaii +L America/Indiana/Knox US/Indiana-Starke +L America/Detroit US/Michigan +L America/Denver US/Mountain +L America/Los_Angeles US/Pacific +L Pacific/Pago_Pago US/Samoa +L Etc/UTC UTC +L Etc/UTC Universal +L Europe/Moscow W-SU +L Etc/UTC Zulu +L America/Argentina/Buenos_Aires America/Buenos_Aires +L America/Argentina/Catamarca America/Catamarca +L America/Argentina/Cordoba America/Cordoba +L America/Indiana/Indianapolis America/Indianapolis +L America/Argentina/Jujuy America/Jujuy +L America/Indiana/Knox America/Knox_IN +L America/Kentucky/Louisville America/Louisville +L America/Argentina/Mendoza America/Mendoza +L America/Puerto_Rico America/Virgin +L Pacific/Pago_Pago Pacific/Samoa +L Africa/Abidjan Africa/Accra +L Africa/Nairobi Africa/Addis_Ababa +L Africa/Nairobi Africa/Asmara +L Africa/Abidjan Africa/Bamako +L Africa/Lagos Africa/Bangui +L Africa/Abidjan Africa/Banjul +L Africa/Maputo Africa/Blantyre +L Africa/Lagos Africa/Brazzaville +L Africa/Maputo Africa/Bujumbura +L Africa/Abidjan Africa/Conakry +L Africa/Abidjan Africa/Dakar +L Africa/Nairobi Africa/Dar_es_Salaam +L Africa/Nairobi Africa/Djibouti +L Africa/Lagos Africa/Douala +L Africa/Abidjan Africa/Freetown +L Africa/Maputo Africa/Gaborone +L Africa/Maputo Africa/Harare +L Africa/Nairobi Africa/Kampala +L Africa/Maputo Africa/Kigali +L Africa/Lagos Africa/Kinshasa +L Africa/Lagos Africa/Libreville +L Africa/Abidjan Africa/Lome +L Africa/Lagos Africa/Luanda +L Africa/Maputo Africa/Lubumbashi +L Africa/Maputo Africa/Lusaka +L Africa/Lagos Africa/Malabo +L Africa/Johannesburg Africa/Maseru +L Africa/Johannesburg Africa/Mbabane +L Africa/Nairobi Africa/Mogadishu +L Africa/Lagos Africa/Niamey +L Africa/Abidjan Africa/Nouakchott +L Africa/Abidjan Africa/Ouagadougou +L Africa/Lagos Africa/Porto-Novo +L America/Puerto_Rico America/Anguilla +L America/Puerto_Rico America/Antigua +L America/Puerto_Rico America/Aruba +L America/Panama America/Atikokan +L America/Puerto_Rico America/Blanc-Sablon +L America/Panama America/Cayman +L America/Phoenix America/Creston +L America/Puerto_Rico America/Curacao +L America/Puerto_Rico America/Dominica +L America/Puerto_Rico America/Grenada +L America/Puerto_Rico America/Guadeloupe +L America/Puerto_Rico America/Kralendijk +L America/Puerto_Rico America/Lower_Princes +L America/Puerto_Rico America/Marigot +L America/Puerto_Rico America/Montserrat +L America/Toronto America/Nassau +L America/Puerto_Rico America/Port_of_Spain +L America/Puerto_Rico America/St_Barthelemy +L America/Puerto_Rico America/St_Kitts +L America/Puerto_Rico America/St_Lucia +L America/Puerto_Rico America/St_Thomas +L America/Puerto_Rico America/St_Vincent +L America/Puerto_Rico America/Tortola +L Pacific/Port_Moresby Antarctica/DumontDUrville +L Pacific/Auckland Antarctica/McMurdo +L Asia/Riyadh Antarctica/Syowa +L Europe/Berlin Arctic/Longyearbyen +L Asia/Riyadh Asia/Aden +L Asia/Qatar Asia/Bahrain +L Asia/Kuching Asia/Brunei +L Asia/Singapore Asia/Kuala_Lumpur +L Asia/Riyadh Asia/Kuwait +L Asia/Dubai Asia/Muscat +L Asia/Bangkok Asia/Phnom_Penh +L Asia/Bangkok Asia/Vientiane +L Africa/Abidjan Atlantic/Reykjavik +L Africa/Abidjan Atlantic/St_Helena +L Europe/Brussels Europe/Amsterdam +L Europe/Prague Europe/Bratislava +L Europe/Zurich Europe/Busingen +L Europe/Berlin Europe/Copenhagen +L Europe/London Europe/Guernsey +L Europe/London Europe/Isle_of_Man +L Europe/London Europe/Jersey +L Europe/Belgrade Europe/Ljubljana +L Europe/Brussels Europe/Luxembourg +L Europe/Helsinki Europe/Mariehamn +L Europe/Paris Europe/Monaco +L Europe/Berlin Europe/Oslo +L Europe/Belgrade Europe/Podgorica +L Europe/Rome Europe/San_Marino +L Europe/Belgrade Europe/Sarajevo +L Europe/Belgrade Europe/Skopje +L Europe/Berlin Europe/Stockholm +L Europe/Zurich Europe/Vaduz +L Europe/Rome Europe/Vatican +L Europe/Belgrade Europe/Zagreb +L Africa/Nairobi Indian/Antananarivo +L Asia/Bangkok Indian/Christmas +L Asia/Yangon Indian/Cocos +L Africa/Nairobi Indian/Comoro +L Indian/Maldives Indian/Kerguelen +L Asia/Dubai Indian/Mahe +L Africa/Nairobi Indian/Mayotte +L Asia/Dubai Indian/Reunion +L Pacific/Port_Moresby Pacific/Chuuk +L Pacific/Tarawa Pacific/Funafuti +L Pacific/Tarawa Pacific/Majuro +L Pacific/Pago_Pago Pacific/Midway +L Pacific/Guadalcanal Pacific/Pohnpei +L Pacific/Guam Pacific/Saipan +L Pacific/Tarawa Pacific/Wake +L Pacific/Tarawa Pacific/Wallis +L Africa/Abidjan Africa/Timbuktu +L America/Argentina/Catamarca America/Argentina/ComodRivadavia +L America/Adak America/Atka +L America/Panama America/Coral_Harbour +L America/Tijuana America/Ensenada +L America/Indiana/Indianapolis America/Fort_Wayne +L America/Toronto America/Montreal +L America/Toronto America/Nipigon +L America/Iqaluit America/Pangnirtung +L America/Rio_Branco America/Porto_Acre +L America/Winnipeg America/Rainy_River +L America/Argentina/Cordoba America/Rosario +L America/Tijuana America/Santa_Isabel +L America/Denver America/Shiprock +L America/Toronto America/Thunder_Bay +L America/Edmonton America/Yellowknife +L Pacific/Auckland Antarctica/South_Pole +L Asia/Ulaanbaatar Asia/Choibalsan +L Asia/Shanghai Asia/Chongqing +L Asia/Shanghai Asia/Harbin +L Asia/Urumqi Asia/Kashgar +L Asia/Jerusalem Asia/Tel_Aviv +L Europe/Berlin Atlantic/Jan_Mayen +L Australia/Sydney Australia/Canberra +L Australia/Hobart Australia/Currie +L Europe/London Europe/Belfast +L Europe/Chisinau Europe/Tiraspol +L Europe/Kyiv Europe/Uzhgorod +L Europe/Kyiv Europe/Zaporozhye +L Pacific/Kanton Pacific/Enderbury +L Pacific/Honolulu Pacific/Johnston +L Pacific/Port_Moresby Pacific/Yap +L Europe/Lisbon WET +L Africa/Nairobi Africa/Asmera +L America/Nuuk America/Godthab +L Asia/Ashgabat Asia/Ashkhabad +L Asia/Kolkata Asia/Calcutta +L Asia/Shanghai Asia/Chungking +L Asia/Dhaka Asia/Dacca +L Europe/Istanbul Asia/Istanbul +L Asia/Kathmandu Asia/Katmandu +L Asia/Macau Asia/Macao +L Asia/Yangon Asia/Rangoon +L Asia/Ho_Chi_Minh Asia/Saigon +L Asia/Thimphu Asia/Thimbu +L Asia/Makassar Asia/Ujung_Pandang +L Asia/Ulaanbaatar Asia/Ulan_Bator +L Atlantic/Faroe Atlantic/Faeroe +L Europe/Kyiv Europe/Kiev +L Asia/Nicosia Europe/Nicosia +L Pacific/Honolulu HST +L America/Los_Angeles PST8PDT +L Pacific/Guadalcanal Pacific/Ponape +L Pacific/Port_Moresby Pacific/Truk diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/zone.tab b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/zone.tab new file mode 100644 index 0000000000000000000000000000000000000000..2ae8a8c439ebd47c02f2e10b766de265fafa514e --- /dev/null +++ b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/zone.tab @@ -0,0 +1,448 @@ +# tzdb timezone descriptions (deprecated version) +# +# This file is in the public domain, so clarified as of +# 2009-05-17 by Arthur David Olson. +# +# From Paul Eggert (2021-09-20): +# This file is intended as a backward-compatibility aid for older programs. +# New programs should use zone1970.tab. This file is like zone1970.tab (see +# zone1970.tab's comments), but with the following additional restrictions: +# +# 1. This file contains only ASCII characters. +# 2. The first data column contains exactly one country code. +# +# Because of (2), each row stands for an area that is the intersection +# of a region identified by a country code and of a timezone where civil +# clocks have agreed since 1970; this is a narrower definition than +# that of zone1970.tab. +# +# Unlike zone1970.tab, a row's third column can be a Link from +# 'backward' instead of a Zone. +# +# This table is intended as an aid for users, to help them select timezones +# appropriate for their practical needs. It is not intended to take or +# endorse any position on legal or territorial claims. +# +#country- +#code coordinates TZ comments +AD +4230+00131 Europe/Andorra +AE +2518+05518 Asia/Dubai +AF +3431+06912 Asia/Kabul +AG +1703-06148 America/Antigua +AI +1812-06304 America/Anguilla +AL +4120+01950 Europe/Tirane +AM +4011+04430 Asia/Yerevan +AO -0848+01314 Africa/Luanda +AQ -7750+16636 Antarctica/McMurdo New Zealand time - McMurdo, South Pole +AQ -6617+11031 Antarctica/Casey Casey +AQ -6835+07758 Antarctica/Davis Davis +AQ -6640+14001 Antarctica/DumontDUrville Dumont-d'Urville +AQ -6736+06253 Antarctica/Mawson Mawson +AQ -6448-06406 Antarctica/Palmer Palmer +AQ -6734-06808 Antarctica/Rothera Rothera +AQ -690022+0393524 Antarctica/Syowa Syowa +AQ -720041+0023206 Antarctica/Troll Troll +AQ -7824+10654 Antarctica/Vostok Vostok +AR -3436-05827 America/Argentina/Buenos_Aires Buenos Aires (BA, CF) +AR -3124-06411 America/Argentina/Cordoba Argentina (most areas: CB, CC, CN, ER, FM, MN, SE, SF) +AR -2447-06525 America/Argentina/Salta Salta (SA, LP, NQ, RN) +AR -2411-06518 America/Argentina/Jujuy Jujuy (JY) +AR -2649-06513 America/Argentina/Tucuman Tucuman (TM) +AR -2828-06547 America/Argentina/Catamarca Catamarca (CT), Chubut (CH) +AR -2926-06651 America/Argentina/La_Rioja La Rioja (LR) +AR -3132-06831 America/Argentina/San_Juan San Juan (SJ) +AR -3253-06849 America/Argentina/Mendoza Mendoza (MZ) +AR -3319-06621 America/Argentina/San_Luis San Luis (SL) +AR -5138-06913 America/Argentina/Rio_Gallegos Santa Cruz (SC) +AR -5448-06818 America/Argentina/Ushuaia Tierra del Fuego (TF) +AS -1416-17042 Pacific/Pago_Pago +AT +4813+01620 Europe/Vienna +AU -3133+15905 Australia/Lord_Howe Lord Howe Island +AU -5430+15857 Antarctica/Macquarie Macquarie Island +AU -4253+14719 Australia/Hobart Tasmania +AU -3749+14458 Australia/Melbourne Victoria +AU -3352+15113 Australia/Sydney New South Wales (most areas) +AU -3157+14127 Australia/Broken_Hill New South Wales (Yancowinna) +AU -2728+15302 Australia/Brisbane Queensland (most areas) +AU -2016+14900 Australia/Lindeman Queensland (Whitsunday Islands) +AU -3455+13835 Australia/Adelaide South Australia +AU -1228+13050 Australia/Darwin Northern Territory +AU -3157+11551 Australia/Perth Western Australia (most areas) +AU -3143+12852 Australia/Eucla Western Australia (Eucla) +AW +1230-06958 America/Aruba +AX +6006+01957 Europe/Mariehamn +AZ +4023+04951 Asia/Baku +BA +4352+01825 Europe/Sarajevo +BB +1306-05937 America/Barbados +BD +2343+09025 Asia/Dhaka +BE +5050+00420 Europe/Brussels +BF +1222-00131 Africa/Ouagadougou +BG +4241+02319 Europe/Sofia +BH +2623+05035 Asia/Bahrain +BI -0323+02922 Africa/Bujumbura +BJ +0629+00237 Africa/Porto-Novo +BL +1753-06251 America/St_Barthelemy +BM +3217-06446 Atlantic/Bermuda +BN +0456+11455 Asia/Brunei +BO -1630-06809 America/La_Paz +BQ +120903-0681636 America/Kralendijk +BR -0351-03225 America/Noronha Atlantic islands +BR -0127-04829 America/Belem Para (east), Amapa +BR -0343-03830 America/Fortaleza Brazil (northeast: MA, PI, CE, RN, PB) +BR -0803-03454 America/Recife Pernambuco +BR -0712-04812 America/Araguaina Tocantins +BR -0940-03543 America/Maceio Alagoas, Sergipe +BR -1259-03831 America/Bahia Bahia +BR -2332-04637 America/Sao_Paulo Brazil (southeast: GO, DF, MG, ES, RJ, SP, PR, SC, RS) +BR -2027-05437 America/Campo_Grande Mato Grosso do Sul +BR -1535-05605 America/Cuiaba Mato Grosso +BR -0226-05452 America/Santarem Para (west) +BR -0846-06354 America/Porto_Velho Rondonia +BR +0249-06040 America/Boa_Vista Roraima +BR -0308-06001 America/Manaus Amazonas (east) +BR -0640-06952 America/Eirunepe Amazonas (west) +BR -0958-06748 America/Rio_Branco Acre +BS +2505-07721 America/Nassau +BT +2728+08939 Asia/Thimphu +BW -2439+02555 Africa/Gaborone +BY +5354+02734 Europe/Minsk +BZ +1730-08812 America/Belize +CA +4734-05243 America/St_Johns Newfoundland, Labrador (SE) +CA +4439-06336 America/Halifax Atlantic - NS (most areas), PE +CA +4612-05957 America/Glace_Bay Atlantic - NS (Cape Breton) +CA +4606-06447 America/Moncton Atlantic - New Brunswick +CA +5320-06025 America/Goose_Bay Atlantic - Labrador (most areas) +CA +5125-05707 America/Blanc-Sablon AST - QC (Lower North Shore) +CA +4339-07923 America/Toronto Eastern - ON & QC (most areas) +CA +6344-06828 America/Iqaluit Eastern - NU (most areas) +CA +484531-0913718 America/Atikokan EST - ON (Atikokan), NU (Coral H) +CA +4953-09709 America/Winnipeg Central - ON (west), Manitoba +CA +744144-0944945 America/Resolute Central - NU (Resolute) +CA +624900-0920459 America/Rankin_Inlet Central - NU (central) +CA +5024-10439 America/Regina CST - SK (most areas) +CA +5017-10750 America/Swift_Current CST - SK (midwest) +CA +5333-11328 America/Edmonton Mountain - AB, BC(E), NT(E), SK(W) +CA +690650-1050310 America/Cambridge_Bay Mountain - NU (west) +CA +682059-1334300 America/Inuvik Mountain - NT (west) +CA +4916-12307 America/Vancouver MST - BC (most areas) +CA +4906-11631 America/Creston MST - BC (Creston) +CA +5546-12014 America/Dawson_Creek MST - BC (Dawson Cr, Ft St John) +CA +5848-12242 America/Fort_Nelson MST - BC (Ft Nelson) +CA +6043-13503 America/Whitehorse MST - Yukon (east) +CA +6404-13925 America/Dawson MST - Yukon (west) +CC -1210+09655 Indian/Cocos +CD -0418+01518 Africa/Kinshasa Dem. Rep. of Congo (west) +CD -1140+02728 Africa/Lubumbashi Dem. Rep. of Congo (east) +CF +0422+01835 Africa/Bangui +CG -0416+01517 Africa/Brazzaville +CH +4723+00832 Europe/Zurich +CI +0519-00402 Africa/Abidjan +CK -2114-15946 Pacific/Rarotonga +CL -3327-07040 America/Santiago most of Chile +CL -4534-07204 America/Coyhaique Aysen Region +CL -5309-07055 America/Punta_Arenas Magallanes Region +CL -2709-10926 Pacific/Easter Easter Island +CM +0403+00942 Africa/Douala +CN +3114+12128 Asia/Shanghai Beijing Time +CN +4348+08735 Asia/Urumqi Xinjiang Time +CO +0436-07405 America/Bogota +CR +0956-08405 America/Costa_Rica +CU +2308-08222 America/Havana +CV +1455-02331 Atlantic/Cape_Verde +CW +1211-06900 America/Curacao +CX -1025+10543 Indian/Christmas +CY +3510+03322 Asia/Nicosia most of Cyprus +CY +3507+03357 Asia/Famagusta Northern Cyprus +CZ +5005+01426 Europe/Prague +DE +5230+01322 Europe/Berlin most of Germany +DE +4742+00841 Europe/Busingen Busingen +DJ +1136+04309 Africa/Djibouti +DK +5540+01235 Europe/Copenhagen +DM +1518-06124 America/Dominica +DO +1828-06954 America/Santo_Domingo +DZ +3647+00303 Africa/Algiers +EC -0210-07950 America/Guayaquil Ecuador (mainland) +EC -0054-08936 Pacific/Galapagos Galapagos Islands +EE +5925+02445 Europe/Tallinn +EG +3003+03115 Africa/Cairo +EH +2709-01312 Africa/El_Aaiun +ER +1520+03853 Africa/Asmara +ES +4024-00341 Europe/Madrid Spain (mainland) +ES +3553-00519 Africa/Ceuta Ceuta, Melilla +ES +2806-01524 Atlantic/Canary Canary Islands +ET +0902+03842 Africa/Addis_Ababa +FI +6010+02458 Europe/Helsinki +FJ -1808+17825 Pacific/Fiji +FK -5142-05751 Atlantic/Stanley +FM +0725+15147 Pacific/Chuuk Chuuk/Truk, Yap +FM +0658+15813 Pacific/Pohnpei Pohnpei/Ponape +FM +0519+16259 Pacific/Kosrae Kosrae +FO +6201-00646 Atlantic/Faroe +FR +4852+00220 Europe/Paris +GA +0023+00927 Africa/Libreville +GB +513030-0000731 Europe/London +GD +1203-06145 America/Grenada +GE +4143+04449 Asia/Tbilisi +GF +0456-05220 America/Cayenne +GG +492717-0023210 Europe/Guernsey +GH +0533-00013 Africa/Accra +GI +3608-00521 Europe/Gibraltar +GL +6411-05144 America/Nuuk most of Greenland +GL +7646-01840 America/Danmarkshavn National Park (east coast) +GL +7029-02158 America/Scoresbysund Scoresbysund/Ittoqqortoormiit +GL +7634-06847 America/Thule Thule/Pituffik +GM +1328-01639 Africa/Banjul +GN +0931-01343 Africa/Conakry +GP +1614-06132 America/Guadeloupe +GQ +0345+00847 Africa/Malabo +GR +3758+02343 Europe/Athens +GS -5416-03632 Atlantic/South_Georgia +GT +1438-09031 America/Guatemala +GU +1328+14445 Pacific/Guam +GW +1151-01535 Africa/Bissau +GY +0648-05810 America/Guyana +HK +2217+11409 Asia/Hong_Kong +HN +1406-08713 America/Tegucigalpa +HR +4548+01558 Europe/Zagreb +HT +1832-07220 America/Port-au-Prince +HU +4730+01905 Europe/Budapest +ID -0610+10648 Asia/Jakarta Java, Sumatra +ID -0002+10920 Asia/Pontianak Borneo (west, central) +ID -0507+11924 Asia/Makassar Borneo (east, south), Sulawesi/Celebes, Bali, Nusa Tengarra, Timor (west) +ID -0232+14042 Asia/Jayapura New Guinea (West Papua / Irian Jaya), Malukus/Moluccas +IE +5320-00615 Europe/Dublin +IL +314650+0351326 Asia/Jerusalem +IM +5409-00428 Europe/Isle_of_Man +IN +2232+08822 Asia/Kolkata +IO -0720+07225 Indian/Chagos +IQ +3321+04425 Asia/Baghdad +IR +3540+05126 Asia/Tehran +IS +6409-02151 Atlantic/Reykjavik +IT +4154+01229 Europe/Rome +JE +491101-0020624 Europe/Jersey +JM +175805-0764736 America/Jamaica +JO +3157+03556 Asia/Amman +JP +353916+1394441 Asia/Tokyo +KE -0117+03649 Africa/Nairobi +KG +4254+07436 Asia/Bishkek +KH +1133+10455 Asia/Phnom_Penh +KI +0125+17300 Pacific/Tarawa Gilbert Islands +KI -0247-17143 Pacific/Kanton Phoenix Islands +KI +0152-15720 Pacific/Kiritimati Line Islands +KM -1141+04316 Indian/Comoro +KN +1718-06243 America/St_Kitts +KP +3901+12545 Asia/Pyongyang +KR +3733+12658 Asia/Seoul +KW +2920+04759 Asia/Kuwait +KY +1918-08123 America/Cayman +KZ +4315+07657 Asia/Almaty most of Kazakhstan +KZ +4448+06528 Asia/Qyzylorda Qyzylorda/Kyzylorda/Kzyl-Orda +KZ +5312+06337 Asia/Qostanay Qostanay/Kostanay/Kustanay +KZ +5017+05710 Asia/Aqtobe Aqtobe/Aktobe +KZ +4431+05016 Asia/Aqtau Mangghystau/Mankistau +KZ +4707+05156 Asia/Atyrau Atyrau/Atirau/Gur'yev +KZ +5113+05121 Asia/Oral West Kazakhstan +LA +1758+10236 Asia/Vientiane +LB +3353+03530 Asia/Beirut +LC +1401-06100 America/St_Lucia +LI +4709+00931 Europe/Vaduz +LK +0656+07951 Asia/Colombo +LR +0618-01047 Africa/Monrovia +LS -2928+02730 Africa/Maseru +LT +5441+02519 Europe/Vilnius +LU +4936+00609 Europe/Luxembourg +LV +5657+02406 Europe/Riga +LY +3254+01311 Africa/Tripoli +MA +3339-00735 Africa/Casablanca +MC +4342+00723 Europe/Monaco +MD +4700+02850 Europe/Chisinau +ME +4226+01916 Europe/Podgorica +MF +1804-06305 America/Marigot +MG -1855+04731 Indian/Antananarivo +MH +0709+17112 Pacific/Majuro most of Marshall Islands +MH +0905+16720 Pacific/Kwajalein Kwajalein +MK +4159+02126 Europe/Skopje +ML +1239-00800 Africa/Bamako +MM +1647+09610 Asia/Yangon +MN +4755+10653 Asia/Ulaanbaatar most of Mongolia +MN +4801+09139 Asia/Hovd Bayan-Olgii, Hovd, Uvs +MO +221150+1133230 Asia/Macau +MP +1512+14545 Pacific/Saipan +MQ +1436-06105 America/Martinique +MR +1806-01557 Africa/Nouakchott +MS +1643-06213 America/Montserrat +MT +3554+01431 Europe/Malta +MU -2010+05730 Indian/Mauritius +MV +0410+07330 Indian/Maldives +MW -1547+03500 Africa/Blantyre +MX +1924-09909 America/Mexico_City Central Mexico +MX +2105-08646 America/Cancun Quintana Roo +MX +2058-08937 America/Merida Campeche, Yucatan +MX +2540-10019 America/Monterrey Durango; Coahuila, Nuevo Leon, Tamaulipas (most areas) +MX +2550-09730 America/Matamoros Coahuila, Nuevo Leon, Tamaulipas (US border) +MX +2838-10605 America/Chihuahua Chihuahua (most areas) +MX +3144-10629 America/Ciudad_Juarez Chihuahua (US border - west) +MX +2934-10425 America/Ojinaga Chihuahua (US border - east) +MX +2313-10625 America/Mazatlan Baja California Sur, Nayarit (most areas), Sinaloa +MX +2048-10515 America/Bahia_Banderas Bahia de Banderas +MX +2904-11058 America/Hermosillo Sonora +MX +3232-11701 America/Tijuana Baja California +MY +0310+10142 Asia/Kuala_Lumpur Malaysia (peninsula) +MY +0133+11020 Asia/Kuching Sabah, Sarawak +MZ -2558+03235 Africa/Maputo +NA -2234+01706 Africa/Windhoek +NC -2216+16627 Pacific/Noumea +NE +1331+00207 Africa/Niamey +NF -2903+16758 Pacific/Norfolk +NG +0627+00324 Africa/Lagos +NI +1209-08617 America/Managua +NL +5222+00454 Europe/Amsterdam +NO +5955+01045 Europe/Oslo +NP +2743+08519 Asia/Kathmandu +NR -0031+16655 Pacific/Nauru +NU -1901-16955 Pacific/Niue +NZ -3652+17446 Pacific/Auckland most of New Zealand +NZ -4357-17633 Pacific/Chatham Chatham Islands +OM +2336+05835 Asia/Muscat +PA +0858-07932 America/Panama +PE -1203-07703 America/Lima +PF -1732-14934 Pacific/Tahiti Society Islands +PF -0900-13930 Pacific/Marquesas Marquesas Islands +PF -2308-13457 Pacific/Gambier Gambier Islands +PG -0930+14710 Pacific/Port_Moresby most of Papua New Guinea +PG -0613+15534 Pacific/Bougainville Bougainville +PH +143512+1205804 Asia/Manila +PK +2452+06703 Asia/Karachi +PL +5215+02100 Europe/Warsaw +PM +4703-05620 America/Miquelon +PN -2504-13005 Pacific/Pitcairn +PR +182806-0660622 America/Puerto_Rico +PS +3130+03428 Asia/Gaza Gaza Strip +PS +313200+0350542 Asia/Hebron West Bank +PT +3843-00908 Europe/Lisbon Portugal (mainland) +PT +3238-01654 Atlantic/Madeira Madeira Islands +PT +3744-02540 Atlantic/Azores Azores +PW +0720+13429 Pacific/Palau +PY -2516-05740 America/Asuncion +QA +2517+05132 Asia/Qatar +RE -2052+05528 Indian/Reunion +RO +4426+02606 Europe/Bucharest +RS +4450+02030 Europe/Belgrade +RU +5443+02030 Europe/Kaliningrad MSK-01 - Kaliningrad +RU +554521+0373704 Europe/Moscow MSK+00 - Moscow area +# The obsolescent zone.tab format cannot represent Europe/Simferopol well. +# Put it in RU section and list as UA. See "territorial claims" above. +# Programs should use zone1970.tab instead; see above. +UA +4457+03406 Europe/Simferopol Crimea +RU +5836+04939 Europe/Kirov MSK+00 - Kirov +RU +4844+04425 Europe/Volgograd MSK+00 - Volgograd +RU +4621+04803 Europe/Astrakhan MSK+01 - Astrakhan +RU +5134+04602 Europe/Saratov MSK+01 - Saratov +RU +5420+04824 Europe/Ulyanovsk MSK+01 - Ulyanovsk +RU +5312+05009 Europe/Samara MSK+01 - Samara, Udmurtia +RU +5651+06036 Asia/Yekaterinburg MSK+02 - Urals +RU +5500+07324 Asia/Omsk MSK+03 - Omsk +RU +5502+08255 Asia/Novosibirsk MSK+04 - Novosibirsk +RU +5322+08345 Asia/Barnaul MSK+04 - Altai +RU +5630+08458 Asia/Tomsk MSK+04 - Tomsk +RU +5345+08707 Asia/Novokuznetsk MSK+04 - Kemerovo +RU +5601+09250 Asia/Krasnoyarsk MSK+04 - Krasnoyarsk area +RU +5216+10420 Asia/Irkutsk MSK+05 - Irkutsk, Buryatia +RU +5203+11328 Asia/Chita MSK+06 - Zabaykalsky +RU +6200+12940 Asia/Yakutsk MSK+06 - Lena River +RU +623923+1353314 Asia/Khandyga MSK+06 - Tomponsky, Ust-Maysky +RU +4310+13156 Asia/Vladivostok MSK+07 - Amur River +RU +643337+1431336 Asia/Ust-Nera MSK+07 - Oymyakonsky +RU +5934+15048 Asia/Magadan MSK+08 - Magadan +RU +4658+14242 Asia/Sakhalin MSK+08 - Sakhalin Island +RU +6728+15343 Asia/Srednekolymsk MSK+08 - Sakha (E), N Kuril Is +RU +5301+15839 Asia/Kamchatka MSK+09 - Kamchatka +RU +6445+17729 Asia/Anadyr MSK+09 - Bering Sea +RW -0157+03004 Africa/Kigali +SA +2438+04643 Asia/Riyadh +SB -0932+16012 Pacific/Guadalcanal +SC -0440+05528 Indian/Mahe +SD +1536+03232 Africa/Khartoum +SE +5920+01803 Europe/Stockholm +SG +0117+10351 Asia/Singapore +SH -1555-00542 Atlantic/St_Helena +SI +4603+01431 Europe/Ljubljana +SJ +7800+01600 Arctic/Longyearbyen +SK +4809+01707 Europe/Bratislava +SL +0830-01315 Africa/Freetown +SM +4355+01228 Europe/San_Marino +SN +1440-01726 Africa/Dakar +SO +0204+04522 Africa/Mogadishu +SR +0550-05510 America/Paramaribo +SS +0451+03137 Africa/Juba +ST +0020+00644 Africa/Sao_Tome +SV +1342-08912 America/El_Salvador +SX +180305-0630250 America/Lower_Princes +SY +3330+03618 Asia/Damascus +SZ -2618+03106 Africa/Mbabane +TC +2128-07108 America/Grand_Turk +TD +1207+01503 Africa/Ndjamena +TF -492110+0701303 Indian/Kerguelen +TG +0608+00113 Africa/Lome +TH +1345+10031 Asia/Bangkok +TJ +3835+06848 Asia/Dushanbe +TK -0922-17114 Pacific/Fakaofo +TL -0833+12535 Asia/Dili +TM +3757+05823 Asia/Ashgabat +TN +3648+01011 Africa/Tunis +TO -210800-1751200 Pacific/Tongatapu +TR +4101+02858 Europe/Istanbul +TT +1039-06131 America/Port_of_Spain +TV -0831+17913 Pacific/Funafuti +TW +2503+12130 Asia/Taipei +TZ -0648+03917 Africa/Dar_es_Salaam +UA +5026+03031 Europe/Kyiv most of Ukraine +UG +0019+03225 Africa/Kampala +UM +2813-17722 Pacific/Midway Midway Islands +UM +1917+16637 Pacific/Wake Wake Island +US +404251-0740023 America/New_York Eastern (most areas) +US +421953-0830245 America/Detroit Eastern - MI (most areas) +US +381515-0854534 America/Kentucky/Louisville Eastern - KY (Louisville area) +US +364947-0845057 America/Kentucky/Monticello Eastern - KY (Wayne) +US +394606-0860929 America/Indiana/Indianapolis Eastern - IN (most areas) +US +384038-0873143 America/Indiana/Vincennes Eastern - IN (Da, Du, K, Mn) +US +410305-0863611 America/Indiana/Winamac Eastern - IN (Pulaski) +US +382232-0862041 America/Indiana/Marengo Eastern - IN (Crawford) +US +382931-0871643 America/Indiana/Petersburg Eastern - IN (Pike) +US +384452-0850402 America/Indiana/Vevay Eastern - IN (Switzerland) +US +415100-0873900 America/Chicago Central (most areas) +US +375711-0864541 America/Indiana/Tell_City Central - IN (Perry) +US +411745-0863730 America/Indiana/Knox Central - IN (Starke) +US +450628-0873651 America/Menominee Central - MI (Wisconsin border) +US +470659-1011757 America/North_Dakota/Center Central - ND (Oliver) +US +465042-1012439 America/North_Dakota/New_Salem Central - ND (Morton rural) +US +471551-1014640 America/North_Dakota/Beulah Central - ND (Mercer) +US +394421-1045903 America/Denver Mountain (most areas) +US +433649-1161209 America/Boise Mountain - ID (south), OR (east) +US +332654-1120424 America/Phoenix MST - AZ (except Navajo) +US +340308-1181434 America/Los_Angeles Pacific +US +611305-1495401 America/Anchorage Alaska (most areas) +US +581807-1342511 America/Juneau Alaska - Juneau area +US +571035-1351807 America/Sitka Alaska - Sitka area +US +550737-1313435 America/Metlakatla Alaska - Annette Island +US +593249-1394338 America/Yakutat Alaska - Yakutat +US +643004-1652423 America/Nome Alaska (west) +US +515248-1763929 America/Adak Alaska - western Aleutians +US +211825-1575130 Pacific/Honolulu Hawaii +UY -345433-0561245 America/Montevideo +UZ +3940+06648 Asia/Samarkand Uzbekistan (west) +UZ +4120+06918 Asia/Tashkent Uzbekistan (east) +VA +415408+0122711 Europe/Vatican +VC +1309-06114 America/St_Vincent +VE +1030-06656 America/Caracas +VG +1827-06437 America/Tortola +VI +1821-06456 America/St_Thomas +VN +1045+10640 Asia/Ho_Chi_Minh +VU -1740+16825 Pacific/Efate +WF -1318-17610 Pacific/Wallis +WS -1350-17144 Pacific/Apia +YE +1245+04512 Asia/Aden +YT -1247+04514 Indian/Mayotte +ZA -2615+02800 Africa/Johannesburg +ZM -1525+02817 Africa/Lusaka +ZW -1750+03103 Africa/Harare diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/zone1970.tab b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/zone1970.tab new file mode 100644 index 0000000000000000000000000000000000000000..a9b47bcb309a699c7570a0712ad83eee1b1e3846 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/zone1970.tab @@ -0,0 +1,375 @@ +# tzdb timezone descriptions +# +# This file is in the public domain. +# +# From Paul Eggert (2025-05-15): +# This file contains a table where each row stands for a timezone where +# civil timestamps have agreed since 1970. Columns are separated by +# a single tab. Lines beginning with ‘#’ are comments. All text uses +# UTF-8 encoding. The columns of the table are as follows: +# +# 1. The countries that overlap the timezone, as a comma-separated list +# of ISO 3166 2-character country codes. +# 2. Latitude and longitude of the timezone’s principal location +# in ISO 6709 sign-degrees-minutes-seconds format, +# either ±DDMM±DDDMM or ±DDMMSS±DDDMMSS, +# first latitude (+ is north), then longitude (+ is east). +# 3. Timezone name used in value of TZ environment variable. +# Please see the theory.html file for how these names are chosen. +# If multiple timezones overlap a country, each has a row in the +# table, with each column 1 containing the country code. +# 4. Comments; present if and only if countries have multiple timezones, +# and useful only for those countries. For example, the comments +# for the row with countries CH,DE,LI and name Europe/Zurich +# are useful only for DE, since CH and LI have no other timezones. +# +# If a timezone covers multiple countries, the most-populous city is used, +# and that country is listed first in column 1; any other countries +# are listed alphabetically by country code. The table is sorted +# first by country code, then (if possible) by an order within the +# country that (1) makes some geographical sense, and (2) puts the +# most populous timezones first, where that does not contradict (1). +# +# This table is intended as an aid for users, to help them select timezones +# appropriate for their practical needs. It is not intended to take or +# endorse any position on legal or territorial claims. +# +#country- +#codes coordinates TZ comments +AD +4230+00131 Europe/Andorra +AE,OM,RE,SC,TF +2518+05518 Asia/Dubai Crozet +AF +3431+06912 Asia/Kabul +AL +4120+01950 Europe/Tirane +AM +4011+04430 Asia/Yerevan +AQ -6617+11031 Antarctica/Casey Casey +AQ -6835+07758 Antarctica/Davis Davis +AQ -6736+06253 Antarctica/Mawson Mawson +AQ -6448-06406 Antarctica/Palmer Palmer +AQ -6734-06808 Antarctica/Rothera Rothera +AQ -720041+0023206 Antarctica/Troll Troll +AQ -7824+10654 Antarctica/Vostok Vostok +AR -3436-05827 America/Argentina/Buenos_Aires Buenos Aires (BA, CF) +AR -3124-06411 America/Argentina/Cordoba most areas: CB, CC, CN, ER, FM, MN, SE, SF +AR -2447-06525 America/Argentina/Salta Salta (SA, LP, NQ, RN) +AR -2411-06518 America/Argentina/Jujuy Jujuy (JY) +AR -2649-06513 America/Argentina/Tucuman Tucumán (TM) +AR -2828-06547 America/Argentina/Catamarca Catamarca (CT), Chubut (CH) +AR -2926-06651 America/Argentina/La_Rioja La Rioja (LR) +AR -3132-06831 America/Argentina/San_Juan San Juan (SJ) +AR -3253-06849 America/Argentina/Mendoza Mendoza (MZ) +AR -3319-06621 America/Argentina/San_Luis San Luis (SL) +AR -5138-06913 America/Argentina/Rio_Gallegos Santa Cruz (SC) +AR -5448-06818 America/Argentina/Ushuaia Tierra del Fuego (TF) +AS,UM -1416-17042 Pacific/Pago_Pago Midway +AT +4813+01620 Europe/Vienna +AU -3133+15905 Australia/Lord_Howe Lord Howe Island +AU -5430+15857 Antarctica/Macquarie Macquarie Island +AU -4253+14719 Australia/Hobart Tasmania +AU -3749+14458 Australia/Melbourne Victoria +AU -3352+15113 Australia/Sydney New South Wales (most areas) +AU -3157+14127 Australia/Broken_Hill New South Wales (Yancowinna) +AU -2728+15302 Australia/Brisbane Queensland (most areas) +AU -2016+14900 Australia/Lindeman Queensland (Whitsunday Islands) +AU -3455+13835 Australia/Adelaide South Australia +AU -1228+13050 Australia/Darwin Northern Territory +AU -3157+11551 Australia/Perth Western Australia (most areas) +AU -3143+12852 Australia/Eucla Western Australia (Eucla) +AZ +4023+04951 Asia/Baku +BB +1306-05937 America/Barbados +BD +2343+09025 Asia/Dhaka +BE,LU,NL +5050+00420 Europe/Brussels +BG +4241+02319 Europe/Sofia +BM +3217-06446 Atlantic/Bermuda +BO -1630-06809 America/La_Paz +BR -0351-03225 America/Noronha Atlantic islands +BR -0127-04829 America/Belem Pará (east), Amapá +BR -0343-03830 America/Fortaleza Brazil (northeast: MA, PI, CE, RN, PB) +BR -0803-03454 America/Recife Pernambuco +BR -0712-04812 America/Araguaina Tocantins +BR -0940-03543 America/Maceio Alagoas, Sergipe +BR -1259-03831 America/Bahia Bahia +BR -2332-04637 America/Sao_Paulo Brazil (southeast: GO, DF, MG, ES, RJ, SP, PR, SC, RS) +BR -2027-05437 America/Campo_Grande Mato Grosso do Sul +BR -1535-05605 America/Cuiaba Mato Grosso +BR -0226-05452 America/Santarem Pará (west) +BR -0846-06354 America/Porto_Velho Rondônia +BR +0249-06040 America/Boa_Vista Roraima +BR -0308-06001 America/Manaus Amazonas (east) +BR -0640-06952 America/Eirunepe Amazonas (west) +BR -0958-06748 America/Rio_Branco Acre +BT +2728+08939 Asia/Thimphu +BY +5354+02734 Europe/Minsk +BZ +1730-08812 America/Belize +CA +4734-05243 America/St_Johns Newfoundland, Labrador (SE) +CA +4439-06336 America/Halifax Atlantic - NS (most areas), PE +CA +4612-05957 America/Glace_Bay Atlantic - NS (Cape Breton) +CA +4606-06447 America/Moncton Atlantic - New Brunswick +CA +5320-06025 America/Goose_Bay Atlantic - Labrador (most areas) +CA,BS +4339-07923 America/Toronto Eastern - ON & QC (most areas) +CA +6344-06828 America/Iqaluit Eastern - NU (most areas) +CA +4953-09709 America/Winnipeg Central - ON (west), Manitoba +CA +744144-0944945 America/Resolute Central - NU (Resolute) +CA +624900-0920459 America/Rankin_Inlet Central - NU (central) +CA +5024-10439 America/Regina CST - SK (most areas) +CA +5017-10750 America/Swift_Current CST - SK (midwest) +CA +5333-11328 America/Edmonton Mountain - AB, BC(E), NT(E), SK(W) +CA +690650-1050310 America/Cambridge_Bay Mountain - NU (west) +CA +682059-1334300 America/Inuvik Mountain - NT (west) +CA +4916-12307 America/Vancouver MST - BC (most areas) +CA +5546-12014 America/Dawson_Creek MST - BC (Dawson Cr, Ft St John) +CA +5848-12242 America/Fort_Nelson MST - BC (Ft Nelson) +CA +6043-13503 America/Whitehorse MST - Yukon (east) +CA +6404-13925 America/Dawson MST - Yukon (west) +CH,DE,LI +4723+00832 Europe/Zurich Büsingen +CI,BF,GH,GM,GN,IS,ML,MR,SH,SL,SN,TG +0519-00402 Africa/Abidjan +CK -2114-15946 Pacific/Rarotonga +CL -3327-07040 America/Santiago most of Chile +CL -4534-07204 America/Coyhaique Aysén Region +CL -5309-07055 America/Punta_Arenas Magallanes Region +CL -2709-10926 Pacific/Easter Easter Island +CN +3114+12128 Asia/Shanghai Beijing Time +CN +4348+08735 Asia/Urumqi Xinjiang Time +CO +0436-07405 America/Bogota +CR +0956-08405 America/Costa_Rica +CU +2308-08222 America/Havana +CV +1455-02331 Atlantic/Cape_Verde +CY +3510+03322 Asia/Nicosia most of Cyprus +CY +3507+03357 Asia/Famagusta Northern Cyprus +CZ,SK +5005+01426 Europe/Prague +DE,DK,NO,SE,SJ +5230+01322 Europe/Berlin most of Germany +DO +1828-06954 America/Santo_Domingo +DZ +3647+00303 Africa/Algiers +EC -0210-07950 America/Guayaquil Ecuador (mainland) +EC -0054-08936 Pacific/Galapagos Galápagos Islands +EE +5925+02445 Europe/Tallinn +EG +3003+03115 Africa/Cairo +EH +2709-01312 Africa/El_Aaiun +ES +4024-00341 Europe/Madrid Spain (mainland) +ES +3553-00519 Africa/Ceuta Ceuta, Melilla +ES +2806-01524 Atlantic/Canary Canary Islands +FI,AX +6010+02458 Europe/Helsinki +FJ -1808+17825 Pacific/Fiji +FK -5142-05751 Atlantic/Stanley +FM +0519+16259 Pacific/Kosrae Kosrae +FO +6201-00646 Atlantic/Faroe +FR,MC +4852+00220 Europe/Paris +GB,GG,IM,JE +513030-0000731 Europe/London +GE +4143+04449 Asia/Tbilisi +GF +0456-05220 America/Cayenne +GI +3608-00521 Europe/Gibraltar +GL +6411-05144 America/Nuuk most of Greenland +GL +7646-01840 America/Danmarkshavn National Park (east coast) +GL +7029-02158 America/Scoresbysund Scoresbysund/Ittoqqortoormiit +GL +7634-06847 America/Thule Thule/Pituffik +GR +3758+02343 Europe/Athens +GS -5416-03632 Atlantic/South_Georgia +GT +1438-09031 America/Guatemala +GU,MP +1328+14445 Pacific/Guam +GW +1151-01535 Africa/Bissau +GY +0648-05810 America/Guyana +HK +2217+11409 Asia/Hong_Kong +HN +1406-08713 America/Tegucigalpa +HT +1832-07220 America/Port-au-Prince +HU +4730+01905 Europe/Budapest +ID -0610+10648 Asia/Jakarta Java, Sumatra +ID -0002+10920 Asia/Pontianak Borneo (west, central) +ID -0507+11924 Asia/Makassar Borneo (east, south), Sulawesi/Celebes, Bali, Nusa Tengarra, Timor (west) +ID -0232+14042 Asia/Jayapura New Guinea (West Papua / Irian Jaya), Malukus/Moluccas +IE +5320-00615 Europe/Dublin +IL +314650+0351326 Asia/Jerusalem +IN +2232+08822 Asia/Kolkata +IO -0720+07225 Indian/Chagos +IQ +3321+04425 Asia/Baghdad +IR +3540+05126 Asia/Tehran +IT,SM,VA +4154+01229 Europe/Rome +JM +175805-0764736 America/Jamaica +JO +3157+03556 Asia/Amman +JP,AU +353916+1394441 Asia/Tokyo Eyre Bird Observatory +KE,DJ,ER,ET,KM,MG,SO,TZ,UG,YT -0117+03649 Africa/Nairobi +KG +4254+07436 Asia/Bishkek +KI,MH,TV,UM,WF +0125+17300 Pacific/Tarawa Gilberts, Marshalls, Wake +KI -0247-17143 Pacific/Kanton Phoenix Islands +KI +0152-15720 Pacific/Kiritimati Line Islands +KP +3901+12545 Asia/Pyongyang +KR +3733+12658 Asia/Seoul +KZ +4315+07657 Asia/Almaty most of Kazakhstan +KZ +4448+06528 Asia/Qyzylorda Qyzylorda/Kyzylorda/Kzyl-Orda +KZ +5312+06337 Asia/Qostanay Qostanay/Kostanay/Kustanay +KZ +5017+05710 Asia/Aqtobe Aqtöbe/Aktobe +KZ +4431+05016 Asia/Aqtau Mangghystaū/Mankistau +KZ +4707+05156 Asia/Atyrau Atyraū/Atirau/Gur’yev +KZ +5113+05121 Asia/Oral West Kazakhstan +LB +3353+03530 Asia/Beirut +LK +0656+07951 Asia/Colombo +LR +0618-01047 Africa/Monrovia +LT +5441+02519 Europe/Vilnius +LV +5657+02406 Europe/Riga +LY +3254+01311 Africa/Tripoli +MA +3339-00735 Africa/Casablanca +MD +4700+02850 Europe/Chisinau +MH +0905+16720 Pacific/Kwajalein Kwajalein +MM,CC +1647+09610 Asia/Yangon +MN +4755+10653 Asia/Ulaanbaatar most of Mongolia +MN +4801+09139 Asia/Hovd Bayan-Ölgii, Hovd, Uvs +MO +221150+1133230 Asia/Macau +MQ +1436-06105 America/Martinique +MT +3554+01431 Europe/Malta +MU -2010+05730 Indian/Mauritius +MV,TF +0410+07330 Indian/Maldives Kerguelen, St Paul I, Amsterdam I +MX +1924-09909 America/Mexico_City Central Mexico +MX +2105-08646 America/Cancun Quintana Roo +MX +2058-08937 America/Merida Campeche, Yucatán +MX +2540-10019 America/Monterrey Durango; Coahuila, Nuevo León, Tamaulipas (most areas) +MX +2550-09730 America/Matamoros Coahuila, Nuevo León, Tamaulipas (US border) +MX +2838-10605 America/Chihuahua Chihuahua (most areas) +MX +3144-10629 America/Ciudad_Juarez Chihuahua (US border - west) +MX +2934-10425 America/Ojinaga Chihuahua (US border - east) +MX +2313-10625 America/Mazatlan Baja California Sur, Nayarit (most areas), Sinaloa +MX +2048-10515 America/Bahia_Banderas Bahía de Banderas +MX +2904-11058 America/Hermosillo Sonora +MX +3232-11701 America/Tijuana Baja California +MY,BN +0133+11020 Asia/Kuching Sabah, Sarawak +MZ,BI,BW,CD,MW,RW,ZM,ZW -2558+03235 Africa/Maputo Central Africa Time +NA -2234+01706 Africa/Windhoek +NC -2216+16627 Pacific/Noumea +NF -2903+16758 Pacific/Norfolk +NG,AO,BJ,CD,CF,CG,CM,GA,GQ,NE +0627+00324 Africa/Lagos West Africa Time +NI +1209-08617 America/Managua +NP +2743+08519 Asia/Kathmandu +NR -0031+16655 Pacific/Nauru +NU -1901-16955 Pacific/Niue +NZ,AQ -3652+17446 Pacific/Auckland New Zealand time +NZ -4357-17633 Pacific/Chatham Chatham Islands +PA,CA,KY +0858-07932 America/Panama EST - ON (Atikokan), NU (Coral H) +PE -1203-07703 America/Lima +PF -1732-14934 Pacific/Tahiti Society Islands +PF -0900-13930 Pacific/Marquesas Marquesas Islands +PF -2308-13457 Pacific/Gambier Gambier Islands +PG,AQ,FM -0930+14710 Pacific/Port_Moresby Papua New Guinea (most areas), Chuuk, Yap, Dumont d’Urville +PG -0613+15534 Pacific/Bougainville Bougainville +PH +143512+1205804 Asia/Manila +PK +2452+06703 Asia/Karachi +PL +5215+02100 Europe/Warsaw +PM +4703-05620 America/Miquelon +PN -2504-13005 Pacific/Pitcairn +PR,AG,CA,AI,AW,BL,BQ,CW,DM,GD,GP,KN,LC,MF,MS,SX,TT,VC,VG,VI +182806-0660622 America/Puerto_Rico AST - QC (Lower North Shore) +PS +3130+03428 Asia/Gaza Gaza Strip +PS +313200+0350542 Asia/Hebron West Bank +PT +3843-00908 Europe/Lisbon Portugal (mainland) +PT +3238-01654 Atlantic/Madeira Madeira Islands +PT +3744-02540 Atlantic/Azores Azores +PW +0720+13429 Pacific/Palau +PY -2516-05740 America/Asuncion +QA,BH +2517+05132 Asia/Qatar +RO +4426+02606 Europe/Bucharest +RS,BA,HR,ME,MK,SI +4450+02030 Europe/Belgrade +RU +5443+02030 Europe/Kaliningrad MSK-01 - Kaliningrad +RU +554521+0373704 Europe/Moscow MSK+00 - Moscow area +# Mention RU and UA alphabetically. See “territorial claims” above. +RU,UA +4457+03406 Europe/Simferopol Crimea +RU +5836+04939 Europe/Kirov MSK+00 - Kirov +RU +4844+04425 Europe/Volgograd MSK+00 - Volgograd +RU +4621+04803 Europe/Astrakhan MSK+01 - Astrakhan +RU +5134+04602 Europe/Saratov MSK+01 - Saratov +RU +5420+04824 Europe/Ulyanovsk MSK+01 - Ulyanovsk +RU +5312+05009 Europe/Samara MSK+01 - Samara, Udmurtia +RU +5651+06036 Asia/Yekaterinburg MSK+02 - Urals +RU +5500+07324 Asia/Omsk MSK+03 - Omsk +RU +5502+08255 Asia/Novosibirsk MSK+04 - Novosibirsk +RU +5322+08345 Asia/Barnaul MSK+04 - Altai +RU +5630+08458 Asia/Tomsk MSK+04 - Tomsk +RU +5345+08707 Asia/Novokuznetsk MSK+04 - Kemerovo +RU +5601+09250 Asia/Krasnoyarsk MSK+04 - Krasnoyarsk area +RU +5216+10420 Asia/Irkutsk MSK+05 - Irkutsk, Buryatia +RU +5203+11328 Asia/Chita MSK+06 - Zabaykalsky +RU +6200+12940 Asia/Yakutsk MSK+06 - Lena River +RU +623923+1353314 Asia/Khandyga MSK+06 - Tomponsky, Ust-Maysky +RU +4310+13156 Asia/Vladivostok MSK+07 - Amur River +RU +643337+1431336 Asia/Ust-Nera MSK+07 - Oymyakonsky +RU +5934+15048 Asia/Magadan MSK+08 - Magadan +RU +4658+14242 Asia/Sakhalin MSK+08 - Sakhalin Island +RU +6728+15343 Asia/Srednekolymsk MSK+08 - Sakha (E), N Kuril Is +RU +5301+15839 Asia/Kamchatka MSK+09 - Kamchatka +RU +6445+17729 Asia/Anadyr MSK+09 - Bering Sea +SA,AQ,KW,YE +2438+04643 Asia/Riyadh Syowa +SB,FM -0932+16012 Pacific/Guadalcanal Pohnpei +SD +1536+03232 Africa/Khartoum +SG,AQ,MY +0117+10351 Asia/Singapore peninsular Malaysia, Concordia +SR +0550-05510 America/Paramaribo +SS +0451+03137 Africa/Juba +ST +0020+00644 Africa/Sao_Tome +SV +1342-08912 America/El_Salvador +SY +3330+03618 Asia/Damascus +TC +2128-07108 America/Grand_Turk +TD +1207+01503 Africa/Ndjamena +TH,CX,KH,LA,VN +1345+10031 Asia/Bangkok north Vietnam +TJ +3835+06848 Asia/Dushanbe +TK -0922-17114 Pacific/Fakaofo +TL -0833+12535 Asia/Dili +TM +3757+05823 Asia/Ashgabat +TN +3648+01011 Africa/Tunis +TO -210800-1751200 Pacific/Tongatapu +TR +4101+02858 Europe/Istanbul +TW +2503+12130 Asia/Taipei +UA +5026+03031 Europe/Kyiv most of Ukraine +US +404251-0740023 America/New_York Eastern (most areas) +US +421953-0830245 America/Detroit Eastern - MI (most areas) +US +381515-0854534 America/Kentucky/Louisville Eastern - KY (Louisville area) +US +364947-0845057 America/Kentucky/Monticello Eastern - KY (Wayne) +US +394606-0860929 America/Indiana/Indianapolis Eastern - IN (most areas) +US +384038-0873143 America/Indiana/Vincennes Eastern - IN (Da, Du, K, Mn) +US +410305-0863611 America/Indiana/Winamac Eastern - IN (Pulaski) +US +382232-0862041 America/Indiana/Marengo Eastern - IN (Crawford) +US +382931-0871643 America/Indiana/Petersburg Eastern - IN (Pike) +US +384452-0850402 America/Indiana/Vevay Eastern - IN (Switzerland) +US +415100-0873900 America/Chicago Central (most areas) +US +375711-0864541 America/Indiana/Tell_City Central - IN (Perry) +US +411745-0863730 America/Indiana/Knox Central - IN (Starke) +US +450628-0873651 America/Menominee Central - MI (Wisconsin border) +US +470659-1011757 America/North_Dakota/Center Central - ND (Oliver) +US +465042-1012439 America/North_Dakota/New_Salem Central - ND (Morton rural) +US +471551-1014640 America/North_Dakota/Beulah Central - ND (Mercer) +US +394421-1045903 America/Denver Mountain (most areas) +US +433649-1161209 America/Boise Mountain - ID (south), OR (east) +US,CA +332654-1120424 America/Phoenix MST - AZ (most areas), Creston BC +US +340308-1181434 America/Los_Angeles Pacific +US +611305-1495401 America/Anchorage Alaska (most areas) +US +581807-1342511 America/Juneau Alaska - Juneau area +US +571035-1351807 America/Sitka Alaska - Sitka area +US +550737-1313435 America/Metlakatla Alaska - Annette Island +US +593249-1394338 America/Yakutat Alaska - Yakutat +US +643004-1652423 America/Nome Alaska (west) +US +515248-1763929 America/Adak Alaska - western Aleutians +US +211825-1575130 Pacific/Honolulu Hawaii +UY -345433-0561245 America/Montevideo +UZ +3940+06648 Asia/Samarkand Uzbekistan (west) +UZ +4120+06918 Asia/Tashkent Uzbekistan (east) +VE +1030-06656 America/Caracas +VN +1045+10640 Asia/Ho_Chi_Minh south Vietnam +VU -1740+16825 Pacific/Efate +WS -1350-17144 Pacific/Apia +ZA,LS,SZ -2615+02800 Africa/Johannesburg +# +# The next section contains experimental tab-separated comments for +# use by user agents like tzselect that identify continents and oceans. +# +# For example, the comment ‘#@AQAntarctica/’ means the country code +# AQ is in the continent Antarctica regardless of the Zone name, +# so Pacific/Auckland should be listed under Antarctica as well as +# under the Pacific because its line’s country codes include AQ. +# +# If more than one country code is affected each is listed separated +# by commas, e.g., ‘#@IS,SHAtlantic/’. If a country code is in +# more than one continent or ocean, each is listed separated by +# commas, e.g., the second column of ‘#@CY,TRAsia/,Europe/’. +# +# These experimental comments are present only for country codes where +# the continent or ocean is not already obvious from the Zone name. +# For example, there is no such comment for RU since it already +# corresponds to Zone names starting with both ‘Europe/’ and ‘Asia/’. +# +#@AQ Antarctica/ +#@IS,SH Atlantic/ +#@CY,TR Asia/,Europe/ +#@SJ Arctic/ +#@CC,CX,KM,MG,YT Indian/ diff --git a/.venv/lib/python3.11/site-packages/pytz/zoneinfo/zonenow.tab b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/zonenow.tab new file mode 100644 index 0000000000000000000000000000000000000000..54e4485d744170715a05c85f779446c7372e8bec --- /dev/null +++ b/.venv/lib/python3.11/site-packages/pytz/zoneinfo/zonenow.tab @@ -0,0 +1,296 @@ +# tzdb timezone descriptions, for users who do not care about old timestamps +# +# This file is in the public domain. +# +# From Paul Eggert (2023-12-18): +# This file contains a table where each row stands for a timezone +# where civil timestamps are predicted to agree from now on. +# This file is like zone1970.tab (see zone1970.tab’s comments), +# but with the following changes: +# +# 1. Each timezone corresponds to a set of clocks that are planned +# to agree from now on. This is a larger set of clocks than in +# zone1970.tab, where each timezone’s clocks must agree from 1970 on. +# 2. The first column is irrelevant and ignored. +# 3. The table is sorted in a different way: +# first by standard time UTC offset; +# then, if DST is used, by daylight saving UTC offset; +# then by time zone abbreviation. +# 4. Every timezone has a nonempty comments column, with wording +# distinguishing the timezone only from other timezones with the +# same UTC offset at some point during the year. +# +# The format of this table is experimental, and may change in future versions. +# +# This table is intended as an aid for users, to help them select timezones +# appropriate for their practical needs. It is not intended to take or +# endorse any position on legal or territorial claims. +# +#XX coordinates TZ comments +# +# -11 - SST +XX -1416-17042 Pacific/Pago_Pago Midway; Samoa (SST) +# +# -11 +XX -1901-16955 Pacific/Niue Niue +# +# -10 - HST +XX +211825-1575130 Pacific/Honolulu Hawaii (HST) +# +# -10 +XX -1732-14934 Pacific/Tahiti Tahiti; Cook Islands +# +# -10/-09 - HST / HDT (North America DST) +XX +515248-1763929 America/Adak western Aleutians in Alaska (HST/HDT) +# +# -09:30 +XX -0900-13930 Pacific/Marquesas Marquesas +# +# -09 +XX -2308-13457 Pacific/Gambier Gambier +# +# -09/-08 - AKST/AKDT (North America DST) +XX +611305-1495401 America/Anchorage most of Alaska (AKST/AKDT) +# +# -08 +XX -2504-13005 Pacific/Pitcairn Pitcairn +# +# -08/-07 - PST/PDT (North America DST) +XX +340308-1181434 America/Los_Angeles Pacific (PST/PDT) - US & Canada; Mexico near US border +# +# -08/-07 - PST/PDT (North America DST) until 2026-11-01 02:00; then MST +XX +4916-12307 America/Vancouver MST - BC (most areas) +# +# -07 - MST +XX +332654-1120424 America/Phoenix Mountain Standard (MST) - Arizona; western Mexico; Yukon +# +# -07/-06 - MST/MDT (North America DST) +XX +394421-1045903 America/Denver Mountain (MST/MDT) - US & Canada; Mexico near US border +# +# -06 +XX -0054-08936 Pacific/Galapagos Galápagos +# +# -06 - CST +XX +1924-09909 America/Mexico_City Central Standard (CST) - Saskatchewan; central Mexico; Central America +# +# -06/-05 (Chile DST) +XX -2709-10926 Pacific/Easter Easter Island +# +# -06/-05 - CST/CDT (North America DST) +XX +415100-0873900 America/Chicago Central (CST/CDT) - US & Canada; Mexico near US border +# +# -05 +XX -1203-07703 America/Lima eastern South America +# +# -05 - EST +XX +175805-0764736 America/Jamaica Eastern Standard (EST) - Caymans; Jamaica; eastern Mexico; Panama +# +# -05/-04 - CST/CDT (Cuba DST) +XX +2308-08222 America/Havana Cuba +# +# -05/-04 - EST/EDT (North America DST) +XX +404251-0740023 America/New_York Eastern (EST/EDT) - US & Canada +# +# -04 +XX +1030-06656 America/Caracas western South America +# +# -04 - AST +XX +1828-06954 America/Santo_Domingo Atlantic Standard (AST) - eastern Caribbean +# +# -04/-03 (Chile DST) +XX -3327-07040 America/Santiago most of Chile +# +# -04/-03 - AST/ADT (North America DST) +XX +4439-06336 America/Halifax Atlantic (AST/ADT) - Canada; Bermuda +# +# -03:30/-02:30 - NST/NDT (North America DST) +XX +4734-05243 America/St_Johns Newfoundland (NST/NDT) +# +# -03 +XX -2332-04637 America/Sao_Paulo eastern and southern South America +# +# -03/-02 (North America DST) +XX +4703-05620 America/Miquelon St Pierre & Miquelon +# +# -02 +XX -0351-03225 America/Noronha Fernando de Noronha; South Georgia +# +# -02/-01 (EU DST) +XX +6411-05144 America/Nuuk most of Greenland +# +# -01 +XX +1455-02331 Atlantic/Cape_Verde Cape Verde +# +# -01/+00 (EU DST) +XX +3744-02540 Atlantic/Azores Azores +# +# +00 - GMT +XX +0519-00402 Africa/Abidjan far western Africa; Iceland (GMT) +# +# +00/+01 - GMT/BST (EU DST) +XX +513030-0000731 Europe/London United Kingdom (GMT/BST) +# +# +00/+01 - WET/WEST (EU DST) +XX +3843-00908 Europe/Lisbon western Europe (WET/WEST) +# +# +00/+02 - Troll DST +XX -720041+0023206 Antarctica/Troll Troll Station in Antarctica +# +# +01 - CET +XX +3647+00303 Africa/Algiers Algeria, Tunisia (CET) +# +# +01 - WAT +XX +0627+00324 Africa/Lagos western Africa (WAT) +# +# +01/+00 - IST/GMT (EU DST in reverse) +XX +5320-00615 Europe/Dublin Ireland (IST/GMT) +# +# +01/+00 - (Morocco DST) +XX +3339-00735 Africa/Casablanca Morocco +# +# +01/+02 - CET/CEST (EU DST) +XX +4852+00220 Europe/Paris central Europe (CET/CEST) +# +# +02 - CAT +XX -2558+03235 Africa/Maputo central Africa (CAT) +# +# +02 - EET +XX +3254+01311 Africa/Tripoli Libya; Kaliningrad (EET) +# +# +02 - SAST +XX -2615+02800 Africa/Johannesburg southern Africa (SAST) +# +# +02/+03 - EET/EEST (EU DST) +XX +3758+02343 Europe/Athens eastern Europe (EET/EEST) +# +# +02/+03 - EET/EEST (Egypt DST) +XX +3003+03115 Africa/Cairo Egypt +# +# +02/+03 - EET/EEST (Lebanon DST) +XX +3353+03530 Asia/Beirut Lebanon +# +# +02/+03 - EET/EEST (Palestine DST) +XX +3130+03428 Asia/Gaza Palestine +# +# +02/+03 - IST/IDT (Israel DST) +XX +314650+0351326 Asia/Jerusalem Israel +# +# +03 +XX +4101+02858 Europe/Istanbul Near East; Belarus +# +# +03 - EAT +XX -0117+03649 Africa/Nairobi eastern Africa (EAT) +# +# +03 - MSK +XX +554521+0373704 Europe/Moscow Moscow (MSK) +# +# +03:30 +XX +3540+05126 Asia/Tehran Iran +# +# +04 +XX +2518+05518 Asia/Dubai Russia; Caucasus; Persian Gulf; Seychelles; Réunion +# +# +04:30 +XX +3431+06912 Asia/Kabul Afghanistan +# +# +05 +XX +4120+06918 Asia/Tashkent Russia; Kazakhstan; Tajikistan; Turkmenistan; Uzbekistan; Maldives +# +# +05 - PKT +XX +2452+06703 Asia/Karachi Pakistan (PKT) +# +# +05:30 +XX +0656+07951 Asia/Colombo Sri Lanka +# +# +05:30 - IST +XX +2232+08822 Asia/Kolkata India (IST) +# +# +05:45 +XX +2743+08519 Asia/Kathmandu Nepal +# +# +06 +XX +2343+09025 Asia/Dhaka Russia; Kyrgyzstan; Bhutan; Bangladesh; Chagos +# +# +06:30 +XX +1647+09610 Asia/Yangon Myanmar; Cocos +# +# +07 +XX +1345+10031 Asia/Bangkok Russia; Indochina; Christmas Island +# +# +07 - WIB +XX -0610+10648 Asia/Jakarta Indonesia (WIB) +# +# +08 +XX +0117+10351 Asia/Singapore Russia; Brunei; Malaysia; Singapore; Concordia +# +# +08 - AWST +XX -3157+11551 Australia/Perth Western Australia (AWST) +# +# +08 - CST +XX +3114+12128 Asia/Shanghai China (CST) +# +# +08 - HKT +XX +2217+11409 Asia/Hong_Kong Hong Kong (HKT) +# +# +08 - PHT +XX +143512+1205804 Asia/Manila Philippines (PHT) +# +# +08 - WITA +XX -0507+11924 Asia/Makassar Indonesia (WITA) +# +# +08:45 +XX -3143+12852 Australia/Eucla Eucla +# +# +09 +XX +5203+11328 Asia/Chita Russia; Palau; East Timor +# +# +09 - JST +XX +353916+1394441 Asia/Tokyo Japan (JST); Eyre Bird Observatory +# +# +09 - KST +XX +3733+12658 Asia/Seoul Korea (KST) +# +# +09 - WIT +XX -0232+14042 Asia/Jayapura Indonesia (WIT) +# +# +09:30 - ACST +XX -1228+13050 Australia/Darwin Northern Territory (ACST) +# +# +09:30/+10:30 - ACST/ACDT (Australia DST) +XX -3455+13835 Australia/Adelaide South Australia (ACST/ACDT) +# +# +10 +XX +4310+13156 Asia/Vladivostok Russia; Yap; Chuuk; Papua New Guinea; Dumont d’Urville +# +# +10 - AEST +XX -2728+15302 Australia/Brisbane Queensland (AEST) +# +# +10 - ChST +XX +1328+14445 Pacific/Guam Mariana Islands (ChST) +# +# +10/+11 - AEST/AEDT (Australia DST) +XX -3352+15113 Australia/Sydney southeast Australia (AEST/AEDT) +# +# +10:30/+11 +XX -3133+15905 Australia/Lord_Howe Lord Howe Island +# +# +11 +XX -0613+15534 Pacific/Bougainville Russia; Kosrae; Bougainville; Solomons +# +# +11/+12 (Australia DST) +XX -2903+16758 Pacific/Norfolk Norfolk Island +# +# +12 +XX +5301+15839 Asia/Kamchatka Russia; Tuvalu; Fiji; etc. +# +# +12/+13 (New Zealand DST) +XX -3652+17446 Pacific/Auckland New Zealand (NZST/NZDT) +# +# +12:45/+13:45 (Chatham DST) +XX -4357-17633 Pacific/Chatham Chatham Islands +# +# +13 +XX -210800-1751200 Pacific/Tongatapu Kanton; Tokelau; Samoa (western); Tonga +# +# +14 +XX +0152-15720 Pacific/Kiritimati Kiritimati diff --git a/.venv/lib/python3.11/site-packages/pyyaml-6.0.3.dist-info/INSTALLER b/.venv/lib/python3.11/site-packages/pyyaml-6.0.3.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..5c69047b2eb8235994febeeae1da4a82365a240a --- /dev/null +++ b/.venv/lib/python3.11/site-packages/pyyaml-6.0.3.dist-info/INSTALLER @@ -0,0 +1 @@ +uv \ No newline at end of file diff --git a/.venv/lib/python3.11/site-packages/pyyaml-6.0.3.dist-info/METADATA b/.venv/lib/python3.11/site-packages/pyyaml-6.0.3.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..702ee6da5865b50b297705b077ea7d5904fead58 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/pyyaml-6.0.3.dist-info/METADATA @@ -0,0 +1,59 @@ +Metadata-Version: 2.4 +Name: PyYAML +Version: 6.0.3 +Summary: YAML parser and emitter for Python +Home-page: https://pyyaml.org/ +Download-URL: https://pypi.org/project/PyYAML/ +Author: Kirill Simonov +Author-email: xi@resolvent.net +License: MIT +Project-URL: Bug Tracker, https://github.com/yaml/pyyaml/issues +Project-URL: CI, https://github.com/yaml/pyyaml/actions +Project-URL: Documentation, https://pyyaml.org/wiki/PyYAMLDocumentation +Project-URL: Mailing lists, http://lists.sourceforge.net/lists/listinfo/yaml-core +Project-URL: Source Code, https://github.com/yaml/pyyaml +Platform: Any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Cython +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Text Processing :: Markup +Requires-Python: >=3.8 +License-File: LICENSE +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: download-url +Dynamic: home-page +Dynamic: license +Dynamic: license-file +Dynamic: platform +Dynamic: project-url +Dynamic: requires-python +Dynamic: summary + +YAML is a data serialization format designed for human readability +and interaction with scripting languages. PyYAML is a YAML parser +and emitter for Python. + +PyYAML features a complete YAML 1.1 parser, Unicode support, pickle +support, capable extension API, and sensible error messages. PyYAML +supports standard YAML tags and provides Python-specific tags that +allow to represent an arbitrary Python object. + +PyYAML is applicable for a broad range of tasks from complex +configuration files to object serialization and persistence. diff --git a/.venv/lib/python3.11/site-packages/pyyaml-6.0.3.dist-info/RECORD b/.venv/lib/python3.11/site-packages/pyyaml-6.0.3.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..fd28f16e1bdd8f0d07b0023af5b3a646f318bf30 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/pyyaml-6.0.3.dist-info/RECORD @@ -0,0 +1,26 @@ +_yaml/__init__.py,sha256=04Ae_5osxahpJHa3XBZUAf4wi6XX32gR8D6X6p64GEA,1402 +pyyaml-6.0.3.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 +pyyaml-6.0.3.dist-info/METADATA,sha256=A8O0Fe040J-u3Ek2DpMHabQMWPaFhebeAOLkkWqFjTQ,2351 +pyyaml-6.0.3.dist-info/RECORD,, +pyyaml-6.0.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pyyaml-6.0.3.dist-info/WHEEL,sha256=BvA_i88wcFUl5ehXLgmhwyDL4XPGrCKn6CTUA9axFDE,190 +pyyaml-6.0.3.dist-info/licenses/LICENSE,sha256=jTko-dxEkP1jVwfLiOsmvXZBAqcoKVQwfT5RZ6V36KQ,1101 +pyyaml-6.0.3.dist-info/top_level.txt,sha256=rpj0IVMTisAjh_1vG3Ccf9v5jpCQwAz6cD1IVU5ZdhQ,11 +yaml/__init__.py,sha256=sZ38wzPWp139cwc5ARZFByUvJxtB07X32FUQAzoFR6c,12311 +yaml/_yaml.cpython-311-x86_64-linux-gnu.so,sha256=qpuw17JhuCZqur2eGfW_L1BdmoS6NgzhZQIpfooDN2U,2650176 +yaml/composer.py,sha256=_Ko30Wr6eDWUeUpauUGT3Lcg9QPBnOPVlTnIMRGJ9FM,4883 +yaml/constructor.py,sha256=kNgkfaeLUkwQYY_Q6Ff1Tz2XVw_pG1xVE9Ak7z-viLA,28639 +yaml/cyaml.py,sha256=6ZrAG9fAYvdVe2FK_w0hmXoG7ZYsoYUwapG8CiC72H0,3851 +yaml/dumper.py,sha256=PLctZlYwZLp7XmeUdwRuv4nYOZ2UBnDIUy8-lKfLF-o,2837 +yaml/emitter.py,sha256=jghtaU7eFwg31bG0B7RZea_29Adi9CKmXq_QjgQpCkQ,43006 +yaml/error.py,sha256=Ah9z-toHJUbE9j-M8YpxgSRM5CgLCcwVzJgLLRF2Fxo,2533 +yaml/events.py,sha256=50_TksgQiE4up-lKo_V-nBy-tAIxkIPQxY5qDhKCeHw,2445 +yaml/loader.py,sha256=UVa-zIqmkFSCIYq_PgSGm4NSJttHY2Rf_zQ4_b1fHN0,2061 +yaml/nodes.py,sha256=gPKNj8pKCdh2d4gr3gIYINnPOaOxGhJAUiYhGRnPE84,1440 +yaml/parser.py,sha256=ilWp5vvgoHFGzvOZDItFoGjD6D42nhlZrZyjAwa0oJo,25495 +yaml/reader.py,sha256=0dmzirOiDG4Xo41RnuQS7K9rkY3xjHiVasfDMNTqCNw,6794 +yaml/representer.py,sha256=IuWP-cAW9sHKEnS0gCqSa894k1Bg4cgTxaDwIcbRQ-Y,14190 +yaml/resolver.py,sha256=9L-VYfm4mWHxUD1Vg4X7rjDRK_7VZd6b92wzq7Y2IKY,9004 +yaml/scanner.py,sha256=YEM3iLZSaQwXcQRg2l2R4MdT0zGP2F9eHkKGKnHyWQY,51279 +yaml/serializer.py,sha256=ChuFgmhU01hj4xgI8GaKv6vfM2Bujwa9i7d2FAHj7cA,4165 +yaml/tokens.py,sha256=lTQIzSVw8Mg9wv459-TjiOQe6wVziqaRlqX2_89rp54,2573 diff --git a/.venv/lib/python3.11/site-packages/pyyaml-6.0.3.dist-info/REQUESTED b/.venv/lib/python3.11/site-packages/pyyaml-6.0.3.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/.venv/lib/python3.11/site-packages/pyyaml-6.0.3.dist-info/WHEEL b/.venv/lib/python3.11/site-packages/pyyaml-6.0.3.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..35bcd5d5bf93e0fed272b2448373b0942627885d --- /dev/null +++ b/.venv/lib/python3.11/site-packages/pyyaml-6.0.3.dist-info/WHEEL @@ -0,0 +1,7 @@ +Wheel-Version: 1.0 +Generator: setuptools (80.9.0) +Root-Is-Purelib: false +Tag: cp311-cp311-manylinux_2_17_x86_64 +Tag: cp311-cp311-manylinux2014_x86_64 +Tag: cp311-cp311-manylinux_2_28_x86_64 + diff --git a/.venv/lib/python3.11/site-packages/pyyaml-6.0.3.dist-info/licenses/LICENSE b/.venv/lib/python3.11/site-packages/pyyaml-6.0.3.dist-info/licenses/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..2f1b8e15e5627d92f0521605c9870bc8e5505cb4 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/pyyaml-6.0.3.dist-info/licenses/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2017-2021 Ingy döt Net +Copyright (c) 2006-2016 Kirill Simonov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.venv/lib/python3.11/site-packages/pyyaml-6.0.3.dist-info/top_level.txt b/.venv/lib/python3.11/site-packages/pyyaml-6.0.3.dist-info/top_level.txt new file mode 100644 index 0000000000000000000000000000000000000000..e6475e911f628412049bc4090d86f23ac403adde --- /dev/null +++ b/.venv/lib/python3.11/site-packages/pyyaml-6.0.3.dist-info/top_level.txt @@ -0,0 +1,2 @@ +_yaml +yaml diff --git a/.venv/lib/python3.11/site-packages/scanpy-1.11.5.dist-info/INSTALLER b/.venv/lib/python3.11/site-packages/scanpy-1.11.5.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..5c69047b2eb8235994febeeae1da4a82365a240a --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy-1.11.5.dist-info/INSTALLER @@ -0,0 +1 @@ +uv \ No newline at end of file diff --git a/.venv/lib/python3.11/site-packages/scanpy-1.11.5.dist-info/METADATA b/.venv/lib/python3.11/site-packages/scanpy-1.11.5.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..bfd43c6f050073fdd465ed3a3e0eca02f76a5d4d --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy-1.11.5.dist-info/METADATA @@ -0,0 +1,200 @@ +Metadata-Version: 2.4 +Name: scanpy +Version: 1.11.5 +Summary: Single-Cell Analysis in Python. +Project-URL: Documentation, https://scanpy.readthedocs.io/ +Project-URL: Source, https://github.com/scverse/scanpy +Project-URL: Homepage, https://scanpy.org +Project-URL: Discourse, https://discourse.scverse.org/c/help/scanpy/37 +Project-URL: Bluesky, https://bsky.app/profile/scverse.bsky.social +Project-URL: Twitter, https://x.com/scverse_team +Author: Alex Wolf, Philipp Angerer, Fidel Ramirez, Isaac Virshup, Sergei Rybakov, Gokcen Eraslan, Tom White, Malte Luecken, Davide Cittaro, Tobias Callies, Marius Lange, Andrés R. Muñoz-Rojas +Maintainer: Severin Dicks +Maintainer-email: Philipp Angerer , Ilan Gold +License-Expression: BSD-3-Clause +License-File: LICENSE +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: Framework :: Jupyter +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Science/Research +Classifier: License :: OSI Approved :: BSD License +Classifier: Natural Language :: English +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX :: Linux +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Topic :: Scientific/Engineering :: Bio-Informatics +Classifier: Topic :: Scientific/Engineering :: Visualization +Requires-Python: >=3.10 +Requires-Dist: anndata>=0.8 +Requires-Dist: h5py>=3.7.0 +Requires-Dist: joblib +Requires-Dist: legacy-api-wrap>=1.4.1 +Requires-Dist: matplotlib>=3.7.5 +Requires-Dist: natsort +Requires-Dist: networkx>=2.7.1 +Requires-Dist: numba!=0.62.0rc1,>=0.57.1 +Requires-Dist: numpy>=1.24.1 +Requires-Dist: packaging>=21.3 +Requires-Dist: pandas>=1.5.3 +Requires-Dist: patsy!=1.0.0 +Requires-Dist: pynndescent>=0.5.13 +Requires-Dist: scikit-learn>=1.1.3 +Requires-Dist: scipy>=1.8.1 +Requires-Dist: seaborn>=0.13.2 +Requires-Dist: session-info2 +Requires-Dist: statsmodels>=0.14.5 +Requires-Dist: tqdm +Requires-Dist: typing-extensions; python_version < '3.13' +Requires-Dist: umap-learn>=0.5.6 +Provides-Extra: bbknn +Requires-Dist: bbknn; extra == 'bbknn' +Provides-Extra: dask +Requires-Dist: anndata[dask]; extra == 'dask' +Requires-Dist: dask[array]>=2022.09.2; extra == 'dask' +Provides-Extra: dask-ml +Requires-Dist: anndata[dask]; extra == 'dask-ml' +Requires-Dist: dask-ml; extra == 'dask-ml' +Requires-Dist: dask[array]>=2022.09.2; extra == 'dask-ml' +Provides-Extra: dev +Requires-Dist: scipy-stubs; extra == 'dev' +Requires-Dist: towncrier; extra == 'dev' +Provides-Extra: doc +Requires-Dist: anndata[dask]; extra == 'doc' +Requires-Dist: dask-ml; extra == 'doc' +Requires-Dist: dask[array]>=2022.09.2; extra == 'doc' +Requires-Dist: igraph; extra == 'doc' +Requires-Dist: igraph>=0.10.8; extra == 'doc' +Requires-Dist: ipython>=7.20; extra == 'doc' +Requires-Dist: leidenalg>=0.9.0; extra == 'doc' +Requires-Dist: myst-nb>=1; extra == 'doc' +Requires-Dist: myst-parser>=2; extra == 'doc' +Requires-Dist: nbsphinx>=0.9; extra == 'doc' +Requires-Dist: sam-algorithm; extra == 'doc' +Requires-Dist: scanpydoc>=0.15.3; extra == 'doc' +Requires-Dist: sphinx-autodoc-typehints>=1.25.2; extra == 'doc' +Requires-Dist: sphinx-book-theme>=1.1.0; extra == 'doc' +Requires-Dist: sphinx-copybutton; extra == 'doc' +Requires-Dist: sphinx-design; extra == 'doc' +Requires-Dist: sphinx-issues>=5.0.1; extra == 'doc' +Requires-Dist: sphinx-tabs; extra == 'doc' +Requires-Dist: sphinx>=8.2.3; extra == 'doc' +Requires-Dist: sphinxcontrib-bibtex; extra == 'doc' +Requires-Dist: sphinxext-opengraph; extra == 'doc' +Provides-Extra: harmony +Requires-Dist: harmonypy; extra == 'harmony' +Provides-Extra: leiden +Requires-Dist: igraph>=0.10.8; extra == 'leiden' +Requires-Dist: leidenalg>=0.9.0; extra == 'leiden' +Provides-Extra: louvain +Requires-Dist: igraph; extra == 'louvain' +Requires-Dist: louvain>=0.8.2; extra == 'louvain' +Requires-Dist: setuptools<81; extra == 'louvain' +Provides-Extra: magic +Requires-Dist: magic-impute>=2.0.4; extra == 'magic' +Provides-Extra: paga +Requires-Dist: igraph; extra == 'paga' +Provides-Extra: rapids +Requires-Dist: cudf>=0.9; extra == 'rapids' +Requires-Dist: cugraph>=0.9; extra == 'rapids' +Requires-Dist: cuml>=0.9; extra == 'rapids' +Provides-Extra: scanorama +Requires-Dist: scanorama; extra == 'scanorama' +Provides-Extra: scrublet +Requires-Dist: scikit-image; extra == 'scrublet' +Provides-Extra: skmisc +Requires-Dist: scikit-misc>=0.1.4; extra == 'skmisc' +Provides-Extra: test +Requires-Dist: anndata[dask]; extra == 'test' +Requires-Dist: dask-ml; extra == 'test' +Requires-Dist: dask[array]>=2022.09.2; extra == 'test' +Requires-Dist: igraph; extra == 'test' +Requires-Dist: igraph>=0.10.8; extra == 'test' +Requires-Dist: leidenalg>=0.9.0; extra == 'test' +Requires-Dist: louvain>=0.8.2; extra == 'test' +Requires-Dist: pytest-cov; extra == 'test' +Requires-Dist: pytest-mock; extra == 'test' +Requires-Dist: pytest-randomly; extra == 'test' +Requires-Dist: pytest-rerunfailures; extra == 'test' +Requires-Dist: pytest-xdist[psutil]; extra == 'test' +Requires-Dist: pytest>=8.2.2; extra == 'test' +Requires-Dist: scikit-image; extra == 'test' +Requires-Dist: scikit-misc>=0.1.4; extra == 'test' +Requires-Dist: setuptools<81; extra == 'test' +Requires-Dist: tuna; extra == 'test' +Requires-Dist: zarr<3; extra == 'test' +Provides-Extra: test-min +Requires-Dist: pytest-cov; extra == 'test-min' +Requires-Dist: pytest-mock; extra == 'test-min' +Requires-Dist: pytest-randomly; extra == 'test-min' +Requires-Dist: pytest-rerunfailures; extra == 'test-min' +Requires-Dist: pytest-xdist[psutil]; extra == 'test-min' +Requires-Dist: pytest>=8.2.2; extra == 'test-min' +Requires-Dist: tuna; extra == 'test-min' +Description-Content-Type: text/markdown + +[![Stars](https://img.shields.io/github/stars/scverse/scanpy?style=flat&logo=GitHub&color=yellow)](https://github.com/scverse/scanpy/stargazers) +[![PyPI](https://img.shields.io/pypi/v/scanpy)](https://pypi.org/project/scanpy) +[![PyPI Downloads](https://img.shields.io/pepy/dt/scanpy?logo=pypi)](https://pepy.tech/project/scanpy) +[![Conda Forge](https://img.shields.io/conda/vn/conda-forge/scanpy) +![Conda Forge Downloads](https://img.shields.io/conda/dn/conda-forge/scanpy?logo=condaforge)](https://anaconda.org/conda-forge/scanpy) +[![Docs](https://readthedocs.com/projects/icb-scanpy/badge/?version=latest)](https://scanpy.readthedocs.io) +[![CI](https://github.com/scverse/scanpy/actions/workflows/ci.yml/badge.svg)](https://github.com/scverse/scanpy/actions/workflows/ci.yml) +[![Discourse topics](https://img.shields.io/discourse/posts?color=yellow&logo=discourse&server=https%3A%2F%2Fdiscourse.scverse.org)](https://discourse.scverse.org/) +[![Chat](https://img.shields.io/badge/zulip-join_chat-%2367b08f.svg)](https://scverse.zulipchat.com) +[![Powered by NumFOCUS](https://img.shields.io/badge/powered%20by-NumFOCUS-orange.svg?style=flat&colorA=E1523D&colorB=007D8A)](https://numfocus.org/) + +# Scanpy – Single-Cell Analysis in Python + +Scanpy is a scalable toolkit for analyzing single-cell gene expression data +built jointly with [anndata][]. It includes +preprocessing, visualization, clustering, trajectory inference and differential +expression testing. The Python-based implementation efficiently deals with +datasets of more than one million cells. + +Discuss usage on the scverse [Discourse][]. Read the [documentation][]. +If you'd like to contribute by opening an issue or creating a pull request, please take a look at our [contribution guide][]. + +[anndata]: https://anndata.readthedocs.io +[discourse]: https://discourse.scverse.org/ +[documentation]: https://scanpy.readthedocs.io + +[//]: # (numfocus-fiscal-sponsor-attribution) + +scanpy is part of the scverse project ([website](https://scverse.org), [governance](https://scverse.org/about/roles)) and is fiscally sponsored by [NumFOCUS](https://numfocus.org/). +If you like scverse and want to support our mission, please consider making a [donation](https://numfocus.org/donate-to-scverse) to support our efforts. + +
+ + + +
+ + +## Citation + +If you use `scanpy` in your work, please cite the `scanpy` publication as follows: + +> **SCANPY: large-scale single-cell gene expression data analysis** +> +> F. Alexander Wolf, Philipp Angerer, Fabian J. Theis +> +> _Genome Biology_ 2018 Feb 06. doi: [10.1186/s13059-017-1382-0](https://doi.org/10.1186/s13059-017-1382-0). + +You can cite the scverse publication as follows: + +> **The scverse project provides a computational ecosystem for single-cell omics data analysis** +> +> Isaac Virshup, Danila Bredikhin, Lukas Heumos, Giovanni Palla, Gregor Sturm, Adam Gayoso, Ilia Kats, Mikaela Koutrouli, Scverse Community, Bonnie Berger, Dana Pe’er, Aviv Regev, Sarah A. Teichmann, Francesca Finotello, F. Alexander Wolf, Nir Yosef, Oliver Stegle & Fabian J. Theis +> +> _Nat Biotechnol._ 2023 Apr 10. doi: [10.1038/s41587-023-01733-8](https://doi.org/10.1038/s41587-023-01733-8). + + +[contribution guide]: CONTRIBUTING.md diff --git a/.venv/lib/python3.11/site-packages/scanpy-1.11.5.dist-info/RECORD b/.venv/lib/python3.11/site-packages/scanpy-1.11.5.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..4d0aecf7743632b74a9e0c2559fa37784c9792b1 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy-1.11.5.dist-info/RECORD @@ -0,0 +1,142 @@ +../../../bin/scanpy,sha256=RNFcP1p11uMYI3fg1kOSiKouu8-ilw_QRKhW2WYgqU0,357 +scanpy-1.11.5.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 +scanpy-1.11.5.dist-info/METADATA,sha256=EIH5CfAIB5OKVEr32hCx77PnK1_5J4RcIn4Ewj_fX4o,9304 +scanpy-1.11.5.dist-info/RECORD,, +scanpy-1.11.5.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scanpy-1.11.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87 +scanpy-1.11.5.dist-info/entry_points.txt,sha256=smWBr8qWkl6Gdk4PtmHMUJHQ1bnENd97XZXd5bsMCXw,51 +scanpy-1.11.5.dist-info/licenses/LICENSE,sha256=FDXut9hhiUN0AXjluOFKvaa-mU8PCjGJS-zCDpRa4r8,1566 +scanpy/__init__.py,sha256=l_ZIUrGOkC_A8LIDR4SYkLjNBYLXv7fDy8_RwKDW3ZQ,2159 +scanpy/__main__.py,sha256=h7jS4fGxS6YRMM5Oaco6u-pMImD8P9qwuYTALeytkYk,113 +scanpy/_compat.py,sha256=dYWEXSlCOwxswWd1CjubX65c0byo2RhfX7_Upg9iOUQ,6848 +scanpy/_settings/__init__.py,sha256=OpRoarpfplbjW0qwu5xndKyFTV2kdx3XHU7IdcrpCHA,16086 +scanpy/_settings/verbosity.py,sha256=x6Dmh1Azn80ZisiOMN6RQb5l2lD34QKfIMB7UeMYmfE,2540 +scanpy/_singleton.py,sha256=BbgZetI76viIVcX9v8vQgglzmfwn0fUQKFwodeyqLj4,2302 +scanpy/_types.py,sha256=4z9sX6zC0QomGlD4zFAKdtIjL1L5RKglRwIIhx3_x80,166 +scanpy/_utils/__init__.py,sha256=gUpxLBWi-CEzEvONIsuFmEIrr-jpmjg9eOJdk-9WaGs,34771 +scanpy/_utils/_doctests.py,sha256=qI9WPNfBCtxmXWjhxt78539YCazfq8Q9Ax61CB840lw,800 +scanpy/_utils/compute/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scanpy/_utils/compute/is_constant.py,sha256=iKTbACGA2ANDyO1vuTclTWM8HyF6XQnajRomdCgF7p4,4178 +scanpy/_utils/random.py,sha256=Pbki5Tln8hFi2MBt-w-zkh8X_R43mg1BI7-NM9YEKGA,6001 +scanpy/cli.py,sha256=oXSvZB-84ebaTz220RUt2-eEvJSQruzo-lvmSWM3THg,4723 +scanpy/datasets/10x_pbmc68k_reduced.h5ad,sha256=5x1B5zfJQVWbfFfJJDvbPSyInCrf3wDjQirGtGeDZ28,1772368 +scanpy/datasets/__init__.py,sha256=1hPN0I6usL5yUS_7_IeSjfSK4cRxBSS_95lQ_o6qxPw,531 +scanpy/datasets/_datasets.py,sha256=sKtLtT0cn-ynnbwjVsa7M4WEoeob4da473_L7wm3dVI,19214 +scanpy/datasets/_ebi_expression_atlas.py,sha256=dk6ooj7IEZBuwH767U5Rc5KSrKz-XauzRVcUJMXM8Es,5772 +scanpy/datasets/_utils.py,sha256=E-A4VgzBOSvB3QnI1S8AHpMgLgZTi45omuRIQABO08o,1146 +scanpy/datasets/krumsiek11.txt,sha256=pJxtBL139hAoEzqbeHIi6_AMT8Atv44rjt2kk6DV3JM,59808 +scanpy/datasets/toggleswitch.txt,sha256=EbcKVXuanJQroV-MidBw2RTTc9Pnb5H0RyxdjhTcdOI,4200 +scanpy/experimental/__init__.py,sha256=VK_pE-ePybICLBxUEV6kdosgRT1hWilDX7EE-a5k400,114 +scanpy/experimental/_docs.py,sha256=QirbWOi3H_QjBjRgJUB4OHmE_xs4Ftcm7q_rhc1DGYY,2787 +scanpy/experimental/pp/__init__.py,sha256=Dj6Xtx4Bs7DcnkffYefUhrabyvSJw5FtYC01YyCL3s4,504 +scanpy/experimental/pp/_highly_variable_genes.py,sha256=Eo8qV2UzPvzZ1HutE_lXUA4JgsW_NJrvjNJ2QB1MdTg,14151 +scanpy/experimental/pp/_normalization.py,sha256=blFCxTQuYicUYJ1s3StEbwxaQlUcPWnY8lEdI5qBgmE,8270 +scanpy/experimental/pp/_recipes.py,sha256=9ivUOPpnoHK7g6P8fyRVj6QnsfZmS4YfiVdq4uJGI8k,5084 +scanpy/external/__init__.py,sha256=GV9A7q3_pdz9N3oNxabNmLMaPqb0cwJjcj_HH5zxZIk,294 +scanpy/external/exporting.py,sha256=B4TjKDEAbi3tFeiV8WCFzYmRTaf90vQEFcHtfZsIrK4,21688 +scanpy/external/pl.py,sha256=WPAX7ciKhfxRx1QXsROI-gtK4jK8wGqBi0VxRACea_o,9292 +scanpy/external/pp/__init__.py,sha256=N0KsiYn2Ocjel2yWCq-kgb_GhbYAQT9W3KY86OduHJ8,726 +scanpy/external/pp/_bbknn.py,sha256=9nZURaxqJvGSq9uMnlfV6sUVay0Um_SEv5KbsKtvsj0,6635 +scanpy/external/pp/_dca.py,sha256=Epee5DQi-RxuAYkSOHRInFacu9ITa137cK_eYEJPLnE,7231 +scanpy/external/pp/_harmony_integrate.py,sha256=HwEZlmWGlOk1AdeMSwVCle9QnqvIwtQ26VWhFZ536Ok,3225 +scanpy/external/pp/_hashsolo.py,sha256=4vzSdyXKqogK54Et-rizNa5JP4UZST4Wvavj-e462jM,16681 +scanpy/external/pp/_magic.py,sha256=6RBEzQY4J3H9e93fN3hu7YxrBM-lS0axOXeoxnd0kbk,7390 +scanpy/external/pp/_mnn_correct.py,sha256=eU0k5ea0XoC--FCiFhZiiHoOHZnWQ3-8gs2hfQ4yM6o,6021 +scanpy/external/pp/_scanorama_integrate.py,sha256=GE8qBBghqmFsjpGL53hJdUQ-fh39irp28mI1xHnO5_8,4664 +scanpy/external/tl/__init__.py,sha256=dpYlH0UlypvXAKQjCS7zboiyTq8WRNj27nUd0BqZkj4,536 +scanpy/external/tl/_harmony_timeseries.py,sha256=aziiEYDlZPXDOrI6Uf3hh3HqXZ-GPwbb7BnKwzqKQEY,6199 +scanpy/external/tl/_palantir.py,sha256=qzIZAUIv6nAQSEy-FIp9ab6xSAv0zwm2IRJZwNzir3A,10273 +scanpy/external/tl/_phate.py,sha256=FEhSWsYi3cGZY9EscX9xzciEJTNUEHABARfMZtwOm88,5807 +scanpy/external/tl/_phenograph.py,sha256=FwJ-Q0e4ne67E_tCBGCWWgFH-Fqp4ZeSJtBvrWcPflY,9358 +scanpy/external/tl/_pypairs.py,sha256=0Kzrb4mnqQI5UZw7Zd5t_k6RMBWr5J44bc36KBiqLpk,4989 +scanpy/external/tl/_sam.py,sha256=414oL0Hwh8Jt3DipEWiLfX2NqLg0q7t1ebLEaUMSQ_4,8371 +scanpy/external/tl/_trimap.py,sha256=RNVfIeAIGBDzSalzHI27K4aAC-5lpusawYnOYJ3IxXA,4539 +scanpy/external/tl/_wishbone.py,sha256=ili6ItX-0nrnQ6CXlZF8lMVqjL6YsHtqqOraL7ZULzg,5131 +scanpy/get/__init__.py,sha256=wzOdjcLE6deG9062vwvaPgoA6sPrPiymWTMX_p2LrOQ,372 +scanpy/get/_aggregated.py,sha256=ksIwQwRvndLc_OIWwGfe7m2fz2y578Q313P4zFkAMmI,14009 +scanpy/get/get.py,sha256=iaIsugH_5lhYZdUlVg5h8onm9bA5lC9z8LyKEyCz1gE,16536 +scanpy/logging.py,sha256=TFgDyOB74OGwiBcWfU3MaW0sNuqbTE1QAr4ypcUGaKY,7202 +scanpy/metrics/__init__.py,sha256=tHoOx1yY6Qq77iEMdbwJwBL2KozSk6lC1VXBWMgB70Y,211 +scanpy/metrics/_common.py,sha256=z85d3-i6QqONWrlBNvWEzq1Uywfa7PwflwPcYIdYpDc,4455 +scanpy/metrics/_gearys_c.py,sha256=-CKV50wgsUJbIIpyepFAS5h7_nENrMOnru0ukNig42g,8244 +scanpy/metrics/_metrics.py,sha256=VImNCqsn44us71oWmh09_2i2wi1jkly1f-pu7Gh1hnk,2469 +scanpy/metrics/_morans_i.py,sha256=shi9mdaTkfDLBI1Bj0YqOq2sbK-TRhTpAZOBCJy08eM,6036 +scanpy/neighbors/__init__.py,sha256=ZILwKQmDQoVT-NUCjE49Jyueubv5Cbp3_lNMDNzoHyc,34890 +scanpy/neighbors/_backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scanpy/neighbors/_backends/_common.py,sha256=38ZmpW11Kx06zd4zpEQ8-X3BO_BQyDxeqDnoarZmtSc,355 +scanpy/neighbors/_backends/rapids.py,sha256=tqoEWRYnXMNv6hB1QbqOabSB12QHlkfl8_39YCx_COA,2947 +scanpy/neighbors/_common.py,sha256=I_rxlHiKDZgMa4M4kevykgPkmoGBulGzoY9i0Lkvl1s,5474 +scanpy/neighbors/_connectivity.py,sha256=2EKwAeSSSlEdPNEV372hviWDOizejpxCaUKQKFTQVNg,4761 +scanpy/neighbors/_doc.py,sha256=LVkmCSjqsHYsKABOV-3f9j-s8MO1bJmzwhusM3332h8,502 +scanpy/neighbors/_types.py,sha256=Q4pptF1-LEJOKmpqLFKZ99peGbeDW0kQI3-I8H4HN-I,1540 +scanpy/plotting/__init__.py,sha256=wjFuAboG30_8TmSbSWahEkFkJNSLexK7xHZiL211KQM,2611 +scanpy/plotting/_anndata.py,sha256=KUSxnwORq8uK4QMujqirfPH-RHinJ2lLzeZaduOsWTA,91536 +scanpy/plotting/_baseplot_class.py,sha256=Cvb9L7Unbmlqu3XuUqMuYHbPUFGlP-pd2wvFJURecNk,32971 +scanpy/plotting/_docs.py,sha256=HwHhWK1kvW8gRiugIswApC5u2ZMGgU5rb-xbmX462aw,13608 +scanpy/plotting/_dotplot.py,sha256=T5tTByOkYAnoIxLIEU8GEHlJDZmBHaKjx7rqE3KRC4A,37423 +scanpy/plotting/_matrixplot.py,sha256=vJnASuJD30YApNIh40qdJTUC4YSoZ1KjkoncU4B8Z0g,14321 +scanpy/plotting/_preprocessing.py,sha256=Scxo4CQznRj3-5uZqS1_1-Q7X-GzhaieqdWuhTiinVM,4559 +scanpy/plotting/_qc.py,sha256=M-tgYY87wj5rvSmr8dljz6ETwJCa0wxJZYCSMKKxSQU,3722 +scanpy/plotting/_rcmod.py,sha256=93JvjX26MjIx3iC9KfV4wSOkDMaAGOAJ0_IwdmJ_mXM,1993 +scanpy/plotting/_scrublet.py,sha256=Jk3_YvhQDsHE3zDHoSBkP2mAHMbEnmuFXdI2cDWmTE8,5036 +scanpy/plotting/_stacked_violin.py,sha256=wFL0pdbwP_XPOMSL66p8CXdNPZce29yYMt-B74XO3qU,31014 +scanpy/plotting/_tools/__init__.py,sha256=TGZLQYZHobGRj_E63feM31KJmPXNkXcSOtI3q4Hr5Lw,51699 +scanpy/plotting/_tools/paga.py,sha256=DhWwMlBoQ2mI1SlmIwwilfEKQFx_OfJHNxu5SSBkqYU,48142 +scanpy/plotting/_tools/scatterplots.py,sha256=kZysyoEmMlmJVxUDhi4V-pC6H_7mfRk8l5MnBJuqmME,45094 +scanpy/plotting/_utils.py,sha256=AXPUpRP8pO71qa5ilAp5aM1HtncrmKRMX6ROD2s9VbM,38231 +scanpy/plotting/palettes.py,sha256=h1RAbItON0Xal0UWgShuI-AEz8XVxx6JMpeRLE3QXqU,4795 +scanpy/preprocessing/__init__.py,sha256=OqKmKc3fCaT96ElMSWsUT6YohzRg7oMQfRbl70n6TtA,1185 +scanpy/preprocessing/_combat.py,sha256=oaBD0iXxBArLjr2JstEC3YVnw2P57ocfU-74ycLUUMI,11894 +scanpy/preprocessing/_deprecated/__init__.py,sha256=ABQLQK1ij1ZNFfHJXzEMx5JOH5KY_y7GOe2vuW974ow,2103 +scanpy/preprocessing/_deprecated/highly_variable_genes.py,sha256=KbN8urObx_GpGErSwH3pYrlQXcRwLqnw9lnBBzTCaPc,10574 +scanpy/preprocessing/_deprecated/sampling.py,sha256=Qg5W4Gqafi6M-0K1AZMFFJhPlNAsVvCuj7G3gAH4AUU,1684 +scanpy/preprocessing/_distributed.py,sha256=G9kdYeVXMm8jexpmeo2ch1LpJ0dyuamu9vcTPUAR8cE,1158 +scanpy/preprocessing/_docs.py,sha256=h6uiiPyigkvdqkH1gv8urp3-QwUNdO_HzDmpMmh7sZE,2826 +scanpy/preprocessing/_highly_variable_genes.py,sha256=BKBPVeGCTYILO5HlqqBgQagqWttlr7FrCLPeFmnv-ms,28210 +scanpy/preprocessing/_normalization.py,sha256=ifiBByVv94hZUwbkiGkRVyKyc7otL1jnf_NxMFChAPM,10883 +scanpy/preprocessing/_pca/__init__.py,sha256=xNe86SCRBujO1PRnO63ETlWq-YTKqav0luN5Y1gehWY,22079 +scanpy/preprocessing/_pca/_compat.py,sha256=vd0Zz3mqYCzWexhDVAVzE5M_KcRqYMuGw3GEAUYU6MU,2208 +scanpy/preprocessing/_pca/_dask.py,sha256=ZiSb84KvsZfivCmJtyK-NTcok1O-k7E7i4h_h4yaYBM,7580 +scanpy/preprocessing/_qc.py,sha256=wRXksNql-VZCXfPXNoLMG5ubnd2sRVbcu0PjhXcxKKA,15037 +scanpy/preprocessing/_recipes.py,sha256=pqMd23E1yOfwao3NsmB_3rN2m5NpRXTLCNSBUi8scLo,5922 +scanpy/preprocessing/_scale.py,sha256=deyGVvvLIPYkkZrTwvMI2xlohnXqb5Y-7pSsj-kGOMo,9679 +scanpy/preprocessing/_scrublet/__init__.py,sha256=2d-SNgJZnW5Xg7znVSdpMHm7incwigW-uF5zY1A0O9A,21891 +scanpy/preprocessing/_scrublet/core.py,sha256=ynxKCII9IxYz09EMplUEwhUGqd_t5u9-TICe43Z_fsc,17293 +scanpy/preprocessing/_scrublet/pipeline.py,sha256=3_-9u8_isPUhH2fgpthLe5k5OTDjq0PvOsjri-cCogI,2753 +scanpy/preprocessing/_scrublet/sparse_utils.py,sha256=Q-IL_Wodj__LM99xTDVfax4PIs8bC24Yb1t4tbhQZqI,1884 +scanpy/preprocessing/_simple.py,sha256=dNT7BtoBIcugP9Dz8Ak6D7nR8kBAJysL9Qh2jopuU-o,40122 +scanpy/preprocessing/_utils.py,sha256=N0MFrX8yWwORrDRNFcXz3SN2GKmND6Ojla-oLd-wWCE,6444 +scanpy/queries/__init__.py,sha256=1OsIeOf0X-vfEy7qzAZGf9Unl_UMbFyeIm2cgKCxKaA,298 +scanpy/queries/_queries.py,sha256=CK9yGkvmhFk0DUPfCRXcz1M9riq-tBeQTT4zxbRZgO0,9265 +scanpy/readwrite.py,sha256=l3mi3ezV4a-gjDxDubl5lV5NXeXjZHNSh5VewRROpbg,37344 +scanpy/sim_models/__init__.py,sha256=ucSfBnr81sP8HUJKtTineZV2B-VrXjiUBOQgYbWob3I,67 +scanpy/sim_models/krumsiek11.txt,sha256=y-0Qe6L4aA8Dfgu7f7OQixOac1pWF0YG7W_HnoFgGIQ,1886 +scanpy/sim_models/krumsiek11_params.txt,sha256=Pvj0fdxjBxdY299H7VFdNQY2JN3mvBnMus1UJsDascY,141 +scanpy/sim_models/toggleswitch.txt,sha256=pqKoHMyIyVG8sLwainMOc_rhzHnt8wnQtnC7UbI5JIQ,438 +scanpy/sim_models/toggleswitch_params.txt,sha256=JWXh4KMyOJhGsy37OmUnux5EiUKFSOgwLd_1nQ1B_x0,146 +scanpy/tools/__init__.py,sha256=189wb6i8glicSpAGxhl7T01N1xWzvNmr6qExiaCGBcM,1320 +scanpy/tools/_dendrogram.py,sha256=DM7WmObtSyrAjPj9JxHA6EdrHUSnWLFtKAWVRaVCbgQ,6697 +scanpy/tools/_diffmap.py,sha256=bHisY_ojsB0wQpnwxzST1sgraRn8Ppz_AjsAQ2q295o,3290 +scanpy/tools/_dpt.py,sha256=lCVs9ODJtk4CJlKc9BzT4PNDNkEpjS6f6Vj6pCk-dbM,50053 +scanpy/tools/_draw_graph.py,sha256=qMlYHmjTSH2Ir51zJF8Hf3jqMMcfN-hmYtZ1E0RsmMs,7883 +scanpy/tools/_embedding_density.py,sha256=AyXN8K20JjpIvsNZUQazcBjRsjBBq8XnXrJPEajJVzs,6272 +scanpy/tools/_ingest.py,sha256=uTMLthd26iiQmZ1HAKbO6VWSqPkv_xGe2P8_2CbMA30,17902 +scanpy/tools/_leiden.py,sha256=5psqoY6C6nvVUwGPZuaqAG39M3SYuRSc-nJok6jqZv0,8725 +scanpy/tools/_louvain.py,sha256=GlfBZXXxzqmL-SVX1Ff0lSFKmOGhzxJAUv81WSpxq3U,9297 +scanpy/tools/_marker_gene_overlap.py,sha256=xsAl59cSUVTXaOrIpCloLZzJjTcRYqXhxUl08BQyMck,10826 +scanpy/tools/_paga.py,sha256=mRahsiRCpMPNeiRPIVS_rrjVLLDrVaCN1X_-O00rkCY,24963 +scanpy/tools/_rank_genes_groups.py,sha256=zUmKrwYGBi_vKS_CKo8S7qEe6kRA45fxB1qamMoSL74,33222 +scanpy/tools/_score_genes.py,sha256=GXQ3i6Nxy80xe9oa-1CjxKuR2yKJzSjA76DUtsIrqx8,11045 +scanpy/tools/_sim.py,sha256=MJaw88FjZijc9tkZanitc_wPg83Nad6MZxb5aJJSL3g,46494 +scanpy/tools/_tsne.py,sha256=rReB5eMxF_vkeelqRw_fX_D9CHIp1ixJXlatX5iTPaI,7428 +scanpy/tools/_umap.py,sha256=WNpLcOy9FqZW6sQYkEGgu2mdp--VhD_aKlac2ODo6t4,10355 +scanpy/tools/_utils.py,sha256=5nTN0guQ7kkzzNOFlfgnmAQbAKr4PbhcTRomMpElosk,3754 +scanpy/tools/_utils_clustering.py,sha256=sJnbc1ZBfM0_ltQFGrWG0_8aTigq5gZu2kIgqE-SNYU,1573 +testing/scanpy/__init__.py,sha256=s-_yVGkYqYMiI3f1mfpABPsWx_dmwrL7MC5Dy1NoDLg,86 +testing/scanpy/_helpers/__init__.py,sha256=hPiKpq-71iZCCU4_WnwudLLZLIt0eModO_CSbnSCoqE,6690 +testing/scanpy/_helpers/data.py,sha256=HBPUMW7Z4cirwOb8OA7qPRVKPI3Wbi6Voqm5OFWmBy0,1530 +testing/scanpy/_pytest/__init__.py,sha256=eL_0NYk5gx-6Ir5gAf5pRVQOo7JX-HBaNWmbc8bpO2M,4537 +testing/scanpy/_pytest/fixtures/__init__.py,sha256=vlXBipcwOgCKxOl0ziSey_5BfqTD7CCM7V2RZyNl3W0,1689 +testing/scanpy/_pytest/fixtures/data.py,sha256=Yw-49QQVuMU4oLloCReQhNivEWe-v2eL08f9JwsD_xE,3688 +testing/scanpy/_pytest/marks.py,sha256=zs5N21hEK5nWCn8QT0zUCU8G1407tvoLReTo5VtnoEE,2588 +testing/scanpy/_pytest/params.py,sha256=jSrR20_hv6tSO7_oZWcQRQ525Lgu1G8MtbN-AiDvw4k,2428 diff --git a/.venv/lib/python3.11/site-packages/scanpy-1.11.5.dist-info/REQUESTED b/.venv/lib/python3.11/site-packages/scanpy-1.11.5.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/.venv/lib/python3.11/site-packages/scanpy-1.11.5.dist-info/WHEEL b/.venv/lib/python3.11/site-packages/scanpy-1.11.5.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..12228d414b6cfed7c39d3781c85c63256a1d7fb5 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy-1.11.5.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.27.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/.venv/lib/python3.11/site-packages/scanpy-1.11.5.dist-info/entry_points.txt b/.venv/lib/python3.11/site-packages/scanpy-1.11.5.dist-info/entry_points.txt new file mode 100644 index 0000000000000000000000000000000000000000..77ec043ee5a2854dee5fbbaa68d7679a6e5d1402 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy-1.11.5.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +scanpy = scanpy.cli:console_main diff --git a/.venv/lib/python3.11/site-packages/scanpy-1.11.5.dist-info/licenses/LICENSE b/.venv/lib/python3.11/site-packages/scanpy-1.11.5.dist-info/licenses/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..bd681d60775a3155c41b559235275c3bb9698997 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy-1.11.5.dist-info/licenses/LICENSE @@ -0,0 +1,30 @@ +BSD 3-Clause License + +Copyright (c) 2025 scverse +Copyright (c) 2017 F. Alexander Wolf, P. Angerer, Theis Lab +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 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. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +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. diff --git a/.venv/lib/python3.11/site-packages/scanpy/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..02066f3a4ce049cb6da46d3044dfa4c8beab072c --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/__init__.py @@ -0,0 +1,99 @@ +"""Single-Cell Analysis in Python.""" + +from __future__ import annotations + +import sys +from importlib.metadata import version +from typing import TYPE_CHECKING + +from packaging.version import Version + +from ._settings import Verbosity, settings + +if Version(version("anndata")) >= Version("0.11.0rc2"): + from anndata.io import ( + read_csv, + read_excel, + read_h5ad, + read_hdf, + read_loom, + read_mtx, + read_text, + read_umi_tools, + ) +else: + from anndata import ( + read_csv, + read_excel, + read_h5ad, + read_hdf, + read_loom, + read_mtx, + read_text, + read_umi_tools, + ) + +from anndata import AnnData, concat + +from . import datasets, experimental, external, get, logging, metrics, queries +from . import plotting as pl +from . import preprocessing as pp +from . import tools as tl +from ._utils import annotate_doc_types +from .neighbors import Neighbors +from .readwrite import read, read_10x_h5, read_10x_mtx, read_visium, write + +if TYPE_CHECKING: + from typing import Any + +__all__ = [ + "AnnData", + "Neighbors", + "Verbosity", + "concat", + "datasets", + "experimental", + "external", + "get", + "logging", + "metrics", + "pl", + "pp", + "queries", + "read", + "read_10x_h5", + "read_10x_mtx", + "read_csv", + "read_excel", + "read_h5ad", + "read_hdf", + "read_loom", + "read_mtx", + "read_text", + "read_umi_tools", + "read_visium", + "set_figure_params", + "settings", + "tl", + "write", +] + + +set_figure_params = settings._set_figure_params + +annotate_doc_types(sys.modules[__name__], "scanpy") + +# has to be done at the end, after everything has been imported +sys.modules.update({f"{__name__}.{m}": globals()[m] for m in ["tl", "pp", "pl"]}) + + +def __getattr__(name: str) -> Any: + if name == "__version__": + import warnings + from importlib.metadata import version + + msg = "`__version__` is deprecated, use `importlib.metadata.version('scanpy')` instead" + warnings.warn(msg, FutureWarning, stacklevel=2) + return version("scanpy") + + raise AttributeError diff --git a/.venv/lib/python3.11/site-packages/scanpy/__main__.py b/.venv/lib/python3.11/site-packages/scanpy/__main__.py new file mode 100644 index 0000000000000000000000000000000000000000..45d0f603d9098cb107c4f87f00f7a18dd3e4fe0b --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/__main__.py @@ -0,0 +1,7 @@ +"""Scanpy CLI entry point.""" + +from __future__ import annotations + +from .cli import console_main + +console_main() diff --git a/.venv/lib/python3.11/site-packages/scanpy/_compat.py b/.venv/lib/python3.11/site-packages/scanpy/_compat.py new file mode 100644 index 0000000000000000000000000000000000000000..b694aa061c1c78c66fad404124b16b62473b9b5f --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/_compat.py @@ -0,0 +1,245 @@ +from __future__ import annotations + +import os +import sys +import warnings +from dataclasses import dataclass, field +from functools import cache, partial, wraps +from importlib.util import find_spec +from pathlib import Path +from types import UnionType +from typing import TYPE_CHECKING, Literal, ParamSpec, TypeVar, cast, overload + +from packaging.version import Version +from scipy import sparse + +if TYPE_CHECKING: + from collections.abc import Callable + from functools import _SingleDispatchCallable + from importlib.metadata import PackageMetadata + + +P = ParamSpec("P") +R = TypeVar("R") + + +_CSMatrix = sparse.csr_matrix | sparse.csc_matrix # noqa: TID251 +"""Only use if you want to specially handle matrices as opposed to arrays""" + +CSRBase = sparse.csr_matrix # noqa: TID251 +CSCBase = sparse.csc_matrix # noqa: TID251 +SpBase = sparse.spmatrix # noqa: TID251 +CSBase = _CSMatrix + + +if TYPE_CHECKING: + # type checkers are confused and can only see …core.Array + from dask.array.core import Array as DaskArray +elif find_spec("dask"): + from dask.array import Array as DaskArray +else: + DaskArray = type("Array", (), {}) + DaskArray.__module__ = "dask.array" + + +if find_spec("zappy") or TYPE_CHECKING: + from zappy.base import ZappyArray +else: + ZappyArray = type("ZappyArray", (), {}) + ZappyArray.__module__ = "zappy.base" + + +__all__ = [ + "DaskArray", + "ZappyArray", + "_numba_threading_layer", + "deprecated", + "fullname", + "njit", + "old_positionals", + "pkg_metadata", + "pkg_version", +] + + +def fullname(typ: type) -> str: + module = typ.__module__ + name = typ.__qualname__ + if module == "builtins" or module is None: + return name + return f"{module}.{name}" + + +if sys.version_info >= (3, 11): + from contextlib import chdir +else: + import os + from contextlib import AbstractContextManager + + @dataclass + class chdir(AbstractContextManager): # noqa: N801 + path: Path + _old_cwd: list[Path] = field(default_factory=list) + + def __enter__(self) -> None: + self._old_cwd.append(Path.cwd()) + os.chdir(self.path) + + def __exit__(self, *_excinfo) -> None: + os.chdir(self._old_cwd.pop()) + + +def pkg_metadata(package: str) -> PackageMetadata: + from importlib.metadata import metadata + + return metadata(package) + + +@cache +def pkg_version(package: str) -> Version: + from importlib.metadata import version + + return Version(version(package)) + + +if find_spec("legacy_api_wrap") or TYPE_CHECKING: + from legacy_api_wrap import legacy_api # noqa: TID251 + + old_positionals = partial(legacy_api, category=FutureWarning) +else: + # legacy_api_wrap is currently a hard dependency, + # but this code makes it possible to run scanpy without it. + def old_positionals(*old_positionals: str): + return lambda func: func + + +if sys.version_info >= (3, 11): + + def add_note(exc: BaseException, note: str) -> None: + exc.add_note(note) + +else: + + def add_note(exc: BaseException, note: str) -> None: + if not hasattr(exc, "__notes__"): + exc.__notes__ = [] + exc.__notes__.append(note) + + +if sys.version_info >= (3, 13): + from warnings import deprecated as _deprecated +else: + from typing_extensions import deprecated as _deprecated + + +deprecated = partial(_deprecated, category=FutureWarning) + + +@overload +def njit(fn: Callable[P, R], /) -> Callable[P, R]: ... +@overload +def njit() -> Callable[[Callable[P, R]], Callable[P, R]]: ... +def njit( + fn: Callable[P, R] | None = None, / +) -> Callable[P, R] | Callable[[Callable[P, R]], Callable[P, R]]: + """Jit-compile a function using numba. + + On call, this function dispatches to a parallel or sequential numba function, + depending on if it has been called from a thread pool. + + See + """ + + def decorator(f: Callable[P, R], /) -> Callable[P, R]: + import numba + + fns: dict[bool, Callable[P, R]] = { + parallel: numba.njit(f, cache=True, parallel=parallel) # noqa: TID251 + for parallel in (True, False) + } + + @wraps(f) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + parallel = not _is_in_unsafe_thread_pool() + if not parallel: + msg = ( + "Detected unsupported threading environment. " + f"Trying to run {f.__name__} in serial mode. " + "In case of problems, install `tbb`." + ) + warnings.warn(msg, stacklevel=2) + return fns[parallel](*args, **kwargs) + + return wrapper + + return decorator if fn is None else decorator(fn) + + +LayerType = Literal["default", "safe", "threadsafe", "forksafe"] +Layer = Literal["tbb", "omp", "workqueue"] + + +LAYERS: dict[LayerType, set[Layer]] = { + "default": {"tbb", "omp", "workqueue"}, + "safe": {"tbb"}, + "threadsafe": {"tbb", "omp"}, + "forksafe": {"tbb", "workqueue", *(() if sys.platform == "linux" else {"omp"})}, +} + + +def _is_in_unsafe_thread_pool() -> bool: + import threading + + current_thread = threading.current_thread() + # ThreadPoolExecutor threads typically have names like 'ThreadPoolExecutor-0_1' + return ( + current_thread.name.startswith("ThreadPoolExecutor") + and _numba_threading_layer() not in LAYERS["threadsafe"] + ) + + +@cache +def _numba_threading_layer() -> Layer: + """Get numba’s threading layer. + + This function implements the algorithm as described in + + """ + import importlib + + import numba + + if (available := LAYERS.get(numba.config.THREADING_LAYER)) is None: + # given by direct name + return numba.config.THREADING_LAYER + + # given by layer type (safe, …) + for layer in cast("list[Layer]", numba.config.THREADING_LAYER_PRIORITY): + if layer not in available: + continue + if layer != "workqueue": + try: # `importlib.util.find_spec` doesn’t work here + importlib.import_module(f"numba.np.ufunc.{layer}pool") + except ImportError: + continue + # the layer has been found + return layer + msg = ( + f"No loadable threading layer: {numba.config.THREADING_LAYER=} " + f" ({available=}, {numba.config.THREADING_LAYER_PRIORITY=})" + ) + raise ValueError(msg) + + +def _register_union( + sdc: _SingleDispatchCallable, typ: type | UnionType +) -> Callable[[Callable[P, R]], Callable[P, R]]: + if sys.version_info >= (3, 11) or not isinstance(typ, UnionType): + return sdc.register(typ) + + def decorator(f: Callable[P, R]) -> Callable[P, R]: + for subtype in typ.__args__: + sdc.register(subtype)(f) + return f + + return decorator diff --git a/.venv/lib/python3.11/site-packages/scanpy/_settings/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/_settings/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..346d33371e109a1dfb0a80859169afe3c481d9c8 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/_settings/__init__.py @@ -0,0 +1,494 @@ +from __future__ import annotations + +import inspect +import sys +from functools import wraps +from pathlib import Path +from time import time +from typing import TYPE_CHECKING, Literal, ParamSpec, TypeVar, get_args + +from .. import logging +from .._compat import old_positionals +from .._singleton import SingletonMeta, documenting +from ..logging import _RootLogger, _set_log_file, _set_log_level +from .verbosity import Verbosity + +if TYPE_CHECKING: + from collections.abc import Callable, Iterable + from types import UnionType + from typing import ClassVar, Concatenate, Self, TextIO + + from .verbosity import _VerbosityName + + # Collected from the print_* functions in matplotlib.backends + _Format = ( + Literal["png", "jpg", "tif", "tiff"] # noqa: PYI030 + | Literal["pdf", "ps", "eps", "svg", "svgz", "pgf"] + | Literal["raw", "rgba"] + ) + + +S = TypeVar("S") +T = TypeVar("T") +P = ParamSpec("P") +R = TypeVar("R") + + +AnnDataFileFormat = Literal["h5ad", "zarr"] + + +def _type_check(var: object, name: str, types: type | UnionType): + if isinstance(var, types): + return + if isinstance(types, type): + possible_types_str = types.__name__ + else: + type_names = [t.__name__ for t in get_args(types)] + possible_types_str = f"{', '.join(type_names[:-1])} or {type_names[-1]}" + msg = f"{name} must be of type {possible_types_str}" + raise TypeError(msg) + + +def _type_check_arg2( + types: type | UnionType, +) -> Callable[[Callable[Concatenate[S, T, P], R]], Callable[Concatenate[S, T, P], R]]: + def decorator( + func: Callable[Concatenate[S, T, P], R], + ) -> Callable[Concatenate[S, T, P], R]: + @wraps(func) + def wrapped(self: S, var: T, *args: P.args, **kwargs: P.kwargs) -> R: + __tracebackhide__ = True + _type_check(var, func.__name__, types) + return func(self, var, *args, **kwargs) + + return wrapped + + return decorator + + +# `type` is only here because of https://github.com/astral-sh/ruff/issues/20225 +class SettingsMeta(SingletonMeta, type): + # logging + _root_logger: _RootLogger + _logfile: TextIO + _verbosity: Verbosity + # rest + _n_pcs: int + _plot_suffix: str + _file_format_data: AnnDataFileFormat + _file_format_figs: str + _autosave: bool + _autoshow: bool + _writedir: Path + _cachedir: Path + _datasetdir: Path + _figdir: Path + _cache_compression: Literal["lzf", "gzip"] | None + _max_memory: float + _n_jobs: int + _categories_to_ignore: list[str] + _frameon: bool + """See set_figure_params.""" + _vector_friendly: bool + """Set to true if you want to include pngs in svgs and pdfs.""" + _low_resolution_warning: bool + """Print warning when saving a figure with low resolution.""" + _start: float + """Time when the settings module is first imported.""" + _previous_time: float + """Variable for timing program parts.""" + _previous_memory_usage: int + """Stores the previous memory usage.""" + + @property + def verbosity(cls) -> Verbosity: + """Verbosity level (default :attr:`Verbosity.warning`).""" + return cls._verbosity + + @verbosity.setter + def verbosity(cls, verbosity: Verbosity | _VerbosityName | int) -> None: + try: + cls._verbosity = ( + Verbosity[verbosity.lower()] + if isinstance(verbosity, str) + else Verbosity(verbosity) + ) + except KeyError: + msg = ( + f"Cannot set verbosity to {verbosity}. " + f"Accepted string values are: {Verbosity.__members__.keys()}" + ) + raise ValueError(msg) from None + _set_log_level(cls, cls._verbosity.level) + + @property + def N_PCS(cls) -> int: # noqa: N802 + """Default number of principal components to use.""" + return cls._n_pcs + + @N_PCS.setter + @_type_check_arg2(int) + def N_PCS(cls, n_pcs: int) -> None: # noqa: N802 + cls._n_pcs = n_pcs + + @property + def plot_suffix(cls) -> str: + """Global suffix that is appended to figure filenames.""" + return cls._plot_suffix + + @plot_suffix.setter + @_type_check_arg2(str) + def plot_suffix(cls, plot_suffix: str) -> None: + cls._plot_suffix = plot_suffix + + @property + def file_format_data(cls) -> AnnDataFileFormat: + """File format for saving AnnData objects.""" + return cls._file_format_data + + @file_format_data.setter + @_type_check_arg2(str) + def file_format_data(cls, file_format: AnnDataFileFormat) -> None: + if file_format not in (file_format_options := get_args(AnnDataFileFormat)): + msg = ( + f"Cannot set file_format_data to {file_format}. " + f"Must be one of {file_format_options}" + ) + raise ValueError(msg) + cls._file_format_data: AnnDataFileFormat = file_format + + @property + def file_format_figs(cls) -> str: + """File format for saving figures. + + For example `'png'`, `'pdf'` or `'svg'`. Many other formats work as well (see + :func:`matplotlib.pyplot.savefig`). + """ + return cls._file_format_figs + + @file_format_figs.setter + @_type_check_arg2(str) + def file_format_figs(cls, figure_format: str) -> None: + cls._file_format_figs = figure_format + + @property + def autosave(cls) -> bool: + """Automatically save figures in :attr:`~scanpy.settings.figdir` (default `False`). + + Do not show plots/figures interactively. + """ + return cls._autosave + + @autosave.setter + @_type_check_arg2(bool) + def autosave(cls, autosave: bool) -> None: + cls._autosave = autosave + + @property + def autoshow(cls) -> bool: + """Automatically show figures if `autosave == False` (default `True`). + + There is no need to call the matplotlib pl.show() in this case. + """ + return cls._autoshow + + @autoshow.setter + @_type_check_arg2(bool) + def autoshow(cls, autoshow: bool) -> None: + cls._autoshow = autoshow + + @property + def writedir(cls) -> Path: + """Directory where the function scanpy.write writes to by default.""" + return cls._writedir + + @writedir.setter + @_type_check_arg2(Path | str) + def writedir(cls, writedir: Path | str) -> None: + cls._writedir = Path(writedir) + + @property + def cachedir(cls) -> Path: + """Directory for cache files (default `'./cache/'`).""" + return cls._cachedir + + @cachedir.setter + @_type_check_arg2(Path | str) + def cachedir(cls, cachedir: Path | str) -> None: + cls._cachedir = Path(cachedir) + + @property + def datasetdir(cls) -> Path: + """Directory for example :mod:`~scanpy.datasets` (default `'./data/'`).""" + return cls._datasetdir + + @datasetdir.setter + @_type_check_arg2(Path | str) + def datasetdir(cls, datasetdir: Path | str) -> None: + cls._datasetdir = Path(datasetdir).resolve() + + @property + def figdir(cls) -> Path: + """Directory for saving figures (default `'./figures/'`).""" + return cls._figdir + + @figdir.setter + @_type_check_arg2(Path | str) + def figdir(cls, figdir: Path | str) -> None: + cls._figdir = Path(figdir) + + @property + def cache_compression(cls) -> Literal["lzf", "gzip"] | None: + """Compression for `sc.read(..., cache=True)` (default `'lzf'`).""" + return cls._cache_compression + + @cache_compression.setter + def cache_compression( + cls, cache_compression: Literal["lzf", "gzip"] | None + ) -> None: + if cache_compression not in {"lzf", "gzip", None}: + msg = ( + f"`cache_compression` ({cache_compression}) " + "must be in {'lzf', 'gzip', None}" + ) + raise ValueError(msg) + cls._cache_compression = cache_compression + + @property + def max_memory(cls) -> int | float: + """Maximum memory usage in Gigabyte. + + Is currently not well respected… + """ + return cls._max_memory + + @max_memory.setter + @_type_check_arg2(int | float) + def max_memory(cls, max_memory: float) -> None: + cls._max_memory = max_memory + + @property + def n_jobs(cls) -> int: + """Default number of jobs/ CPUs to use for parallel computing. + + Set to `-1` in order to use all available cores. + Not all algorithms support special behavior for numbers < `-1`, + so make sure to leave this setting as >= `-1`. + """ + return cls._n_jobs + + @n_jobs.setter + @_type_check_arg2(int) + def n_jobs(cls, n_jobs: int) -> None: + cls._n_jobs = n_jobs + + @property + def logpath(cls) -> Path | None: + """The file path `logfile` was set to.""" + return cls._logpath + + @logpath.setter + @_type_check_arg2(Path | str) + def logpath(cls, logpath: Path | str | None) -> None: + if logpath is None: + cls.logfile = None + cls._logpath = None + return + # set via “file object” branch of logfile.setter + cls.logfile = Path(logpath).open("a") # noqa: SIM115 + cls._logpath = Path(logpath) + + @property + def logfile(cls) -> TextIO: + """The open file to write logs to. + + Set it to a :class:`~pathlib.Path` or :class:`str` to open a new one. + The default `None` corresponds to :obj:`sys.stdout` in jupyter notebooks + and to :obj:`sys.stderr` otherwise. + + For backwards compatibility, setting it to `''` behaves like setting it to `None`. + """ + return cls._logfile + + @logfile.setter + def logfile(cls, logfile: Path | str | TextIO | None) -> None: + if not logfile: # "" or None + logfile = cls._default_logfile() + if isinstance(logfile, Path | str): + cls.logpath = logfile + return + cls._logfile = logfile + cls._logpath = None + _set_log_file(cls) + + @property + def categories_to_ignore(cls) -> list[str]: + """Categories that are omitted in plotting etc.""" + return cls._categories_to_ignore + + @categories_to_ignore.setter + def categories_to_ignore(cls, categories_to_ignore: Iterable[str]) -> None: + categories_to_ignore = list(categories_to_ignore) + for i, cat in enumerate(categories_to_ignore): + _type_check(cat, f"categories_to_ignore[{i}]", str) + cls._categories_to_ignore = categories_to_ignore + + # -------------------------------------------------------------------------------- + # Functions + # -------------------------------------------------------------------------------- + + def set_figure_params(cls, *args, **kwargs) -> None: + cls._set_figure_params(*args, **kwargs) + + @old_positionals( + "scanpy", + "dpi", + "dpi_save", + "frameon", + "vector_friendly", + "fontsize", + "figsize", + "color_map", + "format", + "facecolor", + "transparent", + "ipython_format", + ) + def _set_figure_params( # noqa: PLR0913 + cls, + *, + scanpy: bool = True, + dpi: int = 80, + dpi_save: int = 150, + frameon: bool = True, + vector_friendly: bool = True, + fontsize: int = 14, + figsize: int | None = None, + color_map: str | None = None, + format: _Format = "pdf", + facecolor: str | None = None, + transparent: bool = False, + ipython_format: str | Iterable[str] = "retina", + ) -> None: + """Set resolution/size, styling and format of figures. + + Parameters + ---------- + scanpy + Init default values for :obj:`matplotlib.rcParams` suited for Scanpy. + dpi + Resolution of rendered figures – this influences the size of figures in notebooks. + dpi_save + Resolution of saved figures. + This should typically be higher to achieve publication quality. + frameon + Add frames and axes labels to scatter plots. + vector_friendly + Plot scatter plots using `png` backend even when exporting as `pdf` or `svg`. + fontsize + Set the fontsize for several `rcParams` entries. Ignored if `scanpy=False`. + figsize + Set `rcParams['figure.figsize']`. + color_map + Convenience method for setting the default color map. Ignored if `scanpy=False`. + format + This sets the default format for saving figures: `file_format_figs`. + facecolor + Sets backgrounds via `rcParams['figure.facecolor'] = facecolor` and + `rcParams['axes.facecolor'] = facecolor`. + transparent + Save figures with transparent background. + Sets `rcParams['savefig.transparent']`. + ipython_format + Only concerns the notebook/IPython environment; see + `matplotlib_inline.backend_inline.set_matplotlib_formats + `_ + for details. + + """ + if cls._is_run_from_ipython(): + # No docs yet: https://github.com/ipython/matplotlib-inline/issues/12 + from matplotlib_inline.backend_inline import set_matplotlib_formats + + if isinstance(ipython_format, str): + ipython_format = [ipython_format] + + set_matplotlib_formats(*ipython_format) + + from matplotlib import rcParams + + cls._vector_friendly = vector_friendly + cls.file_format_figs = format + if dpi is not None: + rcParams["figure.dpi"] = dpi + if dpi_save is not None: + rcParams["savefig.dpi"] = dpi_save + if transparent is not None: + rcParams["savefig.transparent"] = transparent + if facecolor is not None: + rcParams["figure.facecolor"] = facecolor + rcParams["axes.facecolor"] = facecolor + if scanpy: + from ..plotting._rcmod import set_rcParams_scanpy + + set_rcParams_scanpy(fontsize=fontsize, color_map=color_map) + if figsize is not None: + rcParams["figure.figsize"] = figsize + cls._frameon = frameon + + @staticmethod + def _is_run_from_ipython(): + """Determine whether we're currently in IPython.""" + import builtins + + return getattr(builtins, "__IPYTHON__", False) + + @classmethod + def _default_logfile(cls) -> TextIO: + return sys.stdout if cls._is_run_from_ipython() else sys.stderr + + def __str__(cls) -> str: + return "\n".join( + f"{k} = {v!r}" + for k, v in inspect.getmembers(cls) + if not k.startswith("_") and k != "getdoc" + ) + + +class settings(metaclass=SettingsMeta): # noqa: N801 + """Settings for scanpy.""" + + def __new__(cls) -> type[Self]: + return cls + + # logging + _root_logger: ClassVar = _RootLogger(logging.WARNING) + _logfile: ClassVar = SettingsMeta._default_logfile() + _logpath: ClassVar = None + _verbosity: ClassVar = Verbosity.warning + # rest + _n_pcs: ClassVar = 50 + _plot_suffix: ClassVar = "" + _file_format_data: ClassVar = "h5ad" + _file_format_figs: ClassVar = "pdf" + _autosave: ClassVar = False + _autoshow: ClassVar = True + _writedir: ClassVar = Path("./write") + _cachedir: ClassVar = Path("./cache") + _datasetdir: ClassVar = Path("./data") + _figdir: ClassVar = Path("./figures") + _cache_compression: ClassVar = "lzf" + _max_memory: ClassVar = 15 + _n_jobs: ClassVar = 1 + _categories_to_ignore: ClassVar = ["N/A", "dontknow", "no_gate", "?"] + _frameon: ClassVar = True + _vector_friendly: ClassVar = False + _low_resolution_warning: ClassVar = True + _start: ClassVar = time() + _previous_time: ClassVar = _start + _previous_memory_usage: ClassVar = -1 + + +if not documenting(): # finish initialization + _set_log_level(settings, settings.verbosity.level) + _set_log_file(settings) diff --git a/.venv/lib/python3.11/site-packages/scanpy/_settings/verbosity.py b/.venv/lib/python3.11/site-packages/scanpy/_settings/verbosity.py new file mode 100644 index 0000000000000000000000000000000000000000..eb4b499337cbbb61431c97ca872620a3af482306 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/_settings/verbosity.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from contextlib import contextmanager +from enum import EnumMeta, IntEnum +from logging import getLevelName +from typing import TYPE_CHECKING + +from .._compat import deprecated + +if TYPE_CHECKING: + from collections.abc import Generator + from typing import Literal + + _VerbosityName = Literal["error", "warning", "info", "hint", "debug"] + _LoggingLevelName = Literal["CRITICAL", "ERROR", "WARNING", "INFO", "HINT", "DEBUG"] + + +_VERBOSITY_TO_LOGLEVEL: dict[int | _VerbosityName, _LoggingLevelName] = { + "error": "ERROR", + "warning": "WARNING", + "info": "INFO", + "hint": "HINT", + "debug": "DEBUG", +} +_VERBOSITY_TO_LOGLEVEL.update(dict(enumerate(list(_VERBOSITY_TO_LOGLEVEL.values())))) + + +class VerbosityMeta(EnumMeta): + @property + @deprecated("Use `Verbosity.warning` instead") + def warn(cls) -> Verbosity: + return Verbosity.warning + + +class Verbosity(IntEnum, metaclass=VerbosityMeta): + """Logging verbosity levels for :attr:`scanpy.settings.verbosity`.""" + + error = 0 + """Error (`0`)""" + warning = 1 + """Warning (`1`)""" + info = 2 + """Info (`2`)""" + hint = 3 + """Hint (`3`)""" + debug = 4 + """Debug (`4`)""" + + def __eq__(self, other: object) -> bool: + if isinstance(other, Verbosity): + return self is other + if isinstance(other, int): + return self.value == other + if isinstance(other, str): + return self.name == other + return NotImplemented + + def __hash__(self) -> int: + # See https://docs.astral.sh/ruff/rules/eq-without-hash/ + return super().__hash__() + + @property + def level(self) -> int: + """The :ref:`logging level ` corresponding to this verbosity level.""" + # getLevelName(str) returns the int level… + return getLevelName(_VERBOSITY_TO_LOGLEVEL[self.name]) + + @contextmanager + def override( + self, verbosity: Verbosity | _VerbosityName | int + ) -> Generator[Verbosity, None, None]: + """Temporarily override verbosity. + + >>> import scanpy as sc + >>> sc.settings.verbosity = sc.Verbosity.info + >>> with sc.settings.verbosity.override(sc.settings.verbosity.debug): + ... sc.settings.verbosity + + >>> sc.settings.verbosity + + """ + from scanpy import settings + + settings.verbosity = verbosity + try: + yield self + finally: + settings.verbosity = self diff --git a/.venv/lib/python3.11/site-packages/scanpy/_singleton.py b/.venv/lib/python3.11/site-packages/scanpy/_singleton.py new file mode 100644 index 0000000000000000000000000000000000000000..06590e2b7db3203ce6465b69b23348ab7c75c227 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/_singleton.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import os +from traceback import extract_stack +from types import FunctionType, MethodType + + +def documenting() -> bool: + """Return whether this is being called from Sphinx.""" + if not os.environ.get("SPHINX_RUNNING"): + return False + for frame in extract_stack(): + # Let any sphinx ext get the docstring + if frame.name in { + "eval_config_file", # Sphinx import + "generate_autosummary_docs", # Autosummary generator + # "parse_generated_content", # Autodoc parser + "get_object_members", # Class level of autodoc + "import_object", # Attr level of autodoc + }: + return True + return False + + +class SingletonMeta(type): + def __new__(mcls, cls_name: str, *args, **kwargs): + cls = super().__new__(mcls, cls_name, *args, **kwargs) + + # We do something differently when we are imported by autosummary. + if documenting(): + props = {} + for name in dir(cls): + if (attr := getattr(mcls, name, None)) is None: + continue + if isinstance(attr, FunctionType | MethodType): + # Circumvent https://github.com/tox-dev/sphinx-autodoc-typehints/pull/157 + setattr(cls, name, getattr(cls, name)) + if name not in cls.__dict__ and isinstance(attr, property): + # Allow autosummary to access the property, not the value + props[name] = getattr(mcls, name) + + def getattribute(_, name: str) -> object: + """Return property or value depending on whether we are in autosummary. + + If an singleton instance property/method is accessed by autodoc/autosummary, + return the property/method object, not the value/bound method. + """ + if documenting() and name in props: + return props[name] + return object.__getattribute__(cls, name) + + mcls.__getattribute__ = getattribute + + return cls + + def __dir__(cls) -> list[str]: + # Deduplicate preserving order + d = dict.fromkeys(super().__dir__()) | dict.fromkeys(dir(type(cls))) + return [k for k in d if k != "mro"] diff --git a/.venv/lib/python3.11/site-packages/scanpy/_types.py b/.venv/lib/python3.11/site-packages/scanpy/_types.py new file mode 100644 index 0000000000000000000000000000000000000000..af2d430fe432518d7102e5a02e949a7a80372b8d --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/_types.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from typing import Literal + +__all__ = ["HVGFlavor"] + +HVGFlavor = Literal["seurat", "cell_ranger", "seurat_v3", "seurat_v3_paper"] diff --git a/.venv/lib/python3.11/site-packages/scanpy/_utils/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/_utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5918dce886953f6b9fa0a676e72baae7d95cfdd9 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/_utils/__init__.py @@ -0,0 +1,1087 @@ +"""Utility functions and classes. + +This file largely consists of the old _utils.py file. Over time, these functions +should be moved of this file. +""" + +from __future__ import annotations + +import importlib.util +import inspect +import re +import sys +import warnings +from contextlib import suppress +from enum import Enum +from functools import partial, reduce, singledispatch, wraps +from operator import mul, or_, truediv +from textwrap import indent +from types import MethodType, ModuleType, UnionType +from typing import ( + TYPE_CHECKING, + Literal, + NamedTuple, + Union, + get_args, + get_origin, + overload, +) +from weakref import WeakSet + +import h5py +import numpy as np +from packaging.version import Version +from scipy import sparse + +from .. import logging as logg +from .._compat import CSBase, DaskArray, _CSMatrix, _register_union, pkg_version +from .._settings import settings +from .compute.is_constant import is_constant # noqa: F401 + +if pkg_version("anndata") >= Version("0.10.0"): + from anndata._core.sparse_dataset import ( + BaseCompressedSparseDataset as SparseDataset, + ) +else: + from anndata._core.sparse_dataset import SparseDataset + + +if TYPE_CHECKING: + from collections.abc import Callable, Iterable, KeysView, Mapping + from pathlib import Path + from typing import Any, TypeVar + + from anndata import AnnData + from igraph import Graph + from numpy.typing import ArrayLike, DTypeLike, NDArray + + from .._compat import CSRBase + from ..neighbors import NeighborsParams, RPForestDict + + _MemoryArray = NDArray | CSBase + _SupportedArray = _MemoryArray | DaskArray + + _SA = TypeVar("_SA", bound=_SupportedArray) + + _ForT = TypeVar("_ForT", bound=Callable | type) + + +LegacyUnionType = type(Union[int, str]) # noqa: UP007 + + +class Empty(Enum): + token = 0 + + def __repr__(self) -> str: + return "_empty" + + +_empty = Empty.token + + +def ensure_igraph() -> None: + if importlib.util.find_spec("igraph"): + return + msg = ( + "Please install the igraph package: " + "`conda install -c conda-forge python-igraph` or " + "`pip3 install igraph`." + ) + raise ImportError(msg) + + +def getdoc(c_or_f: Callable | type) -> str | None: + if getattr(c_or_f, "__doc__", None) is None: + return None + doc = inspect.getdoc(c_or_f) + if isinstance(c_or_f, type) and hasattr(c_or_f, "__init__"): + sig = inspect.signature(c_or_f.__init__) + else: + sig = inspect.signature(c_or_f) + + def type_doc(name: str): + param: inspect.Parameter = sig.parameters[name] + cls = getattr(param.annotation, "__qualname__", repr(param.annotation)) + if param.default is not param.empty: + return f"{cls}, optional (default: {param.default!r})" + else: + return cls + + return "\n".join( + f"{line} : {type_doc(line)}" if line.strip() in sig.parameters else line + for line in doc.split("\n") + ) + + +def renamed_arg(old_name, new_name, *, pos_0: bool = False): + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + if old_name in kwargs: + f_name = func.__name__ + pos_str = ( + ( + f" at first position. Call it as `{f_name}(val, ...)` " + f"instead of `{f_name}({old_name}=val, ...)`" + ) + if pos_0 + else "" + ) + msg = ( + f"In function `{f_name}`, argument `{old_name}` " + f"was renamed to `{new_name}`{pos_str}." + ) + warnings.warn(msg, FutureWarning, stacklevel=3) + if pos_0: + args = (kwargs.pop(old_name), *args) + else: + kwargs[new_name] = kwargs.pop(old_name) + return func(*args, **kwargs) + + return wrapper + + return decorator + + +def _import_name(full_name: str) -> Any: + from importlib import import_module + + parts = full_name.split(".") + obj = import_module(parts[0]) + for _i, name in enumerate(parts[1:]): + i = _i + try: + obj = import_module(f"{obj.__name__}.{name}") + except ModuleNotFoundError: + break + else: + i = len(parts) + for name in parts[i + 1 :]: + try: + obj = getattr(obj, name) + except AttributeError as e: # noqa: PERF203 + msg = f"{parts[:i]}, {parts[i + 1 :]}, {obj} {name}" + raise RuntimeError(msg) from e + return obj + + +def _one_of_ours(obj, root: str): + return ( + hasattr(obj, "__name__") + and not obj.__name__.split(".")[-1].startswith("_") + and getattr( + obj, "__module__", getattr(obj, "__qualname__", obj.__name__) + ).startswith(root) + ) + + +def descend_classes_and_funcs(mod: ModuleType, root: str, encountered=None): + if encountered is None: + encountered = WeakSet() + for obj in vars(mod).values(): + if not _one_of_ours(obj, root) or obj in encountered: + continue + encountered.add(obj) + if callable(obj) and not isinstance(obj, MethodType): + yield obj + if isinstance(obj, type): + for m in vars(obj).values(): + if callable(m) and _one_of_ours(m, root): + yield m + elif isinstance(obj, ModuleType): + if obj.__name__.startswith("scanpy.tests"): + # Python’s import mechanism seems to add this to `scanpy`’s attributes + continue + yield from descend_classes_and_funcs(obj, root, encountered) + + +def annotate_doc_types(mod: ModuleType, root: str): + for c_or_f in descend_classes_and_funcs(mod, root): + with suppress(AttributeError): + c_or_f.getdoc = partial(getdoc, c_or_f) + + +_leading_whitespace_re = re.compile("(^[ ]*)(?:[^ \n])", re.MULTILINE) + + +def _doc_params(**replacements: str): + def dec(obj: _ForT) -> _ForT: + assert obj.__doc__ + assert "\t" not in obj.__doc__ + + # The first line of the docstring is unindented, + # so find indent size starting after it. + start_line_2 = obj.__doc__.find("\n") + 1 + assert start_line_2 > 0, f"{obj.__name__} has single-line docstring." + n_spaces = min( + len(m.group(1)) + for m in _leading_whitespace_re.finditer(obj.__doc__[start_line_2:]) + ) + + # The placeholder is already indented, so only indent subsequent lines + indented_replacements = { + k: indent(v, " " * n_spaces)[n_spaces:] for k, v in replacements.items() + } + obj.__doc__ = obj.__doc__.format_map(indented_replacements) + return obj + + return dec + + +def _check_array_function_arguments(**kwargs): + """Check for invalid arguments when an array is passed. + + Helper for functions that work on either AnnData objects or array-likes. + """ + # TODO: Figure out a better solution for documenting dispatched functions + invalid_args = [k for k, v in kwargs.items() if v is not None] + if len(invalid_args) > 0: + msg = f"Arguments {invalid_args} are only valid if an AnnData object is passed." + raise TypeError(msg) + + +def _check_use_raw( + adata: AnnData, + use_raw: None | bool, # noqa: FBT001 + *, + layer: str | None = None, +) -> bool: + """Normalize checking `use_raw`. + + My intentention here is to also provide a single place to throw a deprecation warning from in future. + """ + if use_raw is not None: + return use_raw + if layer is not None: + return False + return adata.raw is not None + + +# -------------------------------------------------------------------------------- +# Graph stuff +# -------------------------------------------------------------------------------- + + +def get_igraph_from_adjacency(adjacency: CSBase, *, directed: bool = False) -> Graph: + """Get igraph graph from adjacency matrix.""" + import igraph as ig + + sources, targets = adjacency.nonzero() + weights = dematrix(adjacency[sources, targets]).ravel() + g = ig.Graph(directed=directed) + g.add_vertices(adjacency.shape[0]) # this adds adjacency.shape[0] vertices + g.add_edges(list(zip(sources, targets, strict=True))) + with suppress(KeyError): + g.es["weight"] = weights + if g.vcount() != adjacency.shape[0]: + logg.warning( + f"The constructed graph has only {g.vcount()} nodes. " + "Your adjacency matrix contained redundant nodes." + ) + return g + + +# -------------------------------------------------------------------------------- +# Group stuff +# -------------------------------------------------------------------------------- + + +class AssoResult(NamedTuple): + asso_names: list[str] + asso_matrix: NDArray[np.floating] + + +def compute_association_matrix_of_groups( + adata: AnnData, + prediction: str, + reference: str, + *, + normalization: Literal["prediction", "reference"] = "prediction", + threshold: float = 0.01, + max_n_names: int | None = 2, +) -> AssoResult: + """Compute overlaps between groups. + + See ``identify_groups`` for identifying the groups. + + Parameters + ---------- + adata + prediction + Field name of adata.obs. + reference + Field name of adata.obs. + normalization + Whether to normalize with respect to the predicted groups or the + reference groups. + threshold + Do not consider associations whose overlap is below this fraction. + max_n_names + Control how many reference names you want to be associated with per + predicted name. Set to `None`, if you want all. + + Returns + ------- + asso_names + List of associated reference names + (`max_n_names` for each predicted name). + asso_matrix + Matrix where rows correspond to the predicted labels and columns to the + reference labels, entries are proportional to degree of association. + + """ + if normalization not in {"prediction", "reference"}: + msg = '`normalization` needs to be either "prediction" or "reference".' + raise ValueError(msg) + sanitize_anndata(adata) + cats = adata.obs[reference].cat.categories + for cat in cats: + if cat in settings.categories_to_ignore: + logg.info( + f"Ignoring category {cat!r} as it’s in `settings.categories_to_ignore`." + ) + asso_names: list[str] = [] + asso_matrix: list[list[float]] = [] + for ipred_group, pred_group in enumerate(adata.obs[prediction].cat.categories): + if "?" in pred_group: + pred_group = str(ipred_group) # noqa: PLW2901 + # starting from numpy version 1.13, subtractions of boolean arrays are deprecated + mask_pred = adata.obs[prediction].values == pred_group + mask_pred_int = mask_pred.astype(np.int8) + asso_matrix += [[]] + for ref_group in adata.obs[reference].cat.categories: + mask_ref = (adata.obs[reference].values == ref_group).astype(np.int8) + mask_ref_or_pred = mask_ref.copy() + mask_ref_or_pred[mask_pred] = 1 + # e.g. if the pred group is contained in mask_ref, mask_ref and + # mask_ref_or_pred are the same + if normalization == "prediction": + # compute which fraction of the predicted group is contained in + # the ref group + ratio_contained = ( + np.sum(mask_pred_int) - np.sum(mask_ref_or_pred - mask_ref) + ) / np.sum(mask_pred_int) + else: + # compute which fraction of the reference group is contained in + # the predicted group + ratio_contained = ( + np.sum(mask_ref) - np.sum(mask_ref_or_pred - mask_pred_int) + ) / np.sum(mask_ref) + asso_matrix[-1] += [ratio_contained] + name_list_pred = [ + cats[i] if cats[i] not in settings.categories_to_ignore else "" + for i in np.argsort(asso_matrix[-1])[::-1] + if asso_matrix[-1][i] > threshold + ] + asso_names += ["\n".join(name_list_pred[:max_n_names])] + return AssoResult(asso_names=asso_names, asso_matrix=np.array(asso_matrix)) + + +def get_associated_colors_of_groups( + reference_colors: Mapping[int, str], asso_matrix: NDArray[np.floating] +) -> list[dict[str, float]]: + return [ + { + reference_colors[i_ref]: asso_matrix[i_pred, i_ref] + for i_ref in range(asso_matrix.shape[1]) + } + for i_pred in range(asso_matrix.shape[0]) + ] + + +def identify_groups(ref_labels, pred_labels, *, return_overlaps: bool = False): + """Identify which predicted label explains which reference label. + + A predicted label explains the reference label which maximizes the minimum + of ``relative_overlaps_pred`` and ``relative_overlaps_ref``. + + Compare this with ``compute_association_matrix_of_groups``. + + Returns + ------- + A dictionary of length ``len(np.unique(ref_labels))`` that stores for each + reference label the predicted label that best explains it. + + If ``return_overlaps`` is ``True``, this will in addition return the overlap + of the reference group with the predicted group; normalized with respect to + the reference group size and the predicted group size, respectively. + + """ + ref_unique, ref_counts = np.unique(ref_labels, return_counts=True) + ref_dict = dict(zip(ref_unique, ref_counts, strict=True)) + pred_unique, pred_counts = np.unique(pred_labels, return_counts=True) + pred_dict = dict(zip(pred_unique, pred_counts, strict=True)) + associated_predictions = {} + associated_overlaps = {} + for ref_label in ref_unique: + sub_pred_unique, sub_pred_counts = np.unique( + pred_labels[ref_label == ref_labels], return_counts=True + ) + relative_overlaps_pred = [ + sub_pred_counts[i] / pred_dict[n] for i, n in enumerate(sub_pred_unique) + ] + relative_overlaps_ref = [ + sub_pred_counts[i] / ref_dict[ref_label] + for i, n in enumerate(sub_pred_unique) + ] + relative_overlaps = np.c_[relative_overlaps_pred, relative_overlaps_ref] + relative_overlaps_min = np.min(relative_overlaps, axis=1) + pred_best_index = np.argsort(relative_overlaps_min)[::-1] + associated_predictions[ref_label] = sub_pred_unique[pred_best_index] + associated_overlaps[ref_label] = relative_overlaps[pred_best_index] + if return_overlaps: + return associated_predictions, associated_overlaps + else: + return associated_predictions + + +# -------------------------------------------------------------------------------- +# Other stuff +# -------------------------------------------------------------------------------- + + +# backwards compat... remove this in the future +def sanitize_anndata(adata: AnnData) -> None: + """Transform string annotations to categoricals.""" + adata._sanitize() + + +def view_to_actual(adata: AnnData) -> None: + if adata.is_view: + warnings.warn( + "Received a view of an AnnData. Making a copy.", + stacklevel=2, + ) + adata._init_as_actual(adata.copy()) + + +def moving_average(a: np.ndarray, n: int): + """Moving average over one-dimensional array. + + Parameters + ---------- + a + One-dimensional array. + n + Number of entries to average over. n=2 means averaging over the current + the previous entry. + + Returns + ------- + An array view storing the moving average. + + """ # noqa: D401 + ret = np.cumsum(a, dtype=float) + ret[n:] = ret[n:] - ret[:-n] + return ret[n - 1 :] / n + + +# -------------------------------------------------------------------------------- +# Deal with tool parameters +# -------------------------------------------------------------------------------- + + +def update_params( + old_params: Mapping[str, Any], + new_params: Mapping[str, Any], + *, + check: bool = False, +) -> dict[str, Any]: + """Update `old_params` with `new_params`. + + If check==False, this merely adds and overwrites the content of `old_params`. + + If check==True, this only allows updating of parameters that are already + present in `old_params`. + + Parameters + ---------- + old_params + new_params + check + + Returns + ------- + updated_params + + """ + updated_params = dict(old_params) + if new_params: # allow for new_params to be None + for key, val in new_params.items(): + if key not in old_params and check: + msg = ( + f"{key!r} is not a valid parameter key, " + f"consider one of \n{list(old_params.keys())}" + ) + raise ValueError(msg) + if val is not None: + updated_params[key] = val + return updated_params + + +# `get_args` returns `tuple[Any]` so I don’t think it’s possible to get the correct type here +def get_literal_vals(typ: UnionType | Any) -> KeysView[Any]: + """Get all literal values from a Literal or Union of … of Literal type.""" + if isinstance(typ, UnionType | LegacyUnionType): + return reduce( + or_, (dict.fromkeys(get_literal_vals(t)) for t in get_args(typ)) + ).keys() + if get_origin(typ) is Literal: + return dict.fromkeys(get_args(typ)).keys() + msg = f"{typ} is not a valid Literal" + raise TypeError(msg) + + +# -------------------------------------------------------------------------------- +# Others +# -------------------------------------------------------------------------------- + + +@singledispatch +def elem_mul(x: _SupportedArray, y: _SupportedArray) -> _SupportedArray: + raise NotImplementedError + + +@elem_mul.register(np.ndarray) +@_register_union(elem_mul, CSBase) +def _elem_mul_in_mem(x: _MemoryArray, y: _MemoryArray) -> _MemoryArray: + if isinstance(x, CSBase): + # returns coo_matrix, so cast back to input type + return type(x)(x.multiply(y)) + return x * y + + +@elem_mul.register(DaskArray) +def _elem_mul_dask(x: DaskArray, y: DaskArray) -> DaskArray: + import dask.array as da + + return da.map_blocks(elem_mul, x, y) + + +if TYPE_CHECKING: + Scaling_T = TypeVar("Scaling_T", DaskArray, np.ndarray) + + +def broadcast_axis(divisor: Scaling_T, axis: Literal[0, 1]) -> Scaling_T: + divisor = np.ravel(divisor) + if axis: + return divisor[None, :] + return divisor[:, None] + + +def check_op(op): + if op not in {truediv, mul}: + msg = f"{op} not one of truediv or mul" + raise ValueError(msg) + + +@singledispatch +def axis_mul_or_truediv( + x: ArrayLike, + /, + scaling_array: np.ndarray, + axis: Literal[0, 1], + op: Callable[[Any, Any], Any], + *, + allow_divide_by_zero: bool = True, + out: ArrayLike | None = None, +) -> np.ndarray: + check_op(op) + scaling_array = broadcast_axis(scaling_array, axis) + if op is mul: + return np.multiply(x, scaling_array, out=out) + if not allow_divide_by_zero: + scaling_array = scaling_array.copy() + (scaling_array == 0) + return np.true_divide(x, scaling_array, out=out) + + +@_register_union(axis_mul_or_truediv, CSBase) +def _( + x: CSBase, + /, + scaling_array, + axis: Literal[0, 1], + op: Callable[[Any, Any], Any], + *, + allow_divide_by_zero: bool = True, + out: CSBase | None = None, +) -> CSBase: + check_op(op) + if out is not None and x.data is not out.data: + msg = "`out` argument provided but not equal to X. This behavior is not supported for sparse matrix scaling." + raise ValueError(msg) + if not allow_divide_by_zero and op is truediv: + scaling_array = scaling_array.copy() + (scaling_array == 0) + + row_scale = axis == 0 + column_scale = axis == 1 + if row_scale: + + def new_data_op(x): + return op(x.data, np.repeat(scaling_array, np.diff(x.indptr))) + + elif column_scale: + + def new_data_op(x): + return op(x.data, scaling_array.take(x.indices, mode="clip")) + + if x.format == "csr": + indices = x.indices + indptr = x.indptr + if out is not None: + x.data = new_data_op(x) + return x + return sparse.csr_matrix( # noqa: TID251 + (new_data_op(x), indices.copy(), indptr.copy()), shape=x.shape + ) + transposed = x.T + return axis_mul_or_truediv( + transposed, + scaling_array, + op=op, + axis=1 - axis, + out=transposed, + allow_divide_by_zero=allow_divide_by_zero, + ).T + + +def make_axis_chunks( + x: DaskArray, axis: Literal[0, 1] +) -> tuple[tuple[int], tuple[int]]: + if axis == 0: + return (x.chunks[axis], (1,)) + return ((1,), x.chunks[axis]) + + +@axis_mul_or_truediv.register(DaskArray) +def _( + x: DaskArray, + /, + scaling_array: Scaling_T, + axis: Literal[0, 1], + op: Callable[[Any, Any], Any], + *, + allow_divide_by_zero: bool = True, + out: None = None, +) -> DaskArray: + check_op(op) + if out is not None: + msg = "`out` is not `None`. Do not do in-place modifications on dask arrays." + raise TypeError(msg) + + import dask.array as da + + scaling_array = broadcast_axis(scaling_array, axis) + row_scale = axis == 0 + column_scale = axis == 1 + + if isinstance(scaling_array, DaskArray): + if (row_scale and x.chunksize[0] != scaling_array.chunksize[0]) or ( + column_scale + and ( + ( + len(scaling_array.chunksize) == 1 + and x.chunksize[1] != scaling_array.chunksize[0] + ) + or ( + len(scaling_array.chunksize) == 2 + and x.chunksize[1] != scaling_array.chunksize[1] + ) + ) + ): + warnings.warn( + "Rechunking scaling_array in user operation", UserWarning, stacklevel=3 + ) + scaling_array = scaling_array.rechunk(make_axis_chunks(x, axis)) + else: + scaling_array = da.from_array( + scaling_array, + chunks=make_axis_chunks(x, axis), + ) + return da.map_blocks( + axis_mul_or_truediv, + x, + scaling_array, + axis, + op, + meta=x._meta, + out=out, + allow_divide_by_zero=allow_divide_by_zero, + ) + + +@singledispatch +def axis_nnz(x: ArrayLike, /, axis: Literal[0, 1]) -> np.ndarray: + return np.count_nonzero(x, axis=axis) + + +@_register_union(axis_nnz, CSBase) +def _(x: CSBase, axis: Literal[0, 1]) -> np.ndarray: + return x.getnnz(axis=axis) + + +@axis_nnz.register(DaskArray) +def _(x: DaskArray, /, axis: Literal[0, 1]) -> DaskArray: + return x.map_blocks( + partial(axis_nnz, axis=axis), + dtype=np.int64, + meta=np.array([], dtype=np.int64), + drop_axis=axis, + ) + + +@overload +def axis_sum( + x: _CSMatrix, + *, + axis: tuple[Literal[0, 1], ...] | Literal[0, 1] | None = None, + dtype: DTypeLike | None = None, +) -> np.matrix: ... + + +@overload +def axis_sum( + x: np.ndarray, # TODO: or sparray + *, + axis: tuple[Literal[0, 1], ...] | Literal[0, 1] | None = None, + dtype: DTypeLike | None = None, +) -> np.ndarray: ... + + +@singledispatch +def axis_sum( + x: np.ndarray | CSBase, + *, + axis: tuple[Literal[0, 1], ...] | Literal[0, 1] | None = None, + dtype: DTypeLike | None = None, +) -> np.ndarray | np.matrix: + return np.sum(x, axis=axis, dtype=dtype) + + +@axis_sum.register(DaskArray) +def _( + x: DaskArray, + *, + axis: tuple[Literal[0, 1], ...] | Literal[0, 1] | None = None, + dtype: DTypeLike | None = None, +) -> DaskArray: + import dask.array as da + + if dtype is None: + dtype = getattr(np.zeros(1, dtype=x.dtype).sum(), "dtype", object) + + if isinstance(x._meta, np.ndarray) and not isinstance(x._meta, np.matrix): + return x.sum(axis=axis, dtype=dtype) + + def sum_drop_keepdims(*args, **kwargs): + kwargs.pop("computing_meta", None) + # masked operations on sparse produce which numpy matrices gives the same API issues handled here + if isinstance(x._meta, _CSMatrix | np.matrix) or isinstance( + args[0], _CSMatrix | np.matrix + ): + kwargs.pop("keepdims", None) + axis = kwargs["axis"] + if isinstance(axis, tuple): + if len(axis) != 1: + msg = f"`axis_sum` can only sum over one axis when `axis` arg is provided but got {axis} instead" + raise ValueError(msg) + kwargs["axis"] = axis[0] + # returns a np.matrix normally, which is undesireable + return np.array(np.sum(*args, dtype=dtype, **kwargs)) + + def aggregate_sum(*args, **kwargs): + return np.sum(args[0], dtype=dtype, **kwargs) + + return da.reduction( + x, + sum_drop_keepdims, + aggregate_sum, + axis=axis, + dtype=dtype, + meta=np.array([], dtype=dtype), + ) + + +@singledispatch +def check_nonnegative_integers(x: _SupportedArray, /) -> bool | DaskArray: + """Check values of X to ensure it is count data.""" + raise NotImplementedError + + +@check_nonnegative_integers.register(np.ndarray) +@_register_union(check_nonnegative_integers, CSBase) +def _check_nonnegative_integers_in_mem(x: _MemoryArray, /) -> bool: + from numbers import Integral + + data = x if isinstance(x, np.ndarray) else x.data + # Check no negatives + if np.signbit(data).any(): + return False + # Check all are integers + elif issubclass(data.dtype.type, Integral): + return True + return not np.any((data % 1) != 0) + + +@check_nonnegative_integers.register(DaskArray) +def _check_nonnegative_integers_dask(x: DaskArray, /) -> DaskArray: + return x.map_blocks(check_nonnegative_integers, dtype=bool, drop_axis=(0, 1)) + + +def dematrix(x: _SA | np.matrix) -> _SA: + if isinstance(x, np.matrix): + return x.A + if isinstance(x, DaskArray) and isinstance(x._meta, np.matrix): + return x.map_blocks(np.asarray, meta=np.array([], dtype=x.dtype)) + return x + + +def select_groups( + adata: AnnData, + groups_order_subset: Iterable[str] | Literal["all"] = "all", + key: str = "groups", +) -> tuple[list[str], NDArray[np.bool_]]: + """Get subset of groups in adata.obs[key].""" + groups_order = adata.obs[key].cat.categories + if f"{key}_masks" in adata.uns: + groups_masks_obs = adata.uns[f"{key}_masks"] + else: + groups_masks_obs = np.zeros( + (len(adata.obs[key].cat.categories), adata.obs[key].values.size), dtype=bool + ) + for iname, name in enumerate(adata.obs[key].cat.categories): + # if the name is not found, fallback to index retrieval + if name in adata.obs[key].values: + mask_obs = name == adata.obs[key].values + else: + mask_obs = str(iname) == adata.obs[key].values + groups_masks_obs[iname] = mask_obs + groups_ids = list(range(len(groups_order))) + if groups_order_subset != "all": + groups_ids = [] + for name in groups_order_subset: + groups_ids.append( + np.where(adata.obs[key].cat.categories.values == name)[0][0] + ) + if len(groups_ids) == 0: + # fallback to index retrieval + groups_ids = np.where( + np.isin( + np.arange(len(adata.obs[key].cat.categories)).astype(str), + np.array(groups_order_subset), + ) + )[0] + if len(groups_ids) == 0: + logg.debug( + f"{np.array(groups_order_subset)} invalid! specify valid " + f"groups_order (or indices) from {adata.obs[key].cat.categories}", + ) + from sys import exit + + exit(0) + groups_masks_obs = groups_masks_obs[groups_ids] + groups_order_subset = adata.obs[key].cat.categories[groups_ids].values + else: + groups_order_subset = groups_order.values + return groups_order_subset, groups_masks_obs + + +def warn_with_traceback( # noqa: PLR0917 + message, category, filename, lineno, file=None, line=None +) -> None: + """Get full tracebacks when warning is raised by setting. + + warnings.showwarning = warn_with_traceback + + See Also + -------- + https://stackoverflow.com/questions/22373927/get-traceback-of-warnings + + """ + import traceback + + traceback.print_stack() + log = ( # noqa: F841 # TODO Does this need fixing? + file if hasattr(file, "write") else sys.stderr + ) + settings.write(warnings.formatwarning(message, category, filename, lineno, line)) + + +def warn_once(msg: str, category: type[Warning], stacklevel: int = 1): + warnings.warn(msg, category, stacklevel=stacklevel) + # You'd think `'once'` works, but it doesn't at the repl and in notebooks + warnings.filterwarnings("ignore", category=category, message=re.escape(msg)) + + +def check_presence_download(filename: Path, backup_url): + """Check if file is present otherwise download.""" + if not filename.is_file(): + from ..readwrite import _download + + _download(backup_url, filename) + + +def lazy_import(full_name): + """Import a module in a way that it’s only executed on member access.""" + try: + return sys.modules[full_name] + except KeyError: + spec = importlib.util.find_spec(full_name) + module = importlib.util.module_from_spec(spec) + loader = importlib.util.LazyLoader(spec.loader) + # Make module with proper locking and get it inserted into sys.modules. + loader.exec_module(module) + return module + + +# -------------------------------------------------------------------------------- +# Neighbors +# -------------------------------------------------------------------------------- + + +def _fallback_to_uns(dct, conns, dists, conns_key, dists_key): + if conns is None and conns_key in dct: + conns = dct[conns_key] + if dists is None and dists_key in dct: + dists = dct[dists_key] + + return conns, dists + + +class NeighborsView: + """Convenience class for accessing neighbors graph representations. + + Allows to access neighbors distances, connectivities and settings + dictionary in a uniform manner. + + Parameters + ---------- + adata + AnnData object. + key + This defines where to look for neighbors dictionary, + connectivities, distances. + + neigh = NeighborsView(adata, key) + neigh['distances'] + neigh['connectivities'] + neigh['params'] + 'connectivities' in neigh + 'params' in neigh + + is the same as + + adata.obsp[adata.uns[key]['distances_key']] + adata.obsp[adata.uns[key]['connectivities_key']] + adata.uns[key]['params'] + adata.uns[key]['connectivities_key'] in adata.obsp + 'params' in adata.uns[key] + + """ + + def __init__(self, adata: AnnData, key=None): + self._connectivities = None + self._distances = None + + if key is None or key == "neighbors": + if "neighbors" not in adata.uns: + msg = 'No "neighbors" in .uns' + raise KeyError(msg) + self._neighbors_dict = adata.uns["neighbors"] + self._conns_key = "connectivities" + self._dists_key = "distances" + else: + if key not in adata.uns: + msg = f"No {key!r} in .uns" + raise KeyError(msg) + self._neighbors_dict = adata.uns[key] + self._conns_key = self._neighbors_dict["connectivities_key"] + self._dists_key = self._neighbors_dict["distances_key"] + + if self._conns_key in adata.obsp: + self._connectivities = adata.obsp[self._conns_key] + if self._dists_key in adata.obsp: + self._distances = adata.obsp[self._dists_key] + + # fallback to uns + self._connectivities, self._distances = _fallback_to_uns( + self._neighbors_dict, + self._connectivities, + self._distances, + self._conns_key, + self._dists_key, + ) + + @overload + def __getitem__(self, key: Literal["distances", "connectivities"]) -> CSRBase: ... + @overload + def __getitem__(self, key: Literal["params"]) -> NeighborsParams: ... + @overload + def __getitem__(self, key: Literal["rp_forest"]) -> RPForestDict: ... + @overload + def __getitem__(self, key: Literal["connectivities_key"]) -> str: ... + + def __getitem__(self, key: str): + if key == "distances": + if "distances" not in self: + msg = f"No {self._dists_key!r} in .obsp" + raise KeyError(msg) + return self._distances + elif key == "connectivities": + if "connectivities" not in self: + msg = f"No {self._conns_key!r} in .obsp" + raise KeyError(msg) + return self._connectivities + elif key == "connectivities_key": + return self._conns_key + else: + return self._neighbors_dict[key] + + def __contains__(self, key: str) -> bool: + if key == "distances": + return self._distances is not None + elif key == "connectivities": + return self._connectivities is not None + else: + return key in self._neighbors_dict + + +def _choose_graph( + adata: AnnData, obsp: str | None, neighbors_key: str | None +) -> CSBase: + """Choose connectivities from neighbors or another obsp entry.""" + if obsp is not None and neighbors_key is not None: + msg = "You can't specify both obsp, neighbors_key. Please select only one." + raise ValueError(msg) + + if obsp is not None: + return adata.obsp[obsp] + else: + neighbors = NeighborsView(adata, neighbors_key) + if "connectivities" not in neighbors: + msg = ( + "You need to run `pp.neighbors` first to compute a neighborhood graph." + ) + raise ValueError(msg) + return neighbors["connectivities"] + + +def _resolve_axis( + axis: Literal["obs", 0, "var", 1], +) -> tuple[Literal[0], Literal["obs"]] | tuple[Literal[1], Literal["var"]]: + if axis in {0, "obs"}: + return (0, "obs") + if axis in {1, "var"}: + return (1, "var") + msg = f"`axis` must be either 0, 1, 'obs', or 'var', was {axis!r}" + raise ValueError(msg) + + +def is_backed_type(x: object, /) -> bool: + return isinstance(x, SparseDataset | h5py.File | h5py.Dataset) + + +def raise_not_implemented_error_if_backed_type(x: object, method_name: str, /) -> None: + if is_backed_type(x): + msg = f"{method_name} is not implemented for matrices of type {type(x)}" + raise NotImplementedError(msg) diff --git a/.venv/lib/python3.11/site-packages/scanpy/_utils/_doctests.py b/.venv/lib/python3.11/site-packages/scanpy/_utils/_doctests.py new file mode 100644 index 0000000000000000000000000000000000000000..1fd785d9cf356d8e291c21b2c9e2865e8f44da54 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/_utils/_doctests.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from collections.abc import Callable +from typing import TypeVar + +F = TypeVar("F", bound=Callable) + + +def doctest_needs(mod: str) -> Callable[[F], F]: + """Mark function with doctest dependency.""" + + def decorator(func: F) -> F: + func._doctest_needs = mod + return func + + return decorator + + +def doctest_skip(reason: str) -> Callable[[F], F]: + """Mark function so doctest is skipped.""" + if not reason: + msg = "reason must not be empty" + raise ValueError(msg) + + def decorator(func: F) -> F: + func._doctest_skip_reason = reason + return func + + return decorator + + +def doctest_internet(func: F) -> F: + """Mark function so doctest gets the internet mark.""" + func._doctest_internet = True + return func diff --git a/.venv/lib/python3.11/site-packages/scanpy/_utils/compute/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/_utils/compute/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/.venv/lib/python3.11/site-packages/scanpy/_utils/compute/is_constant.py b/.venv/lib/python3.11/site-packages/scanpy/_utils/compute/is_constant.py new file mode 100644 index 0000000000000000000000000000000000000000..212f621cd562a427d78faa86080c7557bd7459bb --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/_utils/compute/is_constant.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +from functools import partial, singledispatch, wraps +from numbers import Integral +from typing import TYPE_CHECKING, overload + +import numba +import numpy as np + +from ..._compat import CSCBase, CSRBase, DaskArray, _register_union, njit + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Literal, TypeVar + + from numpy.typing import NDArray + + from ..._compat import CSBase + + _Array = NDArray | DaskArray | CSBase + + C = TypeVar("C", bound=Callable) + + +def _check_axis_supported(wrapped: C) -> C: + @wraps(wrapped) + def func(a, axis=None): + if axis is not None: + if not isinstance(axis, Integral): + msg = "axis must be integer or None." + raise TypeError(msg) + if axis not in (0, 1): + msg = "We only support axis 0 and 1 at the moment" + raise NotImplementedError(msg) + return wrapped(a, axis) + + return func + + +@overload +def is_constant(a: _Array, axis: None = None) -> bool: ... +@overload +def is_constant(a: _Array, axis: Literal[0, 1]) -> NDArray[np.bool_]: ... + + +@_check_axis_supported +@singledispatch +def is_constant( + a: NDArray, axis: Literal[0, 1] | None = None +) -> bool | NDArray[np.bool_]: + """Check whether values in array are constant. + + Params + ------ + a + Array to check + axis + Axis to reduce over. + + + Returns + ------- + Boolean array, True values were constant. + + Example + ------- + >>> a = np.array([[0, 1], [0, 0]]) + >>> a + array([[0, 1], + [0, 0]]) + >>> is_constant(a) + False + >>> is_constant(a, axis=0) + array([ True, False]) + >>> is_constant(a, axis=1) + array([False, True]) + + """ + raise NotImplementedError() + + +@is_constant.register(np.ndarray) +def _(a: NDArray, axis: Literal[0, 1] | None = None) -> bool | NDArray[np.bool_]: + # Should eventually support nd, not now. + if axis is None: + return bool((a == a.flat[0]).all()) + if axis == 0: + return _is_constant_rows(a.T) + elif axis == 1: + return _is_constant_rows(a) + + +def _is_constant_rows(a: NDArray) -> NDArray[np.bool_]: + b = np.broadcast_to(a[:, 0][:, np.newaxis], a.shape) + return (a == b).all(axis=1) + + +@_register_union(is_constant, CSRBase) +def _(a: CSRBase, axis: Literal[0, 1] | None = None) -> bool | NDArray[np.bool_]: + if axis is None: + if len(a.data) == np.multiply(*a.shape): + return is_constant(a.data) + else: + return (a.data == 0).all() + if axis == 1: + return _is_constant_csr_rows(a.data, a.indptr, a.shape) + elif axis == 0: + a = a.T.tocsr() + return _is_constant_csr_rows(a.data, a.indptr, a.shape) + + +@njit +def _is_constant_csr_rows( + data: NDArray[np.number], + indptr: NDArray[np.integer], + shape: tuple[int, int], +) -> NDArray[np.bool_]: + n = len(indptr) - 1 + result = np.ones(n, dtype=np.bool_) + for i in numba.prange(n): + start = indptr[i] + stop = indptr[i + 1] + val = data[start] if stop - start == shape[1] else 0 + for j in range(start, stop): + if data[j] != val: + result[i] = False + break + return result + + +@_register_union(is_constant, CSCBase) +def _(a: CSCBase, axis: Literal[0, 1] | None = None) -> bool | NDArray[np.bool_]: + if axis is None: + if len(a.data) == np.multiply(*a.shape): + return is_constant(a.data) + else: + return (a.data == 0).all() + if axis == 0: + return _is_constant_csr_rows(a.data, a.indptr, a.shape[::-1]) + elif axis == 1: + a = a.T.tocsc() + return _is_constant_csr_rows(a.data, a.indptr, a.shape[::-1]) + + +@is_constant.register(DaskArray) +def _(a: DaskArray, axis: Literal[0, 1] | None = None) -> bool | NDArray[np.bool_]: + if axis is None: + v = a[tuple(0 for _ in range(a.ndim))].compute() + return (a == v).all() + # TODO: use overlapping blocks and reduction instead of `drop_axis` + return a.map_blocks( + partial(is_constant, axis=axis), + drop_axis=axis, + meta=np.array([], dtype=a.dtype), + ) diff --git a/.venv/lib/python3.11/site-packages/scanpy/_utils/random.py b/.venv/lib/python3.11/site-packages/scanpy/_utils/random.py new file mode 100644 index 0000000000000000000000000000000000000000..4dc31179ddb2bc4b5b2bd53c50505a8fbd8424c5 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/_utils/random.py @@ -0,0 +1,214 @@ +from __future__ import annotations + +import random +from collections.abc import Sequence +from contextlib import contextmanager +from functools import WRAPPER_ASSIGNMENTS, wraps +from typing import TYPE_CHECKING + +import numpy as np +from sklearn.utils import check_random_state + +from . import ensure_igraph + +if TYPE_CHECKING: + from collections.abc import Generator + + from numpy.typing import NDArray + + +__all__ = [ + "RNGLike", + "SeedLike", + "_LegacyRandom", + "ith_k_tuple", + "legacy_numpy_gen", + "random_k_tuples", + "random_str", +] + +SeedLike = int | np.integer | Sequence[int] | np.random.SeedSequence +RNGLike = np.random.Generator | np.random.BitGenerator +_LegacyRandom = int | np.random.RandomState | None + + +################################### +# Compatibility with igraph’s RNG # +################################### + + +class _RNGIgraph: + """Random number generator for igraph so global seed is not changed. + + See :func:`igraph.set_random_number_generator` for the requirements. + """ + + def __init__(self, random_state: int | np.random.RandomState = 0) -> None: + self._rng = check_random_state(random_state) + + def getrandbits(self, k: int) -> int: + return self._rng.tomaxint() & ((1 << k) - 1) + + def randint(self, a: int, b: int) -> int: + return self._rng.randint(a, b + 1) + + def __getattr__(self, attr: str): + return getattr(self._rng, "normal" if attr == "gauss" else attr) + + +@contextmanager +def set_igraph_random_state( + random_state: int | np.random.RandomState, +) -> Generator[None, None, None]: + ensure_igraph() + import igraph + + rng = _RNGIgraph(random_state) + try: + igraph.set_random_number_generator(rng) + yield None + finally: + igraph.set_random_number_generator(random) + + +################################### +# Compatibility with legacy numpy # +################################### + + +def legacy_numpy_gen( + random_state: _LegacyRandom | None = None, +) -> np.random.Generator: + """Return a random generator that behaves like the legacy one.""" + if random_state is not None: + if isinstance(random_state, np.random.RandomState): + np.random.set_state(random_state.get_state(legacy=False)) + return _FakeRandomGen(random_state) + np.random.seed(random_state) + return _FakeRandomGen(np.random.RandomState(np.random.get_bit_generator())) + + +class _FakeRandomGen(np.random.Generator): + _state: np.random.RandomState + + def __init__(self, random_state: np.random.RandomState) -> None: + self._state = random_state + + @classmethod + def _delegate(cls) -> None: + for name, meth in np.random.Generator.__dict__.items(): + if name.startswith("_") or not callable(meth): + continue + + def mk_wrapper(name: str, meth): + # Old pytest versions try to run the doctests + @wraps(meth, assigned=set(WRAPPER_ASSIGNMENTS) - {"__doc__"}) + def wrapper(self: _FakeRandomGen, *args, **kwargs): + return getattr(self._state, name)(*args, **kwargs) + + return wrapper + + setattr(cls, name, mk_wrapper(name, meth)) + + +_FakeRandomGen._delegate() + + +################### +# Random k-tuples # +################### + + +def ith_k_tuple( + indices: NDArray[np.integer], /, *, n: int, k: int +) -> NDArray[np.int64]: + """Calculate the k-tuple corresponding to the given :func:`itertools.product` index. + + Given the `n**k` possible k-tuples, this function returns the k-tuples that + `[list(product(range(n), repeat=k))[i] for i in indices]` would evaluate to. + + Parameters + ---------- + indices + The tuple indices (must all be in `range(n**k)`). + n + The number of possible choices. + k + The length of each tuple. + + Returns + ------- + A 2D array where each row is the k-tuple corresponding to the index. + + Raises + ------ + ValueError + If any of the indices are out of range. + """ + if np.any((indices < 0) | (indices >= n**k)): # pragma: no cover + msg = f"Indices are out of range({n**k})." + raise ValueError(msg) + + power_of_n = n ** np.arange(k - 1, -1, -1) # [n^(k-1), n^(k-2), ..., n^0] + return (indices[:, None] // power_of_n) % n + + +def random_k_tuples( + size: int, *, n: int, k: int, rng: SeedLike | RNGLike | None = None +) -> NDArray[np.int64]: + """Draw `size` distinct `k`-tuples of values in `range(n)` from the `n**k` possible tuples. + + Parameters + ---------- + size + The number of distinct k-tuples to draw. + n + The number of possible choices. + k + The length of the tuple. + rng + The random number generator to use. + + Returns + ------- + A 2D array where each row is a distinct k-tuple. + + Raises + ------ + ValueError + If `size` is greater than the total number of possible k-tuples. + """ + rng = np.random.default_rng(rng) + + total_possible = n**k + if size > total_possible: # pragma: no cover + msg = f"Error: Cannot draw {size} unique k-tuples. Total possible is {total_possible}." + raise ValueError(msg) + + indices = rng.choice(total_possible, size=size, replace=False) + return ith_k_tuple(indices, n=n, k=k) + + +def random_str( + size: int = 1, *, length: int, alphabet: str, rng: SeedLike | RNGLike | None = None +) -> NDArray[np.str_]: + """Draw `size` distinct strings of length `k` from the given alphabet. + + Parameters + ---------- + size + The number of distinct strings to draw. + length + The length of each string. + alphabet + The alphabet to draw from. + rng + The random number generator to use. + + Returns + ------- + A 0-1D array where each element is a distinct string of length `length`. + """ + letters = np.array(list(alphabet), dtype="U1") + indices = random_k_tuples(size, n=len(letters), k=length, rng=rng) + return letters[indices].view(f"U{length}").squeeze() diff --git a/.venv/lib/python3.11/site-packages/scanpy/cli.py b/.venv/lib/python3.11/site-packages/scanpy/cli.py new file mode 100644 index 0000000000000000000000000000000000000000..b3d0f98e864b09153a77e4c7ecb523720c1ac2a6 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/cli.py @@ -0,0 +1,152 @@ +"""Definition for scanpy’s CLI entry point to be used programmatically.""" + +from __future__ import annotations + +import os +import sys +from argparse import ArgumentParser, Namespace, _SubParsersAction +from collections.abc import MutableMapping +from functools import cached_property, partial +from pathlib import Path +from shutil import which +from subprocess import run +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Iterator, Mapping, Sequence + from subprocess import CompletedProcess + from typing import Any + + +class _DelegatingSubparsersAction(_SubParsersAction): + """Like a normal subcommand action, but uses a delegator for more choices.""" + + def __init__(self, *args, _command: str, _runargs: dict[str, Any], **kwargs): + super().__init__(*args, **kwargs) + self.command = _command + self._name_parser_map = self.choices = _CommandDelegator( + _command, self, **_runargs + ) + + +class _CommandDelegator(MutableMapping): + """Provide the ability to delegate, but don’t calculate the whole list until necessary.""" + + def __init__(self, command: str, action: _DelegatingSubparsersAction, **runargs): + self.command = command + self.action = action + self.parser_map = {} + self.runargs = runargs + + def __contains__(self, k: str) -> bool: + if k in self.parser_map: + return True + try: + self[k] + except KeyError: + return False + return True + + def __getitem__(self, k: str) -> ArgumentParser: + try: + return self.parser_map[k] + except KeyError: + if which(f"{self.command}-{k}"): + return _DelegatingParser(self, k) + raise + + def __setitem__(self, k: str, v: ArgumentParser) -> None: + self.parser_map[k] = v + + def __delitem__(self, k: str) -> None: + del self.parser_map[k] + + # These methods retrieve the command list or help with doing it + + def __iter__(self) -> Iterator[str]: + yield from self.parser_map + yield from self.commands + + def __len__(self) -> int: + return len(self.parser_map) + len(self.commands) + + def __hash__(self) -> int: + return hash(self.command) + + def __eq__(self, other: Mapping[str, ArgumentParser]): + if isinstance(other, _CommandDelegator): + return all( + getattr(self, attr) == getattr(other, attr) + for attr in ["command", "action", "parser_map", "runargs"] + ) + return self.parser_map == other + + @cached_property + def commands(self) -> frozenset[str]: + return frozenset( + binary.name[len(self.command) + 1 :] + for bin_dir in os.environ["PATH"].split(os.pathsep) + for binary in Path(bin_dir).glob(f"{self.command}-*") + if os.access(binary, os.X_OK) + ) + + +class _DelegatingParser(ArgumentParser): + """Just sets parse_args().func to run the subcommand.""" + + def __init__(self, cd: _CommandDelegator, subcmd: str): + super().__init__(f"{cd.command}-{subcmd}", add_help=False) + self.cd = cd + self.subcmd = subcmd + + def parse_known_args( + self, + args: Sequence[str] | None = None, + namespace: Namespace | None = None, + ) -> tuple[Namespace, list[str]]: + msg = "Only use DelegatingParser as subparser" + assert args is not None, msg + assert namespace is None, msg + return Namespace(func=partial(run, [self.prog, *args], **self.cd.runargs)), [] + + +def _cmd_settings() -> None: + from ._settings import settings + + print(settings) + + +def main( + argv: Sequence[str] | None = None, *, check: bool = True, **runargs +) -> CompletedProcess | None: + """Run a builtin scanpy command or a scanpy-* subcommand. + + Uses :func:`subcommand.run` for the latter: + `~run(['scanpy', *argv], **runargs)` + """ + parser = ArgumentParser( + description=( + "There are a few packages providing commands. " + "Try e.g. `pip install scanpy-scripts`!" + ) + ) + parser.set_defaults(func=parser.print_help) + + subparsers: _DelegatingSubparsersAction = parser.add_subparsers( + action=_DelegatingSubparsersAction, + _command="scanpy", + _runargs={**runargs, "check": check}, + ) + + parser_settings = subparsers.add_parser("settings") + parser_settings.set_defaults(func=_cmd_settings) + + args = parser.parse_args(argv) + return args.func() + + +def console_main(): + """Serve as CLI entry point and don’t show a Python traceback if a called command fails.""" + cmd = main(check=False) + if cmd is not None: + sys.exit(cmd.returncode) diff --git a/.venv/lib/python3.11/site-packages/scanpy/datasets/10x_pbmc68k_reduced.h5ad b/.venv/lib/python3.11/site-packages/scanpy/datasets/10x_pbmc68k_reduced.h5ad new file mode 100644 index 0000000000000000000000000000000000000000..0a9ccaa2c515771de8dfa0e1b489f137fd4df1a8 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/datasets/10x_pbmc68k_reduced.h5ad @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e71d41e737c941559b7c57c9243bdb3d2c889c2adfdf00e3422ac6b46783676f +size 1772368 diff --git a/.venv/lib/python3.11/site-packages/scanpy/datasets/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/datasets/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..cfdf0f9e7f7148ad386895d35c01dd03a0fd5996 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/datasets/__init__.py @@ -0,0 +1,31 @@ +"""Builtin Datasets.""" + +from __future__ import annotations + +from ._datasets import ( + blobs, + burczynski06, + krumsiek11, + moignard15, + paul15, + pbmc3k, + pbmc3k_processed, + pbmc68k_reduced, + toggleswitch, + visium_sge, +) +from ._ebi_expression_atlas import ebi_expression_atlas + +__all__ = [ + "blobs", + "burczynski06", + "ebi_expression_atlas", + "krumsiek11", + "moignard15", + "paul15", + "pbmc3k", + "pbmc3k_processed", + "pbmc68k_reduced", + "toggleswitch", + "visium_sge", +] diff --git a/.venv/lib/python3.11/site-packages/scanpy/datasets/_datasets.py b/.venv/lib/python3.11/site-packages/scanpy/datasets/_datasets.py new file mode 100644 index 0000000000000000000000000000000000000000..da7870070f02cca175b5bb6112a615a871cd1011 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/datasets/_datasets.py @@ -0,0 +1,555 @@ +from __future__ import annotations + +import warnings +from pathlib import Path +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd +from anndata import AnnData + +from .. import _utils +from .._compat import deprecated, old_positionals +from .._settings import settings +from .._utils._doctests import doctest_internet, doctest_needs +from ..readwrite import read, read_visium +from ._utils import check_datasetdir_exists, filter_oldformatwarning + +if TYPE_CHECKING: + from typing import Literal + + from .._utils.random import _LegacyRandom + + VisiumSampleID = Literal[ + "V1_Breast_Cancer_Block_A_Section_1", + "V1_Breast_Cancer_Block_A_Section_2", + "V1_Human_Heart", + "V1_Human_Lymph_Node", + "V1_Mouse_Kidney", + "V1_Adult_Mouse_Brain", + "V1_Mouse_Brain_Sagittal_Posterior", + "V1_Mouse_Brain_Sagittal_Posterior_Section_2", + "V1_Mouse_Brain_Sagittal_Anterior", + "V1_Mouse_Brain_Sagittal_Anterior_Section_2", + "V1_Human_Brain_Section_1", + "V1_Human_Brain_Section_2", + "V1_Adult_Mouse_Brain_Coronal_Section_1", + "V1_Adult_Mouse_Brain_Coronal_Section_2", + # spaceranger version 1.2.0 + "Targeted_Visium_Human_Cerebellum_Neuroscience", + "Parent_Visium_Human_Cerebellum", + "Targeted_Visium_Human_SpinalCord_Neuroscience", + "Parent_Visium_Human_SpinalCord", + "Targeted_Visium_Human_Glioblastoma_Pan_Cancer", + "Parent_Visium_Human_Glioblastoma", + "Targeted_Visium_Human_BreastCancer_Immunology", + "Parent_Visium_Human_BreastCancer", + "Targeted_Visium_Human_OvarianCancer_Pan_Cancer", + "Targeted_Visium_Human_OvarianCancer_Immunology", + "Parent_Visium_Human_OvarianCancer", + "Targeted_Visium_Human_ColorectalCancer_GeneSignature", + "Parent_Visium_Human_ColorectalCancer", + ] + +HERE = Path(__file__).parent + + +@old_positionals( + "n_variables", "n_centers", "cluster_std", "n_observations", "random_state" +) +def blobs( + *, + n_variables: int = 11, + n_centers: int = 5, + cluster_std: float = 1.0, + n_observations: int = 640, + random_state: _LegacyRandom = 0, +) -> AnnData: + """Gaussian Blobs. + + Parameters + ---------- + n_variables + Dimension of feature space. + n_centers + Number of cluster centers. + cluster_std + Standard deviation of clusters. + n_observations + Number of observations. By default, this is the same observation number + as in :func:`scanpy.datasets.krumsiek11`. + random_state + Determines random number generation for dataset creation. + + Returns + ------- + Annotated data matrix containing a observation annotation 'blobs' that + indicates cluster identity. + + Examples + -------- + >>> import scanpy as sc + >>> sc.datasets.blobs() + AnnData object with n_obs × n_vars = 640 × 11 + obs: 'blobs' + + """ + import sklearn.datasets + + x, y = sklearn.datasets.make_blobs( + n_samples=n_observations, + n_features=n_variables, + centers=n_centers, + cluster_std=cluster_std, + random_state=random_state, + ) + return AnnData(x, obs=dict(blobs=y.astype(str))) + + +@doctest_internet +@check_datasetdir_exists +def burczynski06() -> AnnData: + """Bulk data with conditions ulcerative colitis (UC) and Crohn’s disease (CD) :cite:p:`Burczynski2006`. + + The study assesses transcriptional profiles in peripheral blood mononuclear + cells from 42 healthy individuals, 59 CD patients, and 26 UC patients by + hybridization to microarrays interrogating more than 22,000 sequences. + + Returns + ------- + Annotated data matrix. + + Examples + -------- + >>> import scanpy as sc + >>> sc.datasets.burczynski06() + AnnData object with n_obs × n_vars = 127 × 22283 + obs: 'groups' + + """ + filename = settings.datasetdir / "burczynski06/GDS1615_full.soft.gz" + url = "ftp://ftp.ncbi.nlm.nih.gov/geo/datasets/GDS1nnn/GDS1615/soft/GDS1615_full.soft.gz" + return read(filename, backup_url=url) + + +def krumsiek11() -> AnnData: + r"""Simulated myeloid progenitors :cite:p:`Krumsiek2011`. + + The literature-curated boolean network from :cite:t:`Krumsiek2011` was used to + simulate the data. It describes development to four cell fates annotated in + :attr:`~anndata.AnnData.obs`\ `["cell_type"]`: + “monocyte” (`Mo`), “erythrocyte” (`Ery`), “megakaryocyte” (`Mk`) and “neutrophil” (`Neu`). + + See also the discussion of this data in :cite:t:`Wolf2019`. + + Simulate via :func:`~scanpy.tl.sim`. + + Returns + ------- + Annotated data matrix. + + Examples + -------- + >>> import scanpy as sc + >>> sc.datasets.krumsiek11() + UserWarning: Observation names are not unique. To make them unique, call `.obs_names_make_unique`. + utils.warn_names_duplicates("obs") + AnnData object with n_obs × n_vars = 640 × 11 + obs: 'cell_type' + uns: 'iroot', 'highlights' + + """ # noqa: D401 + with settings.verbosity.override("error"): # suppress output... + adata = read(HERE / "krumsiek11.txt", first_column_names=True) + adata.uns["iroot"] = 0 + fate_labels = {0: "Stem", 159: "Mo", 319: "Ery", 459: "Mk", 619: "Neu"} + adata.uns["highlights"] = fate_labels + cell_type = pd.array(["progenitor"]).repeat(adata.n_obs) + cell_type[80:160] = "Mo" + cell_type[240:320] = "Ery" + cell_type[400:480] = "Mk" + cell_type[560:640] = "Neu" + adata.obs["cell_type"] = cell_type + _utils.sanitize_anndata(adata) + return adata + + +@doctest_internet +@doctest_needs("openpyxl") +@check_datasetdir_exists +def moignard15() -> AnnData: + r"""Hematopoiesis in early mouse embryos :cite:p:`Moignard2015`. + + The data was obtained using qRT–PCR. + :attr:`~anndata.AnnData.X` contains the normalized dCt values from supp. table 7 of the publication. + + :attr:`~anndata.AnnData.obs`\ `["exp_groups"]` contains the stages derived by + flow sorting and GFP marker status: + “primitive streak” (`PS`), “neural plate” (`NP`), “head fold (`HF`), + “four somite” blood/GFP⁺ (4SG), and “four somite” endothelial/GFP¯ (`4SFG`). + + Returns + ------- + Annotated data matrix. + + Examples + -------- + >>> import scanpy as sc + >>> sc.datasets.moignard15() + AnnData object with n_obs × n_vars = 3934 × 42 + obs: 'exp_groups' + uns: 'iroot', 'exp_groups_colors' + + """ + filename = settings.datasetdir / "moignard15/nbt.3154-S3.xlsx" + backup_url = "https://static-content.springer.com/esm/art%3A10.1038%2Fnbt.3154/MediaObjects/41587_2015_BFnbt3154_MOESM4_ESM.xlsx" + adata = read(filename, sheet="dCt_values.txt", backup_url=backup_url) + # filter out 4 genes as in Haghverdi et al. (2016) + gene_subset = ~np.isin(adata.var_names, ["Eif2b1", "Mrpl19", "Polr2a", "Ubc"]) + adata = adata[:, gene_subset].copy() # retain non-removed genes + # choose root cell for DPT analysis as in Haghverdi et al. (2016) + adata.uns["iroot"] = 532 # note that in Matlab/R, counting starts at 1 + # annotate with Moignard et al. (2015) experimental cell groups + groups = { + "HF": "#D7A83E", + "NP": "#7AAE5D", + "PS": "#497ABC", + "4SG": "#AF353A", + "4SFG": "#765099", + } + # annotate each observation/cell + adata.obs["exp_groups"] = [ + next(gname for gname in groups if sname.startswith(gname)) + for sname in adata.obs_names + ] + # fix the order and colors of names in "groups" + adata.obs["exp_groups"] = pd.Categorical( + adata.obs["exp_groups"], categories=list(groups.keys()) + ) + adata.uns["exp_groups_colors"] = list(groups.values()) + return adata + + +@doctest_internet +@check_datasetdir_exists +def paul15() -> AnnData: + """Development of Myeloid Progenitors :cite:p:`Paul2015`. + + Non-logarithmized raw data. + + The data has been sent out by Email from the Amit Lab. An R version for + loading the data can be found `here + `_. + + Returns + ------- + Annotated data matrix. + + Examples + -------- + >>> import scanpy as sc + >>> sc.datasets.paul15() + AnnData object with n_obs × n_vars = 2730 × 3451 + obs: 'paul15_clusters' + uns: 'iroot' + + """ + import h5py + + filename = settings.datasetdir / "paul15/paul15.h5" + filename.parent.mkdir(exist_ok=True) + backup_url = "https://falexwolf.de/data/paul15.h5" + _utils.check_presence_download(filename, backup_url) + with h5py.File(filename, "r") as f: + # Coercing to float32 for backwards compatibility + x = f["data.debatched"][()].astype(np.float32) + gene_names = f["data.debatched_rownames"][()].astype(str) + cell_names = f["data.debatched_colnames"][()].astype(str) + clusters = f["cluster.id"][()].flatten().astype(int) + infogenes_names = f["info.genes_strings"][()].astype(str) + # each row has to correspond to a observation, therefore transpose + adata = AnnData(x.transpose()) + adata.var_names = gene_names + adata.obs_names = cell_names + # names reflecting the cell type identifications from the paper + cell_type = 6 * ["Ery"] + cell_type += "MEP Mk GMP GMP DC Baso Baso Mo Mo Neu Neu Eos Lymph".split() # noqa: SIM905 + adata.obs["paul15_clusters"] = [f"{i}{cell_type[i - 1]}" for i in clusters] + # make string annotations categorical (optional) + _utils.sanitize_anndata(adata) + # just keep the first of the two equivalent names per gene + adata.var_names = [gn.split(";")[0] for gn in adata.var_names] + # remove 10 corrupted gene names + infogenes_names = np.intersect1d(infogenes_names, adata.var_names) + # restrict data array to the 3461 informative genes + adata = adata[:, infogenes_names].copy() + # usually we'd set the root cell to an arbitrary cell in the MEP cluster + # adata.uns['iroot'] = np.flatnonzero(adata.obs['paul15_clusters'] == '7MEP')[0] + # here, set the root cell as in Haghverdi et al. (2016) + # note that other than in Matlab/R, counting starts at 0 + adata.uns["iroot"] = 840 + return adata + + +def toggleswitch() -> AnnData: + """Simulated toggleswitch. + + Data obtained simulating a simple toggleswitch :cite:p:`Gardner2000` + + Simulate via :func:`~scanpy.tl.sim`. + + Returns + ------- + Annotated data matrix. + + Examples + -------- + >>> import scanpy as sc + >>> sc.datasets.toggleswitch() + UserWarning: Observation names are not unique. To make them unique, call `.obs_names_make_unique`. + utils.warn_names_duplicates("obs") + AnnData object with n_obs × n_vars = 200 × 2 + uns: 'iroot' + + """ # noqa: D401 + filename = HERE / "toggleswitch.txt" + adata = read(filename, first_column_names=True) + adata.uns["iroot"] = 0 + return adata + + +@filter_oldformatwarning +def pbmc68k_reduced() -> AnnData: + r"""Subsampled and processed 68k PBMCs. + + `PBMC 68k dataset`_ from 10x Genomics. + + The original PBMC 68k dataset was preprocessed with steps including + :func:`~scanpy.pp.normalize_total`\ [#norm]_ and :func:`~scanpy.pp.scale`. + It was saved keeping only 724 cells and 221 highly variable genes. + + The saved file contains the annotation of cell types (key: `'bulk_labels'`), + UMAP coordinates, louvain clustering and gene rankings based on the + `bulk_labels`. + + .. [#norm] Back when the dataset was created, :func:`~scanpy.pp.normalize_per_cell` was used instead. + .. _PBMC 68k dataset: https://www.10xgenomics.com/datasets/fresh-68-k-pbm-cs-donor-a-1-standard-1-1-0 + + Returns + ------- + Annotated data matrix. + + Examples + -------- + >>> import scanpy as sc + >>> sc.datasets.pbmc68k_reduced() + AnnData object with n_obs × n_vars = 700 × 765 + obs: 'bulk_labels', 'n_genes', 'percent_mito', 'n_counts', 'S_score', 'G2M_score', 'phase', 'louvain' + var: 'n_counts', 'means', 'dispersions', 'dispersions_norm', 'highly_variable' + uns: 'bulk_labels_colors', 'louvain', 'louvain_colors', 'neighbors', 'pca', 'rank_genes_groups' + obsm: 'X_pca', 'X_umap' + varm: 'PCs' + obsp: 'distances', 'connectivities' + + """ + filename = HERE / "10x_pbmc68k_reduced.h5ad" + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=FutureWarning, module="anndata") + return read(filename) + + +@doctest_internet +@filter_oldformatwarning +@check_datasetdir_exists +def pbmc3k() -> AnnData: + r"""3k PBMCs from 10x Genomics. + + The data consists in 3k PBMCs from a Healthy Donor and is freely available + from 10x Genomics (file_ from this webpage_). + + The exact same data is also used in Seurat’s `basic clustering tutorial`_. + + .. _file: https://cf.10xgenomics.com/samples/cell-exp/1.1.0/pbmc3k/pbmc3k_filtered_gene_bc_matrices.tar.gz + .. _webpage: https://support.10xgenomics.com/single-cell-gene-expression/datasets/1.1.0/pbmc3k + .. _basic clustering tutorial: https://satijalab.org/seurat/articles/pbmc3k_tutorial.html + + .. note:: + This downloads 5.9 MB of data upon the first call of the function and stores it in + :attr:`~scanpy.settings.datasetdir`\ `/pbmc3k_raw.h5ad`. + + The following code was run to produce the file. + + .. code:: python + + adata = sc.read_10x_mtx( + # the directory with the `.mtx` file + './data/filtered_gene_bc_matrices/hg19/', + # use gene symbols for the variable names (variables-axis index) + var_names='gene_symbols', + # write a cache file for faster subsequent reading + cache=True, + ) + + adata.var_names_make_unique() # this is unnecessary if using 'gene_ids' + adata.write('write/pbmc3k_raw.h5ad', compression='gzip') + + Returns + ------- + Annotated data matrix. + + Examples + -------- + >>> import scanpy as sc + >>> sc.datasets.pbmc3k() + AnnData object with n_obs × n_vars = 2700 × 32738 + var: 'gene_ids' + + """ + url = "https://falexwolf.de/data/pbmc3k_raw.h5ad" + adata = read(settings.datasetdir / "pbmc3k_raw.h5ad", backup_url=url) + return adata + + +@doctest_internet +@filter_oldformatwarning +@check_datasetdir_exists +def pbmc3k_processed() -> AnnData: + """Processed 3k PBMCs from 10x Genomics. + + Processed using the basic tutorial :doc:`/tutorials/basics/clustering-2017`. + + For preprocessing, cells are filtered out that have few gene counts or too high a `percent_mito`. + The counts are logarithmized and only genes marked by :func:`~scanpy.pp.highly_variable_genes` are retained. + The :attr:`~anndata.AnnData.obs` variables `n_counts` and `percent_mito` are corrected for + using :func:`~scanpy.pp.regress_out`, and values are scaled and clipped by :func:`~scanpy.pp.scale`. + Finally, :func:`~scanpy.pp.pca` and :func:`~scanpy.pp.neighbors` are calculated. + + As analysis steps, the embeddings :func:`~scanpy.tl.tsne` and :func:`~scanpy.tl.umap` are performed. + Communities are identified using :func:`~scanpy.tl.louvain` and marker genes using :func:`~scanpy.tl.rank_genes_groups`. + + Returns + ------- + Annotated data matrix. + + Examples + -------- + >>> import scanpy as sc + >>> sc.datasets.pbmc3k_processed() + AnnData object with n_obs × n_vars = 2638 × 1838 + obs: 'n_genes', 'percent_mito', 'n_counts', 'louvain' + var: 'n_cells' + uns: 'draw_graph', 'louvain', 'louvain_colors', 'neighbors', 'pca', 'rank_genes_groups' + obsm: 'X_pca', 'X_tsne', 'X_umap', 'X_draw_graph_fr' + varm: 'PCs' + obsp: 'distances', 'connectivities' + + """ # noqa: D401 + url = "https://raw.githubusercontent.com/chanzuckerberg/cellxgene/main/example-dataset/pbmc3k.h5ad" + + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=FutureWarning, module="anndata") + return read(settings.datasetdir / "pbmc3k_processed.h5ad", backup_url=url) + + +def _download_visium_dataset( + sample_id: VisiumSampleID, + spaceranger_version: Literal["1.1.0", "1.2.0"], + *, + base_dir: Path | None = None, + download_image: bool = False, +) -> Path: + """Download Visium spatial data from 10x Genomics’ database. + + Params + ------ + sample_id + String name of example visium dataset. + base_dir + Where to download the dataset to. + download_image + Whether to download the high-resolution tissue section. + """ + import tarfile + + if base_dir is None: + base_dir = settings.datasetdir + + url_prefix = f"https://cf.10xgenomics.com/samples/spatial-exp/{spaceranger_version}/{sample_id}" + + sample_dir = base_dir / sample_id + sample_dir.mkdir(exist_ok=True) + + # Download spatial data + tar_filename = f"{sample_id}_spatial.tar.gz" + tar_pth = sample_dir / tar_filename + _utils.check_presence_download( + filename=tar_pth, backup_url=f"{url_prefix}/{tar_filename}" + ) + with tarfile.open(tar_pth) as f: + f.extraction_filter = tarfile.data_filter + for el in f: + if not (sample_dir / el.name).exists(): + f.extract(el, sample_dir) + + # Download counts + _utils.check_presence_download( + filename=sample_dir / "filtered_feature_bc_matrix.h5", + backup_url=f"{url_prefix}/{sample_id}_filtered_feature_bc_matrix.h5", + ) + + # Download image + if download_image: + _utils.check_presence_download( + filename=sample_dir / "image.tif", + backup_url=f"{url_prefix}/{sample_id}_image.tif", + ) + + return sample_dir + + +@deprecated("Use `squidpy.datasets.visium` instead.") +@doctest_internet +@check_datasetdir_exists +def visium_sge( + sample_id: VisiumSampleID = "V1_Breast_Cancer_Block_A_Section_1", + *, + include_hires_tiff: bool = False, +) -> AnnData: + """Processed Visium Spatial Gene Expression data from 10x Genomics’ database. + + .. deprecated:: 1.11.0 + Use :func:`squidpy.datasets.visium` instead. + + The database_ can be browsed online to find the ``sample_id`` you want. + + .. _database: https://support.10xgenomics.com/spatial-gene-expression/datasets + + Parameters + ---------- + sample_id + The ID of the data sample in 10x’s spatial database. + include_hires_tiff + Download and include the high-resolution tissue image (tiff) in + `adata.uns["spatial"][sample_id]["metadata"]["source_image_path"]`. + + Returns + ------- + Annotated data matrix. + + Examples + -------- + >>> import scanpy as sc + >>> sc.datasets.visium_sge(sample_id="V1_Breast_Cancer_Block_A_Section_1") + AnnData object with n_obs × n_vars = 3798 × 36601 + obs: 'in_tissue', 'array_row', 'array_col' + var: 'gene_ids', 'feature_types', 'genome' + uns: 'spatial' + obsm: 'spatial' + + """ # noqa: D401 + spaceranger_version = "1.1.0" if "V1_" in sample_id else "1.2.0" + sample_dir = _download_visium_dataset( + sample_id, spaceranger_version, download_image=include_hires_tiff + ) + source_image_path = sample_dir / "image.tif" if include_hires_tiff else None + return read_visium(sample_dir, source_image_path=source_image_path) diff --git a/.venv/lib/python3.11/site-packages/scanpy/datasets/_ebi_expression_atlas.py b/.venv/lib/python3.11/site-packages/scanpy/datasets/_ebi_expression_atlas.py new file mode 100644 index 0000000000000000000000000000000000000000..a272552d4a8f0025275974ec2bef54d39cbf6598 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/datasets/_ebi_expression_atlas.py @@ -0,0 +1,171 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from urllib.error import HTTPError +from urllib.request import urlopen +from zipfile import ZipFile + +import anndata +import numpy as np +import pandas as pd +from scipy import sparse + +from .. import logging as logg +from .._compat import add_note +from .._settings import settings +from .._utils._doctests import doctest_internet +from ..readwrite import _download +from ._utils import check_datasetdir_exists + +if TYPE_CHECKING: + from pandas._typing import ReadCsvBuffer + + from scanpy._compat import CSRBase + + +CHUNK_SIZE = int(1e7) + + +def _filter_boring(dataframe: pd.DataFrame) -> pd.DataFrame: + unique_vals = dataframe.apply(lambda x: len(x.unique())) + is_boring = (unique_vals == 1) | (unique_vals == len(dataframe)) + return dataframe.loc[:, ~is_boring] + + +def sniff_url(accession: str): + # Note that data is downloaded from gxa/sc/experiment, not experiments + base_url = f"https://www.ebi.ac.uk/gxa/sc/experiments/{accession}/" + try: + with urlopen(base_url): # Check if server up/ dataset exists + pass + except HTTPError as e: + add_note(e, base_url) + raise + + +@check_datasetdir_exists +def download_experiment(accession: str): + sniff_url(accession) + + base_url = f"https://www.ebi.ac.uk/gxa/sc/experiment/{accession}" + design_url = f"{base_url}/download?accessKey=&fileType=experiment-design" + mtx_url = f"{base_url}/download/zip?accessKey=&fileType=quantification-raw" + + experiment_dir = settings.datasetdir / accession + experiment_dir.mkdir(parents=True, exist_ok=True) + + _download(design_url, experiment_dir / "experimental_design.tsv") + _download(mtx_url, experiment_dir / "expression_archive.zip") + + +def read_mtx_from_stream(stream: ReadCsvBuffer[bytes]) -> CSRBase: + curline = stream.readline() + while curline.startswith(b"%"): + curline = stream.readline() + n, m, e = map(int, curline[:-1].split(b" ")) + + dtype_data = np.float32 + max_int32 = np.iinfo(np.int32).max + dtype_coord = np.int64 if n > max_int32 or m > max_int32 else np.int32 + + data = np.ndarray((e,), dtype=dtype_data) + i = np.ndarray((e,), dtype=dtype_coord) + j = np.ndarray((e,), dtype=dtype_coord) + start = 0 + with pd.read_csv( + stream, + sep=r"\s+", + header=None, + dtype={0: dtype_coord, 1: dtype_coord, 2: dtype_data}, + chunksize=CHUNK_SIZE, + ) as reader: + chunk: pd.DataFrame + for chunk in reader: + l = chunk.shape[0] + data[start : start + l] = chunk[2] + i[start : start + l] = chunk[1] - 1 + j[start : start + l] = chunk[0] - 1 + start += l + return sparse.csr_matrix((data, (i, j)), shape=(m, n)) # noqa: TID251 + + +def read_expression_from_archive(archive: ZipFile) -> anndata.AnnData: + info = archive.infolist() + assert len(info) == 3 + mtx_data_info = next(i for i in info if i.filename.endswith(".mtx")) + mtx_rows_info = next(i for i in info if i.filename.endswith(".mtx_rows")) + mtx_cols_info = next(i for i in info if i.filename.endswith(".mtx_cols")) + with archive.open(mtx_data_info, "r") as f: + expr = read_mtx_from_stream(f) + with archive.open(mtx_rows_info, "r") as f: + # TODO: Check what other value could be + varname = pd.read_csv(f, sep="\t", header=None)[1] + with archive.open(mtx_cols_info, "r") as f: + obsname = pd.read_csv(f, sep="\t", header=None).iloc[:, 0] + adata = anndata.AnnData(expr) + adata.var_names = varname + adata.obs_names = obsname + return adata + + +@doctest_internet +def ebi_expression_atlas( + accession: str, *, filter_boring: bool = False +) -> anndata.AnnData: + """Load a dataset from the EBI Single Cell Expression Atlas. + + The atlas_ can be browsed online to find the ``accession`` you want. + Downloaded datasets are saved in the directory specified by + :attr:`~scanpy.settings.datasetdir`. + + .. _atlas: https://www.ebi.ac.uk/gxa/sc/experiments + + Params + ------ + accession + Dataset accession. Like ``E-GEOD-98816`` or ``E-MTAB-4888``. + This can be found in the url on the datasets page, for example E-GEOD-98816_. + + .. _E-GEOD-98816: https://www.ebi.ac.uk/gxa/sc/experiments/E-GEOD-98816/results/tsne + filter_boring + Whether boring labels in `.obs` should be automatically removed, such as + labels with a single or :attr:`~anndata.AnnData.n_obs` distinct values. + + Returns + ------- + Annotated data matrix. + + Example + ------- + >>> import scanpy as sc + >>> sc.datasets.ebi_expression_atlas("E-MTAB-4888") # doctest: +ELLIPSIS + AnnData object with n_obs × n_vars = 2261 × 23899 + obs: 'Sample Characteristic[organism]', 'Sample Characteristic Ontology Term[organism]', ..., 'Factor Value[cell type]', 'Factor Value Ontology Term[cell type]' + + """ + experiment_dir = settings.datasetdir / accession + dataset_path = experiment_dir / f"{accession}.h5ad" + try: + adata = anndata.read_h5ad(dataset_path) + if filter_boring: + adata.obs = _filter_boring(adata.obs) + return adata + except OSError: + # Dataset couldn't be read for whatever reason + pass + + download_experiment(accession) + + logg.info(f"Downloaded {accession} to {experiment_dir.absolute()}") + + with ZipFile(experiment_dir / "expression_archive.zip", "r") as f: + adata = read_expression_from_archive(f) + obs = pd.read_csv(experiment_dir / "experimental_design.tsv", sep="\t", index_col=0) + + adata.obs[obs.columns] = obs + adata.write(dataset_path, compression="gzip") # To be kind to disk space + + if filter_boring: + adata.obs = _filter_boring(adata.obs) + + return adata diff --git a/.venv/lib/python3.11/site-packages/scanpy/datasets/_utils.py b/.venv/lib/python3.11/site-packages/scanpy/datasets/_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..6c1c0e20e3925534fe38f93063c3ff11b6872708 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/datasets/_utils.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import warnings +from functools import wraps +from typing import TYPE_CHECKING + +import anndata as ad +from packaging.version import Version + +from .._compat import pkg_version +from .._settings import settings + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import ParamSpec, TypeVar + + P = ParamSpec("P") + R = TypeVar("R") + + +def check_datasetdir_exists(f: Callable[P, R]) -> Callable[P, R]: + @wraps(f) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + settings.datasetdir.mkdir(exist_ok=True) + return f(*args, **kwargs) + + return wrapper + + +def filter_oldformatwarning(f: Callable[P, R]) -> Callable[P, R]: + """Filter anndata.OldFormatWarning from being thrown by the wrapped function.""" + + @wraps(f) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + with warnings.catch_warnings(): + if pkg_version("anndata") >= Version("0.8"): + warnings.filterwarnings( + "ignore", category=ad.OldFormatWarning, module="anndata" + ) + return f(*args, **kwargs) + + return wrapper diff --git a/.venv/lib/python3.11/site-packages/scanpy/datasets/krumsiek11.txt b/.venv/lib/python3.11/site-packages/scanpy/datasets/krumsiek11.txt new file mode 100644 index 0000000000000000000000000000000000000000..25c432753bdc7fce4069488af2ad8442fa5a2394 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/datasets/krumsiek11.txt @@ -0,0 +1,648 @@ +# model = /Users/alexwolf/hholtz/01_projects/1512_scanpy/scanpy/scanpy/sim_models/krumsiek11.txt +# tmax = 160 +# branching = True +# nrRealizations = 4 +# noiseObs = 0 +# noiseDyn = 0.001 +# seed = 0 +# it Gata2 Gata1 Fog1 EKLF Fli1 SCL Cebpa Pu.1 cJun EgrNab Gfi1 + 0 0.8032 -0.0005 -0.0001 0.0003 0.0013 0.0011 0.7997 0.8017 0.0006 0.0009 0.0002 + 1 0.7239 0.0016 -0.0003 -0.0010 0.0011 0.0022 0.8195 0.7256 0.0784 0.0077 0.0941 + 2 0.6578 0.0013 -0.0010 -0.0027 0.0025 0.0011 0.8358 0.6624 0.1105 0.0239 0.1497 + 3 0.5972 0.0038 -0.0009 -0.0064 0.0007 -0.0036 0.8534 0.5992 0.1271 0.0311 0.1702 + 4 0.5396 0.0071 -0.0029 -0.0063 0.0011 -0.0070 0.8656 0.5470 0.1385 0.0379 0.1752 + 5 0.4897 0.0110 -0.0023 -0.0061 0.0013 -0.0034 0.8759 0.4998 0.1496 0.0459 0.1747 + 6 0.4449 0.0153 -0.0020 -0.0089 0.0003 -0.0057 0.8854 0.4516 0.1569 0.0489 0.1727 + 7 0.4078 0.0131 -0.0013 -0.0121 -0.0024 -0.0076 0.8986 0.4096 0.1666 0.0556 0.1661 + 8 0.3719 0.0156 0.0021 -0.0105 -0.0007 -0.0066 0.9062 0.3742 0.1731 0.0631 0.1623 + 9 0.3424 0.0175 0.0027 -0.0083 -0.0045 -0.0017 0.9135 0.3433 0.1781 0.0698 0.1565 + 10 0.3143 0.0179 -0.0001 -0.0100 -0.0042 -0.0039 0.9191 0.3175 0.1891 0.0785 0.1473 + 11 0.2900 0.0215 -0.0007 -0.0069 -0.0034 -0.0018 0.9280 0.2934 0.1988 0.0844 0.1371 + 12 0.2674 0.0255 -0.0020 -0.0074 -0.0030 -0.0013 0.9373 0.2749 0.2103 0.0942 0.1301 + 13 0.2519 0.0284 -0.0011 -0.0029 -0.0037 -0.0022 0.9424 0.2600 0.2211 0.1020 0.1193 + 14 0.2353 0.0371 0.0009 -0.0017 -0.0018 0.0000 0.9492 0.2439 0.2336 0.1114 0.1089 + 15 0.2210 0.0413 0.0009 -0.0028 0.0019 0.0025 0.9503 0.2353 0.2473 0.1219 0.1018 + 16 0.2077 0.0423 0.0003 -0.0016 0.0047 0.0057 0.9527 0.2248 0.2645 0.1389 0.0915 + 17 0.1986 0.0464 -0.0005 0.0012 0.0025 0.0081 0.9551 0.2187 0.2866 0.1572 0.0846 + 18 0.1914 0.0531 0.0020 0.0035 0.0022 0.0143 0.9573 0.2117 0.3103 0.1756 0.0761 + 19 0.1818 0.0613 0.0037 0.0063 0.0018 0.0179 0.9579 0.2034 0.3332 0.2000 0.0722 + 20 0.1734 0.0666 0.0069 0.0066 0.0053 0.0218 0.9585 0.2051 0.3505 0.2227 0.0672 + 21 0.1720 0.0715 0.0052 0.0112 0.0066 0.0260 0.9586 0.2025 0.3742 0.2433 0.0567 + 22 0.1638 0.0762 0.0084 0.0177 0.0096 0.0289 0.9655 0.1981 0.3971 0.2726 0.0492 + 23 0.1556 0.0826 0.0090 0.0151 0.0125 0.0374 0.9661 0.1974 0.4208 0.2965 0.0461 + 24 0.1515 0.0831 0.0117 0.0169 0.0101 0.0385 0.9724 0.1936 0.4450 0.3236 0.0395 + 25 0.1475 0.0821 0.0123 0.0174 0.0150 0.0429 0.9757 0.1898 0.4697 0.3503 0.0346 + 26 0.1480 0.0842 0.0137 0.0186 0.0133 0.0468 0.9751 0.1900 0.4953 0.3773 0.0297 + 27 0.1440 0.0870 0.0101 0.0184 0.0152 0.0544 0.9734 0.1893 0.5165 0.4037 0.0264 + 28 0.1463 0.0892 0.0090 0.0231 0.0156 0.0551 0.9760 0.1906 0.5320 0.4323 0.0203 + 29 0.1413 0.0919 0.0085 0.0255 0.0193 0.0594 0.9795 0.1887 0.5534 0.4581 0.0198 + 30 0.1415 0.0919 0.0067 0.0273 0.0218 0.0629 0.9853 0.1891 0.5680 0.4826 0.0224 + 31 0.1385 0.0952 0.0072 0.0306 0.0209 0.0687 0.9866 0.1904 0.5856 0.5025 0.0213 + 32 0.1386 0.0993 0.0075 0.0265 0.0268 0.0730 0.9878 0.1916 0.6031 0.5224 0.0198 + 33 0.1337 0.1011 0.0061 0.0237 0.0242 0.0751 0.9897 0.1885 0.6190 0.5403 0.0177 + 34 0.1318 0.1009 0.0046 0.0278 0.0267 0.0793 0.9925 0.1892 0.6339 0.5546 0.0173 + 35 0.1295 0.1035 0.0074 0.0300 0.0266 0.0817 0.9967 0.1855 0.6451 0.5754 0.0171 + 36 0.1284 0.1001 0.0063 0.0299 0.0293 0.0868 0.9949 0.1841 0.6546 0.5917 0.0112 + 37 0.1246 0.1018 0.0112 0.0354 0.0264 0.0901 0.9949 0.1886 0.6651 0.6068 0.0119 + 38 0.1230 0.1057 0.0079 0.0353 0.0300 0.0972 0.9890 0.1858 0.6758 0.6210 0.0118 + 39 0.1184 0.1040 0.0087 0.0357 0.0303 0.0996 0.9889 0.1861 0.6811 0.6300 0.0092 + 40 0.1191 0.1036 0.0092 0.0322 0.0303 0.1016 0.9881 0.1853 0.6913 0.6417 0.0084 + 41 0.1198 0.1035 0.0052 0.0332 0.0306 0.1001 0.9874 0.1870 0.7005 0.6513 0.0069 + 42 0.1171 0.1055 0.0055 0.0339 0.0303 0.1041 0.9877 0.1889 0.7091 0.6590 0.0077 + 43 0.1157 0.1066 0.0084 0.0349 0.0303 0.1031 0.9882 0.1905 0.7142 0.6705 0.0064 + 44 0.1160 0.1097 0.0087 0.0341 0.0321 0.1078 0.9902 0.1886 0.7201 0.6802 0.0046 + 45 0.1141 0.1096 0.0116 0.0362 0.0363 0.1088 0.9904 0.1884 0.7225 0.6883 0.0035 + 46 0.1185 0.1081 0.0096 0.0351 0.0363 0.1055 0.9888 0.1909 0.7314 0.6989 0.0051 + 47 0.1174 0.1046 0.0098 0.0370 0.0369 0.1061 0.9890 0.1883 0.7348 0.7054 0.0022 + 48 0.1135 0.1022 0.0110 0.0408 0.0330 0.1091 0.9909 0.1905 0.7427 0.7131 -0.0011 + 49 0.1121 0.1003 0.0074 0.0365 0.0365 0.1053 0.9905 0.1945 0.7472 0.7213 -0.0017 + 50 0.1081 0.1033 0.0071 0.0346 0.0347 0.1052 0.9898 0.1982 0.7510 0.7302 -0.0023 + 51 0.1103 0.1027 0.0079 0.0363 0.0340 0.1067 0.9896 0.1997 0.7534 0.7336 -0.0017 + 52 0.1047 0.1011 0.0097 0.0364 0.0325 0.1058 0.9923 0.1992 0.7573 0.7389 -0.0003 + 53 0.1025 0.0991 0.0093 0.0366 0.0337 0.1076 0.9941 0.2040 0.7612 0.7454 -0.0040 + 54 0.1005 0.0926 0.0114 0.0321 0.0336 0.1090 0.9901 0.2098 0.7634 0.7517 -0.0053 + 55 0.0997 0.0921 0.0115 0.0339 0.0349 0.1073 0.9903 0.2147 0.7698 0.7554 -0.0021 + 56 0.0951 0.0846 0.0111 0.0332 0.0301 0.1033 0.9878 0.2235 0.7738 0.7626 0.0029 + 57 0.0924 0.0828 0.0075 0.0315 0.0260 0.1016 0.9870 0.2298 0.7837 0.7651 0.0008 + 58 0.0834 0.0783 0.0061 0.0319 0.0253 0.0989 0.9901 0.2409 0.7928 0.7706 0.0024 + 59 0.0813 0.0749 0.0031 0.0287 0.0255 0.0924 0.9896 0.2546 0.7998 0.7774 -0.0016 + 60 0.0768 0.0739 0.0021 0.0278 0.0231 0.0906 0.9857 0.2699 0.8083 0.7835 -0.0024 + 61 0.0722 0.0680 0.0031 0.0277 0.0217 0.0858 0.9876 0.2841 0.8176 0.7923 0.0003 + 62 0.0674 0.0627 0.0038 0.0277 0.0194 0.0808 0.9897 0.3011 0.8278 0.8008 -0.0021 + 63 0.0631 0.0582 0.0021 0.0274 0.0150 0.0737 0.9927 0.3201 0.8357 0.8083 0.0001 + 64 0.0613 0.0532 0.0057 0.0297 0.0130 0.0709 0.9939 0.3414 0.8409 0.8186 -0.0032 + 65 0.0579 0.0485 0.0053 0.0274 0.0107 0.0615 0.9973 0.3643 0.8514 0.8257 -0.0039 + 66 0.0535 0.0452 0.0050 0.0238 0.0132 0.0566 0.9986 0.3888 0.8545 0.8368 0.0008 + 67 0.0477 0.0400 0.0042 0.0218 0.0116 0.0498 0.9957 0.4158 0.8616 0.8432 0.0028 + 68 0.0469 0.0364 0.0036 0.0232 0.0097 0.0487 0.9951 0.4458 0.8701 0.8508 0.0007 + 69 0.0389 0.0306 0.0076 0.0225 0.0100 0.0461 0.9950 0.4745 0.8801 0.8617 -0.0010 + 70 0.0382 0.0282 0.0055 0.0210 0.0079 0.0401 0.9959 0.5078 0.8884 0.8711 -0.0010 + 71 0.0349 0.0258 0.0073 0.0155 0.0071 0.0379 0.9961 0.5373 0.8909 0.8802 -0.0034 + 72 0.0320 0.0208 0.0082 0.0151 0.0080 0.0346 0.9989 0.5653 0.8929 0.8879 -0.0034 + 73 0.0293 0.0187 0.0041 0.0176 0.0085 0.0329 1.0026 0.5954 0.9017 0.8970 -0.0010 + 74 0.0240 0.0158 0.0022 0.0167 0.0050 0.0303 1.0038 0.6247 0.9094 0.9030 0.0002 + 75 0.0220 0.0090 -0.0012 0.0165 0.0034 0.0272 1.0070 0.6518 0.9158 0.9111 0.0011 + 76 0.0211 0.0078 0.0003 0.0161 0.0044 0.0248 1.0038 0.6807 0.9249 0.9182 -0.0004 + 77 0.0172 0.0098 -0.0039 0.0135 0.0038 0.0226 1.0018 0.7108 0.9311 0.9203 0.0011 + 78 0.0169 0.0092 -0.0026 0.0159 0.0003 0.0222 1.0008 0.7342 0.9365 0.9253 -0.0024 + 79 0.0167 0.0094 -0.0009 0.0104 0.0012 0.0208 1.0022 0.7559 0.9428 0.9311 0.0006 + 80 0.0185 0.0098 -0.0003 0.0101 -0.0013 0.0189 0.9999 0.7761 0.9460 0.9362 0.0030 + 81 0.0154 0.0114 0.0026 0.0074 -0.0037 0.0172 0.9994 0.7940 0.9499 0.9424 -0.0009 + 82 0.0126 0.0088 0.0024 0.0053 0.0001 0.0156 1.0002 0.8087 0.9503 0.9475 -0.0000 + 83 0.0134 0.0109 0.0041 0.0088 -0.0014 0.0132 0.9984 0.8265 0.9490 0.9501 -0.0007 + 84 0.0125 0.0123 0.0074 0.0102 -0.0061 0.0118 0.9963 0.8391 0.9515 0.9545 -0.0025 + 85 0.0107 0.0062 0.0096 0.0118 -0.0063 0.0124 0.9984 0.8514 0.9559 0.9538 -0.0040 + 86 0.0059 0.0064 0.0085 0.0098 -0.0053 0.0137 0.9971 0.8692 0.9585 0.9517 -0.0023 + 87 0.0021 0.0086 0.0082 0.0063 -0.0020 0.0124 0.9969 0.8816 0.9620 0.9576 -0.0047 + 88 0.0020 0.0108 0.0068 0.0048 -0.0022 0.0107 0.9977 0.8923 0.9618 0.9594 -0.0029 + 89 -0.0007 0.0139 0.0069 0.0037 0.0010 0.0110 0.9976 0.9006 0.9620 0.9634 -0.0037 + 90 -0.0030 0.0166 0.0068 0.0059 0.0021 0.0133 0.9994 0.9091 0.9675 0.9675 -0.0059 + 91 0.0008 0.0137 0.0064 0.0065 0.0014 0.0107 1.0005 0.9129 0.9680 0.9715 -0.0038 + 92 0.0013 0.0093 0.0072 0.0065 0.0038 0.0074 0.9998 0.9200 0.9722 0.9696 -0.0059 + 93 -0.0029 0.0144 0.0078 0.0070 0.0060 0.0031 1.0009 0.9250 0.9704 0.9735 -0.0061 + 94 -0.0020 0.0116 0.0036 0.0052 0.0057 0.0038 0.9996 0.9346 0.9716 0.9735 -0.0105 + 95 -0.0033 0.0080 0.0035 0.0036 0.0017 0.0006 1.0053 0.9462 0.9707 0.9710 -0.0066 + 96 -0.0007 0.0108 0.0061 0.0022 0.0002 0.0006 1.0030 0.9457 0.9736 0.9694 -0.0085 + 97 -0.0025 0.0108 0.0093 0.0052 -0.0024 -0.0026 1.0027 0.9527 0.9752 0.9695 -0.0089 + 98 -0.0053 0.0130 0.0116 0.0052 -0.0019 -0.0026 1.0038 0.9568 0.9781 0.9704 -0.0043 + 99 -0.0068 0.0112 0.0093 -0.0005 -0.0010 -0.0036 1.0003 0.9568 0.9795 0.9688 -0.0049 + 100 -0.0074 0.0139 0.0063 -0.0010 -0.0047 -0.0024 0.9981 0.9605 0.9802 0.9699 -0.0049 + 101 -0.0061 0.0137 0.0035 0.0012 -0.0037 -0.0012 0.9979 0.9641 0.9809 0.9701 -0.0066 + 102 -0.0059 0.0165 0.0025 0.0017 -0.0053 0.0005 0.9960 0.9642 0.9795 0.9754 -0.0068 + 103 -0.0045 0.0184 0.0014 0.0040 -0.0033 0.0004 0.9965 0.9577 0.9801 0.9744 -0.0065 + 104 -0.0043 0.0163 0.0025 0.0038 0.0009 -0.0003 0.9945 0.9593 0.9806 0.9747 -0.0044 + 105 -0.0045 0.0147 0.0030 0.0044 0.0015 0.0000 0.9942 0.9609 0.9830 0.9734 -0.0003 + 106 -0.0047 0.0131 0.0037 -0.0018 0.0018 0.0049 0.9941 0.9631 0.9853 0.9728 0.0013 + 107 0.0002 0.0101 0.0029 -0.0029 0.0058 0.0068 0.9959 0.9660 0.9824 0.9748 -0.0022 + 108 0.0016 0.0111 0.0001 -0.0078 0.0081 0.0064 0.9959 0.9716 0.9835 0.9735 -0.0042 + 109 0.0025 0.0106 -0.0023 -0.0085 0.0085 0.0027 0.9943 0.9753 0.9868 0.9730 -0.0021 + 110 0.0027 0.0092 -0.0021 -0.0051 0.0070 0.0048 0.9943 0.9794 0.9826 0.9674 -0.0006 + 111 0.0042 0.0062 0.0020 -0.0054 0.0056 0.0059 0.9933 0.9829 0.9873 0.9668 0.0006 + 112 0.0039 0.0068 0.0009 -0.0043 0.0046 0.0025 0.9921 0.9870 0.9891 0.9643 0.0000 + 113 0.0028 0.0096 -0.0007 -0.0031 0.0057 0.0042 0.9922 0.9837 0.9896 0.9661 -0.0009 + 114 0.0019 0.0047 -0.0007 -0.0082 0.0065 0.0035 0.9948 0.9885 0.9905 0.9749 0.0027 + 115 0.0028 0.0038 0.0001 -0.0087 0.0040 0.0014 0.9908 0.9901 0.9883 0.9756 0.0049 + 116 0.0039 0.0005 0.0006 -0.0096 -0.0003 0.0012 0.9952 0.9904 0.9888 0.9733 0.0042 + 117 0.0050 -0.0023 -0.0001 -0.0071 0.0023 0.0027 0.9925 0.9893 0.9932 0.9762 0.0070 + 118 0.0012 -0.0036 -0.0023 -0.0031 -0.0015 0.0026 0.9942 0.9892 0.9905 0.9751 0.0051 + 119 0.0040 -0.0063 -0.0019 -0.0054 0.0018 -0.0013 0.9924 0.9932 0.9899 0.9769 0.0043 + 120 0.0063 -0.0024 -0.0017 -0.0035 0.0039 -0.0023 0.9944 0.9919 0.9901 0.9737 0.0036 + 121 0.0058 -0.0052 -0.0013 -0.0040 0.0065 -0.0003 0.9961 0.9918 0.9902 0.9757 0.0085 + 122 0.0018 -0.0063 0.0011 -0.0058 0.0093 0.0018 0.9963 0.9893 0.9866 0.9750 0.0094 + 123 0.0022 -0.0093 0.0020 -0.0062 0.0066 0.0022 0.9980 0.9903 0.9829 0.9705 0.0088 + 124 0.0019 -0.0062 0.0016 -0.0047 0.0062 0.0021 0.9974 0.9918 0.9846 0.9692 0.0120 + 125 -0.0005 -0.0015 0.0002 -0.0034 0.0061 0.0036 0.9996 0.9918 0.9850 0.9668 0.0124 + 126 -0.0016 -0.0014 -0.0024 -0.0052 0.0070 0.0029 1.0000 0.9946 0.9850 0.9632 0.0118 + 127 -0.0015 -0.0043 -0.0008 -0.0037 0.0049 -0.0014 0.9971 0.9967 0.9822 0.9612 0.0098 + 128 -0.0000 -0.0043 -0.0004 -0.0035 0.0081 -0.0023 1.0001 0.9934 0.9818 0.9589 0.0120 + 129 0.0027 -0.0074 -0.0033 -0.0043 0.0075 0.0013 0.9986 0.9908 0.9843 0.9579 0.0108 + 130 0.0019 -0.0079 -0.0030 -0.0035 0.0092 0.0040 0.9993 0.9894 0.9820 0.9598 0.0080 + 131 0.0006 -0.0051 -0.0023 -0.0014 0.0091 0.0008 0.9975 0.9897 0.9811 0.9603 0.0096 + 132 -0.0002 -0.0048 -0.0008 0.0030 0.0075 0.0011 1.0006 0.9884 0.9795 0.9586 0.0092 + 133 0.0023 -0.0038 0.0014 0.0040 0.0067 0.0015 1.0028 0.9882 0.9801 0.9605 0.0036 + 134 0.0014 -0.0065 0.0020 0.0036 0.0056 0.0016 1.0036 0.9893 0.9826 0.9663 0.0010 + 135 -0.0007 -0.0055 -0.0049 0.0039 0.0080 0.0025 1.0024 0.9888 0.9850 0.9644 0.0024 + 136 -0.0024 -0.0029 -0.0052 0.0036 0.0068 -0.0005 1.0003 0.9927 0.9850 0.9666 -0.0009 + 137 -0.0031 -0.0021 -0.0020 0.0037 0.0069 -0.0019 0.9958 0.9910 0.9841 0.9664 -0.0010 + 138 -0.0030 -0.0011 -0.0076 0.0040 0.0056 0.0010 0.9939 0.9920 0.9847 0.9643 0.0027 + 139 0.0022 -0.0001 -0.0061 0.0021 0.0071 0.0049 0.9942 0.9918 0.9844 0.9643 0.0071 + 140 0.0023 -0.0038 -0.0019 -0.0010 0.0113 0.0033 0.9937 0.9944 0.9869 0.9642 0.0058 + 141 -0.0003 -0.0077 -0.0024 -0.0017 0.0093 0.0013 0.9973 0.9974 0.9832 0.9691 0.0047 + 142 -0.0002 -0.0075 -0.0002 -0.0038 0.0071 0.0033 0.9980 0.9991 0.9821 0.9672 0.0077 + 143 -0.0030 -0.0126 0.0015 -0.0014 0.0051 0.0053 0.9955 0.9991 0.9796 0.9675 0.0066 + 144 -0.0034 -0.0110 0.0002 -0.0015 0.0030 0.0000 1.0002 0.9958 0.9807 0.9680 0.0036 + 145 -0.0023 -0.0103 0.0044 -0.0015 0.0015 0.0004 0.9980 0.9940 0.9825 0.9632 0.0000 + 146 0.0016 -0.0055 0.0046 -0.0005 -0.0004 0.0019 0.9962 0.9964 0.9850 0.9651 -0.0030 + 147 -0.0024 -0.0046 0.0050 -0.0000 -0.0025 -0.0009 0.9988 0.9948 0.9831 0.9667 -0.0070 + 148 -0.0019 -0.0047 0.0046 0.0044 -0.0027 -0.0020 0.9983 0.9936 0.9876 0.9659 -0.0017 + 149 0.0003 -0.0060 0.0020 0.0068 -0.0005 -0.0003 1.0017 0.9924 0.9882 0.9700 0.0009 + 150 0.0028 -0.0084 0.0001 0.0052 -0.0012 0.0010 1.0011 0.9943 0.9870 0.9724 0.0020 + 151 0.0062 -0.0111 -0.0006 0.0063 -0.0020 -0.0004 1.0000 0.9932 0.9869 0.9714 -0.0032 + 152 0.0039 -0.0129 -0.0038 0.0022 -0.0010 -0.0039 0.9987 0.9930 0.9905 0.9704 -0.0045 + 153 0.0051 -0.0115 -0.0064 0.0016 -0.0044 -0.0044 0.9956 0.9956 0.9873 0.9671 -0.0024 + 154 0.0063 -0.0117 -0.0049 0.0019 0.0039 -0.0029 0.9967 0.9937 0.9887 0.9639 -0.0018 + 155 0.0054 -0.0098 -0.0060 0.0015 0.0029 -0.0064 0.9971 0.9938 0.9855 0.9656 0.0008 + 156 0.0053 -0.0098 -0.0015 -0.0005 0.0002 -0.0080 0.9963 0.9955 0.9835 0.9653 0.0025 + 157 0.0047 -0.0100 -0.0010 -0.0038 0.0043 -0.0108 0.9998 0.9949 0.9820 0.9645 0.0019 + 158 0.0023 -0.0092 0.0008 -0.0045 0.0035 -0.0064 1.0021 0.9949 0.9839 0.9718 -0.0002 + 159 0.0046 -0.0072 0.0018 -0.0036 0.0021 -0.0090 1.0048 0.9932 0.9893 0.9727 0.0001 + 0 0.8010 -0.0022 0.0006 0.0015 0.0011 0.0010 0.8006 0.8023 0.0012 -0.0001 0.0001 + 1 0.7284 0.0041 -0.0003 0.0006 -0.0033 -0.0026 0.8190 0.7276 0.0838 0.0093 0.0925 + 2 0.6616 0.0035 -0.0021 -0.0001 -0.0011 0.0020 0.8405 0.6612 0.1179 0.0241 0.1436 + 3 0.5999 0.0101 -0.0041 0.0001 0.0013 -0.0001 0.8545 0.6028 0.1350 0.0346 0.1620 + 4 0.5440 0.0099 -0.0027 -0.0016 0.0025 0.0042 0.8693 0.5464 0.1468 0.0408 0.1701 + 5 0.4967 0.0113 -0.0014 -0.0018 0.0002 0.0052 0.8819 0.4959 0.1593 0.0417 0.1744 + 6 0.4502 0.0148 -0.0003 -0.0066 -0.0010 0.0037 0.8939 0.4550 0.1674 0.0488 0.1762 + 7 0.4116 0.0179 -0.0021 -0.0026 -0.0013 0.0058 0.9043 0.4148 0.1741 0.0538 0.1727 + 8 0.3719 0.0200 -0.0040 -0.0024 0.0002 0.0039 0.9130 0.3749 0.1811 0.0593 0.1686 + 9 0.3395 0.0204 -0.0037 -0.0020 0.0015 0.0070 0.9220 0.3441 0.1851 0.0629 0.1611 + 10 0.3157 0.0243 -0.0035 -0.0038 -0.0005 0.0090 0.9312 0.3184 0.1962 0.0672 0.1511 + 11 0.2927 0.0311 -0.0008 -0.0016 0.0038 0.0071 0.9339 0.2921 0.2057 0.0781 0.1386 + 12 0.2708 0.0356 -0.0049 -0.0009 0.0034 0.0060 0.9413 0.2739 0.2158 0.0845 0.1295 + 13 0.2521 0.0379 -0.0031 0.0022 0.0022 0.0073 0.9490 0.2528 0.2296 0.0945 0.1177 + 14 0.2362 0.0405 -0.0026 0.0034 0.0010 0.0086 0.9547 0.2394 0.2437 0.1072 0.1092 + 15 0.2234 0.0443 -0.0039 0.0056 0.0020 0.0098 0.9583 0.2288 0.2601 0.1218 0.0992 + 16 0.2127 0.0473 -0.0038 0.0068 0.0020 0.0085 0.9623 0.2219 0.2744 0.1386 0.0929 + 17 0.2047 0.0499 -0.0026 0.0067 0.0057 0.0107 0.9698 0.2114 0.2937 0.1583 0.0828 + 18 0.1978 0.0517 -0.0033 0.0080 0.0028 0.0132 0.9742 0.2057 0.3168 0.1786 0.0740 + 19 0.1868 0.0594 -0.0054 0.0069 0.0041 0.0163 0.9829 0.2020 0.3436 0.2011 0.0645 + 20 0.1831 0.0645 -0.0092 0.0061 0.0048 0.0177 0.9855 0.1986 0.3686 0.2213 0.0587 + 21 0.1770 0.0707 -0.0080 0.0079 0.0065 0.0212 0.9879 0.1967 0.3865 0.2469 0.0539 + 22 0.1693 0.0770 -0.0089 0.0105 0.0038 0.0276 0.9885 0.1916 0.4128 0.2742 0.0429 + 23 0.1646 0.0818 -0.0076 0.0122 0.0067 0.0306 0.9909 0.1897 0.4387 0.3045 0.0405 + 24 0.1654 0.0858 -0.0078 0.0137 0.0069 0.0383 0.9925 0.1840 0.4608 0.3276 0.0386 + 25 0.1629 0.0917 -0.0041 0.0137 0.0059 0.0448 0.9932 0.1801 0.4801 0.3554 0.0369 + 26 0.1613 0.0982 -0.0017 0.0184 0.0095 0.0469 0.9971 0.1779 0.4980 0.3820 0.0307 + 27 0.1598 0.1014 -0.0001 0.0234 0.0092 0.0524 1.0007 0.1767 0.5127 0.4030 0.0274 + 28 0.1588 0.1023 0.0033 0.0253 0.0128 0.0617 1.0001 0.1709 0.5313 0.4282 0.0229 + 29 0.1623 0.1061 0.0010 0.0270 0.0107 0.0690 1.0003 0.1667 0.5437 0.4455 0.0197 + 30 0.1617 0.1115 0.0017 0.0279 0.0131 0.0742 1.0010 0.1629 0.5597 0.4675 0.0149 + 31 0.1599 0.1162 0.0044 0.0294 0.0140 0.0815 1.0009 0.1556 0.5704 0.4850 0.0126 + 32 0.1598 0.1257 0.0023 0.0321 0.0166 0.0899 0.9989 0.1496 0.5863 0.5039 0.0108 + 33 0.1605 0.1277 0.0051 0.0355 0.0171 0.1021 0.9973 0.1451 0.5942 0.5198 0.0097 + 34 0.1617 0.1322 0.0035 0.0388 0.0186 0.1144 0.9964 0.1406 0.5955 0.5337 0.0065 + 35 0.1653 0.1372 0.0027 0.0400 0.0233 0.1250 0.9950 0.1402 0.6044 0.5442 0.0062 + 36 0.1687 0.1432 0.0038 0.0460 0.0264 0.1354 0.9937 0.1354 0.6070 0.5519 0.0067 + 37 0.1702 0.1497 0.0081 0.0453 0.0276 0.1436 0.9949 0.1308 0.6099 0.5583 0.0058 + 38 0.1773 0.1585 0.0093 0.0488 0.0323 0.1551 0.9969 0.1283 0.6105 0.5676 0.0024 + 39 0.1812 0.1662 0.0093 0.0523 0.0362 0.1681 0.9986 0.1217 0.6106 0.5711 0.0060 + 40 0.1854 0.1749 0.0119 0.0520 0.0373 0.1837 0.9998 0.1168 0.6087 0.5704 0.0063 + 41 0.1962 0.1837 0.0195 0.0567 0.0465 0.2012 1.0025 0.1120 0.6016 0.5694 0.0076 + 42 0.2050 0.1944 0.0221 0.0611 0.0473 0.2189 1.0032 0.1037 0.5965 0.5631 0.0105 + 43 0.2176 0.2054 0.0232 0.0677 0.0500 0.2334 0.9975 0.1006 0.5854 0.5531 0.0115 + 44 0.2394 0.2212 0.0237 0.0774 0.0556 0.2492 0.9962 0.0924 0.5755 0.5463 0.0131 + 45 0.2559 0.2381 0.0266 0.0839 0.0599 0.2693 0.9923 0.0862 0.5638 0.5329 0.0090 + 46 0.2749 0.2586 0.0313 0.0914 0.0620 0.2953 0.9886 0.0758 0.5468 0.5163 0.0103 + 47 0.3003 0.2814 0.0351 0.0976 0.0650 0.3204 0.9811 0.0689 0.5257 0.4982 0.0078 + 48 0.3248 0.3078 0.0416 0.1060 0.0717 0.3479 0.9760 0.0650 0.5069 0.4811 0.0089 + 49 0.3500 0.3345 0.0466 0.1107 0.0782 0.3781 0.9665 0.0576 0.4836 0.4571 0.0118 + 50 0.3788 0.3665 0.0501 0.1190 0.0827 0.4093 0.9559 0.0486 0.4563 0.4348 0.0107 + 51 0.4105 0.4007 0.0588 0.1308 0.0871 0.4417 0.9433 0.0436 0.4301 0.4067 0.0113 + 52 0.4407 0.4345 0.0662 0.1399 0.0947 0.4768 0.9247 0.0417 0.4051 0.3803 0.0087 + 53 0.4716 0.4666 0.0773 0.1478 0.0956 0.5090 0.9014 0.0396 0.3802 0.3551 0.0117 + 54 0.5031 0.4984 0.0885 0.1567 0.0993 0.5384 0.8763 0.0360 0.3573 0.3319 0.0075 + 55 0.5235 0.5300 0.1004 0.1643 0.1034 0.5712 0.8445 0.0351 0.3344 0.3097 0.0052 + 56 0.5463 0.5585 0.1134 0.1712 0.1054 0.5991 0.8107 0.0316 0.3141 0.2901 0.0037 + 57 0.5675 0.5858 0.1291 0.1819 0.1096 0.6259 0.7738 0.0278 0.2932 0.2701 0.0024 + 58 0.5832 0.6125 0.1426 0.1915 0.1081 0.6487 0.7343 0.0250 0.2737 0.2516 0.0026 + 59 0.5993 0.6388 0.1556 0.1987 0.1113 0.6799 0.6984 0.0259 0.2520 0.2289 0.0054 + 60 0.6089 0.6667 0.1741 0.2063 0.1154 0.6995 0.6580 0.0240 0.2325 0.2175 0.0087 + 61 0.6186 0.6921 0.1895 0.2115 0.1147 0.7219 0.6228 0.0242 0.2181 0.1990 0.0112 + 62 0.6270 0.7108 0.2027 0.2202 0.1125 0.7420 0.5894 0.0259 0.2023 0.1847 0.0103 + 63 0.6305 0.7311 0.2166 0.2245 0.1168 0.7652 0.5562 0.0201 0.1872 0.1696 0.0132 + 64 0.6301 0.7527 0.2349 0.2343 0.1153 0.7824 0.5216 0.0156 0.1736 0.1609 0.0122 + 65 0.6341 0.7738 0.2456 0.2387 0.1153 0.7985 0.4897 0.0148 0.1577 0.1457 0.0154 + 66 0.6352 0.7905 0.2633 0.2428 0.1168 0.8144 0.4607 0.0138 0.1452 0.1345 0.0141 + 67 0.6360 0.8070 0.2763 0.2520 0.1138 0.8305 0.4335 0.0150 0.1350 0.1254 0.0140 + 68 0.6322 0.8231 0.2887 0.2595 0.1102 0.8415 0.4037 0.0115 0.1216 0.1172 0.0174 + 69 0.6290 0.8384 0.2979 0.2678 0.1081 0.8536 0.3766 0.0149 0.1121 0.1032 0.0175 + 70 0.6238 0.8474 0.3051 0.2776 0.1040 0.8605 0.3496 0.0165 0.1039 0.0940 0.0215 + 71 0.6192 0.8593 0.3165 0.2851 0.1014 0.8706 0.3240 0.0165 0.0949 0.0856 0.0212 + 72 0.6156 0.8678 0.3298 0.2935 0.0962 0.8800 0.3031 0.0139 0.0867 0.0802 0.0261 + 73 0.6117 0.8787 0.3425 0.3021 0.0981 0.8878 0.2860 0.0131 0.0786 0.0700 0.0293 + 74 0.6071 0.8869 0.3557 0.3125 0.0958 0.8941 0.2657 0.0145 0.0749 0.0632 0.0355 + 75 0.5986 0.8922 0.3632 0.3190 0.0955 0.9000 0.2488 0.0159 0.0694 0.0570 0.0374 + 76 0.5905 0.8954 0.3657 0.3241 0.0931 0.9087 0.2322 0.0146 0.0654 0.0543 0.0427 + 77 0.5838 0.8963 0.3741 0.3335 0.0956 0.9119 0.2226 0.0167 0.0628 0.0495 0.0489 + 78 0.5813 0.9027 0.3806 0.3409 0.0882 0.9175 0.2105 0.0183 0.0597 0.0471 0.0581 + 79 0.5767 0.9050 0.3907 0.3539 0.0875 0.9220 0.1973 0.0135 0.0556 0.0435 0.0668 + 80 0.5717 0.9083 0.3967 0.3635 0.0866 0.9295 0.1883 0.0145 0.0530 0.0374 0.0707 + 81 0.5698 0.9155 0.4027 0.3710 0.0831 0.9328 0.1767 0.0136 0.0535 0.0327 0.0822 + 82 0.5670 0.9177 0.4084 0.3811 0.0787 0.9375 0.1645 0.0127 0.0462 0.0305 0.0959 + 83 0.5664 0.9246 0.4147 0.3923 0.0778 0.9399 0.1561 0.0129 0.0410 0.0263 0.1112 + 84 0.5642 0.9319 0.4157 0.4011 0.0762 0.9401 0.1501 0.0144 0.0385 0.0212 0.1246 + 85 0.5624 0.9355 0.4219 0.4165 0.0733 0.9446 0.1423 0.0119 0.0330 0.0140 0.1447 + 86 0.5555 0.9437 0.4247 0.4277 0.0723 0.9476 0.1344 0.0151 0.0299 0.0117 0.1783 + 87 0.5533 0.9452 0.4292 0.4351 0.0692 0.9504 0.1257 0.0092 0.0296 0.0066 0.2120 + 88 0.5549 0.9466 0.4325 0.4457 0.0652 0.9532 0.1207 0.0106 0.0254 0.0041 0.2469 + 89 0.5509 0.9508 0.4387 0.4559 0.0621 0.9564 0.1144 0.0085 0.0224 0.0069 0.2793 + 90 0.5523 0.9542 0.4399 0.4610 0.0596 0.9553 0.1056 0.0090 0.0215 0.0059 0.2985 + 91 0.5477 0.9519 0.4398 0.4680 0.0595 0.9630 0.1035 0.0088 0.0198 0.0044 0.3173 + 92 0.5465 0.9524 0.4417 0.4841 0.0552 0.9631 0.1006 0.0093 0.0212 0.0030 0.3342 + 93 0.5435 0.9538 0.4457 0.4936 0.0556 0.9652 0.0974 0.0125 0.0174 0.0028 0.3524 + 94 0.5425 0.9534 0.4488 0.5020 0.0526 0.9637 0.0956 0.0104 0.0148 0.0031 0.3659 + 95 0.5394 0.9545 0.4515 0.5094 0.0483 0.9685 0.0921 0.0113 0.0145 0.0020 0.3746 + 96 0.5364 0.9577 0.4528 0.5258 0.0466 0.9713 0.0902 0.0141 0.0145 0.0047 0.3804 + 97 0.5350 0.9600 0.4567 0.5378 0.0462 0.9690 0.0861 0.0127 0.0136 0.0033 0.3855 + 98 0.5336 0.9612 0.4535 0.5530 0.0445 0.9699 0.0849 0.0090 0.0122 0.0040 0.3898 + 99 0.5333 0.9610 0.4578 0.5634 0.0441 0.9697 0.0875 0.0096 0.0137 0.0021 0.3943 + 100 0.5296 0.9628 0.4592 0.5770 0.0424 0.9745 0.0851 0.0102 0.0123 0.0046 0.3997 + 101 0.5262 0.9617 0.4584 0.5886 0.0419 0.9755 0.0840 0.0094 0.0111 0.0058 0.4003 + 102 0.5256 0.9610 0.4602 0.5961 0.0401 0.9768 0.0840 0.0109 0.0106 0.0056 0.3967 + 103 0.5252 0.9620 0.4632 0.6069 0.0380 0.9755 0.0798 0.0154 0.0100 0.0049 0.3958 + 104 0.5247 0.9625 0.4659 0.6176 0.0363 0.9756 0.0743 0.0110 0.0114 0.0058 0.3909 + 105 0.5224 0.9650 0.4695 0.6248 0.0371 0.9738 0.0750 0.0136 0.0087 0.0053 0.3830 + 106 0.5218 0.9620 0.4710 0.6345 0.0366 0.9751 0.0727 0.0114 0.0077 0.0023 0.3819 + 107 0.5211 0.9625 0.4738 0.6465 0.0395 0.9738 0.0731 0.0121 0.0059 0.0071 0.3777 + 108 0.5192 0.9622 0.4750 0.6501 0.0394 0.9743 0.0735 0.0085 0.0038 0.0036 0.3706 + 109 0.5196 0.9635 0.4745 0.6546 0.0369 0.9774 0.0751 0.0081 0.0055 0.0059 0.3718 + 110 0.5189 0.9642 0.4776 0.6605 0.0359 0.9782 0.0741 0.0079 0.0039 0.0083 0.3682 + 111 0.5158 0.9633 0.4768 0.6613 0.0353 0.9781 0.0703 0.0039 0.0044 0.0073 0.3640 + 112 0.5188 0.9659 0.4794 0.6638 0.0304 0.9792 0.0691 0.0046 -0.0003 0.0008 0.3562 + 113 0.5187 0.9628 0.4797 0.6698 0.0310 0.9807 0.0692 0.0056 0.0027 0.0012 0.3519 + 114 0.5191 0.9646 0.4808 0.6776 0.0297 0.9780 0.0673 0.0093 0.0003 0.0019 0.3478 + 115 0.5201 0.9615 0.4860 0.6857 0.0263 0.9789 0.0673 0.0076 0.0026 0.0059 0.3432 + 116 0.5221 0.9605 0.4821 0.6905 0.0279 0.9820 0.0644 0.0051 0.0029 0.0050 0.3348 + 117 0.5223 0.9593 0.4826 0.6953 0.0199 0.9872 0.0655 0.0035 0.0057 0.0041 0.3308 + 118 0.5240 0.9646 0.4817 0.7012 0.0184 0.9859 0.0629 0.0057 0.0082 0.0007 0.3269 + 119 0.5260 0.9628 0.4831 0.7102 0.0151 0.9831 0.0661 0.0048 0.0063 -0.0021 0.3230 + 120 0.5256 0.9669 0.4795 0.7140 0.0127 0.9820 0.0635 0.0029 0.0019 0.0005 0.3170 + 121 0.5250 0.9660 0.4803 0.7181 0.0105 0.9801 0.0643 -0.0024 0.0008 -0.0015 0.3144 + 122 0.5251 0.9690 0.4778 0.7233 0.0094 0.9789 0.0608 -0.0002 0.0003 -0.0021 0.3110 + 123 0.5278 0.9697 0.4804 0.7299 0.0099 0.9808 0.0577 -0.0039 -0.0016 -0.0034 0.3051 + 124 0.5278 0.9687 0.4840 0.7376 0.0099 0.9774 0.0536 -0.0037 0.0013 -0.0010 0.2971 + 125 0.5252 0.9712 0.4847 0.7412 0.0118 0.9801 0.0540 -0.0032 0.0037 -0.0014 0.2914 + 126 0.5276 0.9702 0.4834 0.7455 0.0146 0.9826 0.0547 -0.0051 0.0052 0.0022 0.2848 + 127 0.5290 0.9717 0.4856 0.7501 0.0118 0.9798 0.0557 -0.0033 0.0037 -0.0016 0.2780 + 128 0.5282 0.9729 0.4852 0.7516 0.0124 0.9792 0.0557 -0.0026 0.0044 -0.0032 0.2760 + 129 0.5260 0.9709 0.4882 0.7558 0.0124 0.9771 0.0583 -0.0037 0.0044 -0.0049 0.2701 + 130 0.5270 0.9734 0.4886 0.7595 0.0113 0.9790 0.0550 -0.0032 0.0028 -0.0047 0.2644 + 131 0.5281 0.9765 0.4870 0.7605 0.0106 0.9772 0.0593 -0.0024 0.0011 -0.0020 0.2591 + 132 0.5247 0.9772 0.4868 0.7643 0.0130 0.9794 0.0593 -0.0026 -0.0014 0.0009 0.2572 + 133 0.5252 0.9779 0.4875 0.7647 0.0084 0.9793 0.0621 -0.0038 0.0029 0.0010 0.2552 + 134 0.5239 0.9783 0.4885 0.7701 0.0086 0.9812 0.0608 -0.0039 0.0008 0.0032 0.2561 + 135 0.5209 0.9749 0.4852 0.7708 0.0099 0.9829 0.0556 -0.0010 0.0007 0.0029 0.2567 + 136 0.5231 0.9754 0.4852 0.7692 0.0112 0.9844 0.0540 0.0041 -0.0015 0.0010 0.2559 + 137 0.5220 0.9749 0.4858 0.7704 0.0111 0.9839 0.0556 -0.0001 0.0022 -0.0014 0.2516 + 138 0.5224 0.9775 0.4882 0.7698 0.0144 0.9854 0.0552 0.0001 0.0014 -0.0027 0.2459 + 139 0.5235 0.9796 0.4915 0.7709 0.0118 0.9852 0.0528 0.0006 -0.0012 -0.0054 0.2453 + 140 0.5250 0.9828 0.4919 0.7703 0.0099 0.9866 0.0515 -0.0036 -0.0029 -0.0067 0.2405 + 141 0.5229 0.9851 0.4914 0.7683 0.0099 0.9875 0.0525 -0.0019 -0.0034 -0.0049 0.2358 + 142 0.5235 0.9850 0.4915 0.7691 0.0071 0.9885 0.0508 -0.0006 -0.0017 -0.0026 0.2347 + 143 0.5216 0.9836 0.4919 0.7739 0.0078 0.9912 0.0548 -0.0036 -0.0007 -0.0012 0.2338 + 144 0.5204 0.9768 0.4917 0.7724 0.0089 0.9875 0.0534 -0.0071 -0.0011 -0.0009 0.2304 + 145 0.5205 0.9735 0.4889 0.7759 0.0078 0.9862 0.0521 -0.0042 -0.0050 0.0017 0.2289 + 146 0.5192 0.9777 0.4866 0.7793 0.0085 0.9875 0.0526 -0.0046 -0.0030 -0.0031 0.2290 + 147 0.5206 0.9777 0.4859 0.7787 0.0155 0.9841 0.0529 -0.0047 -0.0065 -0.0018 0.2277 + 148 0.5178 0.9770 0.4878 0.7762 0.0166 0.9844 0.0550 -0.0068 -0.0041 -0.0046 0.2303 + 149 0.5216 0.9768 0.4887 0.7730 0.0136 0.9828 0.0566 -0.0071 -0.0026 -0.0045 0.2297 + 150 0.5237 0.9734 0.4914 0.7712 0.0157 0.9866 0.0585 -0.0056 0.0014 -0.0047 0.2330 + 151 0.5226 0.9705 0.4927 0.7715 0.0166 0.9824 0.0573 -0.0038 -0.0002 -0.0044 0.2322 + 152 0.5177 0.9735 0.4895 0.7726 0.0137 0.9794 0.0564 -0.0017 0.0006 -0.0031 0.2335 + 153 0.5216 0.9711 0.4915 0.7718 0.0163 0.9836 0.0547 -0.0005 0.0004 -0.0005 0.2348 + 154 0.5215 0.9710 0.4913 0.7756 0.0169 0.9843 0.0566 0.0035 -0.0044 0.0007 0.2346 + 155 0.5169 0.9751 0.4893 0.7757 0.0146 0.9823 0.0575 0.0046 -0.0064 0.0005 0.2333 + 156 0.5152 0.9722 0.4888 0.7798 0.0136 0.9842 0.0600 0.0044 -0.0058 0.0018 0.2352 + 157 0.5177 0.9746 0.4913 0.7838 0.0133 0.9871 0.0616 0.0045 -0.0037 0.0005 0.2394 + 158 0.5177 0.9796 0.4886 0.7842 0.0107 0.9895 0.0621 0.0049 -0.0025 0.0010 0.2461 + 159 0.5121 0.9746 0.4868 0.7861 0.0122 0.9896 0.0635 0.0033 -0.0022 0.0014 0.2524 + 0 0.8011 -0.0020 -0.0005 -0.0025 -0.0006 -0.0005 0.7999 0.8025 -0.0011 0.0004 -0.0023 + 1 0.7254 -0.0040 0.0018 -0.0014 0.0009 -0.0037 0.8206 0.7303 0.0774 0.0073 0.0925 + 2 0.6563 -0.0001 0.0010 -0.0015 0.0012 -0.0051 0.8369 0.6562 0.1091 0.0186 0.1556 + 3 0.5972 0.0033 -0.0034 -0.0019 0.0058 -0.0040 0.8547 0.5993 0.1197 0.0248 0.1866 + 4 0.5387 0.0009 -0.0055 -0.0043 0.0070 -0.0026 0.8688 0.5474 0.1274 0.0289 0.2038 + 5 0.4913 -0.0005 -0.0087 -0.0068 0.0032 -0.0028 0.8842 0.4983 0.1326 0.0303 0.2164 + 6 0.4448 0.0068 -0.0113 -0.0058 0.0014 -0.0074 0.8962 0.4568 0.1390 0.0327 0.2277 + 7 0.4046 0.0077 -0.0125 -0.0061 0.0016 -0.0087 0.9053 0.4188 0.1407 0.0365 0.2330 + 8 0.3729 0.0101 -0.0114 -0.0016 -0.0019 -0.0089 0.9145 0.3851 0.1477 0.0360 0.2315 + 9 0.3390 0.0158 -0.0080 0.0004 0.0004 -0.0087 0.9220 0.3573 0.1494 0.0354 0.2327 + 10 0.3094 0.0170 -0.0073 -0.0005 0.0023 -0.0084 0.9328 0.3339 0.1502 0.0366 0.2313 + 11 0.2857 0.0235 -0.0061 0.0026 0.0028 -0.0105 0.9386 0.3113 0.1486 0.0383 0.2252 + 12 0.2658 0.0286 -0.0042 -0.0024 0.0022 -0.0090 0.9442 0.2947 0.1539 0.0371 0.2226 + 13 0.2496 0.0353 -0.0101 -0.0030 0.0044 -0.0030 0.9510 0.2759 0.1558 0.0437 0.2225 + 14 0.2329 0.0379 -0.0061 -0.0000 0.0035 -0.0011 0.9579 0.2657 0.1509 0.0434 0.2163 + 15 0.2161 0.0457 -0.0048 0.0002 0.0042 0.0031 0.9606 0.2531 0.1480 0.0467 0.2124 + 16 0.2053 0.0471 -0.0025 0.0014 0.0062 0.0016 0.9666 0.2394 0.1459 0.0494 0.2069 + 17 0.1936 0.0497 -0.0036 -0.0002 0.0088 0.0066 0.9692 0.2358 0.1440 0.0522 0.1984 + 18 0.1842 0.0547 -0.0031 0.0023 0.0051 0.0115 0.9741 0.2265 0.1497 0.0572 0.1891 + 19 0.1788 0.0579 -0.0036 0.0007 0.0036 0.0164 0.9756 0.2217 0.1537 0.0592 0.1796 + 20 0.1775 0.0596 -0.0037 0.0037 0.0061 0.0181 0.9745 0.2183 0.1601 0.0639 0.1682 + 21 0.1706 0.0646 -0.0070 0.0078 0.0064 0.0223 0.9785 0.2118 0.1681 0.0657 0.1579 + 22 0.1650 0.0673 -0.0081 0.0080 0.0055 0.0245 0.9810 0.2081 0.1738 0.0720 0.1508 + 23 0.1597 0.0743 -0.0079 0.0093 0.0086 0.0299 0.9832 0.2023 0.1820 0.0757 0.1418 + 24 0.1548 0.0776 -0.0043 0.0112 0.0107 0.0332 0.9863 0.1975 0.1899 0.0829 0.1312 + 25 0.1544 0.0805 0.0033 0.0116 0.0133 0.0383 0.9858 0.1931 0.1956 0.0886 0.1259 + 26 0.1507 0.0833 0.0069 0.0102 0.0140 0.0443 0.9919 0.1916 0.2080 0.0951 0.1203 + 27 0.1465 0.0834 0.0088 0.0136 0.0128 0.0465 0.9910 0.1897 0.2156 0.1039 0.1122 + 28 0.1417 0.0823 0.0072 0.0154 0.0157 0.0489 0.9936 0.1866 0.2312 0.1144 0.1092 + 29 0.1432 0.0863 0.0065 0.0133 0.0189 0.0510 0.9940 0.1873 0.2468 0.1232 0.1000 + 30 0.1409 0.0904 0.0065 0.0144 0.0202 0.0546 0.9975 0.1846 0.2601 0.1344 0.0916 + 31 0.1404 0.0933 0.0092 0.0141 0.0191 0.0588 0.9977 0.1864 0.2787 0.1504 0.0829 + 32 0.1336 0.0973 0.0129 0.0148 0.0191 0.0609 0.9984 0.1857 0.2982 0.1677 0.0751 + 33 0.1338 0.0959 0.0133 0.0136 0.0249 0.0649 0.9971 0.1835 0.3159 0.1889 0.0702 + 34 0.1319 0.0984 0.0178 0.0173 0.0295 0.0680 0.9936 0.1844 0.3342 0.2119 0.0644 + 35 0.1310 0.1022 0.0176 0.0187 0.0306 0.0736 0.9944 0.1818 0.3574 0.2297 0.0612 + 36 0.1287 0.1046 0.0183 0.0249 0.0328 0.0765 0.9916 0.1802 0.3742 0.2494 0.0563 + 37 0.1310 0.1101 0.0177 0.0258 0.0343 0.0820 0.9921 0.1784 0.3941 0.2735 0.0499 + 38 0.1330 0.1113 0.0163 0.0292 0.0341 0.0884 0.9929 0.1759 0.4161 0.2965 0.0487 + 39 0.1331 0.1108 0.0168 0.0301 0.0328 0.0966 0.9899 0.1746 0.4367 0.3187 0.0448 + 40 0.1325 0.1170 0.0188 0.0313 0.0335 0.1004 0.9907 0.1715 0.4538 0.3406 0.0392 + 41 0.1313 0.1186 0.0159 0.0325 0.0334 0.1083 0.9959 0.1721 0.4705 0.3639 0.0325 + 42 0.1327 0.1228 0.0169 0.0357 0.0351 0.1130 0.9935 0.1714 0.4889 0.3908 0.0292 + 43 0.1318 0.1231 0.0182 0.0402 0.0392 0.1134 0.9964 0.1687 0.5106 0.4135 0.0249 + 44 0.1324 0.1231 0.0154 0.0420 0.0405 0.1171 0.9979 0.1619 0.5261 0.4353 0.0234 + 45 0.1343 0.1249 0.0152 0.0387 0.0427 0.1220 0.9968 0.1609 0.5370 0.4564 0.0224 + 46 0.1300 0.1314 0.0128 0.0384 0.0446 0.1286 0.9946 0.1644 0.5527 0.4742 0.0189 + 47 0.1304 0.1380 0.0110 0.0411 0.0501 0.1323 0.9924 0.1626 0.5645 0.4891 0.0177 + 48 0.1319 0.1409 0.0112 0.0408 0.0522 0.1358 0.9959 0.1565 0.5794 0.5105 0.0158 + 49 0.1320 0.1423 0.0148 0.0405 0.0527 0.1439 0.9954 0.1541 0.5936 0.5271 0.0134 + 50 0.1337 0.1444 0.0149 0.0394 0.0538 0.1524 0.9932 0.1501 0.6048 0.5377 0.0165 + 51 0.1403 0.1446 0.0189 0.0433 0.0591 0.1615 0.9928 0.1409 0.6108 0.5469 0.0167 + 52 0.1458 0.1515 0.0225 0.0448 0.0618 0.1697 0.9947 0.1330 0.6093 0.5478 0.0163 + 53 0.1517 0.1589 0.0183 0.0453 0.0622 0.1766 0.9942 0.1302 0.6084 0.5494 0.0173 + 54 0.1658 0.1653 0.0184 0.0475 0.0625 0.1858 0.9959 0.1264 0.6064 0.5526 0.0146 + 55 0.1742 0.1778 0.0195 0.0495 0.0659 0.1936 0.9920 0.1210 0.6082 0.5535 0.0123 + 56 0.1822 0.1883 0.0212 0.0500 0.0709 0.2076 0.9921 0.1145 0.6020 0.5559 0.0104 + 57 0.1953 0.2046 0.0223 0.0519 0.0750 0.2202 0.9906 0.1086 0.6021 0.5551 0.0082 + 58 0.2072 0.2177 0.0236 0.0568 0.0770 0.2356 0.9866 0.1018 0.5924 0.5497 0.0087 + 59 0.2204 0.2341 0.0253 0.0591 0.0813 0.2546 0.9867 0.0959 0.5819 0.5469 0.0066 + 60 0.2384 0.2500 0.0309 0.0648 0.0905 0.2749 0.9860 0.0891 0.5676 0.5356 0.0107 + 61 0.2590 0.2732 0.0331 0.0696 0.0950 0.2949 0.9830 0.0800 0.5518 0.5183 0.0120 + 62 0.2855 0.2967 0.0355 0.0749 0.0987 0.3245 0.9796 0.0707 0.5339 0.5023 0.0119 + 63 0.3114 0.3265 0.0413 0.0799 0.1034 0.3546 0.9674 0.0636 0.5160 0.4791 0.0158 + 64 0.3361 0.3510 0.0468 0.0843 0.1122 0.3824 0.9585 0.0582 0.4911 0.4562 0.0133 + 65 0.3642 0.3795 0.0541 0.0863 0.1179 0.4124 0.9469 0.0555 0.4691 0.4329 0.0085 + 66 0.3915 0.4060 0.0620 0.0934 0.1285 0.4424 0.9331 0.0504 0.4429 0.4113 0.0060 + 67 0.4212 0.4380 0.0669 0.0992 0.1376 0.4741 0.9155 0.0448 0.4192 0.3838 0.0060 + 68 0.4505 0.4717 0.0783 0.1032 0.1436 0.5062 0.8945 0.0403 0.3923 0.3633 0.0053 + 69 0.4778 0.5056 0.0902 0.1048 0.1540 0.5419 0.8721 0.0370 0.3690 0.3413 0.0060 + 70 0.5013 0.5348 0.1019 0.1075 0.1638 0.5713 0.8471 0.0393 0.3464 0.3200 0.0068 + 71 0.5229 0.5656 0.1118 0.1113 0.1708 0.5998 0.8157 0.0334 0.3250 0.3020 0.0077 + 72 0.5374 0.5936 0.1254 0.1195 0.1780 0.6251 0.7827 0.0300 0.3022 0.2822 0.0120 + 73 0.5561 0.6225 0.1379 0.1237 0.1819 0.6522 0.7452 0.0282 0.2821 0.2621 0.0086 + 74 0.5745 0.6476 0.1486 0.1276 0.1891 0.6749 0.7064 0.0290 0.2654 0.2415 0.0102 + 75 0.5860 0.6673 0.1637 0.1308 0.1967 0.7012 0.6700 0.0286 0.2482 0.2281 0.0101 + 76 0.5977 0.6887 0.1796 0.1275 0.2039 0.7211 0.6309 0.0275 0.2277 0.2135 0.0094 + 77 0.6047 0.7132 0.1913 0.1240 0.2066 0.7383 0.5963 0.0271 0.2098 0.1965 0.0095 + 78 0.6062 0.7335 0.2059 0.1224 0.2078 0.7560 0.5624 0.0255 0.1964 0.1884 0.0105 + 79 0.6109 0.7530 0.2235 0.1244 0.2128 0.7755 0.5265 0.0242 0.1815 0.1737 0.0091 + 80 0.6146 0.7702 0.2400 0.1220 0.2214 0.7929 0.4944 0.0226 0.1658 0.1568 0.0106 + 81 0.6133 0.7925 0.2536 0.1253 0.2293 0.8087 0.4595 0.0204 0.1536 0.1447 0.0145 + 82 0.6132 0.8084 0.2681 0.1247 0.2353 0.8239 0.4301 0.0179 0.1474 0.1325 0.0138 + 83 0.6123 0.8224 0.2820 0.1242 0.2394 0.8347 0.4021 0.0184 0.1329 0.1249 0.0150 + 84 0.6145 0.8367 0.2948 0.1239 0.2436 0.8485 0.3738 0.0174 0.1194 0.1171 0.0159 + 85 0.6145 0.8518 0.3048 0.1233 0.2476 0.8616 0.3481 0.0153 0.1079 0.1057 0.0195 + 86 0.6105 0.8596 0.3164 0.1170 0.2511 0.8726 0.3232 0.0189 0.0982 0.0987 0.0252 + 87 0.6082 0.8679 0.3239 0.1148 0.2573 0.8837 0.3023 0.0140 0.0918 0.0916 0.0231 + 88 0.6029 0.8785 0.3343 0.1136 0.2647 0.8909 0.2793 0.0152 0.0855 0.0827 0.0240 + 89 0.5972 0.8892 0.3419 0.1132 0.2733 0.8962 0.2563 0.0143 0.0805 0.0739 0.0212 + 90 0.5930 0.8977 0.3520 0.1102 0.2835 0.9015 0.2454 0.0153 0.0723 0.0667 0.0227 + 91 0.5904 0.9027 0.3626 0.1064 0.2885 0.9040 0.2300 0.0175 0.0702 0.0563 0.0269 + 92 0.5854 0.9079 0.3738 0.1042 0.2973 0.9088 0.2161 0.0166 0.0677 0.0502 0.0335 + 93 0.5791 0.9123 0.3815 0.0991 0.3081 0.9134 0.2014 0.0150 0.0644 0.0448 0.0424 + 94 0.5734 0.9196 0.3842 0.0967 0.3144 0.9150 0.1886 0.0161 0.0571 0.0446 0.0510 + 95 0.5720 0.9242 0.3939 0.0955 0.3255 0.9216 0.1806 0.0166 0.0550 0.0433 0.0613 + 96 0.5693 0.9266 0.3993 0.0931 0.3340 0.9285 0.1703 0.0191 0.0493 0.0434 0.0690 + 97 0.5637 0.9325 0.4048 0.0911 0.3437 0.9311 0.1634 0.0172 0.0440 0.0363 0.0776 + 98 0.5640 0.9374 0.4062 0.0882 0.3511 0.9336 0.1551 0.0163 0.0359 0.0336 0.0900 + 99 0.5573 0.9415 0.4106 0.0880 0.3570 0.9397 0.1488 0.0160 0.0337 0.0302 0.1021 + 100 0.5567 0.9418 0.4160 0.0857 0.3680 0.9424 0.1414 0.0140 0.0342 0.0283 0.1116 + 101 0.5563 0.9457 0.4248 0.0818 0.3785 0.9429 0.1325 0.0116 0.0287 0.0268 0.1233 + 102 0.5556 0.9516 0.4335 0.0794 0.3867 0.9433 0.1265 0.0125 0.0311 0.0262 0.1334 + 103 0.5563 0.9528 0.4388 0.0723 0.3959 0.9424 0.1186 0.0110 0.0295 0.0263 0.1412 + 104 0.5528 0.9601 0.4433 0.0700 0.4111 0.9493 0.1117 0.0067 0.0232 0.0254 0.1458 + 105 0.5508 0.9611 0.4462 0.0657 0.4208 0.9500 0.1056 0.0076 0.0220 0.0250 0.1501 + 106 0.5461 0.9659 0.4464 0.0589 0.4324 0.9558 0.1045 0.0097 0.0207 0.0235 0.1582 + 107 0.5407 0.9671 0.4543 0.0574 0.4467 0.9570 0.0989 0.0075 0.0153 0.0199 0.1668 + 108 0.5386 0.9722 0.4552 0.0545 0.4626 0.9565 0.0942 0.0052 0.0142 0.0222 0.1756 + 109 0.5342 0.9736 0.4578 0.0511 0.4796 0.9571 0.0923 0.0042 0.0114 0.0231 0.1771 + 110 0.5322 0.9778 0.4610 0.0522 0.4957 0.9609 0.0895 0.0053 0.0083 0.0188 0.1760 + 111 0.5300 0.9788 0.4647 0.0483 0.5048 0.9638 0.0876 0.0070 0.0057 0.0175 0.1814 + 112 0.5297 0.9799 0.4645 0.0448 0.5176 0.9651 0.0842 0.0090 0.0092 0.0147 0.1867 + 113 0.5276 0.9806 0.4622 0.0438 0.5283 0.9657 0.0863 0.0104 0.0090 0.0162 0.1956 + 114 0.5225 0.9840 0.4665 0.0443 0.5430 0.9695 0.0836 0.0095 0.0089 0.0148 0.1990 + 115 0.5211 0.9893 0.4701 0.0392 0.5553 0.9722 0.0823 0.0105 0.0054 0.0160 0.2040 + 116 0.5193 0.9865 0.4723 0.0388 0.5671 0.9713 0.0817 0.0090 0.0065 0.0148 0.2036 + 117 0.5193 0.9890 0.4766 0.0380 0.5799 0.9712 0.0767 0.0101 0.0086 0.0167 0.2095 + 118 0.5183 0.9896 0.4782 0.0351 0.5924 0.9741 0.0725 0.0099 0.0075 0.0155 0.2106 + 119 0.5161 0.9925 0.4793 0.0352 0.6065 0.9739 0.0703 0.0086 0.0039 0.0138 0.2103 + 120 0.5121 0.9907 0.4835 0.0379 0.6172 0.9718 0.0726 0.0059 0.0044 0.0106 0.2170 + 121 0.5095 0.9902 0.4881 0.0357 0.6224 0.9717 0.0693 0.0079 0.0015 0.0098 0.2182 + 122 0.5113 0.9913 0.4882 0.0269 0.6319 0.9723 0.0666 0.0065 0.0035 0.0069 0.2238 + 123 0.5080 0.9950 0.4878 0.0221 0.6414 0.9749 0.0679 0.0083 0.0038 0.0060 0.2287 + 124 0.5101 0.9940 0.4849 0.0224 0.6498 0.9791 0.0648 0.0056 0.0015 0.0086 0.2334 + 125 0.5097 0.9976 0.4885 0.0212 0.6592 0.9789 0.0648 0.0103 0.0005 0.0095 0.2318 + 126 0.5057 0.9954 0.4880 0.0220 0.6733 0.9736 0.0656 0.0104 -0.0017 0.0097 0.2330 + 127 0.5044 0.9930 0.4898 0.0208 0.6828 0.9782 0.0685 0.0081 -0.0008 0.0078 0.2322 + 128 0.5033 0.9910 0.4898 0.0212 0.6892 0.9773 0.0693 0.0104 -0.0011 0.0051 0.2389 + 129 0.5076 0.9891 0.4886 0.0177 0.6959 0.9761 0.0666 0.0102 -0.0013 0.0043 0.2472 + 130 0.5089 0.9918 0.4873 0.0151 0.7038 0.9751 0.0669 0.0107 -0.0000 0.0060 0.2537 + 131 0.5096 0.9946 0.4864 0.0158 0.7087 0.9759 0.0662 0.0090 0.0014 0.0074 0.2531 + 132 0.5150 0.9942 0.4861 0.0175 0.7135 0.9762 0.0645 0.0068 0.0024 0.0066 0.2531 + 133 0.5124 0.9952 0.4907 0.0158 0.7191 0.9779 0.0661 0.0073 0.0016 0.0050 0.2577 + 134 0.5115 0.9897 0.4923 0.0148 0.7236 0.9791 0.0627 0.0048 -0.0000 0.0086 0.2573 + 135 0.5152 0.9910 0.4919 0.0172 0.7278 0.9808 0.0647 0.0041 -0.0010 0.0081 0.2586 + 136 0.5129 0.9949 0.4913 0.0154 0.7304 0.9836 0.0635 0.0043 0.0001 0.0086 0.2590 + 137 0.5117 0.9940 0.4897 0.0154 0.7370 0.9834 0.0637 0.0065 -0.0014 0.0064 0.2578 + 138 0.5111 0.9947 0.4906 0.0105 0.7411 0.9822 0.0628 0.0071 -0.0002 0.0062 0.2585 + 139 0.5059 0.9909 0.4917 0.0096 0.7463 0.9849 0.0639 0.0074 0.0020 0.0049 0.2570 + 140 0.5058 0.9928 0.4974 0.0142 0.7496 0.9908 0.0642 0.0050 0.0001 0.0020 0.2554 + 141 0.5055 0.9967 0.4981 0.0137 0.7509 0.9943 0.0608 0.0054 0.0008 -0.0023 0.2575 + 142 0.5062 0.9998 0.4972 0.0122 0.7577 0.9924 0.0588 0.0051 -0.0021 -0.0018 0.2588 + 143 0.5055 0.9987 0.5011 0.0120 0.7635 0.9931 0.0608 0.0049 -0.0036 0.0009 0.2559 + 144 0.5046 0.9983 0.5047 0.0113 0.7656 0.9929 0.0602 0.0035 -0.0039 0.0005 0.2540 + 145 0.5054 0.9974 0.5035 0.0138 0.7677 0.9931 0.0571 0.0030 -0.0067 0.0031 0.2547 + 146 0.5028 0.9979 0.5043 0.0154 0.7718 0.9952 0.0581 0.0027 -0.0047 0.0012 0.2523 + 147 0.4982 0.9966 0.5033 0.0169 0.7733 0.9911 0.0558 0.0015 -0.0032 -0.0016 0.2493 + 148 0.4979 0.9980 0.5038 0.0172 0.7720 0.9973 0.0562 0.0014 -0.0000 0.0046 0.2453 + 149 0.4959 0.9982 0.5009 0.0138 0.7731 1.0009 0.0557 0.0022 -0.0017 0.0030 0.2397 + 150 0.4973 1.0001 0.5020 0.0150 0.7747 1.0013 0.0581 0.0062 0.0021 0.0013 0.2369 + 151 0.4971 1.0003 0.5028 0.0184 0.7751 1.0012 0.0570 0.0021 0.0018 0.0026 0.2399 + 152 0.4955 1.0007 0.5024 0.0157 0.7759 0.9991 0.0558 -0.0012 -0.0017 0.0022 0.2361 + 153 0.4950 1.0031 0.5029 0.0165 0.7745 1.0004 0.0555 -0.0029 -0.0040 0.0037 0.2348 + 154 0.4970 1.0024 0.5041 0.0162 0.7801 0.9984 0.0566 -0.0053 -0.0010 0.0050 0.2328 + 155 0.4966 1.0016 0.5048 0.0152 0.7782 0.9955 0.0560 -0.0054 -0.0012 0.0016 0.2293 + 156 0.4905 1.0031 0.5033 0.0147 0.7761 0.9929 0.0565 -0.0067 0.0007 -0.0002 0.2303 + 157 0.4941 0.9997 0.5060 0.0156 0.7775 0.9950 0.0571 -0.0037 0.0015 0.0028 0.2296 + 158 0.4966 1.0008 0.5079 0.0102 0.7777 0.9905 0.0555 -0.0021 0.0005 0.0008 0.2281 + 159 0.4995 1.0036 0.5071 0.0104 0.7802 0.9926 0.0551 -0.0024 -0.0018 0.0001 0.2324 + 0 0.7991 0.0030 0.0015 -0.0011 -0.0013 0.0001 0.7999 0.7997 0.0009 0.0005 -0.0028 + 1 0.7229 0.0061 0.0023 -0.0015 0.0024 0.0020 0.8221 0.7238 0.0784 0.0061 0.0974 + 2 0.6557 0.0089 0.0049 0.0003 0.0022 -0.0008 0.8427 0.6586 0.1064 0.0161 0.1634 + 3 0.5938 0.0120 0.0014 0.0018 0.0008 -0.0021 0.8570 0.5991 0.1222 0.0221 0.2027 + 4 0.5360 0.0112 -0.0008 -0.0026 0.0021 -0.0061 0.8665 0.5443 0.1258 0.0223 0.2254 + 5 0.4843 0.0130 -0.0076 -0.0001 0.0046 -0.0032 0.8797 0.4969 0.1297 0.0243 0.2431 + 6 0.4403 0.0182 -0.0042 0.0007 0.0029 -0.0001 0.8905 0.4516 0.1316 0.0266 0.2552 + 7 0.4012 0.0225 -0.0060 -0.0002 0.0048 -0.0006 0.8955 0.4120 0.1313 0.0289 0.2700 + 8 0.3662 0.0246 -0.0032 0.0014 0.0031 -0.0015 0.9050 0.3781 0.1262 0.0266 0.2734 + 9 0.3346 0.0276 -0.0015 0.0034 0.0043 0.0017 0.9119 0.3458 0.1230 0.0277 0.2782 + 10 0.3092 0.0298 0.0008 0.0041 0.0034 0.0071 0.9238 0.3190 0.1199 0.0296 0.2785 + 11 0.2840 0.0318 0.0012 0.0005 0.0041 0.0089 0.9317 0.2980 0.1211 0.0364 0.2791 + 12 0.2640 0.0345 0.0007 0.0007 0.0037 0.0114 0.9383 0.2782 0.1189 0.0347 0.2723 + 13 0.2441 0.0372 -0.0025 -0.0012 0.0053 0.0130 0.9438 0.2680 0.1190 0.0344 0.2707 + 14 0.2253 0.0436 -0.0024 0.0021 0.0091 0.0133 0.9463 0.2548 0.1152 0.0334 0.2700 + 15 0.2107 0.0468 -0.0024 0.0076 0.0081 0.0161 0.9484 0.2442 0.1101 0.0311 0.2690 + 16 0.1984 0.0538 -0.0006 0.0091 0.0087 0.0199 0.9568 0.2342 0.1098 0.0317 0.2727 + 17 0.1857 0.0587 -0.0003 0.0087 0.0084 0.0221 0.9613 0.2272 0.1058 0.0298 0.2760 + 18 0.1770 0.0645 0.0011 0.0090 0.0060 0.0222 0.9653 0.2208 0.1063 0.0291 0.2785 + 19 0.1664 0.0673 0.0020 0.0078 0.0094 0.0278 0.9689 0.2125 0.1041 0.0294 0.2794 + 20 0.1590 0.0738 0.0015 0.0092 0.0068 0.0289 0.9700 0.2054 0.1042 0.0299 0.2807 + 21 0.1509 0.0722 0.0020 0.0078 0.0038 0.0353 0.9718 0.2007 0.0995 0.0284 0.2862 + 22 0.1492 0.0714 0.0025 0.0140 0.0066 0.0381 0.9720 0.1993 0.0989 0.0286 0.2911 + 23 0.1445 0.0706 0.0049 0.0148 0.0107 0.0411 0.9767 0.1999 0.0957 0.0306 0.2930 + 24 0.1402 0.0730 0.0063 0.0143 0.0122 0.0407 0.9761 0.2024 0.0999 0.0315 0.2912 + 25 0.1389 0.0725 0.0069 0.0163 0.0140 0.0406 0.9781 0.2060 0.0972 0.0301 0.2917 + 26 0.1364 0.0761 0.0068 0.0130 0.0147 0.0441 0.9750 0.2033 0.0948 0.0285 0.2932 + 27 0.1363 0.0786 0.0082 0.0151 0.0153 0.0463 0.9812 0.2048 0.0921 0.0291 0.2963 + 28 0.1339 0.0796 0.0069 0.0145 0.0171 0.0539 0.9840 0.2069 0.0884 0.0310 0.2958 + 29 0.1305 0.0847 0.0085 0.0156 0.0150 0.0565 0.9853 0.2108 0.0883 0.0348 0.2913 + 30 0.1276 0.0846 0.0087 0.0156 0.0124 0.0569 0.9906 0.2103 0.0888 0.0282 0.2906 + 31 0.1238 0.0827 0.0080 0.0176 0.0158 0.0603 0.9893 0.2130 0.0886 0.0267 0.2899 + 32 0.1210 0.0770 0.0100 0.0174 0.0171 0.0605 0.9918 0.2194 0.0885 0.0290 0.2966 + 33 0.1187 0.0773 0.0107 0.0185 0.0164 0.0598 0.9913 0.2219 0.0878 0.0299 0.2996 + 34 0.1158 0.0758 0.0095 0.0171 0.0140 0.0573 0.9951 0.2252 0.0877 0.0324 0.3007 + 35 0.1131 0.0785 0.0121 0.0212 0.0153 0.0587 0.9946 0.2275 0.0886 0.0303 0.3019 + 36 0.1122 0.0777 0.0109 0.0218 0.0127 0.0596 0.9941 0.2355 0.0893 0.0283 0.3032 + 37 0.1072 0.0748 0.0119 0.0230 0.0139 0.0585 0.9989 0.2410 0.0910 0.0281 0.3086 + 38 0.1028 0.0746 0.0094 0.0226 0.0179 0.0600 1.0018 0.2489 0.0894 0.0280 0.3093 + 39 0.0990 0.0747 0.0085 0.0216 0.0202 0.0572 1.0025 0.2566 0.0881 0.0277 0.3119 + 40 0.0972 0.0720 0.0094 0.0201 0.0221 0.0575 1.0000 0.2602 0.0866 0.0225 0.3188 + 41 0.0910 0.0694 0.0116 0.0167 0.0212 0.0590 0.9977 0.2646 0.0859 0.0206 0.3350 + 42 0.0881 0.0635 0.0127 0.0144 0.0243 0.0575 0.9993 0.2765 0.0842 0.0192 0.3470 + 43 0.0777 0.0648 0.0107 0.0142 0.0227 0.0554 1.0009 0.2916 0.0783 0.0182 0.3636 + 44 0.0751 0.0614 0.0109 0.0140 0.0234 0.0511 0.9961 0.3049 0.0730 0.0199 0.3763 + 45 0.0672 0.0573 0.0062 0.0126 0.0243 0.0472 0.9971 0.3239 0.0715 0.0206 0.3870 + 46 0.0605 0.0557 0.0099 0.0120 0.0209 0.0449 0.9990 0.3487 0.0693 0.0166 0.3983 + 47 0.0558 0.0487 0.0070 0.0078 0.0213 0.0421 1.0022 0.3729 0.0685 0.0143 0.4193 + 48 0.0541 0.0413 0.0061 0.0081 0.0214 0.0373 1.0017 0.3980 0.0627 0.0150 0.4442 + 49 0.0473 0.0383 0.0069 0.0082 0.0187 0.0349 1.0021 0.4264 0.0628 0.0124 0.4633 + 50 0.0406 0.0333 0.0086 0.0041 0.0166 0.0321 1.0035 0.4560 0.0572 0.0091 0.4903 + 51 0.0375 0.0293 0.0067 0.0051 0.0111 0.0277 1.0031 0.4869 0.0538 0.0077 0.5204 + 52 0.0345 0.0260 0.0056 0.0032 0.0092 0.0229 1.0014 0.5207 0.0548 0.0092 0.5525 + 53 0.0355 0.0241 0.0018 0.0041 0.0093 0.0204 0.9994 0.5490 0.0520 0.0077 0.5806 + 54 0.0338 0.0206 0.0042 0.0010 0.0103 0.0208 0.9973 0.5815 0.0465 0.0034 0.6118 + 55 0.0295 0.0149 0.0014 0.0002 0.0086 0.0173 0.9992 0.6120 0.0456 0.0037 0.6484 + 56 0.0271 0.0161 -0.0009 0.0042 0.0087 0.0183 0.9966 0.6404 0.0462 0.0017 0.6776 + 57 0.0234 0.0168 -0.0031 0.0043 0.0105 0.0146 0.9972 0.6661 0.0422 0.0024 0.7058 + 58 0.0197 0.0159 -0.0013 0.0039 0.0117 0.0134 0.9977 0.6918 0.0429 0.0049 0.7283 + 59 0.0190 0.0129 -0.0024 0.0057 0.0121 0.0116 0.9980 0.7171 0.0406 0.0068 0.7472 + 60 0.0188 0.0127 -0.0019 0.0055 0.0136 0.0106 0.9989 0.7410 0.0410 0.0035 0.7613 + 61 0.0136 0.0091 -0.0000 0.0055 0.0110 0.0114 1.0007 0.7634 0.0377 0.0030 0.7768 + 62 0.0130 0.0047 0.0062 0.0049 0.0092 0.0106 1.0040 0.7838 0.0360 0.0054 0.7941 + 63 0.0121 0.0046 0.0084 0.0015 0.0085 0.0119 1.0038 0.8002 0.0354 0.0076 0.8068 + 64 0.0067 0.0030 0.0063 0.0018 0.0057 0.0093 1.0053 0.8150 0.0358 0.0093 0.8085 + 65 0.0036 0.0040 0.0098 0.0048 0.0029 0.0103 1.0043 0.8335 0.0347 0.0092 0.8087 + 66 0.0022 0.0014 0.0081 0.0054 0.0016 0.0070 1.0051 0.8450 0.0368 0.0106 0.8054 + 67 0.0035 0.0008 0.0072 0.0045 -0.0017 0.0118 1.0106 0.8645 0.0355 0.0123 0.7970 + 68 0.0019 -0.0007 0.0076 0.0025 -0.0015 0.0157 1.0082 0.8786 0.0353 0.0115 0.7900 + 69 -0.0005 0.0007 0.0056 0.0028 -0.0028 0.0182 1.0054 0.8891 0.0327 0.0156 0.7795 + 70 0.0001 0.0043 0.0040 0.0038 -0.0027 0.0153 1.0046 0.9017 0.0336 0.0166 0.7638 + 71 0.0010 0.0058 0.0050 0.0017 0.0019 0.0136 1.0057 0.9121 0.0304 0.0145 0.7487 + 72 -0.0005 0.0014 0.0029 0.0017 0.0003 0.0109 1.0029 0.9236 0.0279 0.0121 0.7384 + 73 -0.0007 0.0043 -0.0017 0.0010 0.0004 0.0126 1.0038 0.9318 0.0295 0.0122 0.7401 + 74 0.0017 0.0049 -0.0038 0.0020 -0.0004 0.0123 1.0060 0.9391 0.0310 0.0104 0.7420 + 75 0.0020 0.0058 -0.0027 0.0012 -0.0029 0.0137 1.0065 0.9440 0.0288 0.0112 0.7460 + 76 0.0056 0.0060 -0.0008 -0.0000 -0.0048 0.0128 1.0063 0.9488 0.0278 0.0064 0.7508 + 77 0.0010 0.0034 0.0003 -0.0009 -0.0041 0.0157 1.0050 0.9543 0.0263 0.0042 0.7670 + 78 -0.0035 0.0037 -0.0005 -0.0013 -0.0014 0.0136 1.0037 0.9554 0.0244 0.0030 0.7852 + 79 -0.0036 0.0011 -0.0017 0.0015 -0.0033 0.0083 1.0048 0.9596 0.0232 0.0023 0.8049 + 80 -0.0045 0.0013 -0.0018 0.0058 -0.0019 0.0051 1.0035 0.9609 0.0182 0.0031 0.8223 + 81 -0.0054 0.0006 0.0006 0.0042 0.0025 0.0081 1.0022 0.9671 0.0173 0.0013 0.8407 + 82 -0.0044 -0.0017 0.0009 0.0039 0.0020 0.0053 1.0022 0.9687 0.0191 0.0033 0.8540 + 83 -0.0062 -0.0005 0.0028 0.0042 -0.0001 0.0053 1.0035 0.9725 0.0186 0.0011 0.8643 + 84 -0.0032 -0.0016 0.0028 0.0041 -0.0030 0.0062 1.0058 0.9703 0.0221 0.0036 0.8738 + 85 -0.0008 -0.0045 0.0032 0.0015 -0.0052 0.0076 1.0098 0.9704 0.0200 0.0038 0.8789 + 86 -0.0009 -0.0052 0.0032 0.0030 -0.0049 0.0053 1.0059 0.9697 0.0174 0.0017 0.8827 + 87 -0.0004 -0.0044 0.0010 0.0014 -0.0041 0.0082 1.0084 0.9742 0.0192 0.0018 0.8915 + 88 0.0004 -0.0036 -0.0013 -0.0013 -0.0069 0.0088 1.0077 0.9741 0.0203 -0.0020 0.9007 + 89 0.0016 -0.0057 -0.0028 -0.0019 -0.0092 0.0088 1.0056 0.9772 0.0227 -0.0029 0.9079 + 90 0.0043 -0.0044 -0.0029 -0.0042 -0.0110 0.0089 1.0077 0.9774 0.0187 -0.0027 0.9122 + 91 0.0029 -0.0048 -0.0007 -0.0074 -0.0067 0.0103 1.0058 0.9793 0.0164 -0.0001 0.9190 + 92 0.0046 -0.0024 0.0024 -0.0066 -0.0074 0.0084 1.0077 0.9780 0.0156 0.0033 0.9253 + 93 0.0057 0.0025 0.0060 -0.0083 -0.0069 0.0096 1.0043 0.9815 0.0139 -0.0000 0.9315 + 94 0.0058 -0.0008 0.0053 -0.0048 -0.0079 0.0051 1.0027 0.9819 0.0128 -0.0021 0.9381 + 95 0.0049 0.0001 0.0076 -0.0053 -0.0099 0.0030 1.0040 0.9834 0.0174 0.0006 0.9442 + 96 0.0026 0.0016 0.0087 -0.0013 -0.0097 -0.0002 1.0010 0.9825 0.0152 -0.0037 0.9445 + 97 0.0010 -0.0015 0.0051 -0.0056 -0.0109 -0.0001 0.9991 0.9858 0.0147 0.0001 0.9469 + 98 0.0017 -0.0009 0.0059 -0.0041 -0.0127 0.0027 1.0012 0.9888 0.0157 0.0004 0.9482 + 99 0.0033 0.0025 0.0038 -0.0023 -0.0113 0.0005 0.9991 0.9893 0.0158 -0.0009 0.9526 + 100 0.0022 -0.0025 0.0025 -0.0013 -0.0115 -0.0025 1.0013 0.9851 0.0158 -0.0003 0.9539 + 101 0.0027 -0.0007 0.0033 -0.0021 -0.0107 -0.0073 0.9994 0.9876 0.0167 0.0040 0.9537 + 102 0.0010 -0.0020 0.0027 -0.0006 -0.0094 -0.0082 0.9983 0.9895 0.0161 0.0061 0.9528 + 103 -0.0018 0.0012 0.0018 0.0010 -0.0121 -0.0074 0.9983 0.9941 0.0151 0.0051 0.9486 + 104 -0.0016 -0.0011 0.0026 -0.0014 -0.0084 -0.0067 1.0020 0.9956 0.0121 0.0063 0.9434 + 105 -0.0007 -0.0027 -0.0022 -0.0006 -0.0084 -0.0050 1.0045 1.0011 0.0128 0.0055 0.9396 + 106 -0.0002 -0.0030 -0.0008 -0.0023 -0.0017 -0.0051 1.0037 1.0037 0.0119 0.0047 0.9403 + 107 -0.0000 -0.0031 -0.0037 -0.0025 -0.0061 -0.0018 1.0033 1.0054 0.0141 0.0054 0.9383 + 108 0.0010 -0.0060 -0.0048 -0.0004 -0.0048 -0.0024 1.0029 1.0067 0.0164 0.0070 0.9398 + 109 0.0010 -0.0050 -0.0075 -0.0036 -0.0073 0.0003 1.0010 1.0062 0.0178 0.0050 0.9357 + 110 0.0013 -0.0048 -0.0074 -0.0052 -0.0027 -0.0005 0.9988 1.0052 0.0160 0.0050 0.9303 + 111 0.0031 -0.0044 -0.0053 0.0004 -0.0020 0.0006 0.9990 1.0037 0.0170 0.0060 0.9304 + 112 0.0040 -0.0049 -0.0044 0.0020 -0.0009 -0.0012 1.0010 1.0015 0.0203 0.0063 0.9264 + 113 0.0024 -0.0035 -0.0028 0.0016 -0.0004 -0.0013 1.0027 1.0006 0.0214 0.0027 0.9278 + 114 0.0036 -0.0006 -0.0018 0.0002 -0.0010 0.0011 1.0038 1.0032 0.0185 0.0014 0.9326 + 115 0.0036 -0.0017 -0.0026 0.0049 -0.0051 0.0024 1.0048 1.0039 0.0179 -0.0011 0.9367 + 116 0.0036 -0.0032 -0.0034 0.0032 -0.0023 0.0036 1.0062 1.0058 0.0195 -0.0015 0.9401 + 117 0.0018 -0.0049 0.0001 0.0010 0.0000 0.0044 1.0066 1.0055 0.0204 -0.0021 0.9432 + 118 -0.0000 -0.0055 -0.0014 -0.0013 -0.0025 0.0020 1.0021 1.0051 0.0189 -0.0044 0.9453 + 119 -0.0035 -0.0040 -0.0032 -0.0007 -0.0018 -0.0000 1.0024 1.0047 0.0187 -0.0066 0.9389 + 120 -0.0071 -0.0056 -0.0025 0.0002 -0.0020 -0.0040 1.0032 1.0047 0.0155 -0.0102 0.9278 + 121 -0.0053 -0.0073 -0.0031 0.0033 0.0006 -0.0011 1.0012 1.0025 0.0137 -0.0045 0.9215 + 122 -0.0010 -0.0068 -0.0031 0.0045 -0.0015 -0.0032 0.9998 1.0044 0.0113 -0.0052 0.9236 + 123 -0.0017 -0.0023 -0.0046 0.0052 0.0033 -0.0023 0.9987 1.0078 0.0105 -0.0070 0.9217 + 124 -0.0025 0.0006 -0.0001 0.0069 0.0048 -0.0008 0.9951 1.0050 0.0144 -0.0072 0.9174 + 125 -0.0014 0.0029 -0.0026 0.0083 0.0030 0.0000 0.9967 1.0052 0.0130 -0.0051 0.9144 + 126 0.0001 0.0038 -0.0015 0.0065 0.0026 0.0015 0.9945 1.0036 0.0144 -0.0030 0.9207 + 127 0.0019 0.0020 0.0014 0.0059 0.0048 0.0008 0.9966 1.0070 0.0151 -0.0008 0.9236 + 128 0.0013 0.0018 -0.0016 0.0077 0.0045 -0.0000 0.9966 1.0074 0.0135 -0.0022 0.9318 + 129 0.0014 -0.0000 -0.0017 0.0060 0.0053 0.0002 0.9890 1.0061 0.0140 -0.0034 0.9351 + 130 0.0012 0.0023 0.0008 0.0046 0.0053 0.0032 0.9867 1.0054 0.0131 -0.0067 0.9380 + 131 0.0045 0.0011 0.0021 0.0024 0.0070 0.0006 0.9872 1.0040 0.0125 -0.0076 0.9325 + 132 0.0077 0.0017 0.0034 0.0055 0.0049 -0.0010 0.9914 1.0053 0.0113 -0.0099 0.9200 + 133 0.0061 -0.0006 0.0036 0.0096 0.0063 -0.0023 0.9914 1.0067 0.0126 -0.0062 0.9187 + 134 0.0046 -0.0029 0.0047 0.0083 0.0047 -0.0058 0.9915 1.0034 0.0144 -0.0047 0.9198 + 135 0.0025 -0.0020 0.0026 0.0064 0.0047 -0.0078 0.9926 1.0030 0.0153 -0.0054 0.9198 + 136 -0.0023 -0.0027 0.0013 0.0059 0.0014 -0.0077 0.9938 1.0012 0.0143 -0.0046 0.9246 + 137 0.0004 -0.0045 0.0011 0.0046 0.0035 -0.0082 0.9927 1.0007 0.0133 -0.0049 0.9222 + 138 -0.0006 -0.0044 0.0010 0.0071 0.0049 -0.0112 0.9953 1.0034 0.0123 -0.0061 0.9246 + 139 -0.0025 -0.0051 -0.0003 0.0076 0.0030 -0.0067 0.9932 1.0029 0.0140 -0.0055 0.9267 + 140 -0.0038 -0.0068 0.0007 0.0066 -0.0003 -0.0066 0.9921 1.0051 0.0130 -0.0046 0.9243 + 141 -0.0023 -0.0119 0.0046 0.0063 0.0037 -0.0059 0.9913 1.0040 0.0146 -0.0022 0.9326 + 142 -0.0034 -0.0117 0.0020 0.0028 0.0030 -0.0026 0.9923 1.0021 0.0158 -0.0021 0.9364 + 143 -0.0047 -0.0134 0.0031 0.0018 0.0046 -0.0006 0.9908 0.9996 0.0136 -0.0015 0.9376 + 144 -0.0035 -0.0163 0.0017 0.0016 0.0035 -0.0008 0.9938 0.9976 0.0130 0.0009 0.9446 + 145 -0.0020 -0.0149 -0.0009 0.0021 0.0013 -0.0018 0.9938 0.9941 0.0150 -0.0055 0.9497 + 146 -0.0036 -0.0151 0.0024 -0.0013 0.0012 0.0004 0.9976 0.9917 0.0123 -0.0032 0.9441 + 147 -0.0032 -0.0116 0.0010 0.0008 -0.0024 0.0006 1.0010 0.9934 0.0108 -0.0065 0.9492 + 148 -0.0023 -0.0083 0.0027 0.0005 0.0006 -0.0014 1.0014 0.9927 0.0136 -0.0067 0.9400 + 149 -0.0010 -0.0060 0.0022 0.0034 -0.0011 -0.0018 0.9997 0.9933 0.0132 -0.0049 0.9404 + 150 -0.0024 -0.0073 -0.0011 0.0036 -0.0024 -0.0008 0.9975 0.9958 0.0165 -0.0064 0.9398 + 151 0.0007 -0.0051 0.0008 0.0036 -0.0030 -0.0012 1.0006 0.9932 0.0161 -0.0076 0.9372 + 152 -0.0008 -0.0076 0.0025 0.0028 -0.0048 -0.0003 1.0016 0.9920 0.0150 -0.0095 0.9325 + 153 -0.0029 -0.0045 0.0027 0.0051 -0.0046 -0.0039 0.9989 0.9913 0.0177 -0.0087 0.9232 + 154 0.0002 -0.0020 0.0061 0.0048 -0.0043 -0.0016 0.9989 0.9911 0.0152 -0.0072 0.9160 + 155 -0.0010 -0.0020 0.0015 0.0029 -0.0055 -0.0004 0.9990 0.9915 0.0107 -0.0076 0.9119 + 156 0.0028 -0.0030 0.0009 0.0049 -0.0073 0.0020 1.0000 0.9946 0.0127 -0.0082 0.9095 + 157 0.0030 -0.0035 0.0008 0.0052 -0.0066 0.0001 1.0014 0.9977 0.0125 -0.0046 0.9069 + 158 0.0063 -0.0023 -0.0005 0.0081 -0.0046 -0.0006 0.9983 0.9970 0.0109 -0.0030 0.9118 + 159 0.0049 0.0002 -0.0013 0.0054 -0.0036 0.0009 1.0011 0.9967 0.0066 -0.0033 0.9177 diff --git a/.venv/lib/python3.11/site-packages/scanpy/datasets/toggleswitch.txt b/.venv/lib/python3.11/site-packages/scanpy/datasets/toggleswitch.txt new file mode 100644 index 0000000000000000000000000000000000000000..0a69d3c8e2bd03813bc36068a697dda8c785a238 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/datasets/toggleswitch.txt @@ -0,0 +1,200 @@ + 0 0.8052 0.7880 + 1 0.7378 0.7237 + 2 0.6568 0.6589 + 3 0.5926 0.5886 + 4 0.5454 0.5314 + 5 0.4869 0.4905 + 6 0.4490 0.4493 + 7 0.4000 0.4066 + 8 0.3722 0.3757 + 9 0.3355 0.3014 + 10 0.3004 0.3247 + 11 0.2922 0.2865 + 12 0.2728 0.2553 + 13 0.2533 0.2504 + 14 0.2341 0.2504 + 15 0.2275 0.2192 + 16 0.2160 0.2120 + 17 0.2164 0.2017 + 18 0.2030 0.1865 + 19 0.2145 0.1973 + 20 0.1901 0.1991 + 21 0.1857 0.1772 + 22 0.2140 0.1781 + 23 0.1823 0.1899 + 24 0.1820 0.1734 + 25 0.2038 0.1831 + 26 0.1878 0.1833 + 27 0.2071 0.1546 + 28 0.1868 0.1561 + 29 0.1931 0.1545 + 30 0.1971 0.1606 + 31 0.2048 0.1480 + 32 0.2007 0.1665 + 33 0.2005 0.1567 + 34 0.2335 0.1688 + 35 0.1923 0.1687 + 36 0.2037 0.1463 + 37 0.2114 0.1620 + 38 0.2258 0.1510 + 39 0.2349 0.1464 + 40 0.2294 0.1453 + 41 0.2369 0.1557 + 42 0.2425 0.1334 + 43 0.2240 0.1440 + 44 0.2479 0.1287 + 45 0.2506 0.1361 + 46 0.2693 0.1287 + 47 0.2716 0.1226 + 48 0.2859 0.1214 + 49 0.2877 0.1017 + 50 0.2892 0.1042 + 51 0.2916 0.0954 + 52 0.3211 0.0971 + 53 0.3365 0.0777 + 54 0.3389 0.0806 + 55 0.3828 0.0666 + 56 0.3880 0.0715 + 57 0.4092 0.0604 + 58 0.4341 0.0590 + 59 0.4671 0.0591 + 60 0.4888 0.0547 + 61 0.5179 0.0392 + 62 0.5429 0.0457 + 63 0.5542 0.0351 + 64 0.5775 0.0466 + 65 0.6189 0.0559 + 66 0.6583 0.0100 + 67 0.6781 0.0164 + 68 0.6738 0.0121 + 69 0.7266 0.0161 + 70 0.7365 0.0267 + 71 0.7676 0.0108 + 72 0.7802 0.0189 + 73 0.7919 0.0223 + 74 0.7964 0.0000 + 75 0.8168 0.0206 + 76 0.8357 -0.0017 + 77 0.8590 0.0064 + 78 0.8631 -0.0063 + 79 0.8778 0.0118 + 80 0.8959 0.0151 + 81 0.8889 0.0045 + 82 0.8988 -0.0017 + 83 0.9048 0.0015 + 84 0.9058 0.0184 + 85 0.9106 0.0078 + 86 0.9427 0.0061 + 87 0.9504 -0.0172 + 88 0.9399 -0.0146 + 89 0.9312 -0.0045 + 90 0.9287 0.0014 + 91 0.9580 -0.0219 + 92 0.9492 0.0117 + 93 0.9513 -0.0021 + 94 0.9775 -0.0108 + 95 0.9771 0.0123 + 96 0.9576 0.0039 + 97 0.9823 -0.0137 + 98 0.9726 0.0075 + 99 0.9791 -0.0249 + 0 0.7856 0.7909 + 1 0.7232 0.7316 + 2 0.6652 0.6568 + 3 0.5844 0.5871 + 4 0.5261 0.5367 + 5 0.4828 0.4746 + 6 0.4317 0.4340 + 7 0.3951 0.4049 + 8 0.3363 0.3738 + 9 0.3265 0.3413 + 10 0.3014 0.3120 + 11 0.2903 0.2915 + 12 0.2452 0.2735 + 13 0.2541 0.2485 + 14 0.2376 0.2455 + 15 0.2073 0.2364 + 16 0.2148 0.2256 + 17 0.2077 0.2291 + 18 0.2010 0.2009 + 19 0.1830 0.2009 + 20 0.1730 0.1904 + 21 0.1850 0.1854 + 22 0.1864 0.1781 + 23 0.1844 0.2072 + 24 0.1840 0.1829 + 25 0.1703 0.1929 + 26 0.1910 0.1755 + 27 0.1793 0.1873 + 28 0.1672 0.1985 + 29 0.1709 0.1870 + 30 0.1602 0.1806 + 31 0.1674 0.1905 + 32 0.1586 0.1792 + 33 0.1521 0.1885 + 34 0.1617 0.1938 + 35 0.1813 0.1820 + 36 0.1710 0.1927 + 37 0.1813 0.1973 + 38 0.1668 0.1812 + 39 0.1697 0.1911 + 40 0.1802 0.1937 + 41 0.1641 0.2023 + 42 0.1605 0.1742 + 43 0.1634 0.2003 + 44 0.1485 0.2187 + 45 0.1607 0.1984 + 46 0.1578 0.2125 + 47 0.1378 0.2091 + 48 0.1630 0.2080 + 49 0.1525 0.2239 + 50 0.1459 0.2286 + 51 0.1377 0.2348 + 52 0.1228 0.2197 + 53 0.1515 0.2485 + 54 0.1118 0.2391 + 55 0.1275 0.2648 + 56 0.1248 0.2458 + 57 0.1199 0.2800 + 58 0.1128 0.2795 + 59 0.0929 0.2965 + 60 0.1039 0.3032 + 61 0.0973 0.3214 + 62 0.0875 0.3213 + 63 0.0771 0.3611 + 64 0.0652 0.3762 + 65 0.0894 0.3851 + 66 0.0579 0.4128 + 67 0.0593 0.4368 + 68 0.0598 0.4614 + 69 0.0671 0.5108 + 70 0.0523 0.5170 + 71 0.0545 0.5370 + 72 0.0182 0.5819 + 73 0.0376 0.6036 + 74 0.0318 0.6148 + 75 0.0389 0.6596 + 76 0.0265 0.6772 + 77 0.0222 0.7121 + 78 0.0297 0.7199 + 79 0.0300 0.7359 + 80 0.0108 0.7798 + 81 0.0335 0.7786 + 82 0.0247 0.8034 + 83 0.0248 0.8071 + 84 -0.0007 0.8537 + 85 0.0087 0.8359 + 86 0.0065 0.8508 + 87 -0.0025 0.8516 + 88 0.0052 0.8746 + 89 0.0136 0.8971 + 90 0.0023 0.8802 + 91 0.0133 0.9154 + 92 0.0067 0.9180 + 93 0.0115 0.9287 + 94 0.0010 0.9201 + 95 0.0070 0.9266 + 96 0.0115 0.9389 + 97 0.0145 0.9367 + 98 0.0012 0.9513 + 99 0.0075 0.9499 diff --git a/.venv/lib/python3.11/site-packages/scanpy/experimental/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/experimental/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6bfafdbaf2db090475a4a21100cec04109715fba --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/experimental/__init__.py @@ -0,0 +1,7 @@ +"""Experimental functions and modules.""" + +from __future__ import annotations + +from . import pp + +__all__ = ["pp"] diff --git a/.venv/lib/python3.11/site-packages/scanpy/experimental/_docs.py b/.venv/lib/python3.11/site-packages/scanpy/experimental/_docs.py new file mode 100644 index 0000000000000000000000000000000000000000..c6f1bf2f8bca946563fec75d6f00ceade206d3bd --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/experimental/_docs.py @@ -0,0 +1,80 @@ +"""Shared docstrings for experimental function parameters.""" + +from __future__ import annotations + +doc_adata = """\ +adata + The annotated data matrix of shape `n_obs` × `n_vars`. + Rows correspond to cells and columns to genes. +""" + +doc_dist_params = """\ +theta + The negative binomial overdispersion parameter `theta` for Pearson residuals. + Higher values correspond to less overdispersion \ + (`var = mean + mean^2/theta`), and `theta=np.inf` corresponds to a Poisson model. +clip + Determines if and how residuals are clipped: + + * If `None`, residuals are clipped to the interval \ + `[-sqrt(n_obs), sqrt(n_obs)]`, where `n_obs` is the number of cells in the dataset (default behavior). + * If any scalar `c`, residuals are clipped to the interval `[-c, c]`. Set \ + `clip=np.inf` for no clipping. +""" + +doc_check_values = """\ +check_values + If `True`, checks if counts in selected layer are integers as expected by this + function, and return a warning if non-integers are found. Otherwise, proceed + without checking. Setting this to `False` can speed up code for large datasets. +""" + +doc_layer = """\ +layer + Layer to use as input instead of `X`. If `None`, `X` is used. +""" + +doc_subset = """\ +subset + Inplace subset to highly-variable genes if `True` otherwise merely indicate + highly variable genes. +""" + +doc_genes_batch_chunk = """\ +n_top_genes + Number of highly-variable genes to keep. Mandatory if `flavor='seurat_v3'` or + `flavor='pearson_residuals'`. +batch_key + If specified, highly-variable genes are selected within each batch separately + and merged. This simple process avoids the selection of batch-specific genes + and acts as a lightweight batch correction method. Genes are first sorted by + how many batches they are a HVG. If `flavor='pearson_residuals'`, ties are + broken by the median rank (across batches) based on within-batch residual + variance. +chunksize + If `flavor='pearson_residuals'`, this dertermines how many genes are processed at + once while computing the residual variance. Choosing a smaller value will reduce + the required memory. +""" + +doc_pca_chunk = """\ +n_comps + Number of principal components to compute in the PCA step. +random_state + Random seed for setting the initial states for the optimization in the PCA step. +kwargs_pca + Dictionary of further keyword arguments passed on to `scanpy.pp.pca()`. +""" + +doc_inplace = """\ +inplace + If `True`, update `adata` with results. Otherwise, return results. See below for + details of what is returned. +""" + +doc_copy = """\ +copy + If `True`, the function runs on a copy of the input object and returns the + modified copy. Otherwise, the input object is modified direcly. Not compatible + with `inplace=False`. +""" diff --git a/.venv/lib/python3.11/site-packages/scanpy/experimental/pp/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/experimental/pp/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d1b5f7aff6a8f4717957f55692dbf37bfe7d147c --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/experimental/pp/__init__.py @@ -0,0 +1,17 @@ +"""Experimental preprocessing functions.""" + +from __future__ import annotations + +from scanpy.experimental.pp._highly_variable_genes import highly_variable_genes +from scanpy.experimental.pp._normalization import ( + normalize_pearson_residuals, + normalize_pearson_residuals_pca, +) +from scanpy.experimental.pp._recipes import recipe_pearson_residuals + +__all__ = [ + "highly_variable_genes", + "normalize_pearson_residuals", + "normalize_pearson_residuals_pca", + "recipe_pearson_residuals", +] diff --git a/.venv/lib/python3.11/site-packages/scanpy/experimental/pp/_highly_variable_genes.py b/.venv/lib/python3.11/site-packages/scanpy/experimental/pp/_highly_variable_genes.py new file mode 100644 index 0000000000000000000000000000000000000000..540907eeae9798bbfa8117f26e8b67bf8ff4b9c6 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/experimental/pp/_highly_variable_genes.py @@ -0,0 +1,406 @@ +from __future__ import annotations + +import warnings +from functools import partial +from math import sqrt +from typing import TYPE_CHECKING + +import numba +import numpy as np +import pandas as pd +from anndata import AnnData + +from ... import logging as logg +from ..._compat import CSBase, njit +from ..._settings import Verbosity, settings +from ..._utils import _doc_params, check_nonnegative_integers, view_to_actual +from ...experimental._docs import ( + doc_adata, + doc_check_values, + doc_dist_params, + doc_genes_batch_chunk, + doc_inplace, + doc_layer, +) +from ...get import _get_obs_rep +from ...preprocessing._distributed import materialize_as_ndarray +from ...preprocessing._utils import _get_mean_var + +if TYPE_CHECKING: + from typing import Literal + + from numpy.typing import NDArray + + +@njit +def _calculate_res_sparse( + indptr: NDArray[np.integer], + index: NDArray[np.integer], + data: NDArray[np.float64], + *, + sums_genes: NDArray[np.float64], + sums_cells: NDArray[np.float64], + sum_total: np.float64, + clip: np.float64, + theta: np.float64, + n_genes: int, + n_cells: int, +) -> NDArray[np.float64]: + def get_value(cell: int, sparse_idx: int, stop_idx: int) -> np.float64: + """Return the value at the specified cell location if it exists, or zero otherwise.""" + if sparse_idx < stop_idx and index[sparse_idx] == cell: + return data[sparse_idx] + else: + return np.float64(0.0) + + def clac_clipped_res_sparse(gene: int, cell: int, value: np.float64) -> np.float64: + mu = sums_genes[gene] * sums_cells[cell] / sum_total + mu_sum = value - mu + pre_res = mu_sum / sqrt(mu + mu * mu / theta) + res = np.float64(min(max(pre_res, -clip), clip)) + return res + + residuals = np.zeros(n_genes, dtype=np.float64) + for gene in numba.prange(n_genes): + start_idx = indptr[gene] + stop_idx = indptr[gene + 1] + + sparse_idx = start_idx + var_sum = np.float64(0.0) + sum_clipped_res = np.float64(0.0) + for cell in range(n_cells): + value = get_value(cell, sparse_idx, stop_idx) + clipped_res = clac_clipped_res_sparse(gene, cell, value) + if value > 0: + sparse_idx += 1 + sum_clipped_res += clipped_res + + mean_clipped_res = sum_clipped_res / n_cells + sparse_idx = start_idx + for cell in range(n_cells): + value = get_value(cell, sparse_idx, stop_idx) + clipped_res = clac_clipped_res_sparse(gene, cell, value) + if value > 0: + sparse_idx += 1 + diff = clipped_res - mean_clipped_res + var_sum += diff * diff + + residuals[gene] = var_sum / n_cells + return residuals + + +@njit +def _calculate_res_dense( + matrix, + *, + sums_genes: NDArray[np.float64], + sums_cells: NDArray[np.float64], + sum_total: np.float64, + clip: np.float64, + theta: np.float64, + n_genes: int, + n_cells: int, +) -> NDArray[np.float64]: + def clac_clipped_res_dense(gene: int, cell: int) -> np.float64: + mu = sums_genes[gene] * sums_cells[cell] / sum_total + value = matrix[cell, gene] + + mu_sum = value - mu + pre_res = mu_sum / sqrt(mu + mu * mu / theta) + res = np.float64(min(max(pre_res, -clip), clip)) + return res + + residuals = np.zeros(n_genes, dtype=np.float64) + + for gene in numba.prange(n_genes): + sum_clipped_res = np.float64(0.0) + for cell in range(n_cells): + sum_clipped_res += clac_clipped_res_dense(gene, cell) + mean_clipped_res = sum_clipped_res / n_cells + + var_sum = np.float64(0.0) + for cell in range(n_cells): + clipped_res = clac_clipped_res_dense(gene, cell) + diff = clipped_res - mean_clipped_res + var_sum += diff * diff + + residuals[gene] = var_sum / n_cells + return residuals + + +def _highly_variable_pearson_residuals( # noqa: PLR0912, PLR0915 + adata: AnnData, + *, + theta: float = 100, + clip: float | None = None, + n_top_genes: int = 1000, + batch_key: str | None = None, + chunksize: int = 1000, + check_values: bool = True, + layer: str | None = None, + subset: bool = False, + inplace: bool = True, +) -> pd.DataFrame | None: + view_to_actual(adata) + x = _get_obs_rep(adata, layer=layer) + computed_on = layer if layer else "adata.X" + + # Check for raw counts + if check_values and not check_nonnegative_integers(x): + warnings.warn( + "`flavor='pearson_residuals'` expects raw count data, but non-integers were found.", + UserWarning, + stacklevel=3, + ) + # check theta + if theta <= 0: + # TODO: would "underdispersion" with negative theta make sense? + # then only theta=0 were undefined.. + msg = "Pearson residuals require theta > 0" + raise ValueError(msg) + # prepare clipping + + if batch_key is None: + batch_info = np.zeros(adata.shape[0], dtype=int) + else: + batch_info = adata.obs[batch_key].values + n_batches = len(np.unique(batch_info)) + + # Get pearson residuals for each batch separately + residual_gene_vars = [] + for batch in np.unique(batch_info): + adata_subset_prefilter = adata[batch_info == batch] + x_batch_prefilter = _get_obs_rep(adata_subset_prefilter, layer=layer) + + # Filter out zero genes + with settings.verbosity.override(Verbosity.error): + nonzero_genes = np.ravel(x_batch_prefilter.sum(axis=0)) != 0 + adata_subset = adata_subset_prefilter[:, nonzero_genes] + x_batch = _get_obs_rep(adata_subset, layer=layer) + + # Prepare clipping + if clip is None: + n = x_batch.shape[0] + clip = np.sqrt(n) + if clip < 0: + msg = "Pearson residuals require `clip>=0` or `clip=None`." + raise ValueError(msg) + + if isinstance(x_batch, CSBase): + x_batch = x_batch.tocsc() + x_batch.eliminate_zeros() + calculate_res = partial( + _calculate_res_sparse, + x_batch.indptr, + x_batch.indices, + x_batch.data.astype(np.float64), + ) + else: + x_batch = np.array(x_batch, dtype=np.float64, order="F") + calculate_res = partial(_calculate_res_dense, x_batch) + + sums_genes = np.array(x_batch.sum(axis=0)).ravel() + sums_cells = np.array(x_batch.sum(axis=1)).ravel() + sum_total = np.sum(sums_genes) + + residual_gene_var = calculate_res( + sums_genes=sums_genes, + sums_cells=sums_cells, + sum_total=np.float64(sum_total), + clip=np.float64(clip), + theta=np.float64(theta), + n_genes=x_batch.shape[1], + n_cells=x_batch.shape[0], + ) + + # Add 0 values for genes that were filtered out + unmasked_residual_gene_var = np.zeros(len(nonzero_genes)) + unmasked_residual_gene_var[nonzero_genes] = residual_gene_var + residual_gene_vars.append(unmasked_residual_gene_var.reshape(1, -1)) + + residual_gene_vars = np.concatenate(residual_gene_vars, axis=0) + + # Get rank per gene within each batch + # argsort twice gives ranks, small rank means most variable + ranks_residual_var = np.argsort(np.argsort(-residual_gene_vars, axis=1), axis=1) + ranks_residual_var = ranks_residual_var.astype(np.float32) + # count in how many batches a genes was among the n_top_genes + highly_variable_nbatches = np.sum( + (ranks_residual_var < n_top_genes).astype(int), axis=0 + ) + # set non-top genes within each batch to nan + ranks_residual_var[ranks_residual_var >= n_top_genes] = np.nan + ranks_masked_array = np.ma.masked_invalid(ranks_residual_var) + # Median rank across batches, ignoring batches in which gene was not selected + medianrank_residual_var = np.ma.median(ranks_masked_array, axis=0).filled(np.nan) + + means, variances = materialize_as_ndarray(_get_mean_var(x)) + df = pd.DataFrame.from_dict( + dict( + means=means, + variances=variances, + residual_variances=np.mean(residual_gene_vars, axis=0), + highly_variable_rank=medianrank_residual_var, + highly_variable_nbatches=highly_variable_nbatches.astype(np.int64), + highly_variable_intersection=highly_variable_nbatches == n_batches, + ) + ) + df = df.set_index(adata.var_names) + + # Sort genes by how often they selected as hvg within each batch and + # break ties with median rank of residual variance across batches + df.sort_values( + ["highly_variable_nbatches", "highly_variable_rank"], + ascending=[False, True], + na_position="last", + inplace=True, + ) + + high_var = np.zeros(df.shape[0], dtype=bool) + high_var[:n_top_genes] = True + df["highly_variable"] = high_var + df = df.loc[adata.var_names, :] + + if inplace: + adata.uns["hvg"] = {"flavor": "pearson_residuals", "computed_on": computed_on} + logg.hint( + "added\n" + " 'highly_variable', boolean vector (adata.var)\n" + " 'highly_variable_rank', float vector (adata.var)\n" + " 'highly_variable_nbatches', int vector (adata.var)\n" + " 'highly_variable_intersection', boolean vector (adata.var)\n" + " 'means', float vector (adata.var)\n" + " 'variances', float vector (adata.var)\n" + " 'residual_variances', float vector (adata.var)" + ) + adata.var["means"] = df["means"].values + adata.var["variances"] = df["variances"].values + adata.var["residual_variances"] = df["residual_variances"] + adata.var["highly_variable_rank"] = df["highly_variable_rank"].values + if batch_key is not None: + adata.var["highly_variable_nbatches"] = df[ + "highly_variable_nbatches" + ].values + adata.var["highly_variable_intersection"] = df[ + "highly_variable_intersection" + ].values + adata.var["highly_variable"] = df["highly_variable"].values + + if subset: + adata._inplace_subset_var(df["highly_variable"].values) + + else: + if batch_key is None: + df = df.drop( + ["highly_variable_nbatches", "highly_variable_intersection"], axis=1 + ) + if subset: + df = df.iloc[df.highly_variable.values, :] + + return df + + +@_doc_params( + adata=doc_adata, + dist_params=doc_dist_params, + genes_batch_chunk=doc_genes_batch_chunk, + check_values=doc_check_values, + layer=doc_layer, + inplace=doc_inplace, +) +def highly_variable_genes( # noqa: PLR0913 + adata: AnnData, + *, + theta: float = 100, + clip: float | None = None, + n_top_genes: int | None = None, + batch_key: str | None = None, + chunksize: int = 1000, + flavor: Literal["pearson_residuals"] = "pearson_residuals", + check_values: bool = True, + layer: str | None = None, + subset: bool = False, + inplace: bool = True, +) -> pd.DataFrame | None: + """Select highly variable genes using analytic Pearson residuals :cite:p:`Lause2021`. + + In :cite:t:`Lause2021`, Pearson residuals of a negative binomial offset model are computed + (with overdispersion `theta` shared across genes). By default, overdispersion + `theta=100` is used and residuals are clipped to `sqrt(n_obs)`. Finally, genes + are ranked by residual variance. + + Expects raw count input. + + Parameters + ---------- + {adata} + {dist_params} + {genes_batch_chunk} + flavor + Choose the flavor for identifying highly variable genes. In this experimental + version, only 'pearson_residuals' is functional. + {check_values} + {layer} + subset + If `True`, subset the data to highly-variable genes after finding them. + Otherwise merely indicate highly variable genes in `adata.var` (see below). + {inplace} + + Returns + ------- + If `inplace=True`, `adata.var` is updated with the following fields. Otherwise, + returns the same fields as :class:`~pandas.DataFrame`. + + highly_variable : :class:`bool` + boolean indicator of highly-variable genes. + means : :class:`float` + means per gene. + variances : :class:`float` + variance per gene. + residual_variances : :class:`float` + For `flavor='pearson_residuals'`, residual variance per gene. Averaged in the + case of multiple batches. + highly_variable_rank : :class:`float` + For `flavor='pearson_residuals'`, rank of the gene according to residual. + variance, median rank in the case of multiple batches. + highly_variable_nbatches : :class:`int` + If `batch_key` given, denotes in how many batches genes are detected as HVG. + highly_variable_intersection : :class:`bool` + If `batch_key` given, denotes the genes that are highly variable in all batches. + + Notes + ----- + Experimental version of `sc.pp.highly_variable_genes()` + + """ + logg.info("extracting highly variable genes") + + if not isinstance(adata, AnnData): + msg = ( + "`pp.highly_variable_genes` expects an `AnnData` argument, " + "pass `inplace=False` if you want to return a `pd.DataFrame`." + ) + raise ValueError(msg) + + if flavor == "pearson_residuals": + if n_top_genes is None: + msg = ( + "`pp.highly_variable_genes` requires the argument `n_top_genes`" + " for `flavor='pearson_residuals'`" + ) + raise ValueError(msg) + return _highly_variable_pearson_residuals( + adata, + layer=layer, + n_top_genes=n_top_genes, + batch_key=batch_key, + theta=theta, + clip=clip, + chunksize=chunksize, + subset=subset, + check_values=check_values, + inplace=inplace, + ) + else: + msg = "This is an experimental API and only `flavor=pearson_residuals` is available." + raise ValueError(msg) diff --git a/.venv/lib/python3.11/site-packages/scanpy/experimental/pp/_normalization.py b/.venv/lib/python3.11/site-packages/scanpy/experimental/pp/_normalization.py new file mode 100644 index 0000000000000000000000000000000000000000..5b73ae89c169c879da68139dc62435c352cfdb8f --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/experimental/pp/_normalization.py @@ -0,0 +1,261 @@ +from __future__ import annotations + +from types import MappingProxyType +from typing import TYPE_CHECKING +from warnings import warn + +import numpy as np +from anndata import AnnData + +from scanpy._compat import CSBase + +from ... import logging as logg +from ..._utils import ( + _doc_params, + _empty, + check_nonnegative_integers, + view_to_actual, +) +from ...experimental._docs import ( + doc_adata, + doc_check_values, + doc_copy, + doc_dist_params, + doc_inplace, + doc_layer, + doc_pca_chunk, +) +from ...get import _get_obs_rep, _set_obs_rep +from ...preprocessing._docs import doc_mask_var_hvg +from ...preprocessing._pca import _handle_mask_var, pca + +if TYPE_CHECKING: + from collections.abc import Mapping + from typing import Any + + from ..._utils import Empty + + +def _pearson_residuals( + x: CSBase | np.ndarray, /, theta, clip, check_values, *, copy: bool = False +): + x = x.copy() if copy else x + + # check theta + if theta <= 0: + # TODO: would "underdispersion" with negative theta make sense? + # then only theta=0 were undefined.. + msg = "Pearson residuals require theta > 0" + raise ValueError(msg) + # prepare clipping + if clip is None: + n = x.shape[0] + clip = np.sqrt(n) + if clip < 0: + msg = "Pearson residuals require `clip>=0` or `clip=None`." + raise ValueError(msg) + + if check_values and not check_nonnegative_integers(x): + warn( + "`normalize_pearson_residuals()` expects raw count data, but non-integers were found.", + UserWarning, + stacklevel=3, + ) + + if isinstance(x, CSBase): + sums_genes = np.sum(x, axis=0) + sums_cells = np.sum(x, axis=1) + sum_total = np.sum(sums_genes).squeeze() + else: + sums_genes = np.sum(x, axis=0, keepdims=True) + sums_cells = np.sum(x, axis=1, keepdims=True) + sum_total = np.sum(sums_genes) + + mu = np.array(sums_cells @ sums_genes / sum_total) + diff = np.array(x - mu) + residuals = diff / np.sqrt(mu + mu**2 / theta) + + # clip + residuals = np.clip(residuals, a_min=-clip, a_max=clip) + + return residuals + + +@_doc_params( + adata=doc_adata, + dist_params=doc_dist_params, + check_values=doc_check_values, + layer=doc_layer, + inplace=doc_inplace, + copy=doc_copy, +) +def normalize_pearson_residuals( + adata: AnnData, + *, + theta: float = 100, + clip: float | None = None, + check_values: bool = True, + layer: str | None = None, + inplace: bool = True, + copy: bool = False, +) -> AnnData | dict[str, np.ndarray] | None: + """Apply analytic Pearson residual normalization, based on :cite:t:`Lause2021`. + + The residuals are based on a negative binomial offset model with overdispersion + `theta` shared across genes. By default, residuals are clipped to `sqrt(n_obs)` + and overdispersion `theta=100` is used. + + Expects raw count input. + + Params + ------ + {adata} + {dist_params} + {check_values} + {layer} + {inplace} + {copy} + + Returns + ------- + If `inplace=True`, `adata.X` or the selected layer in `adata.layers` is updated + with the normalized values. `adata.uns` is updated with the following fields. + If `inplace=False`, the same fields are returned as dictionary with the + normalized values in `results_dict['X']`. + + `.uns['pearson_residuals_normalization']['theta']` + The used value of the overdisperion parameter theta. + `.uns['pearson_residuals_normalization']['clip']` + The used value of the clipping parameter. + `.uns['pearson_residuals_normalization']['computed_on']` + The name of the layer on which the residuals were computed. + + """ + if copy: + if not inplace: + msg = "`copy=True` cannot be used with `inplace=False`." + raise ValueError(msg) + adata = adata.copy() + + view_to_actual(adata) + x = _get_obs_rep(adata, layer=layer) + computed_on = layer if layer else "adata.X" + + msg = f"computing analytic Pearson residuals on {computed_on}" + start = logg.info(msg) + + residuals = _pearson_residuals(x, theta, clip, check_values, copy=not inplace) + settings_dict = dict(theta=theta, clip=clip, computed_on=computed_on) + + if inplace: + _set_obs_rep(adata, residuals, layer=layer) + adata.uns["pearson_residuals_normalization"] = settings_dict + else: + results_dict = dict(X=residuals, **settings_dict) + + logg.info(" finished ({time_passed})", time=start) + + if copy: + return adata + elif not inplace: + return results_dict + + +@_doc_params( + adata=doc_adata, + dist_params=doc_dist_params, + pca_chunk=doc_pca_chunk, + mask_var_hvg=doc_mask_var_hvg, + check_values=doc_check_values, + inplace=doc_inplace, +) +def normalize_pearson_residuals_pca( + adata: AnnData, + *, + theta: float = 100, + clip: float | None = None, + n_comps: int | None = 50, + random_state: float = 0, + kwargs_pca: Mapping[str, Any] = MappingProxyType({}), + mask_var: np.ndarray | str | None | Empty = _empty, + use_highly_variable: bool | None = None, + check_values: bool = True, + inplace: bool = True, +) -> AnnData | None: + """Apply analytic Pearson residual normalization and PCA, based on :cite:t:`Lause2021`. + + The residuals are based on a negative binomial offset model with overdispersion + `theta` shared across genes. By default, residuals are clipped to `sqrt(n_obs)`, + overdispersion `theta=100` is used, and PCA is run with 50 components. + + Operates on the subset of highly variable genes in `adata.var['highly_variable']` + by default. Expects raw count input. + + Params + ------ + {adata} + {dist_params} + {pca_chunk} + {mask_var_hvg} + {check_values} + {inplace} + + Returns + ------- + If `inplace=False`, returns the Pearson residual-based PCA results (as :class:`~anndata.AnnData` + object). If `inplace=True`, updates `adata` with the following fields: + + `.uns['pearson_residuals_normalization']['pearson_residuals_df']` + The subset of highly variable genes, normalized by Pearson residuals. + `.uns['pearson_residuals_normalization']['theta']` + The used value of the overdisperion parameter theta. + `.uns['pearson_residuals_normalization']['clip']` + The used value of the clipping parameter. + + `.obsm['X_pca']` + PCA representation of data after gene selection (if applicable) and Pearson + residual normalization. + `.varm['PCs']` + The principal components containing the loadings. When `inplace=True` and + `use_highly_variable=True`, this will contain empty rows for the genes not + selected. + `.uns['pca']['variance_ratio']` + Ratio of explained variance. + `.uns['pca']['variance']` + Explained variance, equivalent to the eigenvalues of the covariance matrix. + + """ + # Unify new mask argument and deprecated use_highly_varible argument + _, mask_var = _handle_mask_var( + adata, mask_var, use_highly_variable=use_highly_variable + ) + del use_highly_variable + + if mask_var is not None: + adata_sub = adata[:, mask_var].copy() + adata_pca = AnnData( + adata_sub.X.copy(), obs=adata_sub.obs[[]], var=adata_sub.var[[]] + ) + else: + adata_pca = AnnData(adata.X.copy(), obs=adata.obs[[]], var=adata.var[[]]) + + normalize_pearson_residuals( + adata_pca, theta=theta, clip=clip, check_values=check_values + ) + pca(adata_pca, n_comps=n_comps, random_state=random_state, **kwargs_pca) + n_comps = adata_pca.obsm["X_pca"].shape[1] # might be None + + if inplace: + norm_settings = adata_pca.uns["pearson_residuals_normalization"] + norm_dict = dict(**norm_settings, pearson_residuals_df=adata_pca.to_df()) + if mask_var is not None: + adata.varm["PCs"] = np.zeros(shape=(adata.n_vars, n_comps)) + adata.varm["PCs"][mask_var] = adata_pca.varm["PCs"] + else: + adata.varm["PCs"] = adata_pca.varm["PCs"] + adata.uns["pca"] = adata_pca.uns["pca"] + adata.uns["pearson_residuals_normalization"] = norm_dict + adata.obsm["X_pca"] = adata_pca.obsm["X_pca"] + return None + else: + return adata_pca diff --git a/.venv/lib/python3.11/site-packages/scanpy/experimental/pp/_recipes.py b/.venv/lib/python3.11/site-packages/scanpy/experimental/pp/_recipes.py new file mode 100644 index 0000000000000000000000000000000000000000..27d272fc4ddb7add6b62aeb926299c47ed6d9d80 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/experimental/pp/_recipes.py @@ -0,0 +1,151 @@ +from __future__ import annotations + +from types import MappingProxyType +from typing import TYPE_CHECKING + +import numpy as np + +from scanpy import experimental +from scanpy._utils import _doc_params +from scanpy.experimental._docs import ( + doc_adata, + doc_check_values, + doc_dist_params, + doc_genes_batch_chunk, + doc_inplace, + doc_pca_chunk, +) +from scanpy.preprocessing import pca + +if TYPE_CHECKING: + from collections.abc import Mapping + from typing import Any + + import pandas as pd + from anndata import AnnData + + +@_doc_params( + adata=doc_adata, + dist_params=doc_dist_params, + genes_batch_chunk=doc_genes_batch_chunk, + pca_chunk=doc_pca_chunk, + check_values=doc_check_values, + inplace=doc_inplace, +) +def recipe_pearson_residuals( # noqa: PLR0913 + adata: AnnData, + *, + theta: float = 100, + clip: float | None = None, + n_top_genes: int = 1000, + batch_key: str | None = None, + chunksize: int = 1000, + n_comps: int | None = 50, + random_state: float | None = 0, + kwargs_pca: Mapping[str, Any] = MappingProxyType({}), + check_values: bool = True, + inplace: bool = True, +) -> tuple[AnnData, pd.DataFrame] | None: + """Full pipeline for HVG selection and normalization by analytic Pearson residuals :cite:p:`Lause2021`. + + Applies gene selection based on Pearson residuals. On the resulting subset, + Pearson residual normalization and PCA are performed. + + Expects raw count input. + + Params + ------ + {adata} + {dist_params} + {genes_batch_chunk} + {pca_chunk} + {check_values} + {inplace} + + Returns + ------- + If `inplace=False`, separately returns the gene selection results (as + :class:`~pandas.DataFrame`) and Pearson residual-based PCA results (as + :class:`~anndata.AnnData`). If `inplace=True`, updates `adata` with the + following fields for gene selection results: + + `.var['highly_variable']` : bool + boolean indicator of highly-variable genes. + `.var['means']` : float + means per gene. + `.var['variances']` : float + variances per gene. + `.var['residual_variances']` : float + Pearson residual variance per gene. Averaged in the case of multiple + batches. + `.var['highly_variable_rank']` : float + Rank of the gene according to residual variance, median rank in the + case of multiple batches. + `.var['highly_variable_nbatches']` : int + If batch_key is given, this denotes in how many batches genes are + detected as HVG. + `.var['highly_variable_intersection']` : bool + If batch_key is given, this denotes the genes that are highly variable + in all batches. + + The following fields contain Pearson residual-based PCA results and + normalization settings: + + `.uns['pearson_residuals_normalization']['pearson_residuals_df']` + The subset of highly variable genes, normalized by Pearson residuals. + `.uns['pearson_residuals_normalization']['theta']` + The used value of the overdisperion parameter theta. + `.uns['pearson_residuals_normalization']['clip']` + The used value of the clipping parameter. + + `.obsm['X_pca']` + PCA representation of data after gene selection and Pearson residual + normalization. + `.varm['PCs']` + The principal components containing the loadings. When `inplace=True` this + will contain empty rows for the genes not selected during HVG selection. + `.uns['pca']['variance_ratio']` + Ratio of explained variance. + `.uns['pca']['variance']` + Explained variance, equivalent to the eigenvalues of the covariance matrix. + + """ + hvg_args = dict( + flavor="pearson_residuals", + n_top_genes=n_top_genes, + batch_key=batch_key, + theta=theta, + clip=clip, + chunksize=chunksize, + check_values=check_values, + ) + + if inplace: + experimental.pp.highly_variable_genes(adata, **hvg_args, inplace=True) + # TODO: are these copies needed? + adata_pca = adata[:, adata.var["highly_variable"]].copy() + else: + hvg = experimental.pp.highly_variable_genes(adata, **hvg_args, inplace=False) + # TODO: are these copies needed? + adata_pca = adata[:, hvg["highly_variable"]].copy() + + experimental.pp.normalize_pearson_residuals( + adata_pca, theta=theta, clip=clip, check_values=check_values + ) + pca(adata_pca, n_comps=n_comps, random_state=random_state, **kwargs_pca) + + if inplace: + normalization_param = adata_pca.uns["pearson_residuals_normalization"] + normalization_dict = dict( + **normalization_param, pearson_residuals_df=adata_pca.to_df() + ) + + adata.uns["pca"] = adata_pca.uns["pca"] + adata.varm["PCs"] = np.zeros(shape=(adata.n_vars, n_comps)) + adata.varm["PCs"][adata.var["highly_variable"]] = adata_pca.varm["PCs"] + adata.uns["pearson_residuals_normalization"] = normalization_dict + adata.obsm["X_pca"] = adata_pca.obsm["X_pca"] + return None + else: + return adata_pca, hvg diff --git a/.venv/lib/python3.11/site-packages/scanpy/external/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/external/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2087fc2671d2b93996e44992f5aeddfb498fd3a0 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/external/__init__.py @@ -0,0 +1,13 @@ +"""External preprocessing and analysis tools and their plotting.""" + +from __future__ import annotations + +import sys + +from .. import _utils +from . import exporting, pl, pp, tl + +_utils.annotate_doc_types(sys.modules[__name__], "scanpy") +del sys, _utils + +__all__ = ["exporting", "pl", "pp", "tl"] diff --git a/.venv/lib/python3.11/site-packages/scanpy/external/exporting.py b/.venv/lib/python3.11/site-packages/scanpy/external/exporting.py new file mode 100644 index 0000000000000000000000000000000000000000..21a3589aace500ea02205cc3bd2a5c43579f3abc --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/external/exporting.py @@ -0,0 +1,606 @@ +"""Exporting to formats for other software.""" + +from __future__ import annotations + +import json +import logging as logg +from pathlib import Path +from typing import TYPE_CHECKING + +import h5py +import matplotlib.pyplot as plt +import numpy as np +import scipy.sparse +from pandas.api.types import CategoricalDtype + +from .._compat import old_positionals +from .._utils import NeighborsView +from ..preprocessing._utils import _get_mean_var + +if TYPE_CHECKING: + from collections.abc import Iterable, Mapping + + from anndata import AnnData + +__all__ = ["cellbrowser", "spring_project"] + + +@old_positionals( + "subplot_name", + "cell_groupings", + "custom_color_tracks", + "total_counts_key", + "neighbors_key", + "overwrite", +) +def spring_project( # noqa: PLR0912, PLR0915 + adata: AnnData, + project_dir: Path | str, + embedding_method: str, + *, + subplot_name: str | None = None, + cell_groupings: str | Iterable[str] | None = None, + custom_color_tracks: str | Iterable[str] | None = None, + total_counts_key: str = "n_counts", + neighbors_key: str | None = None, + overwrite: bool = False, +) -> None: + """Export to a SPRING project directory :cite:p:`Weinreb2017`. + + Visualize annotation present in `adata`. By default, export all gene expression data + from `adata.raw` and categorical and continuous annotations present in `adata.obs`. + + See `SPRING `__ or :cite:t:`Weinreb2017` for details. + + Parameters + ---------- + adata + Annotated data matrix: `adata.uns['neighbors']` needs to + be present. + project_dir + Path to directory for exported SPRING files. + embedding_method + Name of a 2-D embedding in `adata.obsm` + subplot_name + Name of subplot folder to be created at `project_dir+"/"+subplot_name` + cell_groupings + Instead of importing all categorical annotations when `None`, + pass a list of keys for `adata.obs`. + custom_color_tracks + Specify specific `adata.obs` keys for continuous coloring. + total_counts_key + Name of key for total transcript counts in `adata.obs`. + overwrite + When `True`, existing counts matrices in `project_dir` are overwritten. + + Examples + -------- + See this `tutorial `__. + + """ + # need to get nearest neighbors first + if neighbors_key is None: + neighbors_key = "neighbors" + + if neighbors_key not in adata.uns: + msg = "Run `sc.pp.neighbors` first." + raise ValueError(msg) + + # check that requested 2-D embedding has been generated + if embedding_method not in adata.obsm: + if f"X_{embedding_method}" in adata.obsm: + embedding_method = f"X_{embedding_method}" + elif embedding_method in adata.uns: + embedding_method = f"X_{embedding_method}_{adata.uns[embedding_method]['params']['layout']}" + else: + msg = f"Run the specified embedding method `{embedding_method}` first." + raise ValueError(msg) + + coords = adata.obsm[embedding_method] + + # Make project directory and subplot directory (subplot has same name as project) + # For now, the subplot is just all cells in adata + project_dir = Path(project_dir) + subplot_dir = ( + project_dir.parent if subplot_name is None else project_dir / subplot_name + ) + subplot_dir.mkdir(parents=True, exist_ok=True) + print(f"Writing subplot to {subplot_dir}") + + # Write counts matrices as hdf5 files and npz if they do not already exist + # or if user requires overwrite. + # To do: check if Alex's h5sparse format will allow fast loading from just + # one file. + write_counts_matrices = True + base_dir_filelist = [ + "counts_norm_sparse_genes.hdf5", + "counts_norm_sparse_cells.hdf5", + "counts_norm.npz", + "total_counts.txt", + "genes.txt", + ] + if all((project_dir / f).is_file() for f in base_dir_filelist): + if not overwrite: + logg.warning( + f"{project_dir} is an existing SPRING folder. A new subplot will be created, but " + "you must set `overwrite=True` to overwrite counts matrices." + ) + write_counts_matrices = False + else: + logg.warning(f"Overwriting the files in {project_dir}.") + + # Ideally, all genes will be written from adata.raw + if adata.raw is not None: + x = adata.raw.X.tocsc() + gene_list = list(adata.raw.var_names) + else: + x = adata.X.tocsc() + gene_list = list(adata.var_names) + + # Keep track of total counts per cell if present + if total_counts_key in adata.obs: + total_counts = np.array(adata.obs[total_counts_key]) + else: + total_counts = x.sum(1).A1 + + # Write the counts matrices to project directory + if write_counts_matrices: + write_hdf5_genes(x, gene_list, project_dir / "counts_norm_sparse_genes.hdf5") + write_hdf5_cells(x, project_dir / "counts_norm_sparse_cells.hdf5") + write_sparse_npz(x, project_dir / "counts_norm.npz") + with (project_dir / "genes.txt").open("w") as o: + for g in gene_list: + o.write(f"{g}\n") + np.savetxt(project_dir / "total_counts.txt", total_counts) + + # Get categorical and continuous metadata + categorical_extras = {} + continuous_extras = {} + if cell_groupings is None: + for obs_name in adata.obs: + if isinstance(adata.obs[obs_name].dtype, CategoricalDtype): + categorical_extras[obs_name] = [str(x) for x in adata.obs[obs_name]] + else: + if isinstance(cell_groupings, str): + cell_groupings = [cell_groupings] + for obs_name in cell_groupings: + if obs_name not in adata.obs: + logg.warning(f"Cell grouping {obs_name!r} is not in adata.obs") + elif isinstance(adata.obs[obs_name].dtype, CategoricalDtype): + categorical_extras[obs_name] = [str(x) for x in adata.obs[obs_name]] + else: + logg.warning( + f"Cell grouping {obs_name!r} is not a categorical variable" + ) + if custom_color_tracks is None: + for obs_name in adata.obs: + if not isinstance(adata.obs[obs_name].dtype, CategoricalDtype): + continuous_extras[obs_name] = np.array(adata.obs[obs_name]) + else: + if isinstance(custom_color_tracks, str): + custom_color_tracks = [custom_color_tracks] + for obs_name in custom_color_tracks: + if obs_name not in adata.obs: + logg.warning(f"Custom color track {obs_name!r} is not in adata.obs") + elif not isinstance(adata.obs[obs_name].dtype, CategoricalDtype): + continuous_extras[obs_name] = np.array(adata.obs[obs_name]) + else: + logg.warning( + f"Custom color track {obs_name!r} is not a continuous variable" + ) + + # Write continuous colors + continuous_extras["Uniform"] = np.zeros(x.shape[0]) + _write_color_tracks(continuous_extras, subplot_dir / "color_data_gene_sets.csv") + + # Create and write a dictionary of color profiles to be used by the visualizer + color_stats = {} + color_stats = _get_color_stats_genes(color_stats, x, gene_list) + color_stats = _get_color_stats_custom(color_stats, continuous_extras) + _write_color_stats(subplot_dir / "color_stats.json", color_stats) + + # Write categorical data + categorical_coloring_data = {} + categorical_coloring_data = _build_categ_colors( + categorical_coloring_data, categorical_extras + ) + _write_cell_groupings( + subplot_dir / "categorical_coloring_data.json", categorical_coloring_data + ) + + # Write graph in two formats for backwards compatibility + edges = _get_edges(adata, neighbors_key) + _write_graph(subplot_dir / "graph_data.json", x.shape[0], edges) + _write_edges(subplot_dir / "edges.csv", edges) + + # Write cell filter; for now, subplots must be generated from within SPRING, + # so cell filter includes all cells. + np.savetxt(subplot_dir / "cell_filter.txt", np.arange(x.shape[0]), fmt="%i") + np.save(subplot_dir / "cell_filter.npy", np.arange(x.shape[0])) + + # Write 2-D coordinates, after adjusting to roughly match SPRING's default d3js force layout parameters + coords = coords - coords.min(0)[None, :] + coords = ( + coords * (np.array([1000, 1000]) / coords.ptp(0))[None, :] + + np.array([200, -200])[None, :] + ) + np.savetxt( + subplot_dir / "coordinates.txt", + np.hstack((np.arange(x.shape[0])[:, None], coords)), + fmt="%i,%.6f,%.6f", + ) + + # Write some useful intermediates, if they exist + if "X_pca" in adata.obsm: + np.savez_compressed( + subplot_dir / "intermediates.npz", + Epca=adata.obsm["X_pca"], + total_counts=total_counts, + ) + + # Write PAGA data, if present + if "paga" in adata.uns: + clusts = np.array(adata.obs[adata.uns["paga"]["groups"]].cat.codes) + uniq_clusts = adata.obs[adata.uns["paga"]["groups"]].cat.categories + paga_coords = [coords[clusts == i, :].mean(0) for i in range(len(uniq_clusts))] + _export_paga_to_spring(adata, paga_coords, subplot_dir / "PAGA_data.json") + + +# -------------------------------------------------------------------------------- +# Helper Functions +# -------------------------------------------------------------------------------- + + +def _get_edges(adata, neighbors_key=None): + neighbors = NeighborsView(adata, neighbors_key) + if "distances" in neighbors: # these are sparse matrices + matrix = neighbors["distances"] + else: + matrix = neighbors["connectivities"] + matrix = matrix.tocoo() + edges = [(i, j) for i, j in zip(matrix.row, matrix.col, strict=True)] + + return edges + + +def write_hdf5_genes(x, /, gene_list, filename): + """SPRING standard: `filename = main_spring_dir / "counts_norm_sparse_genes.hdf5"`.""" + x = x.tocsc() + + hf = h5py.File(filename, "w") + counts_group = hf.create_group("counts") + cix_group = hf.create_group("cell_ix") + + hf.attrs["ncells"] = x.shape[0] + hf.attrs["ngenes"] = x.shape[1] + + for g, gene in enumerate(gene_list): + counts = x[:, g].toarray().squeeze() + cell_ix = np.nonzero(counts)[0] + counts = counts[cell_ix] + counts_group.create_dataset(gene, data=counts) + cix_group.create_dataset(gene, data=cell_ix) + + hf.close() + + +def write_hdf5_cells(x, /, filename): + """SPRING standard: `filename = main_spring_dir / "counts_norm_sparse_cells.hdf5"`.""" + x = x.tocsr() + + hf = h5py.File(filename, "w") + counts_group = hf.create_group("counts") + gix_group = hf.create_group("gene_ix") + + hf.attrs["ncells"] = x.shape[0] + hf.attrs["ngenes"] = x.shape[1] + + for c in range(x.shape[0]): + counts = x[c, :].toarray().squeeze() + gene_ix = np.nonzero(counts)[0] + counts = counts[gene_ix] + counts_group.create_dataset(str(c), data=counts) + gix_group.create_dataset(str(c), data=gene_ix) + + hf.close() + + +def write_sparse_npz(x, /, filename, *, compressed: bool = False): + """SPRING standard: filename = f"{main_spring_dir}/counts_norm.npz".""" + x = x.tocsc() + scipy.sparse.save_npz(filename, x, compressed=compressed) + + +def _write_graph(filename, n_nodes, edges): + nodes = [{"name": int(i), "number": int(i)} for i in range(n_nodes)] + edges = [{"source": int(i), "target": int(j), "distance": 0} for i, j in edges] + out = {"nodes": nodes, "links": edges} + Path(filename).write_text(json.dumps(out, indent=4, separators=(",", ": "))) + + +def _write_edges(filename, edges): + with Path(filename).open("w") as f: + for e in edges: + f.write(f"{e[0]};{e[1]}\n") + + +def _write_color_tracks(ctracks, fname): + out = [] + for name, score in ctracks.items(): + line = ",".join([name, *(f"{x:.3f}" for x in score)]) + out += [line] + out = sorted(out, key=lambda x: x.split(",")[0]) + Path(fname).write_text("\n".join(out)) + + +def _frac_to_hex(frac): + r, g, b = tuple(np.array(np.array(plt.cm.jet(frac)[:3]) * 255, dtype=int)) + return f"#{r:02x}{g:02x}{b:02x}" + + +def _get_color_stats_genes(color_stats, x, gene_list): + means, variances = _get_mean_var(x) + stdevs = np.zeros(variances.shape, dtype=float) + stdevs[variances > 0] = np.sqrt(variances[variances > 0]) + mins = x.min(0).todense().A1 + maxes = x.max(0).todense().A1 + + pctl = 99.6 + pctl_n = (100 - pctl) / 100.0 * x.shape[0] + pctls = np.zeros(x.shape[1], dtype=float) + for g in range(x.shape[1]): + n_nonzero = x.indptr[g + 1] - x.indptr[g] + if n_nonzero > pctl_n: + pctls[g] = np.percentile( + x.data[x.indptr[g] : x.indptr[g + 1]], 100 - 100 * pctl_n / n_nonzero + ) + else: + pctls[g] = 0 + color_stats[gene_list[g]] = tuple( + map(float, (means[g], stdevs[g], mins[g], maxes[g], pctls[g])) + ) + return color_stats + + +def _get_color_stats_custom(color_stats, custom_colors): + for k, v in custom_colors.items(): + color_stats[k] = tuple( + map( + float, + (np.mean(v), np.std(v), np.min(v), np.max(v), np.percentile(v, 99)), + ) + ) + return color_stats + + +def _write_color_stats(filename, color_stats): + Path(filename).write_text(json.dumps(color_stats, indent=4, sort_keys=True)) + + +def _build_categ_colors(categorical_coloring_data, cell_groupings): + for k, labels in cell_groupings.items(): + label_colors = { + l: _frac_to_hex(float(i) / len(set(labels))) + for i, l in enumerate(list(set(labels))) + } + categorical_coloring_data[k] = { + "label_colors": label_colors, + "label_list": labels, + } + return categorical_coloring_data + + +def _write_cell_groupings(filename, categorical_coloring_data): + Path(filename).write_text( + json.dumps(categorical_coloring_data, indent=4, sort_keys=True) + ) + + +def _export_paga_to_spring(adata, paga_coords, outpath) -> None: + # retrieve node data + group_key = adata.uns["paga"]["groups"] + names = adata.obs[group_key].cat.categories + coords = [list(xy) for xy in paga_coords] + + sizes = list(adata.uns[f"{group_key}_sizes"]) + clus_labels = adata.obs[group_key].cat.codes.values + cell_groups = [ + [int(j) for j in np.nonzero(clus_labels == i)[0]] for i in range(len(names)) + ] + + if f"{group_key}_colors" in adata.uns: + colors = list(adata.uns[f"{group_key}_colors"]) + else: + import scanpy.plotting.utils + + scanpy.plotting.utils.add_colors_for_categorical_sample_annotation( + adata, group_key + ) + colors = list(adata.uns[f"{group_key}_colors"]) + + # retrieve edge level data + sources, targets = adata.uns["paga"]["connectivities"].nonzero() + weights = np.sqrt(adata.uns["paga"]["connectivities"].data) / 3 + + # save a threshold weight for showing edges so that by default, + # the number of edges shown is 8X the number of nodes + if len(names) * 8 > len(weights): + min_edge_weight_view = 0 + else: + min_edge_weight_view = sorted(weights)[-len(names) * 8] + + # save another threshold for even saving edges at all, with 100 edges per node + if len(weights) < 100 * len(names): + min_edge_weight_save = 0 + else: + min_edge_weight_save = sorted(weights)[-len(names) * 100] + + # make node list + nodes = [] + for i, name, xy, color, size, cells in zip( + range(len(names)), names, coords, colors, sizes, cell_groups, strict=True + ): + nodes.append({ + "index": i, + "size": int(size), + "color": color, + "coordinates": xy, + "cells": cells, + "name": name, + }) + + # make link list, avoid redundant encoding (graph is undirected) + links = [] + for source, target, weight in zip(sources, targets, weights, strict=True): + if source < target and weight > min_edge_weight_save: + links.append({ + "source": int(source), + "target": int(target), + "weight": float(weight), + }) + + # save data about edge weights + edge_weight_meta = { + "min_edge_weight": min_edge_weight_view, + "max_edge_weight": np.max(weights), + } + + paga_data = {"nodes": nodes, "links": links, "edge_weight_meta": edge_weight_meta} + + import json + + Path(outpath).write_text(json.dumps(paga_data, indent=4)) + + +@old_positionals( + "embedding_keys", + "annot_keys", + "cluster_field", + "nb_marker", + "skip_matrix", + "html_dir", + "port", + "do_debug", +) +def cellbrowser( # noqa: PLR0913 + adata: AnnData, + data_dir: Path | str, + data_name: str, + *, + embedding_keys: Iterable[str] | Mapping[str, str] | str | None = None, + annot_keys: Iterable[str] | Mapping[str, str] | None = ( + "louvain", + "percent_mito", + "n_genes", + "n_counts", + ), + cluster_field: str = "louvain", + nb_marker: int = 50, + skip_matrix: bool = False, + html_dir: Path | str | None = None, + port: int | None = None, + do_debug: bool = False, +): + """Export adata to a UCSC Cell Browser project directory. + + If `html_dir` is set, subsequently build the html files from the project directory into `html_dir`. + If `port` is set, start an HTTP server in the background and serve `html_dir` on `port`. + + By default, export all gene expression data from `adata.raw`, the + annotations `louvain`, `percent_mito`, `n_genes` and `n_counts` and the top + `nb_marker` cluster markers. All existing files in data_dir are + overwritten, except `cellbrowser.conf`. + + See `UCSC Cellbrowser `__ for + details. + + Parameters + ---------- + adata + Annotated data matrix + data_dir + Path to directory for exported Cell Browser files. + Usually these are the files `exprMatrix.tsv.gz`, `meta.tsv`, + coordinate files like `tsne.coords.tsv`, + and cluster marker gene lists like `markers.tsv`. + A file `cellbrowser.conf` is also created with pointers to these files. + As a result, each adata object should have its own project_dir. + data_name + Name of dataset in Cell Browser, a string without special characters. + This is written to `data_dir/cellbrowser.conf`. + Ideally this is a short unique name for the dataset, + like `"pbmc3k"` or `"tabulamuris"`. + embedding_keys + 2-D embeddings in `adata.obsm` to export. + The prefix `X_` or `X_draw_graph_` is not necessary. + Coordinates missing from `adata` are skipped. + By default (or when specifying `'all'` or `None`), these keys are tried: + [`"tsne"`, `"umap"`, `"pagaFa"`, `"pagaFr"`, `"pagaUmap"`, `"phate"`, + `"fa"`, `"fr"`, `"kk"`, `"drl"`, `"rt"`, `"trimap"`]. + For these, default display labels are automatically used. + For other values, you can specify a mapping from coordinate name to + display label, e.g. `{"tsne": "t-SNE by Scanpy"}`. + annot_keys + Annotations in `adata.obsm` to export. + Can be a mapping from annotation column name to display label. + Specify `None` for all available columns in `.obs`. + skip_matrix + Do not export the matrix. + If you had previously exported this adata into the same `data_dir`, + then there is no need to export the whole matrix again. + This option will make the export a lot faster, + e.g. when only coordinates or meta data were changed. + html_dir + If this variable is set, the export will build html + files from `data_dir` to `html_dir`, creating html/js/json files. + Usually there is one global html output directory for all datasets. + Often, `html_dir` is located under a webserver's (like Apache) + htdocs directory or is copied to one. + A directory `html_dir`/`project_name` will be created and + an index.html will be created under `html_dir` for all subdirectories. + Existing files will be overwritten. + If do not to use html_dir, + you can use the command line tool `cbBuild` to build the html directory. + port + If this variable and `html_dir` are set, + Python's built-in web server will be spawned as a daemon in the + background and serve the files under `html_dir`. + To kill the process, call `cellbrowser.cellbrowser.stop()`. + do_debug + Activate debugging output + + Examples + -------- + See this + `tutorial `__. + + """ + try: + import cellbrowser.cellbrowser as cb + except ImportError: + logg.error( + "The package cellbrowser is not installed. " + "Install with 'pip install cellbrowser' and retry." + ) + raise + + data_dir = str(data_dir) + + cb.setDebug(do_debug) + cb.scanpyToCellbrowser( + adata, + data_dir, + data_name, + coordFields=embedding_keys, + metaFields=annot_keys, + clusterField=cluster_field, + nb_marker=nb_marker, + skipMatrix=skip_matrix, + doDebug=None, + ) + + if html_dir is not None: + html_dir = str(html_dir) + cb.build(data_dir, html_dir, doDebug=None) + if port is not None: + cb.serve(html_dir, port) diff --git a/.venv/lib/python3.11/site-packages/scanpy/external/pl.py b/.venv/lib/python3.11/site-packages/scanpy/external/pl.py new file mode 100644 index 0000000000000000000000000000000000000000..2aa3a9584a2ae3ae249ed30787cc6e5cabc0896e --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/external/pl.py @@ -0,0 +1,362 @@ +"""Plotting functions for external tools.""" + +from __future__ import annotations + +import contextlib +from typing import TYPE_CHECKING + +import matplotlib.pyplot as plt +import numpy as np +from anndata import AnnData # noqa: TC002 +from matplotlib.axes import Axes # noqa: TC002 +from sklearn.utils import deprecated + +from .._compat import old_positionals +from .._utils import _doc_params +from .._utils._doctests import doctest_needs +from ..plotting import _scrublet, _utils, embedding +from ..plotting._docs import ( + doc_adata_color_etc, + doc_edges_arrows, + doc_scatter_embedding, + doc_show_save_ax, +) +from ..plotting._tools.scatterplots import _wraps_plot_scatter +from .tl._wishbone import _anndata_to_wishbone + +if TYPE_CHECKING: + from collections.abc import Collection + from typing import Any + + +__all__ = [ + "harmony_timeseries", + "phate", + "sam", + "trimap", + "wishbone_marker_trajectory", +] + + +@doctest_needs("phate") +@_wraps_plot_scatter +@_doc_params( + adata_color_etc=doc_adata_color_etc, + edges_arrows=doc_edges_arrows, + scatter_bulk=doc_scatter_embedding, + show_save_ax=doc_show_save_ax, +) +def phate(adata: AnnData, **kwargs) -> list[Axes] | None: + """Scatter plot in PHATE basis. + + Parameters + ---------- + {adata_color_etc} + {edges_arrows} + {scatter_bulk} + {show_save_ax} + + Returns + ------- + If `show==False`, a list of :class:`~matplotlib.axes.Axes` objects. + Every second element corresponds to the 'right margin' + drawing area for color bars and legends. + + Examples + -------- + >>> from anndata import AnnData + >>> import scanpy.external as sce + >>> import phate + >>> data, branches = phate.tree.gen_dla( + ... n_dim=100, + ... n_branch=20, + ... branch_length=100, + ... ) + >>> data.shape + (2000, 100) + >>> adata = AnnData(data) + >>> adata.obs["branches"] = branches + >>> sce.tl.phate(adata, k=5, a=20, t=150) + >>> adata.obsm["X_phate"].shape + (2000, 2) + >>> sce.pl.phate( + ... adata, + ... color="branches", + ... color_map="tab20", + ... ) + + """ + return embedding(adata, "phate", **kwargs) + + +@_wraps_plot_scatter +@_doc_params( + adata_color_etc=doc_adata_color_etc, + edges_arrows=doc_edges_arrows, + scatter_bulk=doc_scatter_embedding, + show_save_ax=doc_show_save_ax, +) +def trimap(adata: AnnData, **kwargs) -> Axes | list[Axes] | None: + """Scatter plot in TriMap basis. + + Parameters + ---------- + {adata_color_etc} + {edges_arrows} + {scatter_bulk} + {show_save_ax} + + Returns + ------- + If `show==False` a :class:`~matplotlib.axes.Axes` or a list of it. + + """ + return embedding(adata, "trimap", **kwargs) + + +@_wraps_plot_scatter +@_doc_params( + adata_color_etc=doc_adata_color_etc, + edges_arrows=doc_edges_arrows, + scatter_bulk=doc_scatter_embedding, + show_save_ax=doc_show_save_ax, +) +def harmony_timeseries( + adata: AnnData, *, show: bool = True, return_fig: bool = False, **kwargs +) -> Axes | list[Axes] | None: + """Scatter plot in Harmony force-directed layout basis. + + Parameters + ---------- + {adata_color_etc} + {edges_arrows} + {scatter_bulk} + {show_save_ax} + + Returns + ------- + If `return_fig` is True, a :class:`~matplotlib.figure.Figure`. + If `show==False` a :class:`~matplotlib.axes.Axes` or a list of it. + + """ + tp_name = adata.uns["harmony_timepoint_var"] + tps = adata.obs[tp_name].unique() + + fig, axes = plt.subplots(1, len(tps)) + for i, tp in enumerate(tps): + p = embedding( + adata, + "harmony", + color=tp_name, + groups=tp, + title=tp, + show=False, + ax=axes[i], + legend_loc="none", + ) + p.set_axis_off() + if return_fig: + return fig + if show: + return None + return axes + + +@old_positionals("c", "cmap", "linewidth", "edgecolor", "axes", "colorbar", "s") +def sam( + adata: AnnData, + projection: str | np.ndarray = "X_umap", + *, + c: str | np.ndarray | None = None, + cmap: str = "Spectral_r", + linewidth: float = 0.0, + edgecolor: str = "k", + axes: Axes | None = None, + colorbar: bool = True, + s: float = 10.0, + **kwargs: Any, +) -> Axes: + """Scatter plot using the SAM projection or another input projection. + + Parameters + ---------- + projection + A case-sensitive string indicating the projection to display (a key + in adata.obsm) or a 2D numpy array with cell coordinates. If None, + projection defaults to UMAP. + c + Cell color values overlaid on the projection. Can be a string from adata.obs + to overlay cluster assignments / annotations or a 1D numpy array. + axes + Plot output to the specified, existing axes. If None, create new + figure window. + kwargs + all keyword arguments in matplotlib.pyplot.scatter are eligible. + + """ + if isinstance(projection, str): + try: + dt = adata.obsm[projection] + except KeyError as e: + msg = "Please create a projection first using run_umap or run_tsne" + raise ValueError(msg) from e + else: + dt = projection + + if axes is None: + plt.figure() + axes = plt.gca() + + if c is None: + axes.scatter( + dt[:, 0], dt[:, 1], s=s, linewidth=linewidth, edgecolor=edgecolor, **kwargs + ) + return axes + + if isinstance(c, str): + with contextlib.suppress(KeyError): + c = np.array(list(adata.obs[c])) + + if isinstance(c[0], str | np.str_) and isinstance(c, np.ndarray | list): + import samalg.utilities as ut + + i = ut.convert_annotations(c) + ui, ai = np.unique(i, return_index=True) + cax = axes.scatter( + dt[:, 0], + dt[:, 1], + c=i, + cmap=cmap, + s=s, + linewidth=linewidth, + edgecolor=edgecolor, + **kwargs, + ) + + if colorbar: + cbar = plt.colorbar(cax, ax=axes, ticks=ui) + cbar.ax.set_yticklabels(c[ai]) + else: + if not isinstance(c, np.ndarray | list): + colorbar = False + i = c + + cax = axes.scatter( + dt[:, 0], + dt[:, 1], + c=i, + cmap=cmap, + s=s, + linewidth=linewidth, + edgecolor=edgecolor, + **kwargs, + ) + + if colorbar: + plt.colorbar(cax, ax=axes) + return axes + + +@old_positionals( + "no_bins", + "smoothing_factor", + "min_delta", + "show_variance", + "figsize", + "return_fig", + "show", + "save", + "ax", +) +@_doc_params(show_save_ax=doc_show_save_ax) +def wishbone_marker_trajectory( # noqa: PLR0913 + adata: AnnData, + markers: Collection[str], + *, + no_bins: int = 150, + smoothing_factor: int = 1, + min_delta: float = 0.1, + show_variance: bool = False, + figsize: tuple[float, float] | None = None, + return_fig: bool = False, + show: bool = True, + save: str | bool | None = None, + ax: Axes | None = None, +): + """Plot marker trends along trajectory, and return trajectory branches. + + Intended for further analysis and visualization (heatmap, etc.). + + Parameters + ---------- + adata + Annotated data matrix. + markers + Iterable of markers/genes to be plotted. + show_variance + Logical indicating if the trends should be accompanied with variance. + no_bins + Number of bins for calculating marker density. + smoothing_factor + Parameter controlling the degree of smoothing. + min_delta + Minimum difference in marker expression after normalization to show + separate trends for the two branches. + figsize + width, height + return_fig + Return the matplotlib figure. + {show_save_ax} + + Returns + ------- + Updates `adata` with the following fields: + + `trunk_wishbone` : :class:`pandas.DataFrame` (`adata.uns`) + Computed values before branching + `branch1_wishbone` : :class:`pandas.DataFrame` (`adata.uns`) + Computed values for the first branch + `branch2_wishbone` : :class:`pandas.DataFrame` (`adata.uns`) + Computed values for the second branch. + + """ + wb = _anndata_to_wishbone(adata) + + if figsize is None: + width = 2 * len(markers) + height = 0.75 * len(markers) + else: + width, height = figsize + + if ax: + fig = ax.figure + else: + fig = plt.figure(figsize=(width, height)) + ax = plt.gca() + + ret_values, fig, ax = wb.plot_marker_trajectory( + markers=markers, + show_variance=show_variance, + no_bins=no_bins, + smoothing_factor=smoothing_factor, + min_delta=min_delta, + fig=fig, + ax=ax, + ) + + adata.uns["trunk_wishbone"] = ret_values["Trunk"] + adata.uns["branch1_wishbone"] = ret_values["Branch1"] + adata.uns["branch2_wishbone"] = ret_values["Branch2"] + + _utils.savefig_or_show("wishbone_trajectory", show=show, save=save) + + if return_fig: + return fig + if show: + return None + return ax + + +scrublet_score_distribution = deprecated("Import from sc.pl instead")( + _scrublet.scrublet_score_distribution +) diff --git a/.venv/lib/python3.11/site-packages/scanpy/external/pp/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/external/pp/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..71b1dae74dbc2695cf183e981e87cc750eeb1632 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/external/pp/__init__.py @@ -0,0 +1,29 @@ +"""External preprocessing functions.""" + +from __future__ import annotations + +from sklearn.utils import deprecated + +from ...preprocessing import _scrublet +from ._bbknn import bbknn +from ._dca import dca +from ._harmony_integrate import harmony_integrate +from ._hashsolo import hashsolo +from ._magic import magic +from ._mnn_correct import mnn_correct +from ._scanorama_integrate import scanorama_integrate + +scrublet = deprecated("Import from sc.pp instead")(_scrublet.scrublet) +scrublet_simulate_doublets = deprecated("Import from sc.pp instead")( + _scrublet.scrublet_simulate_doublets +) + +__all__ = [ + "bbknn", + "dca", + "harmony_integrate", + "hashsolo", + "magic", + "mnn_correct", + "scanorama_integrate", +] diff --git a/.venv/lib/python3.11/site-packages/scanpy/external/pp/_bbknn.py b/.venv/lib/python3.11/site-packages/scanpy/external/pp/_bbknn.py new file mode 100644 index 0000000000000000000000000000000000000000..feda84542a0750d014a0430693ce42868b127bd4 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/external/pp/_bbknn.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ..._compat import old_positionals +from ..._utils._doctests import doctest_needs + +if TYPE_CHECKING: + from collections.abc import Callable + + from anndata import AnnData + from sklearn.metrics import DistanceMetric + + +@old_positionals("batch_key", "use_rep", "approx", "use_annoy", "metric", "copy") +@doctest_needs("bbknn") +def bbknn( # noqa: PLR0913 + adata: AnnData, + *, + batch_key: str = "batch", + use_rep: str = "X_pca", + approx: bool = True, + use_annoy: bool = True, + metric: str | Callable | DistanceMetric = "euclidean", + copy: bool = False, + neighbors_within_batch: int = 3, + n_pcs: int = 50, + trim: int | None = None, + annoy_n_trees: int = 10, + pynndescent_n_neighbors: int = 30, + pynndescent_random_state: int = 0, + use_faiss: bool = True, + set_op_mix_ratio: float = 1.0, + local_connectivity: int = 1, + **kwargs, +) -> AnnData | None: + """Batch balanced kNN :cite:p:`Polanski2019`. + + Batch balanced kNN alters the kNN procedure to identify each cell's top neighbours in + each batch separately instead of the entire cell pool with no accounting for batch. + The nearest neighbours for each batch are then merged to create a final list of + neighbours for the cell. Aligns batches in a quick and lightweight manner. + + For use in the scanpy workflow as an alternative to :func:`~scanpy.pp.neighbors`. + + .. note:: + + This is just a wrapper of :func:`bbknn.bbknn`: up to date docstring, + more information and bug reports there. + + Params + ------ + adata + Needs the PCA computed and stored in `adata.obsm["X_pca"]`. + batch_key + `adata.obs` column name discriminating between your batches. + use_rep + The dimensionality reduction in `.obsm` to use for neighbour detection. Defaults to PCA. + approx + If `True`, use approximate neighbour finding - annoy or PyNNDescent. This results + in a quicker run time for large datasets while also potentially increasing the degree of + batch correction. + use_annoy + Only used when `approx=True`. If `True`, will use annoy for neighbour finding. If + `False`, will use pyNNDescent instead. + metric + What distance metric to use. The options depend on the choice of neighbour algorithm. + + "euclidean", the default, is always available. + + Annoy supports "angular", "manhattan" and "hamming". + + PyNNDescent supports metrics listed in `pynndescent.distances.named_distances` + and custom functions, including compiled Numba code. + + >>> import pynndescent + >>> pynndescent.distances.named_distances.keys() # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + dict_keys(['euclidean', 'l2', 'sqeuclidean', 'manhattan', 'taxicab', 'l1', 'chebyshev', 'linfinity', + 'linfty', 'linf', 'minkowski', 'seuclidean', 'standardised_euclidean', 'wminkowski', ...]) + + KDTree supports members of :class:`sklearn.neighbors.KDTree`’s ``valid_metrics`` list, or parameterised + :class:`~sklearn.metrics.DistanceMetric` objects: + + >>> import sklearn.neighbors + >>> sklearn.neighbors.KDTree.valid_metrics + ['euclidean', 'l2', 'minkowski', 'p', 'manhattan', 'cityblock', 'l1', 'chebyshev', 'infinity'] + + .. note:: check the relevant documentation for up-to-date lists. + copy + If `True`, return a copy instead of writing to the supplied adata. + neighbors_within_batch + How many top neighbours to report for each batch; total number of neighbours in + the initial k-nearest-neighbours computation will be this number times the number + of batches. This then serves as the basis for the construction of a symmetrical + matrix of connectivities. + n_pcs + How many dimensions (in case of PCA, principal components) to use in the analysis. + trim + Trim the neighbours of each cell to these many top connectivities. May help with + population independence and improve the tidiness of clustering. The lower the value the + more independent the individual populations, at the cost of more conserved batch effect. + If `None`, sets the parameter value automatically to 10 times `neighbors_within_batch` + times the number of batches. Set to 0 to skip. + annoy_n_trees + Only used with annoy neighbour identification. The number of trees to construct in the + annoy forest. More trees give higher precision when querying, at the cost of increased + run time and resource intensity. + pynndescent_n_neighbors + Only used with pyNNDescent neighbour identification. The number of neighbours to include + in the approximate neighbour graph. More neighbours give higher precision when querying, + at the cost of increased run time and resource intensity. + pynndescent_random_state + Only used with pyNNDescent neighbour identification. The RNG seed to use when creating + the graph. + use_faiss + If `approx=False` and the metric is "euclidean", use the faiss package to compute + nearest neighbours if installed. This improves performance at a minor cost to numerical + precision as faiss operates on float32. + set_op_mix_ratio + UMAP connectivity computation parameter, float between 0 and 1, controlling the + blend between a connectivity matrix formed exclusively from mutual nearest neighbour + pairs (0) and a union of all observed neighbour relationships with the mutual pairs + emphasised (1) + local_connectivity + UMAP connectivity computation parameter, how many nearest neighbors of each cell + are assumed to be fully connected (and given a connectivity value of 1) + + Returns + ------- + The `adata` with the batch-corrected graph. + + """ + try: + from bbknn import bbknn + except ImportError as e: + msg = "Please install bbknn: `pip install bbknn`." + raise ImportError(msg) from e + return bbknn( + adata=adata, + batch_key=batch_key, + use_rep=use_rep, + approx=approx, + use_annoy=use_annoy, + metric=metric, + copy=copy, + neighbors_within_batch=neighbors_within_batch, + n_pcs=n_pcs, + trim=trim, + annoy_n_trees=annoy_n_trees, + pynndescent_n_neighbors=pynndescent_n_neighbors, + pynndescent_random_state=pynndescent_random_state, + use_faiss=use_faiss, + set_op_mix_ratio=set_op_mix_ratio, + local_connectivity=local_connectivity, + **kwargs, + ) diff --git a/.venv/lib/python3.11/site-packages/scanpy/external/pp/_dca.py b/.venv/lib/python3.11/site-packages/scanpy/external/pp/_dca.py new file mode 100644 index 0000000000000000000000000000000000000000..57d4b69e55be376cd343c919f94ea3626747e2eb --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/external/pp/_dca.py @@ -0,0 +1,212 @@ +from __future__ import annotations + +from types import MappingProxyType +from typing import TYPE_CHECKING + +from ..._compat import old_positionals + +if TYPE_CHECKING: + from collections.abc import Mapping, Sequence + from typing import Any, Literal + + from anndata import AnnData + + from ..._utils.random import _LegacyRandom + + _AEType = Literal["zinb-conddisp", "zinb", "nb-conddisp", "nb"] + + +@old_positionals( + "ae_type", + "normalize_per_cell", + "scale", + "log1p", + "hidden_size", + "hidden_dropout", + "batchnorm", + "activation", + "init", + "network_kwds", + "epochs", + "reduce_lr", + "early_stop", + "batch_size", + "optimizer", + "random_state", + "threads", + "learning_rate", + "verbose", + "training_kwds", + "return_model", + "return_info", + "copy", +) +def dca( # noqa: PLR0913 + adata: AnnData, + mode: Literal["denoise", "latent"] = "denoise", + *, + ae_type: _AEType = "nb-conddisp", + normalize_per_cell: bool = True, + scale: bool = True, + log1p: bool = True, + # network args + hidden_size: Sequence[int] = (64, 32, 64), + hidden_dropout: float | Sequence[float] = 0.0, + batchnorm: bool = True, + activation: str = "relu", + init: str = "glorot_uniform", + network_kwds: Mapping[str, Any] = MappingProxyType({}), + # training args + epochs: int = 300, + reduce_lr: int = 10, + early_stop: int = 15, + batch_size: int = 32, + optimizer: str = "RMSprop", + random_state: _LegacyRandom = 0, + threads: int | None = None, + learning_rate: float | None = None, + verbose: bool = False, + training_kwds: Mapping[str, Any] = MappingProxyType({}), + return_model: bool = False, + return_info: bool = False, + copy: bool = False, +) -> AnnData | None: + """Deep count autoencoder :cite:p:`Eraslan2019`. + + Fits a count autoencoder to the raw count data given in the anndata object + in order to denoise the data and to capture hidden representation of + cells in low dimensions. Type of the autoencoder and return values are + determined by the parameters. + + .. note:: + More information and bug reports `here `__. + + Parameters + ---------- + adata + An anndata file with `.raw` attribute representing raw counts. + mode + `denoise` overwrites `adata.X` with denoised expression values. + In `latent` mode DCA adds `adata.obsm['X_dca']` to given adata + object. This matrix represent latent representation of cells via DCA. + ae_type + Type of the autoencoder. Return values and the architecture is + determined by the type e.g. `nb` does not provide dropout + probabilities. Types that end with "-conddisp", assumes that dispersion is mean dependant. + normalize_per_cell + If true, library size normalization is performed using + the `sc.pp.normalize_per_cell` function in Scanpy and saved into adata + object. Mean layer is re-introduces library size differences by + scaling the mean value of each cell in the output layer. See the + manuscript for more details. + scale + If true, the input of the autoencoder is centered using + `sc.pp.scale` function of Scanpy. Note that the output is kept as raw + counts as loss functions are designed for the count data. + log1p + If true, the input of the autoencoder is log transformed with a + pseudocount of one using `sc.pp.log1p` function of Scanpy. + hidden_size + Width of hidden layers. + hidden_dropout + Probability of weight dropout in the autoencoder (per layer if list + or tuple). + batchnorm + If true, batch normalization is performed. + activation + Activation function of hidden layers. + init + Initialization method used to initialize weights. + network_kwds + Additional keyword arguments for the autoencoder. + epochs + Number of total epochs in training. + reduce_lr + Reduces learning rate if validation loss does not improve in given number of epochs. + early_stop + Stops training if validation loss does not improve in given number of epochs. + batch_size + Number of samples in the batch used for SGD. + optimizer + Type of optimization method used for training. + random_state + Seed for python, numpy and tensorflow. + threads + Number of threads to use in training. All cores are used by default. + learning_rate + Learning rate to use in the training. + verbose + If true, prints additional information about training and architecture. + training_kwds + Additional keyword arguments for the training process. + return_model + If true, trained autoencoder object is returned. See "Returns". + return_info + If true, all additional parameters of DCA are stored in `adata.obsm` such as dropout + probabilities (obsm['X_dca_dropout']) and estimated dispersion values + (obsm['X_dca_dispersion']), in case that autoencoder is of type + zinb or zinb-conddisp. + copy + If true, a copy of anndata is returned. + + Returns + ------- + If `copy` is true and `return_model` is false, AnnData object is returned. + + In "denoise" mode, `adata.X` is overwritten with the denoised values. + In "latent" mode, latent low dimensional representation of cells are stored + in `adata.obsm['X_dca']` and `adata.X` is not modified. + Note that these values are not corrected for library size effects. + + If `return_info` is true, all estimated distribution parameters are stored + in AnnData like this: + + `.obsm["X_dca_dropout"]` + The mixture coefficient (pi) of the zero component in ZINB, + i.e. dropout probability (if `ae_type` is `zinb` or `zinb-conddisp`). + `.obsm["X_dca_dispersion"]` + The dispersion parameter of NB. + `.uns["dca_loss_history"]` + The loss history of the training. + See `.history` attribute of Keras History class for mode details. + + Finally, the raw counts are stored in `.raw` attribute of AnnData object. + + If `return_model` is given, trained model is returned. + When both `copy` and `return_model` are true, + a tuple of anndata and model is returned in that order. + + """ + try: + from dca.api import dca + except ImportError as e: + msg = "Please install dca package (>= 0.2.1) via `pip install dca`" + raise ImportError(msg) from e + + return dca( + adata, + mode=mode, + ae_type=ae_type, + normalize_per_cell=normalize_per_cell, + scale=scale, + log1p=log1p, + hidden_size=hidden_size, + hidden_dropout=hidden_dropout, + batchnorm=batchnorm, + activation=activation, + init=init, + network_kwds=network_kwds, + epochs=epochs, + reduce_lr=reduce_lr, + early_stop=early_stop, + batch_size=batch_size, + optimizer=optimizer, + random_state=random_state, + threads=threads, + learning_rate=learning_rate, + verbose=verbose, + training_kwds=training_kwds, + return_model=return_model, + return_info=return_info, + copy=copy, + ) diff --git a/.venv/lib/python3.11/site-packages/scanpy/external/pp/_harmony_integrate.py b/.venv/lib/python3.11/site-packages/scanpy/external/pp/_harmony_integrate.py new file mode 100644 index 0000000000000000000000000000000000000000..0cfcc3216f3085cc10ed72afdf6778b965b0af3d --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/external/pp/_harmony_integrate.py @@ -0,0 +1,99 @@ +"""Use harmony to integrate cells from different experiments.""" + +from __future__ import annotations + +from collections.abc import Sequence # noqa: TC003 +from typing import TYPE_CHECKING + +import numpy as np + +from ..._compat import old_positionals +from ..._utils._doctests import doctest_needs + +if TYPE_CHECKING: + from anndata import AnnData + + +@old_positionals("basis", "adjusted_basis") +@doctest_needs("harmonypy") +def harmony_integrate( + adata: AnnData, + key: str | Sequence[str], + *, + basis: str = "X_pca", + adjusted_basis: str = "X_pca_harmony", + **kwargs, +): + """Use harmonypy :cite:p:`Korsunsky2019` to integrate different experiments. + + Harmony :cite:p:`Korsunsky2019` is an algorithm for integrating single-cell + data from multiple experiments. This function uses the python + port of Harmony, ``harmonypy``, to integrate single-cell data + stored in an AnnData object. As Harmony works by adjusting the + principal components, this function should be run after performing + PCA but before computing the neighbor graph, as illustrated in the + example below. + + Parameters + ---------- + adata + The annotated data matrix. + key + The name of the column in ``adata.obs`` that differentiates + among experiments/batches. To integrate over two or more covariates, + you can pass multiple column names as a list. See ``vars_use`` + parameter of the ``harmonypy`` package for more details. + basis + The name of the field in ``adata.obsm`` where the PCA table is + stored. Defaults to ``'X_pca'``, which is the default for + ``sc.pp.pca()``. + adjusted_basis + The name of the field in ``adata.obsm`` where the adjusted PCA + table will be stored after running this function. Defaults to + ``X_pca_harmony``. + kwargs + Any additional arguments will be passed to + ``harmonypy.run_harmony()``. + + Returns + ------- + Updates adata with the field ``adata.obsm[obsm_out_field]``, + containing principal components adjusted by Harmony such that + different experiments are integrated. + + Example + ------- + First, load libraries and example dataset, and preprocess. + + >>> import scanpy as sc + >>> import scanpy.external as sce + >>> adata = sc.datasets.pbmc3k() + >>> sc.pp.recipe_zheng17(adata) + >>> sc.pp.pca(adata) + + We now arbitrarily assign a batch metadata variable to each cell + for the sake of example, but during real usage there would already + be a column in ``adata.obs`` giving the experiment each cell came + from. + + >>> adata.obs["batch"] = 1350 * ["a"] + 1350 * ["b"] + + Finally, run harmony. Afterwards, there will be a new table in + ``adata.obsm`` containing the adjusted PC's. + + >>> sce.pp.harmony_integrate(adata, "batch") + >>> "X_pca_harmony" in adata.obsm + True + + """ + try: + import harmonypy + except ImportError as e: + msg = "\nplease install harmonypy:\n\n\tpip install harmonypy" + raise ImportError(msg) from e + + x = adata.obsm[basis].astype(np.float64) + + harmony_out = harmonypy.run_harmony(x, adata.obs, key, **kwargs) + + adata.obsm[adjusted_basis] = harmony_out.Z_corr.T diff --git a/.venv/lib/python3.11/site-packages/scanpy/external/pp/_hashsolo.py b/.venv/lib/python3.11/site-packages/scanpy/external/pp/_hashsolo.py new file mode 100644 index 0000000000000000000000000000000000000000..8713c239560fde6dafa72c4913eedcf15e4bdacc --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/external/pp/_hashsolo.py @@ -0,0 +1,464 @@ +"""A probabilistic cell hashing demultiplexing method. + +HashSolo generates a noise distribution and signal distribution +for each hashing barcode from empirically observed counts. +These distributions are updates from the global signal and noise barcode distributions, +which helps in the setting where not many cells are observed. +For a hashing barcode: + +Signal distributions + are estimated from samples where that hashing barcode has the highest count. + +Noise distributions + are estimated from samples where that hashing barcode is one the k-2 lowest barcodes, + where k is the number of barcodes. + +We test each of the following hypotheses in a bayesian fashion, +and select the most probable hypothesis. + +A doublet + should have its two highest barcode counts most likely + coming from a signal distribution for those barcodes. + +A singlet + should have its highest barcode from a signal distribution, + and its second highest barcode from a noise distribution. + +A negative two highest barcodes + should come from noise distributions. +""" + +from __future__ import annotations + +from itertools import product +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd +from scipy.stats import norm + +from ..._compat import old_positionals +from ..._utils import check_nonnegative_integers +from ..._utils._doctests import doctest_skip + +if TYPE_CHECKING: + from collections.abc import Sequence + + from anndata import AnnData + from numpy.typing import ArrayLike, NDArray + + +def _calculate_log_likelihoods( # noqa: PLR0915 + data: np.ndarray, number_of_noise_barcodes: int +) -> tuple[NDArray[np.float64], NDArray[np.float64], dict[int, str]]: + """Calculate log likelihoods for each hypothesis, negative, singlet, doublet. + + Parameters + ---------- + data + cells by hashing counts matrix + number_of_noise_barcodes + number of barcodes to used to calculated noise distribution + + Returns + ------- + log_likelihoods_for_each_hypothesis + a 2d np.array log likelihood of each hypothesis + all_indices + counter_to_barcode_combo + + """ + + def gaussian_updates( + data: np.ndarray, mu_o: float, std_o: float + ) -> tuple[float, float]: + """Update parameters of your gaussian. + + See . + + Parameters + ---------- + data + 1-d array of counts + mu_o + global mean for hashing count distribution + std_o + global std for hashing count distribution + + Returns + ------- + mean + of gaussian + std + of gaussian + + """ + lam_o = 1 / (std_o**2) + n = len(data) + lam = 1 / np.var(data) if len(data) > 1 else lam_o + lam_n = lam_o + n * lam + mu_n = ( + (np.mean(data) * n * lam + mu_o * lam_o) / lam_n if len(data) > 0 else mu_o + ) + return mu_n, (1 / (lam_n / (n + 1))) ** (1 / 2) + + eps = 1e-15 + # probabilites for negative, singlet, doublets + log_likelihoods_for_each_hypothesis = np.zeros((data.shape[0], 3)) + + all_indices = np.empty(data.shape[0]) + num_of_barcodes = data.shape[1] + number_of_non_noise_barcodes = ( + num_of_barcodes - number_of_noise_barcodes + if number_of_noise_barcodes is not None + else 2 + ) + + num_of_noise_barcodes = num_of_barcodes - number_of_non_noise_barcodes + + # assume log normal + data = np.log(data + 1) + data_arg = np.argsort(data, axis=1) + data_sort = np.sort(data, axis=1) + + # global signal and noise counts useful for when we have few cells + # barcodes with the highest number of counts are assumed to be a true signal + # barcodes with rank < k are considered to be noise + global_signal_counts = np.ravel(data_sort[:, -1]) + global_noise_counts = np.ravel(data_sort[:, :-number_of_non_noise_barcodes]) + global_mu_signal_o, global_sigma_signal_o = ( + np.mean(global_signal_counts), + np.std(global_signal_counts), + ) + global_mu_noise_o, global_sigma_noise_o = ( + np.mean(global_noise_counts), + np.std(global_noise_counts), + ) + + noise_params_dict = {} + signal_params_dict = {} + + # for each barcode get empirical noise and signal distribution parameterization + for x in np.arange(num_of_barcodes): + sample_barcodes = data[:, x] + sample_barcodes_noise_idx = np.where(data_arg[:, :num_of_noise_barcodes] == x)[ + 0 + ] + sample_barcodes_signal_idx = np.where(data_arg[:, -1] == x) + + # get noise and signal counts + noise_counts = sample_barcodes[sample_barcodes_noise_idx] + signal_counts = sample_barcodes[sample_barcodes_signal_idx] + + # get parameters of distribution, assuming lognormal do update from global values + noise_param = gaussian_updates( + noise_counts, global_mu_noise_o, global_sigma_noise_o + ) + signal_param = gaussian_updates( + signal_counts, global_mu_signal_o, global_sigma_signal_o + ) + noise_params_dict[x] = noise_param + signal_params_dict[x] = signal_param + + counter_to_barcode_combo: dict[int, str] = {} + counter = 0 + + # for each combination of noise and signal barcode calculate probiltiy of in silico and real cell hypotheses + for noise_sample_idx, signal_sample_idx in product( + np.arange(num_of_barcodes), np.arange(num_of_barcodes) + ): + signal_subset = data_arg[:, -1] == signal_sample_idx + noise_subset = data_arg[:, -2] == noise_sample_idx + subset = signal_subset & noise_subset + if sum(subset) == 0: + continue + + indices = np.where(subset)[0] + barcode_combo = "_".join([str(noise_sample_idx), str(signal_sample_idx)]) + all_indices[np.where(subset)[0]] = counter + counter_to_barcode_combo[counter] = barcode_combo + counter += 1 + noise_params = noise_params_dict[noise_sample_idx] + signal_params = signal_params_dict[signal_sample_idx] + + # calculate probabilties for each hypothesis for each cell + data_subset = data[subset] + log_signal_signal_probs = np.log( + norm.pdf( + data_subset[:, signal_sample_idx], + *signal_params[:-2], + loc=signal_params[-2], + scale=signal_params[-1], + ) + + eps + ) + signal_noise_params = signal_params_dict[noise_sample_idx] + log_noise_signal_probs = np.log( + norm.pdf( + data_subset[:, noise_sample_idx], + loc=signal_noise_params[-2], + scale=signal_noise_params[-1], + ) + + eps + ) + + log_noise_noise_probs = np.log( + norm.pdf( + data_subset[:, noise_sample_idx], + loc=noise_params[-2], + scale=noise_params[-1], + ) + + eps + ) + log_signal_noise_probs = np.log( + norm.pdf( + data_subset[:, signal_sample_idx], + loc=noise_params[-2], + scale=noise_params[-1], + ) + + eps + ) + + probs_of_negative = np.sum( + [log_noise_noise_probs, log_signal_noise_probs], axis=0 + ) + probs_of_singlet = np.sum( + [log_noise_noise_probs, log_signal_signal_probs], axis=0 + ) + probs_of_doublet = np.sum( + [log_noise_signal_probs, log_signal_signal_probs], axis=0 + ) + log_probs_list = [probs_of_negative, probs_of_singlet, probs_of_doublet] + + # each cell and each hypothesis probability + for prob_idx, log_prob in enumerate(log_probs_list): + log_likelihoods_for_each_hypothesis[indices, prob_idx] = log_prob + return ( + log_likelihoods_for_each_hypothesis, + all_indices, + counter_to_barcode_combo, + ) + + +def _calculate_bayes_rule( + data: np.ndarray, priors: ArrayLike, number_of_noise_barcodes: int +) -> dict[str, np.ndarray]: + """Calculate bayes rule from log likelihoods. + + Parameters + ---------- + data + Anndata object filled only with hashing counts + priors + a list of your prior for each hypothesis + first element is your prior for the negative hypothesis + second element is your prior for the singlet hypothesis + third element is your prior for the doublet hypothesis + We use [0.01, 0.8, 0.19] by default because we assume the barcodes + in your cell hashing matrix are those cells which have passed QC + in the transcriptome space, e.g. UMI counts, pct mito reads, etc. + number_of_noise_barcodes + number of barcodes to used to calculated noise distribution + + Returns + ------- + A dict of bayes key results with the following entries: + + `"most_likely_hypothesis"` + A 1d np.array of the most likely hypothesis + `"probs_hypotheses"` + A 2d np.array probability of each hypothesis + `"log_likelihoods_for_each_hypothesis"` + A 2d np.array log likelihood of each hypothesis + + """ + priors = np.array(priors) + log_likelihoods_for_each_hypothesis, _, _ = _calculate_log_likelihoods( + data, number_of_noise_barcodes + ) + probs_hypotheses = ( + np.exp(log_likelihoods_for_each_hypothesis) + * priors + / np.sum( + np.multiply(np.exp(log_likelihoods_for_each_hypothesis), priors), + axis=1, + )[:, None] + ) + most_likely_hypothesis = np.argmax(probs_hypotheses, axis=1) + return { + "most_likely_hypothesis": most_likely_hypothesis, + "probs_hypotheses": probs_hypotheses, + "log_likelihoods_for_each_hypothesis": log_likelihoods_for_each_hypothesis, + } + + +@old_positionals( + "priors", "pre_existing_clusters", "number_of_noise_barcodes", "inplace" +) +@doctest_skip("Illustrative but not runnable doctest code") +def hashsolo( + adata: AnnData, + cell_hashing_columns: Sequence[str], + *, + priors: tuple[float, float, float] = (0.01, 0.8, 0.19), + pre_existing_clusters: str | None = None, + number_of_noise_barcodes: int | None = None, + inplace: bool = True, +) -> AnnData | None: + """Probabilistic demultiplexing of cell hashing data using HashSolo :cite:p:`Bernstein2020`. + + .. note:: + More information and bug reports `here `__. + + Parameters + ---------- + adata + The (annotated) data matrix of shape `n_obs` × `n_vars`. + Rows correspond to cells and columns to genes. + cell_hashing_columns + `.obs` columns that contain cell hashing counts. + priors + Prior probabilities of each hypothesis, in + the order `[negative, singlet, doublet]`. The default is set to + `[0.01, 0.8, 0.19]` assuming barcode counts are from cells that + have passed QC in the transcriptome space, e.g. UMI counts, pct + mito reads, etc. + pre_existing_clusters + The column in `.obs` containing pre-existing cluster assignments + (e.g. Leiden clusters or cell types, but not batch assignments). + If provided, demultiplexing will be performed separately for each + cluster. + number_of_noise_barcodes + The number of barcodes used to create the noise distribution. + Defaults to `len(cell_hashing_columns) - 2`. + inplace + Whether to update `adata` in-place or return a copy. + + Returns + ------- + A copy of the input `adata` if `inplace=False`, otherwise the input + `adata`. The following fields are added: + + `.obs["most_likely_hypothesis"]` + Index of the most likely hypothesis, where `0` corresponds to negative, + `1` to singlet, and `2` to doublet. + `.obs["cluster_feature"]` + The cluster assignments used for demultiplexing. + `.obs["negative_hypothesis_probability"]` + Probability of the negative hypothesis. + `.obs["singlet_hypothesis_probability"]` + Probability of the singlet hypothesis. + `.obs["doublet_hypothesis_probability"]` + Probability of the doublet hypothesis. + `.obs["Classification"]`: + Classification of the cell, one of the barcodes in `cell_hashing_columns`, + `"Negative"`, or `"Doublet"`. + + Examples + -------- + >>> import anndata + >>> import scanpy.external as sce + >>> adata = anndata.read_h5ad("data.h5ad") + >>> sce.pp.hashsolo(adata, ["Hash1", "Hash2", "Hash3"]) + >>> adata.obs.head() + + """ + print( + "Please cite HashSolo paper:\nhttps://www.cell.com/cell-systems/fulltext/S2405-4712(20)30195-2" + ) + adata = adata.copy() if not inplace else adata + data = adata.obs[cell_hashing_columns].values + if not check_nonnegative_integers(data): + msg = "Cell hashing counts must be non-negative" + raise ValueError(msg) + if (number_of_noise_barcodes is not None) and ( + number_of_noise_barcodes >= len(cell_hashing_columns) + ): + msg = "number_of_noise_barcodes must be at least one less \ + than the number of samples you have as determined by the number of \ + cell_hashing_columns you've given as input " + raise ValueError(msg) + num_of_cells = adata.shape[0] + results = pd.DataFrame( + np.zeros((num_of_cells, 6)), + columns=[ + "most_likely_hypothesis", + "probs_hypotheses", + "cluster_feature", + "negative_hypothesis_probability", + "singlet_hypothesis_probability", + "doublet_hypothesis_probability", + ], + index=adata.obs_names, + ) + if pre_existing_clusters is not None: + cluster_features = pre_existing_clusters + unique_cluster_features = np.unique(adata.obs[cluster_features]) + for cluster_feature in unique_cluster_features: + cluster_feature_bool_vector = adata.obs[cluster_features] == cluster_feature + posterior_dict = _calculate_bayes_rule( + data[cluster_feature_bool_vector], + priors, + number_of_noise_barcodes, + ) + results.loc[cluster_feature_bool_vector, "most_likely_hypothesis"] = ( + posterior_dict["most_likely_hypothesis"] + ) + results.loc[cluster_feature_bool_vector, "cluster_feature"] = ( + cluster_feature + ) + results.loc[ + cluster_feature_bool_vector, "negative_hypothesis_probability" + ] = posterior_dict["probs_hypotheses"][:, 0] + results.loc[ + cluster_feature_bool_vector, "singlet_hypothesis_probability" + ] = posterior_dict["probs_hypotheses"][:, 1] + results.loc[ + cluster_feature_bool_vector, "doublet_hypothesis_probability" + ] = posterior_dict["probs_hypotheses"][:, 2] + else: + posterior_dict = _calculate_bayes_rule(data, priors, number_of_noise_barcodes) + results.loc[:, "most_likely_hypothesis"] = posterior_dict[ + "most_likely_hypothesis" + ] + results.loc[:, "cluster_feature"] = 0 + results.loc[:, "negative_hypothesis_probability"] = posterior_dict[ + "probs_hypotheses" + ][:, 0] + results.loc[:, "singlet_hypothesis_probability"] = posterior_dict[ + "probs_hypotheses" + ][:, 1] + results.loc[:, "doublet_hypothesis_probability"] = posterior_dict[ + "probs_hypotheses" + ][:, 2] + + adata.obs["most_likely_hypothesis"] = results.loc[ + adata.obs_names, "most_likely_hypothesis" + ] + adata.obs["cluster_feature"] = results.loc[adata.obs_names, "cluster_feature"] + adata.obs["negative_hypothesis_probability"] = results.loc[ + adata.obs_names, "negative_hypothesis_probability" + ] + adata.obs["singlet_hypothesis_probability"] = results.loc[ + adata.obs_names, "singlet_hypothesis_probability" + ] + adata.obs["doublet_hypothesis_probability"] = results.loc[ + adata.obs_names, "doublet_hypothesis_probability" + ] + + adata.obs["Classification"] = None + adata.obs.loc[adata.obs["most_likely_hypothesis"] == 2, "Classification"] = ( + "Doublet" + ) + adata.obs.loc[adata.obs["most_likely_hypothesis"] == 0, "Classification"] = ( + "Negative" + ) + all_sings = adata.obs["most_likely_hypothesis"] == 1 + singlet_sample_index = np.argmax( + adata.obs.loc[all_sings, cell_hashing_columns].values, axis=1 + ) + adata.obs.loc[all_sings, "Classification"] = adata.obs[ + cell_hashing_columns + ].columns[singlet_sample_index] + + return adata if not inplace else None diff --git a/.venv/lib/python3.11/site-packages/scanpy/external/pp/_magic.py b/.venv/lib/python3.11/site-packages/scanpy/external/pp/_magic.py new file mode 100644 index 0000000000000000000000000000000000000000..a30a7964b2fda9132d47a986f1966aa6c4ac26c0 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/external/pp/_magic.py @@ -0,0 +1,210 @@ +"""Denoise high-dimensional data using MAGIC.""" + +from __future__ import annotations + +from types import NoneType +from typing import TYPE_CHECKING + +from packaging.version import Version + +from ... import logging as logg +from ..._compat import pkg_version +from ..._settings import settings +from ..._utils._doctests import doctest_needs + +if TYPE_CHECKING: + from collections.abc import Sequence + from typing import Literal + + from anndata import AnnData + + from ..._utils.random import _LegacyRandom + +MIN_VERSION = "2.0" + + +@doctest_needs("magic") +def magic( # noqa: PLR0913 + adata: AnnData, + name_list: Literal["all_genes", "pca_only"] | Sequence[str] | None = None, + *, + knn: int = 5, + decay: float | None = 1, + knn_max: int | None = None, + t: Literal["auto"] | int = 3, + n_pca: int | None = 100, + solver: Literal["exact", "approximate"] = "exact", + knn_dist: str = "euclidean", + random_state: _LegacyRandom = None, + n_jobs: int | None = None, + verbose: bool = False, + copy: bool | None = None, + **kwargs, +) -> AnnData | None: + """Markov Affinity-based Graph Imputation of Cells (MAGIC) API :cite:p:`vanDijk2018`. + + MAGIC is an algorithm for denoising and transcript recover of single cells + applied to single-cell sequencing data. MAGIC builds a graph from the data + and uses diffusion to smooth out noise and recover the data manifold. + + The algorithm implemented here has changed primarily in two ways + compared to the algorithm described in :cite:t:`vanDijk2018`. Firstly, we use + the adaptive kernel described in :cite:t:`Moon2019` for + improved stability. Secondly, data diffusion is applied + in the PCA space, rather than the data space, for speed and + memory improvements. + + More information and bug reports + `here `__. For help, visit + . + + Parameters + ---------- + adata + An anndata file with `.raw` attribute representing raw counts. + name_list + Denoised genes to return. The default `'all_genes'`/`None` + may require a large amount of memory if the input data is sparse. + Another possibility is `'pca_only'`. + knn + number of nearest neighbors on which to build kernel. + decay + sets decay rate of kernel tails. + If None, alpha decaying kernel is not used. + knn_max + maximum number of nearest neighbors with nonzero connection. + If `None`, will be set to 3 * `knn`. + t + power to which the diffusion operator is powered. + This sets the level of diffusion. If 'auto', t is selected + according to the Procrustes disparity of the diffused data. + n_pca + Number of principal components to use for calculating + neighborhoods. For extremely large datasets, using + n_pca < 20 allows neighborhoods to be calculated in + roughly log(n_samples) time. If `None`, no PCA is performed. + solver + Which solver to use. "exact" uses the implementation described + in :cite:t:`vanDijk2018`. "approximate" uses a faster + implementation that performs imputation in the PCA space and then + projects back to the gene space. Note, the "approximate" solver may + return negative values. + knn_dist + recommended values: 'euclidean', 'cosine', 'precomputed' + Any metric from `scipy.spatial.distance` can be used + distance metric for building kNN graph. If 'precomputed', + `data` should be an n_samples x n_samples distance or + affinity matrix. + random_state + Random seed. Defaults to the global `numpy` random number generator. + n_jobs + Number of threads to use in training. All cores are used by default. + verbose + If `True` or an integer `>= 2`, print status messages. + If `None`, `sc.settings.verbosity` is used. + copy + If true, a copy of anndata is returned. If `None`, `copy` is True if + `genes` is not `'all_genes'` or `'pca_only'`. `copy` may only be False + if `genes` is `'all_genes'` or `'pca_only'`, as the resultant data + will otherwise have different column names from the input data. + kwargs + Additional arguments to `magic.MAGIC`. + + Returns + ------- + If `copy` is True, AnnData object is returned. + + If `subset_genes` is not `all_genes`, PCA on MAGIC values of cells are + stored in `adata.obsm['X_magic']` and `adata.X` is not modified. + + The raw counts are stored in `.raw` attribute of AnnData object. + + Examples + -------- + >>> import scanpy as sc + >>> import scanpy.external as sce + >>> adata = sc.datasets.paul15() + >>> sc.pp.normalize_per_cell(adata) + >>> sc.pp.sqrt(adata) # or sc.pp.log1p(adata) + >>> adata_magic = sce.pp.magic(adata, name_list=["Mpo", "Klf1", "Ifitm1"], knn=5) + >>> adata_magic.shape + (2730, 3) + >>> sce.pp.magic(adata, name_list="pca_only", knn=5) + >>> adata.obsm["X_magic"].shape + (2730, 100) + >>> sce.pp.magic(adata, name_list="all_genes", knn=5) + >>> adata.X.shape + (2730, 3451) + + """ + try: + from magic import MAGIC + except ImportError as e: + msg = "Please install magic package via `pip install magic-impute`" + raise ImportError(msg) from e + else: + if pkg_version("magic-impute") < Version(MIN_VERSION): + msg = ( + "scanpy requires magic-impute >= " + f"v{MIN_VERSION} (detected: v{pkg_version('magic-impute')}). " + "Please update magic package via `pip install -U magic-impute`" + ) + raise ImportError(msg) + + start = logg.info("computing MAGIC") + all_or_pca = isinstance(name_list, str | NoneType) + if all_or_pca and name_list not in {"all_genes", "pca_only", None}: + msg = ( + "Invalid string value for `name_list`: " + "Only `'all_genes'` and `'pca_only'` are allowed." + ) + raise ValueError(msg) + if copy is None: + copy = not all_or_pca + elif not all_or_pca and not copy: + msg = ( + "Can only perform MAGIC in-place with `name_list=='all_genes' or " + f"`name_list=='pca_only'` (got {name_list}). Consider setting " + "`copy=True`" + ) + raise ValueError(msg) + adata = adata.copy() if copy else adata + n_jobs = settings.n_jobs if n_jobs is None else n_jobs + + x_magic = MAGIC( + knn=knn, + decay=decay, + knn_max=knn_max, + t=t, + n_pca=n_pca, + solver=solver, + knn_dist=knn_dist, + random_state=random_state, + n_jobs=n_jobs, + verbose=verbose, + **kwargs, + ).fit_transform(adata, genes=name_list) + logg.info( + " finished", + time=start, + deep=( + "added\n 'X_magic', PCA on MAGIC coordinates (adata.obsm)" + if name_list == "pca_only" + else "" + ), + ) + # update AnnData instance + if name_list == "pca_only": + # special case – update adata.obsm with smoothed values + adata.obsm["X_magic"] = x_magic.X + elif copy: + # just return X_magic + x_magic.raw = adata + adata = x_magic + else: + # replace data with smoothed data + adata.raw = adata + adata.X = x_magic.X + + if copy: + return adata diff --git a/.venv/lib/python3.11/site-packages/scanpy/external/pp/_mnn_correct.py b/.venv/lib/python3.11/site-packages/scanpy/external/pp/_mnn_correct.py new file mode 100644 index 0000000000000000000000000000000000000000..53fbd931708519c4ecb99367665d0f7b3356ab89 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/external/pp/_mnn_correct.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ..._settings import settings + +if TYPE_CHECKING: + from collections.abc import Collection, Sequence + from typing import Any, Literal + + import numpy as np + import pandas as pd + from anndata import AnnData + + +def mnn_correct( # noqa: PLR0913 + *datas: AnnData | np.ndarray, + var_index: Collection[str] | None = None, + var_subset: Collection[str] | None = None, + batch_key: str = "batch", + index_unique: str = "-", + batch_categories: Collection[Any] | None = None, + k: int = 20, + sigma: float = 1.0, + cos_norm_in: bool = True, + cos_norm_out: bool = True, + svd_dim: int | None = None, + var_adj: bool = True, + compute_angle: bool = False, + mnn_order: Sequence[int] | None = None, + svd_mode: Literal["svd", "rsvd", "irlb"] = "rsvd", + do_concatenate: bool = True, + save_raw: bool = False, + n_jobs: int | None = None, + **kwargs, +) -> tuple[ + np.ndarray | AnnData, + list[pd.DataFrame], + list[tuple[float | None, int]] | None, +]: + """Correct batch effects by matching mutual nearest neighbors :cite:p:`Haghverdi2018` :cite:p:`Kang2018`. + + This uses the implementation of mnnpy_ :cite:p:`Kang2018`. + + Depending on `do_concatenate`, returns matrices or `AnnData` objects in the + original order containing corrected expression values or a concatenated + matrix or AnnData object. + + Be reminded that it is not advised to use the corrected data matrices for + differential expression testing. + + More information and bug reports `here `__. + + .. _mnnpy: https://github.com/chriscainx/mnnpy + + Parameters + ---------- + datas + Expression matrices or AnnData objects. Matrices should be shaped like + n_obs × n_vars (n_cell × n_gene) and have consistent number of columns. + AnnData objects should have same number of variables. + var_index + The index (list of str) of vars (genes). Necessary when using only a + subset of vars to perform MNN correction, and should be supplied with + `var_subset`. When `datas` are AnnData objects, `var_index` is ignored. + var_subset + The subset of vars (list of str) to be used when performing MNN + correction. Typically, a list of highly variable genes (HVGs). + When set to `None`, uses all vars. + batch_key + The `batch_key` for :meth:`~anndata.AnnData.concatenate`. + Only valid when `do_concatenate` and supplying `AnnData` objects. + index_unique + The `index_unique` for :meth:`~anndata.AnnData.concatenate`. + Only valid when `do_concatenate` and supplying `AnnData` objects. + batch_categories + The `batch_categories` for :meth:`~anndata.AnnData.concatenate`. + Only valid when `do_concatenate` and supplying AnnData objects. + k + Number of mutual nearest neighbors. + sigma + The bandwidth of the Gaussian smoothing kernel used to compute the + correction vectors. Default is 1. + cos_norm_in + Whether cosine normalization should be performed on the input data prior + to calculating distances between cells. + cos_norm_out + Whether cosine normalization should be performed prior to computing corrected expression values. + svd_dim + The number of dimensions to use for summarizing biological substructure + within each batch. If None, biological components will not be removed + from the correction vectors. + var_adj + Whether to adjust variance of the correction vectors. Note this step + takes most computing time. + compute_angle + Whether to compute the angle between each cell’s correction vector and + the biological subspace of the reference batch. + mnn_order + The order in which batches are to be corrected. When set to None, datas + are corrected sequentially. + svd_mode + `'svd'` computes SVD using a non-randomized SVD-via-ID algorithm, + while `'rsvd'` uses a randomized version. `'irlb'` perfores + truncated SVD by implicitly restarted Lanczos bidiagonalization + (forked from https://github.com/airysen/irlbpy). + do_concatenate + Whether to concatenate the corrected matrices or AnnData objects. Default is True. + save_raw + Whether to save the original expression data in the + :attr:`~anndata.AnnData.raw` attribute. + n_jobs + The number of jobs. When set to `None`, automatically uses + :attr:`scanpy.settings.n_jobs`. + kwargs + optional keyword arguments for irlb. + + Returns + ------- + datas + Corrected matrix/matrices or AnnData object/objects, depending on the + input type and `do_concatenate`. + mnn_list + A list containing MNN pairing information as DataFrames in each iteration step. + angle_list + A list containing angles of each batch. + + """ + if len(datas) < 2: + return datas, [], [] + + try: + import mnnpy + from mnnpy import mnn_correct + except ImportError as e: + msg = "Please install the package mnnpy (https://github.com/chriscainx/mnnpy). " + raise ImportError(msg) from e + + n_jobs = settings.n_jobs if n_jobs is None else n_jobs + + if n_jobs < 2: + mnnpy.settings.normalization = "single" + else: + mnnpy.settings.normalization = "parallel" + + datas, mnn_list, angle_list = mnn_correct( + *datas, + var_index=var_index, + var_subset=var_subset, + batch_key=batch_key, + index_unique=index_unique, + batch_categories=batch_categories, + k=k, + sigma=sigma, + cos_norm_in=cos_norm_in, + cos_norm_out=cos_norm_out, + svd_dim=svd_dim, + var_adj=var_adj, + compute_angle=compute_angle, + mnn_order=mnn_order, + svd_mode=svd_mode, + do_concatenate=do_concatenate, + save_raw=save_raw, + n_jobs=n_jobs, + **kwargs, + ) + return datas, mnn_list, angle_list diff --git a/.venv/lib/python3.11/site-packages/scanpy/external/pp/_scanorama_integrate.py b/.venv/lib/python3.11/site-packages/scanpy/external/pp/_scanorama_integrate.py new file mode 100644 index 0000000000000000000000000000000000000000..c02c351c1fdb719c1dce2a7a5abd8c4a913513f1 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/external/pp/_scanorama_integrate.py @@ -0,0 +1,148 @@ +"""Use Scanorama to integrate cells from different experiments.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np + +from ..._compat import old_positionals +from ..._utils._doctests import doctest_needs + +if TYPE_CHECKING: + from anndata import AnnData + + +@old_positionals( + "basis", "adjusted_basis", "knn", "sigma", "approx", "alpha", "batch_size" +) +@doctest_needs("scanorama") +def scanorama_integrate( + adata: AnnData, + key: str, + *, + basis: str = "X_pca", + adjusted_basis: str = "X_scanorama", + knn: int = 20, + sigma: float = 15, + approx: bool = True, + alpha: float = 0.10, + batch_size: int = 5000, + **kwargs, +) -> None: + """Use Scanorama :cite:p:`Hie2019` to integrate different experiments. + + Scanorama :cite:p:`Hie2019` is an algorithm for integrating single-cell + data from multiple experiments stored in an AnnData object. This + function should be run after performing PCA but before computing + the neighbor graph, as illustrated in the example below. + + This uses the implementation of scanorama_ :cite:p:`Hie2019`. + + .. _scanorama: https://github.com/brianhie/scanorama + + Parameters + ---------- + adata + The annotated data matrix. + key + The name of the column in ``adata.obs`` that differentiates + among experiments/batches. Cells from the same batch must be + contiguously stored in ``adata``. + basis + The name of the field in ``adata.obsm`` where the PCA table is + stored. Defaults to ``'X_pca'``, which is the default for + ``sc.pp.pca()``. + adjusted_basis + The name of the field in ``adata.obsm`` where the integrated + embeddings will be stored after running this function. Defaults + to ``X_scanorama``. + knn + Number of nearest neighbors to use for matching. + sigma + Correction smoothing parameter on Gaussian kernel. + approx + Use approximate nearest neighbors with Python ``annoy``; + greatly speeds up matching runtime. + alpha + Alignment score minimum cutoff. + batch_size + The batch size used in the alignment vector computation. Useful + when integrating very large (>100k samples) datasets. Set to + large value that runs within available memory. + kwargs + Any additional arguments will be passed to + ``scanorama.assemble()``. + + Returns + ------- + Updates adata with the field ``adata.obsm[adjusted_basis]``, + containing Scanorama embeddings such that different experiments + are integrated. + + Example + ------- + First, load libraries and example dataset, and preprocess. + + >>> import scanpy as sc + >>> import scanpy.external as sce + >>> adata = sc.datasets.pbmc3k() + >>> sc.pp.recipe_zheng17(adata) + >>> sc.pp.pca(adata) + + We now arbitrarily assign a batch metadata variable to each cell + for the sake of example, but during real usage there would already + be a column in ``adata.obs`` giving the experiment each cell came + from. + + >>> adata.obs["batch"] = 1350 * ["a"] + 1350 * ["b"] + + Finally, run Scanorama. Afterwards, there will be a new table in + ``adata.obsm`` containing the Scanorama embeddings. + + >>> sce.pp.scanorama_integrate(adata, "batch", verbose=1) + Processing datasets a <=> b + >>> "X_scanorama" in adata.obsm + True + + """ + try: + import scanorama + except ImportError as e: + msg = "\nplease install Scanorama:\n\n\tpip install scanorama" + raise ImportError(msg) from e + + # Get batch indices in linear time. + curr_batch = None + batch_names = [] + name2idx = {} + for idx in range(adata.X.shape[0]): + batch_name = adata.obs[key].iat[idx] + if batch_name != curr_batch: + curr_batch = batch_name + if batch_name in batch_names: + # Contiguous batches important for preserving cell order. + msg = "Detected non-contiguous batches." + raise ValueError(msg) + batch_names.append(batch_name) # Preserve name order. + name2idx[batch_name] = [] + name2idx[batch_name].append(idx) + + # Separate batches. + datasets_dimred = [ + adata.obsm[basis][name2idx[batch_name]] for batch_name in batch_names + ] + + # Integrate. + integrated = scanorama.assemble( + datasets_dimred, # Assemble in low dimensional space. + knn=knn, + sigma=sigma, + approx=approx, + alpha=alpha, + ds_names=batch_names, + batch_size=batch_size, + **kwargs, + ) + + adata.obsm[adjusted_basis] = np.concatenate(integrated) diff --git a/.venv/lib/python3.11/site-packages/scanpy/external/tl/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/external/tl/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..383dc2778fbde32b4265aadeef8c8b9387273059 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/external/tl/__init__.py @@ -0,0 +1,25 @@ +"""External analysis tools.""" + +from __future__ import annotations + +from ._harmony_timeseries import harmony_timeseries +from ._palantir import palantir, palantir_results +from ._phate import phate +from ._phenograph import phenograph +from ._pypairs import cyclone, sandbag +from ._sam import sam +from ._trimap import trimap +from ._wishbone import wishbone + +__all__ = [ + "cyclone", + "harmony_timeseries", + "palantir", + "palantir_results", + "phate", + "phenograph", + "sam", + "sandbag", + "trimap", + "wishbone", +] diff --git a/.venv/lib/python3.11/site-packages/scanpy/external/tl/_harmony_timeseries.py b/.venv/lib/python3.11/site-packages/scanpy/external/tl/_harmony_timeseries.py new file mode 100644 index 0000000000000000000000000000000000000000..be2da92169c430f98d3a68dcb1dff8d972cffe86 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/external/tl/_harmony_timeseries.py @@ -0,0 +1,169 @@ +"""Harmony time series for data visualization with augmented affinity matrix at discrete time points.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd + +from ... import logging as logg +from ..._compat import old_positionals +from ..._utils._doctests import doctest_needs + +if TYPE_CHECKING: + from anndata import AnnData + + +@old_positionals("n_neighbors", "n_components", "n_jobs", "copy") +@doctest_needs("harmony") +def harmony_timeseries( + adata: AnnData, + tp: str, + *, + n_neighbors: int = 30, + n_components: int | None = 1000, + n_jobs: int = -2, + copy: bool = False, +) -> AnnData | None: + """Harmony time series for data visualization with augmented affinity matrix at discrete time points :cite:p:`Nowotschin2019`. + + Harmony time series is a framework for data visualization, trajectory + detection and interpretation for scRNA-seq data measured at discrete + time points. Harmony constructs an augmented affinity matrix by augmenting + the kNN graph affinity matrix with mutually nearest neighbors between + successive time points. This augmented affinity matrix forms the basis for + generated a force directed layout for visualization and also serves as input + for computing the diffusion operator which can be used for trajectory + detection using Palantir_. + + .. _Palantir: https://github.com/dpeerlab/Palantir + + .. note:: + More information and bug reports `here + `__. + + Parameters + ---------- + adata + Annotated data matrix of shape n_obs `×` n_vars. Rows correspond to + cells and columns to genes. Rows represent two or more time points, + where replicates of the same time point are consecutive in order. + tp + key name of observation annotation `.obs` representing time points. Time + points should be categorical of `dtype=category`. The unique categories for + the categorical will be used as the time points to construct the timepoint + connections. + n_neighbors + Number of nearest neighbors for graph construction. + n_components + Minimum number of principal components to use. Specify `None` to use + pre-computed components. The higher the value the better to capture 85% of the + variance. + n_jobs + Nearest Neighbors will be computed in parallel using n_jobs. + copy + Return a copy instead of writing to `adata`. + + Returns + ------- + Depending on `copy`, returns or updates `.obsm`, `.obsp` and `.uns` with the following: + + **X_harmony** - :class:`~numpy.ndarray` (:attr:`~anndata.AnnData.obsm`, dtype `float`) + force directed layout + **harmony_aff** - :class:`~scipy.sparse.csr_matrix` (:attr:`~anndata.AnnData.obsp`, dtype `float`) + affinity matrix + **harmony_aff_aug** - :class:`~scipy.sparse.csr_matrix` (:attr:`~anndata.AnnData.obsp`, dtype `float`) + augmented affinity matrix + **harmony_timepoint_var** - `str` (:attr:`~anndata.AnnData.uns`) + The name of the variable passed as `tp` + **harmony_timepoint_connections** - :class:`~numpy.ndarray` (:attr:`~anndata.AnnData.uns`, dtype `str`) + The links between time points + + Example + ------- + + >>> from itertools import product + >>> import pandas as pd + >>> from anndata import AnnData + >>> import scanpy as sc + >>> import scanpy.external as sce + + **Load** `AnnData` + + A sample with real data is available here_. + + .. _here: https://github.com/dpeerlab/Harmony/tree/master/data + + Random data sets of three time points with two replicates each: + + >>> adata_ref = sc.datasets.pbmc3k() + >>> start = [596, 615, 1682, 1663, 1409, 1432] + >>> adata = AnnData.concatenate( + ... *(adata_ref[i : i + 1000] for i in start), + ... join="outer", + ... batch_key="sample", + ... batch_categories=[f"sa{i}_Rep{j}" for i, j in product((1, 2, 3), (1, 2))], + ... ) + >>> time_points = adata.obs["sample"].str.split("_", expand=True)[0] + >>> adata.obs["time_points"] = pd.Categorical( + ... time_points, categories=["sa1", "sa2", "sa3"] + ... ) + + Normalize and filter for highly expressed genes + + >>> sc.pp.normalize_total(adata, target_sum=10000) + >>> sc.pp.log1p(adata) + >>> sc.pp.highly_variable_genes(adata, n_top_genes=1000, subset=True) + + Run harmony_timeseries + + >>> sce.tl.harmony_timeseries(adata, tp="time_points", n_components=500) + + Plot time points: + + >>> sce.pl.harmony_timeseries(adata) + + For further demonstration of Harmony visualizations please follow the notebook + `Harmony_sample_notebook.ipynb + `_. + It provides a comprehensive guide to draw *gene expression trends*, + amongst other things. + + """ + try: + import harmony + except ImportError as e: + msg = "\nplease install harmony:\n\n\tpip install harmonyTS" + raise ImportError(msg) from e + + adata = adata.copy() if copy else adata + logg.info("Harmony augmented affinity matrix") + + if adata.obs[tp].dtype.name != "category": + msg = f"{tp!r} column does not contain Categorical data" + raise ValueError(msg) + timepoints = adata.obs[tp].cat.categories.tolist() + timepoint_connections = pd.DataFrame(np.array([timepoints[:-1], timepoints[1:]]).T) + + # compute the augmented and non-augmented affinity matrices + aug_aff, aff = harmony.core.augmented_affinity_matrix( + data_df=adata.to_df(), + timepoints=adata.obs[tp], + timepoint_connections=timepoint_connections, + n_neighbors=n_neighbors, + n_jobs=n_jobs, + pc_components=n_components, + ) + + # Force directed layouts + layout = harmony.plot.force_directed_layout(aug_aff, adata.obs.index) + + adata.obsm["X_harmony"] = np.asarray(layout) + adata.obsp["harmony_aff"] = aff + adata.obsp["harmony_aff_aug"] = aug_aff + adata.uns["harmony_timepoint_var"] = tp + adata.uns["harmony_timepoint_connections"] = np.asarray(timepoint_connections) + + return adata if copy else None diff --git a/.venv/lib/python3.11/site-packages/scanpy/external/tl/_palantir.py b/.venv/lib/python3.11/site-packages/scanpy/external/tl/_palantir.py new file mode 100644 index 0000000000000000000000000000000000000000..1d4e580687bae1d1d0683496a5a6b872fb79d20e --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/external/tl/_palantir.py @@ -0,0 +1,341 @@ +"""Run Diffusion maps using the adaptive anisotropic kernel.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pandas as pd + +from ... import logging as logg +from ..._compat import old_positionals +from ..._utils._doctests import doctest_needs + +if TYPE_CHECKING: + from anndata import AnnData + + +@old_positionals( + "n_components", + "knn", + "alpha", + "use_adjacency_matrix", + "distances_key", + "n_eigs", + "impute_data", + "n_steps", + "copy", +) +@doctest_needs("palantir") +def palantir( + adata: AnnData, + *, + n_components: int = 10, + knn: int = 30, + alpha: float = 0, + use_adjacency_matrix: bool = False, + distances_key: str | None = None, + n_eigs: int | None = None, + impute_data: bool = True, + n_steps: int = 3, + copy: bool = False, +) -> AnnData | None: + """Run Diffusion maps using the adaptive anisotropic kernel :cite:p:`Setty2019`. + + Palantir is an algorithm to align cells along differentiation trajectories. + Palantir models differentiation as a stochastic process where stem cells + differentiate to terminally differentiated cells by a series of steps through + a low dimensional phenotypic manifold. Palantir effectively captures the + continuity in cell states and the stochasticity in cell fate determination. + Palantir has been designed to work with multidimensional single cell data + from diverse technologies such as Mass cytometry and single cell RNA-seq. + + .. note:: + More information and bug reports `here `__. + + Parameters + ---------- + adata + An AnnData object. + n_components + Number of diffusion components. + knn + Number of nearest neighbors for graph construction. + alpha + Normalization parameter for the diffusion operator. + use_adjacency_matrix + Use adaptive anisotropic adjacency matrix, instead of PCA projections + (default) to compute diffusion components. + distances_key + With `use_adjacency_matrix=True`, use the indicated distances key for `.obsp`. + If `None`, `'distances'`. + n_eigs + Number of eigen vectors to use. If `None` specified, the number of eigen + vectors will be determined using eigen gap. Passed to + `palantir.utils.determine_multiscale_space`. + impute_data + Impute data using MAGIC. + n_steps + Number of steps in the diffusion operator. Passed to + `palantir.utils.run_magic_imputation`. + copy + Return a copy instead of writing to `adata`. + + Returns + ------- + Depending on `copy`, returns or updates `adata` with the following fields: + + **Diffusion maps**, + used for magic imputation, and to generate multi-scale data matrix, + + - X_palantir_diff_comp - :class:`~numpy.ndarray` (:attr:`~anndata.AnnData.obsm`, dtype `float`) + Array of Diffusion components. + - palantir_EigenValues - :class:`~numpy.ndarray` (:attr:`~anndata.AnnData.uns`, dtype `float`) + Array of corresponding eigen values. + - palantir_diff_op - :class:`~scipy.sparse.csr_matrix` (:attr:`~anndata.AnnData.obsp`, dtype `float`) + The diffusion operator matrix. + + **Multi scale space results**, + used to build tsne on diffusion components, and to compute branch probabilities + and waypoints, + + - X_palantir_multiscale - :class:`~numpy.ndarray` (:attr:`~anndata.AnnData.obsm`, dtype `float`) + Multi scale data matrix. + + **MAGIC imputation**, + used for plotting gene expression on tsne, and gene expression trends, + + - palantir_imp - :class:`~numpy.ndarray` (:attr:`~anndata.AnnData.layers`, dtype `float`) + Imputed data matrix (MAGIC imputation). + + Example + ------- + >>> import scanpy.external as sce + >>> import scanpy as sc + + A sample data is available `here `_. + + **Load sample data** + + >>> adata = sc.read_csv(filename="Palantir/data/marrow_sample_scseq_counts.csv.gz") + + *Cleanup and normalize* + + >>> sc.pp.filter_cells(adata, min_counts=1000) + >>> sc.pp.filter_genes(adata, min_counts=10) + >>> sc.pp.normalize_per_cell(adata) + >>> sc.pp.log1p(adata) + + **Data preprocessing** + + Palantir builds diffusion maps using one of two optional inputs: + + *Principal component analysis* + + >>> sc.pp.pca(adata, n_comps=300) + + or, + + *Nearist neighbors graph* + + >>> sc.pp.neighbors(adata, knn=30) + + *Diffusion maps* + + Palantir determines the diffusion maps of the data as an estimate of the low + dimensional phenotypic manifold of the data. + + >>> sce.tl.palantir(adata, n_components=5, knn=30) + + if pre-computed distances are to be used, + + >>> sce.tl.palantir( + ... adata, + ... n_components=5, + ... knn=30, + ... use_adjacency_matrix=True, + ... distances_key="distances", + ... ) + + **Visualizing Palantir results** + + *tSNE visualization* + + important for Palantir! + + Palantir constructs the tSNE map in the embedded space since these maps better + represent the differentiation trajectories. + + >>> sc.tl.tsne(adata, n_pcs=2, use_rep="X_palantir_multiscale", perplexity=150) + + *tsne by cell size* + + >>> sc.pl.tsne(adata, color="n_counts") + + *Imputed gene expression visualized on tSNE maps* + + >>> sc.pl.tsne( + ... adata, + ... gene_symbols=["CD34", "MPO", "GATA1", "IRF8"], + ... layer="palantir_imp", + ... color=["CD34", "MPO", "GATA1", "IRF8"], + ... ) + + **Running Palantir** + + Palantir can be run by specifying an approximate early cell. While Palantir + automatically determines the terminal states, they can also be specified using the + `termine_states` parameter. + + >>> start_cell = "Run5_164698952452459" + >>> pr_res = sce.tl.palantir_results( + ... adata, + ... early_cell=start_cell, + ... ms_data="X_palantir_multiscale", + ... num_waypoints=500, + ... ) + + .. note:: + A `start_cell` must be defined for every data set. The start cell for + this dataset was chosen based on high expression of CD34. + + At this point the returned Palantir object `pr_res` can be used for all downstream + analysis and plotting. Please consult this notebook + `Palantir_sample_notebook.ipynb + `_. + It provides a comprehensive guide to draw *gene expression trends*, amongst other + things. + + """ + _check_import() + from palantir.utils import ( + determine_multiscale_space, + run_diffusion_maps, + run_magic_imputation, + ) + + adata = adata.copy() if copy else adata + + logg.info("Palantir Diffusion Maps in progress ...") + + if use_adjacency_matrix: + df = adata.obsp[distances_key] if distances_key else adata.obsp["distances"] + else: + df = pd.DataFrame(adata.obsm["X_pca"], index=adata.obs_names) + + # Diffusion maps + dm_res = run_diffusion_maps( + df, + n_components=n_components, + knn=knn, + alpha=alpha, + ) + # Determine the multi scale space of the data + ms_data = determine_multiscale_space(dm_res=dm_res, n_eigs=n_eigs) + + # MAGIC imputation + if impute_data: + imp_df = run_magic_imputation( + data=adata.to_df(), dm_res=dm_res, n_steps=n_steps + ) + adata.layers["palantir_imp"] = imp_df + + ( + adata.obsm["X_palantir_diff_comp"], + adata.uns["palantir_EigenValues"], + adata.obsp["palantir_diff_op"], + adata.obsm["X_palantir_multiscale"], + ) = ( + dm_res["EigenVectors"].to_numpy(), + dm_res["EigenValues"].to_numpy(), + dm_res["T"], + ms_data.to_numpy(), + ) + + return adata if copy else None + + +@old_positionals( + "ms_data", + "terminal_states", + "knn", + "num_waypoints", + "n_jobs", + "scale_components", + "use_early_cell_as_start", + "max_iterations", +) +def palantir_results( + adata: AnnData, + early_cell: str, + *, + ms_data: str = "X_palantir_multiscale", + terminal_states: list | None = None, + knn: int = 30, + num_waypoints: int = 1200, + n_jobs: int = -1, + scale_components: bool = True, + use_early_cell_as_start: bool = False, + max_iterations: int = 25, +) -> AnnData | None: + """Run Palantir. + + A convenience function that wraps `palantir.core.run_palantir` to compute branch + probabilities and waypoints. + + Parameters + ---------- + adata + An AnnData object. + early_cell + Start cell for pseudotime construction. + ms_data + Palantir multi scale data matrix, + terminal_states + List of user defined terminal states + knn + Number of nearest neighbors for graph construction. + num_waypoints + Number of waypoints to sample. + n_jobs + Number of jobs for parallel processing. + scale_components + Transform features by scaling each feature to a given range. Consult the + documentation for `sklearn.preprocessing.minmax_scale`. + use_early_cell_as_start + Use `early_cell` as `start_cell`, instead of determining it from the boundary + cells closest to the defined `early_cell`. + max_iterations + Maximum number of iterations for pseudotime convergence. + + Returns + ------- + PResults object with pseudotime, entropy, branch probabilities and waypoints. + + """ + logg.info("Palantir computing waypoints..") + + _check_import() + from palantir.core import run_palantir + + ms_data = pd.DataFrame(adata.obsm[ms_data], index=adata.obs_names) + pr_res = run_palantir( + ms_data, + early_cell=early_cell, + terminal_states=terminal_states, + knn=knn, + num_waypoints=num_waypoints, + n_jobs=n_jobs, + scale_components=scale_components, + use_early_cell_as_start=use_early_cell_as_start, + max_iterations=max_iterations, + ) + + return pr_res + + +def _check_import(): + try: + import palantir # noqa: F401 + except ImportError as e: + msg = "\nplease install palantir:\n\tpip install palantir" + raise ImportError(msg) from e diff --git a/.venv/lib/python3.11/site-packages/scanpy/external/tl/_phate.py b/.venv/lib/python3.11/site-packages/scanpy/external/tl/_phate.py new file mode 100644 index 0000000000000000000000000000000000000000..16905273a801c2d0fba65aa5f71a19237589eb88 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/external/tl/_phate.py @@ -0,0 +1,183 @@ +"""Embed high-dimensional data using PHATE.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ... import logging as logg +from ..._compat import old_positionals +from ..._settings import settings +from ..._utils._doctests import doctest_needs + +if TYPE_CHECKING: + from typing import Literal + + from anndata import AnnData + + from ..._utils.random import _LegacyRandom + + +@old_positionals( + "k", + "a", + "n_landmark", + "t", + "gamma", + "n_pca", + "knn_dist", + "mds_dist", + "mds", + "n_jobs", + "random_state", + "verbose", + "copy", +) +@doctest_needs("phate") +def phate( # noqa: PLR0913 + adata: AnnData, + n_components: int = 2, + *, + k: int = 5, + a: int = 15, + n_landmark: int = 2000, + t: int | str = "auto", + gamma: float = 1.0, + n_pca: int = 100, + knn_dist: str = "euclidean", + mds_dist: str = "euclidean", + mds: Literal["classic", "metric", "nonmetric"] = "metric", + n_jobs: int | None = None, + random_state: _LegacyRandom = None, + verbose: bool | int | None = None, + copy: bool = False, + **kwargs, +) -> AnnData | None: + """PHATE :cite:p:`Moon2019`. + + Potential of Heat-diffusion for Affinity-based Trajectory Embedding (PHATE) + embeds high dimensional single-cell data into two or three dimensions for + visualization of biological progressions. + + For more information and access to the object-oriented interface, read the + `PHATE documentation `__. For + tutorials, bug reports, and R/MATLAB implementations, visit the `PHATE + GitHub page `__. For help + using PHATE, go `here `__. + + Parameters + ---------- + adata + Annotated data matrix. + n_components + number of dimensions in which the data will be embedded + k + number of nearest neighbors on which to build kernel + a + sets decay rate of kernel tails. + If None, alpha decaying kernel is not used + n_landmark + number of landmarks to use in fast PHATE + t + power to which the diffusion operator is powered + sets the level of diffusion. If 'auto', t is selected + according to the knee point in the Von Neumann Entropy of + the diffusion operator + gamma + Informational distance constant between -1 and 1. + `gamma=1` gives the PHATE log potential, `gamma=0` gives + a square root potential. + n_pca + Number of principal components to use for calculating + neighborhoods. For extremely large datasets, using + n_pca < 20 allows neighborhoods to be calculated in + log(n_samples) time. + knn_dist + recommended values: 'euclidean' and 'cosine' + Any metric from `scipy.spatial.distance` can be used + distance metric for building kNN graph + mds_dist + recommended values: 'euclidean' and 'cosine' + Any metric from `scipy.spatial.distance` can be used + distance metric for MDS + mds + Selects which MDS algorithm is used for dimensionality reduction. + n_jobs + The number of jobs to use for the computation. + If `None`, `sc.settings.n_jobs` is used. + If -1 all CPUs are used. If 1 is given, no parallel computing code is + used at all, which is useful for debugging. + For n_jobs below -1, (n_cpus + 1 + n_jobs) are used. Thus for + n_jobs = -2, all CPUs but one are used + random_state + Random seed. Defaults to the global `numpy` random number generator + verbose + If `True` or an :class:`int`/:class:`~scanpy.Verbosity` ≥ 2/:attr:`~scanpy.Verbosity.hint`, print status messages. + If `None`, :attr:`scanpy.settings.verbosity` is used. + copy + Return a copy instead of writing to `adata`. + kwargs + Additional arguments to `phate.PHATE` + + Returns + ------- + Depending on `copy`, returns or updates `adata` with the following fields. + + **X_phate** : `np.ndarray`, (`adata.obs`, shape=[n_samples, n_components], dtype `float`) + PHATE coordinates of data. + + Examples + -------- + >>> from anndata import AnnData + >>> import scanpy.external as sce + >>> import phate + >>> tree_data, tree_clusters = phate.tree.gen_dla( + ... n_dim=100, + ... n_branch=20, + ... branch_length=100, + ... ) + >>> tree_data.shape + (2000, 100) + >>> adata = AnnData(tree_data) + >>> sce.tl.phate(adata, k=5, a=20, t=150) + >>> adata.obsm["X_phate"].shape + (2000, 2) + >>> sce.pl.phate(adata) + + """ + start = logg.info("computing PHATE") + adata = adata.copy() if copy else adata + verbosity = settings.verbosity if verbose is None else verbose + verbose = verbosity if isinstance(verbosity, bool) else verbosity >= 2 + n_jobs = settings.n_jobs if n_jobs is None else n_jobs + try: + import phate + except ImportError as e: + msg = ( + "You need to install the package `phate`: please run `pip install " + "--user phate` in a terminal." + ) + raise ImportError(msg) from e + x_phate = phate.PHATE( + n_components=n_components, + k=k, + a=a, + n_landmark=n_landmark, + t=t, + gamma=gamma, + n_pca=n_pca, + knn_dist=knn_dist, + mds_dist=mds_dist, + mds=mds, + n_jobs=n_jobs, + random_state=random_state, + verbose=verbose, + **kwargs, + ).fit_transform(adata) + # update AnnData instance + adata.obsm["X_phate"] = x_phate # annotate samples with PHATE coordinates + logg.info( + " finished", + time=start, + deep=("added\n 'X_phate', PHATE coordinates (adata.obsm)"), + ) + return adata if copy else None diff --git a/.venv/lib/python3.11/site-packages/scanpy/external/tl/_phenograph.py b/.venv/lib/python3.11/site-packages/scanpy/external/tl/_phenograph.py new file mode 100644 index 0000000000000000000000000000000000000000..4ccb9df105e0fa378fc26bfbfd7208c8664c7dd6 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/external/tl/_phenograph.py @@ -0,0 +1,291 @@ +"""Perform clustering using PhenoGraph.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pandas as pd +from anndata import AnnData +from packaging.version import Version + +from ... import logging as logg +from ..._compat import old_positionals, pkg_version +from ..._utils import renamed_arg +from ..._utils._doctests import doctest_needs + +if TYPE_CHECKING: + from typing import Any, Literal + + import numpy as np + + from ..._compat import SpBase + from ...tools._leiden import MutableVertexPartition + + +@renamed_arg("adata", "data", pos_0=True) +@old_positionals( + "k", + "directed", + "prune", + "min_cluster_size", + "jaccard", + "primary_metric", + "n_jobs", + "q_tol", + "louvain_time_limit", + "nn_method", + "partition_type", + "resolution_parameter", + "n_iterations", + "use_weights", + "seed", + "copy", +) +@doctest_needs("phenograph") +def phenograph( # noqa: PLR0913 + data: AnnData | np.ndarray | SpBase, + clustering_algo: Literal["louvain", "leiden"] | None = "louvain", + *, + k: int = 30, + directed: bool = False, + prune: bool = False, + min_cluster_size: int = 10, + jaccard: bool = True, + primary_metric: Literal[ + "euclidean", + "manhattan", + "correlation", + "cosine", + ] = "euclidean", + n_jobs: int = -1, + q_tol: float = 1e-3, + louvain_time_limit: int = 2000, + nn_method: Literal["kdtree", "brute"] = "kdtree", + partition_type: type[MutableVertexPartition] | None = None, + resolution_parameter: float = 1, + n_iterations: int = -1, + use_weights: bool = True, + seed: int | None = None, + copy: bool = False, + **kargs: Any, +) -> tuple[np.ndarray | None, SpBase, float | None] | None: + """PhenoGraph clustering :cite:p:`Levine2015`. + + **PhenoGraph** is a clustering method designed for high-dimensional single-cell + data. It works by creating a graph ("network") representing phenotypic similarities + between cells and then identifying communities in this graph. It supports both + Louvain_ and Leiden_ algorithms for community detection. + + .. _Louvain: https://louvain-igraph.readthedocs.io/en/latest/ + + .. _Leiden: https://leidenalg.readthedocs.io/en/latest/reference.html + + .. note:: + More information and bug reports `here + `__. + + Parameters + ---------- + data + AnnData, or Array of data to cluster, or sparse matrix of k-nearest neighbor + graph. If ndarray, n-by-d array of n cells in d dimensions. if sparse matrix, + n-by-n adjacency matrix. + clustering_algo + Choose between `'Louvain'` or `'Leiden'` algorithm for clustering. + k + Number of nearest neighbors to use in first step of graph construction. + directed + Whether to use a symmetric (default) or asymmetric (`'directed'`) graph. + The graph construction process produces a directed graph, which is symmetrized + by one of two methods (see `prune` below). + prune + `prune=False`, symmetrize by taking the average between the graph and its + transpose. `prune=True`, symmetrize by taking the product between the graph + and its transpose. + min_cluster_size + Cells that end up in a cluster smaller than min_cluster_size are considered + outliers and are assigned to -1 in the cluster labels. + jaccard + If `True`, use Jaccard metric between k-neighborhoods to build graph. If + `False`, use a Gaussian kernel. + primary_metric + Distance metric to define nearest neighbors. Note that performance will be + slower for correlation and cosine. + n_jobs + Nearest Neighbors and Jaccard coefficients will be computed in parallel using + n_jobs. If 1 is given, no parallelism is used. If set to -1, all CPUs are used. + For n_jobs below -1, `n_cpus + 1 + n_jobs` are used. + q_tol + Tolerance, i.e. precision, for monitoring modularity optimization. + louvain_time_limit + Maximum number of seconds to run modularity optimization. If exceeded the best + result so far is returned. + nn_method + Whether to use brute force or kdtree for nearest neighbor search. + For very large high-dimensional data sets, brute force, with parallel + computation, performs faster than kdtree. + partition_type + Defaults to :class:`~leidenalg.RBConfigurationVertexPartition`. For the + available options, consult the documentation for + :func:`~leidenalg.find_partition`. + resolution_parameter + A parameter value controlling the coarseness of the clustering in Leiden. Higher + values lead to more clusters. Set to `None` if overriding `partition_type` to + one that does not accept a `resolution_parameter`. + n_iterations + Number of iterations to run the Leiden algorithm. If the number of iterations is + negative, the Leiden algorithm is run until an iteration in which there was no + improvement. + use_weights + Use vertices in the Leiden computation. + seed + Leiden initialization of the optimization. + copy + Return a copy or write to `adata`. + kargs + Additional arguments passed to :func:`~leidenalg.find_partition` and the + constructor of the `partition_type`. + + Returns + ------- + Depending on `copy`, returns or updates `adata` with the following fields: + + **communities** - :class:`~numpy.ndarray` (:attr:`~anndata.AnnData.obs`, dtype `int`) + integer array of community assignments for each row in data. + + **graph** - :class:`~scipy.sparse.spmatrix` (:attr:`~anndata.AnnData.obsp`, dtype `float`) + the graph that was used for clustering. + + **Q** - `float` (:attr:`~anndata.AnnData.uns`, dtype `float`) + the modularity score for communities on graph. + + Example + ------- + >>> from anndata import AnnData + >>> import scanpy as sc + >>> import scanpy.external as sce + >>> import numpy as np + >>> import pandas as pd + + With annotated data as input: + + >>> adata = sc.datasets.pbmc3k() + >>> sc.pp.normalize_per_cell(adata) + + Then do PCA: + + >>> sc.pp.pca(adata, n_comps=100) + + Compute phenograph clusters: + + **Louvain** community detection + + >>> sce.tl.phenograph(adata, clustering_algo="louvain", k=30) + + **Leiden** community detection + + >>> sce.tl.phenograph(adata, clustering_algo="leiden", k=30) + + Return only `Graph` object + + >>> sce.tl.phenograph(adata, clustering_algo=None, k=30) + + Now to show phenograph on tSNE (for example): + + Compute tSNE: + + >>> sc.tl.tsne(adata, random_state=7) + + Plot phenograph clusters on tSNE: + + >>> sc.pl.tsne( + ... adata, + ... color=["pheno_louvain", "pheno_leiden"], + ... s=100, + ... palette=sc.pl.palettes.vega_20_scanpy, + ... legend_fontsize=10, + ... ) + + Cluster and cluster centroids for input Numpy ndarray + + >>> df = np.random.rand(1000, 40) + >>> dframe = pd.DataFrame(df) + >>> dframe.index, dframe.columns = ( + ... map(str, dframe.index), + ... map(str, dframe.columns), + ... ) + >>> adata = AnnData(dframe) + >>> sc.pp.pca(adata, n_comps=20) + >>> sce.tl.phenograph(adata, clustering_algo="leiden", k=50) + >>> sc.tl.tsne(adata, random_state=1) + >>> sc.pl.tsne( + ... adata, + ... color=["pheno_leiden"], + ... s=100, + ... palette=sc.pl.palettes.vega_20_scanpy, + ... legend_fontsize=10, + ... ) + + """ + start = logg.info("PhenoGraph clustering") + + try: + import phenograph + + assert pkg_version("phenograph") >= Version("1.5.3") + except (ImportError, AssertionError, AttributeError) as e: + msg = ( + "please install the latest release of phenograph:\n\t" + "pip install -U PhenoGraph" + ) + raise ImportError(msg) from e + + if isinstance(data, AnnData): + adata = data + try: + data = data.obsm["X_pca"] + except KeyError as e: + msg = "Please run `sc.pp.pca` on `data` and try again!" + raise KeyError(msg) from e + else: + adata = None + copy = True + + comm_key = ( + f"pheno_{clustering_algo}" if clustering_algo in ["louvain", "leiden"] else "" + ) + ig_key = f"pheno_{'jaccard' if jaccard else 'gaussian'}_ig" + q_key = f"pheno_{'jaccard' if jaccard else 'gaussian'}_q" + + communities, graph, q = phenograph.cluster( + data=data, + clustering_algo=clustering_algo, + k=k, + directed=directed, + prune=prune, + min_cluster_size=min_cluster_size, + jaccard=jaccard, + primary_metric=primary_metric, + n_jobs=n_jobs, + q_tol=q_tol, + louvain_time_limit=louvain_time_limit, + nn_method=nn_method, + partition_type=partition_type, + resolution_parameter=resolution_parameter, + n_iterations=n_iterations, + use_weights=use_weights, + seed=seed, + **kargs, + ) + + logg.info(" finished", time=start) + + if copy: + return communities, graph, q + + if adata is not None: + adata.obsp[ig_key] = graph.tocsr() + if comm_key: + adata.obs[comm_key] = pd.Categorical(communities) + if q: + adata.uns[q_key] = q diff --git a/.venv/lib/python3.11/site-packages/scanpy/external/tl/_pypairs.py b/.venv/lib/python3.11/site-packages/scanpy/external/tl/_pypairs.py new file mode 100644 index 0000000000000000000000000000000000000000..d98d7ca07647d63df2a534fc8de37f257c50ebf6 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/external/tl/_pypairs.py @@ -0,0 +1,160 @@ +"""Calculate scores based on relative expression change of maker pairs.""" + +from __future__ import annotations + +from importlib.util import find_spec +from typing import TYPE_CHECKING + +from packaging.version import Version + +from ..._compat import pkg_version +from ..._settings import settings +from ..._utils._doctests import doctest_needs + +if TYPE_CHECKING: + from collections.abc import Collection, Mapping + + import pandas as pd + from anndata import AnnData + + Genes = Collection[str | int | bool] + + +@doctest_needs("pypairs") +def sandbag( + adata: AnnData, + annotation: Mapping[str, Genes] | None = None, + *, + fraction: float = 0.65, + filter_genes: Genes | None = None, + filter_samples: Genes | None = None, +) -> dict[str, list[tuple[str, str]]]: + """Calculate marker pairs of genes :cite:p:`Scialdone2015,Fechtner2018`. + + Calculates the pairs of genes serving as marker pairs for each phase, + based on a matrix of gene counts and an annotation of known phases. + + This reproduces the approach of :cite:t:`Scialdone2015` in the implementation of + :cite:t:`Fechtner2018`. + + More information and bug reports `here + `__. + + Parameters + ---------- + adata + The annotated data matrix. + annotation + Mapping from category to genes, e.g. `{'phase': [Gene1, ...]}`. + Defaults to ``data.vars['category']``. + fraction + Fraction of cells per category where marker criteria must be satisfied. + filter_genes + Genes for sampling the reference set. Defaults to all genes. + filter_samples + Cells for sampling the reference set. Defaults to all samples. + + Returns + ------- + A dict mapping from category to lists of marker pairs, e.g.: + `{'Category_1': [(Gene_1, Gene_2), ...], ...}`. + + Examples + -------- + >>> from scanpy.external.tl import sandbag + >>> from pypairs import datasets + >>> adata = datasets.leng15() + >>> marker_pairs = sandbag(adata, fraction=0.5) + + """ + _check_available() + from pypairs import settings as pp_settings + from pypairs.pairs import sandbag + + pp_settings.verbosity = settings.verbosity + pp_settings.n_jobs = settings.n_jobs + pp_settings.writedir = settings.writedir + pp_settings.cachedir = settings.cachedir + pp_settings.logfile = settings.logfile + + return sandbag( + data=adata, + annotation=annotation, + fraction=fraction, + filter_genes=filter_genes, + filter_samples=filter_samples, + ) + + +def cyclone( + adata: AnnData, + marker_pairs: Mapping[str, Collection[tuple[str, str]]] | None = None, + *, + iterations: int = 1000, + min_iter: int = 100, + min_pairs: int = 50, +) -> pd.DataFrame: + """Assign scores and predicted class to observations :cite:p:`Scialdone2015` :cite:p:`Fechtner2018`. + + Calculates scores for each observation and each phase and assigns prediction + based on marker pairs indentified by :func:`~scanpy.external.tl.sandbag`. + + This reproduces the approach of :cite:t:`Scialdone2015` in the implementation of + :cite:t:`Fechtner2018`. + + Parameters + ---------- + adata + The annotated data matrix. + marker_pairs + Mapping of categories to lists of marker pairs. + See :func:`~scanpy.external.tl.sandbag` output. + iterations + An integer scalar specifying the number of + iterations for random sampling to obtain a cycle score. + min_iter + An integer scalar specifying the minimum number of iterations + for score estimation. + min_pairs + An integer scalar specifying the minimum number of pairs + for score estimation. + + Returns + ------- + A :class:`~pandas.DataFrame` with samples as index and categories as columns + with scores for each category for each sample and a additional column with + the name of the max scoring category for each sample. + + If `marker_pairs` contains only the cell cycle categories G1, S and G2M an + additional column `pypairs_cc_prediction` will be added. + Where category S is assigned to samples where G1 and G2M score are < 0.5. + + """ + _check_available() + from pypairs import settings as pp_settings + from pypairs.pairs import cyclone + + pp_settings.verbosity = settings.verbosity + pp_settings.n_jobs = settings.n_jobs + pp_settings.writedir = settings.writedir + pp_settings.cachedir = settings.cachedir + pp_settings.logfile = settings.logfile + + return cyclone( + data=adata, + marker_pairs=marker_pairs, + iterations=iterations, + min_iter=min_iter, + min_pairs=min_pairs, + ) + + +def _check_available() -> None: + if not find_spec("pypairs"): + msg = "You need to install the package `pypairs`." + raise ImportError(msg) + + min_version = Version("3.0.9") + if pkg_version("pypairs") < min_version: + msg = f"Please only use `pypairs` >= {min_version}" + raise ImportError(msg) diff --git a/.venv/lib/python3.11/site-packages/scanpy/external/tl/_sam.py b/.venv/lib/python3.11/site-packages/scanpy/external/tl/_sam.py new file mode 100644 index 0000000000000000000000000000000000000000..e805d4b26fb5284ee8be29f86d0d093a03a9d495 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/external/tl/_sam.py @@ -0,0 +1,240 @@ +"""Run the Self-Assembling Manifold algorithm.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ... import logging as logg +from ..._compat import old_positionals +from ..._utils._doctests import doctest_needs + +if TYPE_CHECKING: + from typing import Literal + + from anndata import AnnData + from samalg import SAM + + +@old_positionals( + "max_iter", + "num_norm_avg", + "k", + "distance", + "standardization", + "weight_pcs", + "sparse_pca", + "n_pcs", + "n_genes", + "projection", + "inplace", + "verbose", +) +@doctest_needs("samalg") +def sam( # noqa: PLR0913 + adata: AnnData, + *, + max_iter: int = 10, + num_norm_avg: int = 50, + k: int = 20, + distance: str = "correlation", + standardization: Literal["Normalizer", "StandardScaler", "None"] = "StandardScaler", + weight_pcs: bool = False, + sparse_pca: bool = False, + n_pcs: int | None = 150, + n_genes: int | None = 3000, + projection: Literal["umap", "tsne", "None"] = "umap", + inplace: bool = True, + verbose: bool = True, +) -> SAM | tuple[SAM, AnnData]: + """Self-Assembling Manifolds single-cell RNA sequencing analysis tool :cite:p:`Tarashansky2019`. + + SAM iteratively rescales the input gene expression matrix to emphasize + genes that are spatially variable along the intrinsic manifold of the data. + It outputs the gene weights, nearest neighbor matrix, and a 2D projection. + + The AnnData input should contain unstandardized, non-negative values. + Preferably, the data should be log-normalized and no genes should be filtered out. + + + Parameters + ---------- + k + The number of nearest neighbors to identify for each cell. + + distance + The distance metric to use when identifying nearest neighbors. + Can be any of the distance metrics supported by + :func:`~scipy.spatial.distance.pdist`. + + max_iter + The maximum number of iterations SAM will run. + + projection + If 'tsne', generates a t-SNE embedding. If 'umap', generates a UMAP + embedding. If 'None', no embedding will be generated. + + standardization + If 'Normalizer', use sklearn.preprocessing.Normalizer, which + normalizes expression data prior to PCA such that each cell has + unit L2 norm. If 'StandardScaler', use + sklearn.preprocessing.StandardScaler, which normalizes expression + data prior to PCA such that each gene has zero mean and unit + variance. Otherwise, do not normalize the expression data. We + recommend using 'StandardScaler' for large datasets with many + expected cell types and 'Normalizer' otherwise. If 'None', no + transformation is applied. + + num_norm_avg + The top 'num_norm_avg' dispersions are averaged to determine the + normalization factor when calculating the weights. This prevents + genes with large spatial dispersions from skewing the distribution + of weights. + + weight_pcs + If True, scale the principal components by their eigenvalues. In + datasets with many expected cell types, setting this to False might + improve the resolution as these cell types might be encoded by lower- + variance principal components. + + sparse_pca + If True, uses an implementation of PCA that accepts sparse inputs. + This way, we no longer need a temporary dense copy of the sparse data. + However, this implementation is slower and so is only worth using when + memory constraints become noticeable. + + n_pcs + Determines the number of top principal components selected at each + iteration of the SAM algorithm. If None, this number is chosen + automatically based on the size of the dataset. If weight_pcs is + set to True, this parameter primarily affects the runtime of the SAM + algorithm (more PCs = longer runtime). + + n_genes + Determines the number of top SAM-weighted genes to use at each iteration + of the SAM algorithm. If None, this number is chosen automatically + based on the size of the dataset. This parameter primarily affects + the runtime of the SAM algorithm (more genes = longer runtime). For + extremely homogeneous datasets, decreasing `n_genes` may improve + clustering resolution. + + inplace + Set fields in `adata` if True. Otherwise, returns a copy. + + verbose + If True, displays SAM log statements. + + Returns + ------- + sam_obj if inplace is True or (sam_obj,AnnData) otherwise + + adata - AnnData + `.var['weights']` + SAM weights for each gene. + `.var['spatial_dispersions']` + Spatial dispersions for each gene (these are used to compute the + SAM weights) + `.uns['sam']` + Dictionary of SAM-specific outputs, such as the parameters + used for preprocessing ('preprocess_args') and running + ('run_args') SAM. + `.uns['neighbors']` + A dictionary with key 'connectivities' containing the kNN adjacency + matrix output by SAM. If built-in scanpy dimensionality reduction + methods are to be used using the SAM-output AnnData, users + should recompute the neighbors using `.obs['X_pca']` with + `scanpy.pp.neighbors`. + `.obsm['X_pca']` + The principal components output by SAM. + `.obsm['X_umap']` + The UMAP projection output by SAM. + `.layers['X_disp']` + The expression matrix used for nearest-neighbor averaging. + `.layers['X_knn_avg']` + The nearest-neighbor-averaged expression data used for computing the + spatial dispersions of genes. + + Example + ------- + >>> import scanpy.external as sce + >>> import scanpy as sc + + *** Running SAM *** + + Assuming we are given an AnnData object called `adata`, we can run the SAM + algorithm as follows: + + >>> sam_obj = sce.tl.sam(adata, inplace=True) + + The input AnnData object should contain unstandardized, non-negative + expression values. Preferably, the data should be log-normalized and no + genes should be filtered out. + + Please see the documentation for a description of all available parameters. + + For more detailed tutorials, please visit the original Github repository: + https://github.com/atarashansky/self-assembling-manifold/tree/master/tutorial + + *** Plotting *** + + To visualize the output, we can use: + + >>> sce.pl.sam(adata, projection="X_umap") + + `sce.pl.sam` accepts all keyword arguments used in the + `matplotlib.pyplot.scatter` function. + + *** SAMGUI *** + + SAM comes with the SAMGUI module, a graphical-user interface written with + `Plotly` and `ipythonwidgets` for interactively exploring and annotating + the scRNAseq data and running SAM. + + Dependencies can be installed with Anaconda by following the instructions in + the self-assembling-manifold Github README: + https://github.com/atarashansky/self-assembling-manifold + + In a Jupyter notebook, execute the following to launch the interface: + + >>> from samalg.gui import SAMGUI + >>> sam_gui = SAMGUI(sam_obj) # sam_obj is your SAM object + >>> sam_gui.SamPlot + + This can also be enabled in Jupyer Lab by following the instructions in the + self-assembling-manifold README. + + """ + try: + from samalg import SAM + except ImportError as e: + msg = ( + "\nplease install sam-algorithm: \n\n" + "\tgit clone git://github.com/atarashansky/self-assembling-manifold.git\n" + "\tcd self-assembling-manifold\n" + "\tpip install ." + ) + raise ImportError(msg) from e + + logg.info("Self-assembling manifold") + + s = SAM(counts=adata, inplace=inplace) + + logg.info("Running SAM") + s.run( + max_iter=max_iter, + num_norm_avg=num_norm_avg, + k=k, + distance=distance, + preprocessing=standardization, + weight_PCs=weight_pcs, + npcs=n_pcs, + n_genes=n_genes, + projection=projection, + sparse_pca=sparse_pca, + verbose=verbose, + ) + + s.adata.uns["sam"] = {} + for attr in ["nnm", "preprocess_args", "run_args", "ranked_genes"]: + s.adata.uns["sam"][attr] = s.adata.uns.pop(attr, None) + + return s if inplace else (s, s.adata) diff --git a/.venv/lib/python3.11/site-packages/scanpy/external/tl/_trimap.py b/.venv/lib/python3.11/site-packages/scanpy/external/tl/_trimap.py new file mode 100644 index 0000000000000000000000000000000000000000..b670863f132b7749e03d012de82795e4cbaef50c --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/external/tl/_trimap.py @@ -0,0 +1,143 @@ +"""Embed high-dimensional data using TriMap.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ... import logging as logg +from ..._compat import CSBase, old_positionals +from ..._settings import settings +from ..._utils._doctests import doctest_needs + +if TYPE_CHECKING: + from typing import Literal + + from anndata import AnnData + + +@old_positionals( + "n_inliers", + "n_outliers", + "n_random", + "metric", + "weight_adj", + "lr", + "n_iters", + "verbose", + "copy", +) +@doctest_needs("trimap") +def trimap( # noqa: PLR0913 + adata: AnnData, + n_components: int = 2, + *, + n_inliers: int = 10, + n_outliers: int = 5, + n_random: int = 5, + metric: Literal["angular", "euclidean", "hamming", "manhattan"] = "euclidean", + weight_adj: float = 500.0, + lr: float = 1000.0, + n_iters: int = 400, + verbose: bool | int | None = None, + copy: bool = False, +) -> AnnData | None: + """TriMap: Large-scale Dimensionality Reduction Using Triplets :cite:p:`Amid2019`. + + TriMap is a dimensionality reduction method that uses triplet constraints + to form a low-dimensional embedding of a set of points. The triplet + constraints are of the form "point i is closer to point j than point k". + The triplets are sampled from the high-dimensional representation of the + points and a weighting scheme is used to reflect the importance of each + triplet. + + TriMap provides a significantly better global view of the data than the + other dimensionality reduction methods such t-SNE, LargeVis, and UMAP. + The global structure includes relative distances of the clusters, multiple + scales in the data, and the existence of possible outliers. We define a + global score to quantify the quality of an embedding in reflecting the + global structure of the data. + + Parameters + ---------- + adata + Annotated data matrix. + n_components + Number of dimensions of the embedding. + n_inliers + Number of inlier points for triplet constraints. + n_outliers + Number of outlier points for triplet constraints. + n_random + Number of random triplet constraints per point. + metric + Distance measure: 'angular', 'euclidean', 'hamming', 'manhattan'. + weight_adj + Adjusting the weights using a non-linear transformation. + lr + Learning rate. + n_iters + Number of iterations. + verbose + If `True`, print the progress report. + If `None`, `sc.settings.verbosity` is used. + copy + Return a copy instead of writing to `adata`. + + Returns + ------- + Depending on `copy`, returns or updates `adata` with the following fields. + + **X_trimap** : :class:`~numpy.ndarray`, (:attr:`~anndata.AnnData.obsm`, shape=(n_samples, n_components), dtype `float`) + TriMap coordinates of data. + + Example + ------- + + >>> import scanpy as sc + >>> import scanpy.external as sce + >>> pbmc = sc.datasets.pbmc68k_reduced() + >>> pbmc = sce.tl.trimap(pbmc, copy=True) + >>> sce.pl.trimap(pbmc, color=["bulk_labels"], s=10) + + """ + try: + from trimap import TRIMAP + except ImportError as e: + msg = "\nplease install trimap: \n\n\tsudo pip install trimap" + raise ImportError(msg) from e + adata = adata.copy() if copy else adata + start = logg.info("computing TriMap") + adata = adata.copy() if copy else adata + verbosity = settings.verbosity if verbose is None else verbose + verbose = verbosity if isinstance(verbosity, bool) else verbosity > 0 + + if "X_pca" in adata.obsm: + n_dim_pca = adata.obsm["X_pca"].shape[1] + x = adata.obsm["X_pca"][:, : min(n_dim_pca, 100)] + else: + x = adata.X + if isinstance(x, CSBase): + msg = ( + "trimap currently does not support sparse matrices. Please" + "use a dense matrix or apply pca first." + ) + raise ValueError(msg) + logg.warning("`X_pca` not found. Run `sc.pp.pca` first for speedup.") + x_trimap = TRIMAP( + n_dims=n_components, + n_inliers=n_inliers, + n_outliers=n_outliers, + n_random=n_random, + lr=lr, + distance=metric, + weight_adj=weight_adj, + n_iters=n_iters, + verbose=verbose, + ).fit_transform(x) + adata.obsm["X_trimap"] = x_trimap + logg.info( + " finished", + time=start, + deep="added\n 'X_trimap', TriMap coordinates (adata.obsm)", + ) + return adata if copy else None diff --git a/.venv/lib/python3.11/site-packages/scanpy/external/tl/_wishbone.py b/.venv/lib/python3.11/site-packages/scanpy/external/tl/_wishbone.py new file mode 100644 index 0000000000000000000000000000000000000000..0fff78dec25e6a157f010621c14bb82a9bd38472 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/external/tl/_wishbone.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +from collections.abc import Collection +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd + +from ... import logging +from ..._compat import old_positionals +from ..._utils._doctests import doctest_needs + +if TYPE_CHECKING: + from collections.abc import Iterable + + from anndata import AnnData + + +@old_positionals("branch", "k", "components", "num_waypoints") +@doctest_needs("wishbone") +def wishbone( + adata: AnnData, + start_cell: str, + *, + branch: bool = True, + k: int = 15, + components: Iterable[int] = (1, 2, 3), + num_waypoints: int | Collection = 250, +): + """Identify bifurcating developmental trajectories from single-cell data :cite:p:`Setty2016`. + + Wishbone is an algorithm for positioning single cells along bifurcating + developmental trajectories with high resolution. Wishbone uses multi-dimensional + single-cell data, such as mass cytometry or RNA-Seq data, as input and orders cells + according to their developmental progression, and it pinpoints bifurcation points + by labeling each cell as pre-bifurcation or as one of two post-bifurcation cell + fates. + + .. note:: + More information and bug reports `here + `__. + + Parameters + ---------- + adata + Annotated data matrix. + start_cell + Desired start cell from `obs_names`. + branch + Use True for Wishbone and False for Wanderlust. + k + Number of nearest neighbors for graph construction. + components + Components to use for running Wishbone. + num_waypoints + Number of waypoints to sample. + + Returns + ------- + Updates `adata` with the following fields: + + `trajectory_wishbone` : (`adata.obs`, dtype `float64`) + Computed trajectory positions. + `branch_wishbone` : (`adata.obs`, dtype `int64`) + Assigned branches. + + Example + ------- + + >>> import scanpy.external as sce + >>> import scanpy as sc + + **Loading Data and Pre-processing** + + >>> adata = sc.datasets.pbmc3k() + >>> sc.pp.normalize_per_cell(adata) + >>> sc.pp.pca(adata) + >>> sc.tl.tsne(adata=adata, n_pcs=5, perplexity=30) + >>> sc.pp.neighbors(adata, n_pcs=15, n_neighbors=10) + >>> sc.tl.diffmap(adata, n_comps=10) + + **Running Wishbone Core Function** + + Usually, the start cell for a dataset should be chosen based on high expression of + the gene of interest: + + >>> sce.tl.wishbone( + ... adata=adata, start_cell='ACAAGAGACTTATC-1', + ... components=[2, 3], num_waypoints=150, + ... ) + + **Visualizing Wishbone results** + + >>> sc.pl.tsne(adata, color=['trajectory_wishbone', 'branch_wishbone']) + >>> markers = ['C1QA', 'PSAP', 'CD79A', 'CD79B', 'CST3', 'LYZ', 'MALAT1'] + >>> sce.pl.wishbone_marker_trajectory(adata, markers, show=True) + + For further demonstration of Wishbone methods and visualization please follow the + notebooks in the package `Wishbone_for_single_cell_RNAseq.ipynb + `_.\ + + """ + try: + from wishbone.core import wishbone as c_wishbone + except ImportError as e: + msg = "\nplease install wishbone:\n\n\thttps://github.com/dpeerlab/wishbone" + raise ImportError(msg) from e + + # Start cell index + s = np.where(adata.obs_names == start_cell)[0] + if len(s) == 0: + msg = ( + f"Start cell {start_cell} not found in data. " + "Please rerun with correct start cell." + ) + raise RuntimeError(msg) + if isinstance(num_waypoints, Collection): + diff = np.setdiff1d(num_waypoints, adata.obs.index) + if diff.size > 0: + logging.warning( + "Some of the specified waypoints are not in the data. " + "These will be removed" + ) + num_waypoints = diff.tolist() + elif num_waypoints > adata.shape[0]: + msg = ( + "num_waypoints parameter is higher than the number of cells in the " + "dataset. Please select a smaller number" + ) + raise RuntimeError(msg) + s = s[0] + + # Run the algorithm + components = list(components) + res = c_wishbone( + adata.obsm["X_diffmap"][:, components], + s=s, + k=k, + l=k, + num_waypoints=num_waypoints, + branch=branch, + ) + + # Assign results + trajectory = res["Trajectory"] + trajectory = (trajectory - np.min(trajectory)) / ( + np.max(trajectory) - np.min(trajectory) + ) + adata.obs["trajectory_wishbone"] = np.asarray(trajectory) + + # branch_ = None + if branch: + branches = res["Branches"].astype(int) + adata.obs["branch_wishbone"] = np.asarray(branches) + + +def _anndata_to_wishbone(adata: AnnData): + from wishbone.wb import SCData, Wishbone + + scdata = SCData(adata.to_df()) + scdata.diffusion_eigenvectors = pd.DataFrame( + adata.obsm["X_diffmap"], index=adata.obs_names + ) + wb = Wishbone(scdata) + wb.trajectory = adata.obs["trajectory_wishbone"] + wb.branch = adata.obs["branch_wishbone"] + return wb diff --git a/.venv/lib/python3.11/site-packages/scanpy/get/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/get/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..18ff8342297f898bdc4c015b5e7bebbc06acffff --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/get/__init__.py @@ -0,0 +1,23 @@ +"""Get data from AnnData.""" + +from __future__ import annotations + +from ._aggregated import aggregate +from .get import ( + _check_mask, + _get_obs_rep, + _set_obs_rep, + obs_df, + rank_genes_groups_df, + var_df, +) + +__all__ = [ + "_check_mask", + "_get_obs_rep", + "_set_obs_rep", + "aggregate", + "obs_df", + "rank_genes_groups_df", + "var_df", +] diff --git a/.venv/lib/python3.11/site-packages/scanpy/get/_aggregated.py b/.venv/lib/python3.11/site-packages/scanpy/get/_aggregated.py new file mode 100644 index 0000000000000000000000000000000000000000..fc6c44746f6f648ed44b61cb6ec31e129f3e489b --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/get/_aggregated.py @@ -0,0 +1,446 @@ +from __future__ import annotations + +from functools import singledispatch +from typing import TYPE_CHECKING, Literal + +import numpy as np +import pandas as pd +from anndata import AnnData, utils +from scipy import sparse +from sklearn.utils.sparsefuncs import csc_median_axis_0 + +from .._compat import CSBase, _register_union +from .._utils import _resolve_axis, get_literal_vals +from .get import _check_mask + +if TYPE_CHECKING: + from collections.abc import Collection, Iterable + + from numpy.typing import NDArray + + Array = np.ndarray | CSBase + +# Used with get_literal_vals +AggType = Literal["count_nonzero", "mean", "sum", "var", "median"] + + +class Aggregate: + """Functionality for generic grouping and aggregating. + + There is currently support for count_nonzero, sum, mean, and variance. + + **Implementation** + + Moments are computed using weighted sum aggregation of data by some feature + via multiplication by a sparse coordinate matrix A. + + Runtime is effectively computation of the product `A @ X`, i.e. the count of (non-zero) + entries in X with multiplicity the number of group memberships for that entry. + This is `O(data)` for partitions (each observation belonging to exactly one group), + independent of the number of groups. + + Params + ------ + groupby + :class:`~pandas.Categorical` containing values for grouping by. + data + Data matrix for aggregation. + mask + Mask to be used for aggregation. + """ + + def __init__( + self, + groupby: pd.Categorical, + data: Array, + *, + mask: NDArray[np.bool_] | None = None, + ) -> None: + self.groupby = groupby + self.indicator_matrix = sparse_indicator(groupby, mask=mask) + self.data = data + + groupby: pd.Categorical + indicator_matrix: sparse.coo_matrix + data: Array + + def count_nonzero(self) -> NDArray[np.integer]: + """Count the number of observations in each group. + + Returns + ------- + Array of counts. + + """ + # pattern = self.data._with_data(np.broadcast_to(1, len(self.data.data))) + # return self.indicator_matrix @ pattern + return utils.asarray(self.indicator_matrix @ (self.data != 0)) + + def sum(self) -> Array: + """Compute the sum per feature per group of observations. + + Returns + ------- + Array of sum. + + """ + return utils.asarray(self.indicator_matrix @ self.data) + + def mean(self) -> Array: + """Compute the mean per feature per group of observations. + + Returns + ------- + Array of mean. + + """ + return ( + utils.asarray(self.indicator_matrix @ self.data) + / np.bincount(self.groupby.codes)[:, None] + ) + + def mean_var(self, dof: int = 1) -> tuple[np.ndarray, np.ndarray]: + """Compute the count, as well as mean and variance per feature, per group of observations. + + The formula `Var(X) = E(X^2) - E(X)^2` suffers loss of precision when the variance is a + very small fraction of the squared mean. In particular, when X is constant, the formula may + nonetheless be non-zero. By default, our implementation resets the variance to exactly zero + when the computed variance, relative to the squared mean, nears limit of precision of the + floating-point significand. + + Params + ------ + dof + Degrees of freedom for variance. + + Returns + ------- + Object with `count`, `mean`, and `var` attributes. + + """ + assert dof >= 0 + + group_counts = np.bincount(self.groupby.codes) + mean_ = self.mean() + # sparse matrices do not support ** for elementwise power. + mean_sq = ( + utils.asarray(self.indicator_matrix @ _power(self.data, 2)) + / group_counts[:, None] + ) + sq_mean = mean_**2 + var_ = mean_sq - sq_mean + # TODO: Why these values exactly? Because they are high relative to the datatype? + # (unchanged from original code: https://github.com/scverse/anndata/pull/564) + precision = 2 << (42 if self.data.dtype == np.float64 else 20) + # detects loss of precision in mean_sq - sq_mean, which suggests variance is 0 + var_[precision * var_ < sq_mean] = 0 + if dof != 0: + var_ *= (group_counts / (group_counts - dof))[:, np.newaxis] + return mean_, var_ + + def median(self) -> Array: + """Compute the median per feature per group of observations. + + Returns + ------- + Array of median. + + """ + medians = [] + for group in np.unique(self.groupby.codes): + group_mask = self.groupby.codes == group + group_data = self.data[group_mask] + if isinstance(group_data, CSBase): + if group_data.format != "csc": + group_data = group_data.tocsc() + medians.append(csc_median_axis_0(group_data)) + else: + medians.append(np.median(group_data, axis=0)) + return np.array(medians) + + +def _power(x: Array, power: float) -> Array: + """Generate elementwise power of a matrix. + + Needed for non-square sparse matrices because they do not support `**` so the `.power` function is used. + + Params + ------ + X + Matrix whose power is to be raised. + power + Integer power value + + Returns + ------- + Matrix whose power has been raised. + + """ + return x**power if isinstance(x, np.ndarray) else x.power(power) + + +def aggregate( # noqa: PLR0912 + adata: AnnData, + by: str | Collection[str], + func: AggType | Iterable[AggType], + *, + axis: Literal["obs", 0, "var", 1] | None = None, + mask: NDArray[np.bool_] | str | None = None, + dof: int = 1, + layer: str | None = None, + obsm: str | None = None, + varm: str | None = None, +) -> AnnData: + """Aggregate data matrix based on some categorical grouping. + + This function is useful for pseudobulking as well as plotting. + + Aggregation to perform is specified by `func`, which can be a single metric or a + list of metrics. Each metric is computed over the group and results in a new layer + in the output `AnnData` object. + + If none of `layer`, `obsm`, or `varm` are passed in, `X` will be used for aggregation data. + + Params + ------ + adata + :class:`~anndata.AnnData` to be aggregated. + by + Key of the column to be grouped-by. + func + How to aggregate. + axis + Axis on which to find group by column. + mask + Boolean mask (or key to column containing mask) to apply along the axis. + dof + Degrees of freedom for variance. Defaults to 1. + layer + If not None, key for aggregation data. + obsm + If not None, key for aggregation data. + varm + If not None, key for aggregation data. + + Returns + ------- + Aggregated :class:`~anndata.AnnData`. + + Examples + -------- + Calculating mean expression and number of nonzero entries per cluster: + + >>> import scanpy as sc, pandas as pd + >>> pbmc = sc.datasets.pbmc3k_processed().raw.to_adata() + >>> pbmc.shape + (2638, 13714) + >>> aggregated = sc.get.aggregate( + ... pbmc, by="louvain", func=["mean", "count_nonzero"] + ... ) + >>> aggregated + AnnData object with n_obs × n_vars = 8 × 13714 + obs: 'louvain' + var: 'n_cells' + layers: 'mean', 'count_nonzero' + + We can group over multiple columns: + + >>> pbmc.obs["percent_mito_binned"] = pd.cut(pbmc.obs["percent_mito"], bins=5) + >>> sc.get.aggregate( + ... pbmc, by=["louvain", "percent_mito_binned"], func=["mean", "count_nonzero"] + ... ) + AnnData object with n_obs × n_vars = 40 × 13714 + obs: 'louvain', 'percent_mito_binned' + var: 'n_cells' + layers: 'mean', 'count_nonzero' + + Note that this filters out any combination of groups that wasn't present in the original data. + + """ + if not isinstance(adata, AnnData): + msg = ( + "sc.get.aggregate is currently only implemented for AnnData input, " + f"was passed {type(adata)}." + ) + raise NotImplementedError(msg) + if axis is None: + axis = 1 if varm else 0 + axis, axis_name = _resolve_axis(axis) + mask = _check_mask(adata, mask, axis_name) + data = adata.X + if sum(p is not None for p in [varm, obsm, layer]) > 1: + msg = "Please only provide one (or none) of varm, obsm, or layer" + raise TypeError(msg) + + if varm is not None: + if axis != 1: + msg = "varm can only be used when axis is 1" + raise ValueError(msg) + data = adata.varm[varm] + elif obsm is not None: + if axis != 0: + msg = "obsm can only be used when axis is 0" + raise ValueError(msg) + data = adata.obsm[obsm] + elif layer is not None: + data = adata.layers[layer] + if axis == 1: + data = data.T + elif axis == 1: + # i.e., all of `varm`, `obsm`, `layers` are None so we use `X` which must be transposed + data = data.T + + dim_df = getattr(adata, axis_name) + categorical, new_label_df = _combine_categories(dim_df, by) + # Actual computation + layers = _aggregate( + data, + by=categorical, + func=func, + mask=mask, + dof=dof, + ) + + # Define new var dataframe + if obsm or varm: + if isinstance(data, pd.DataFrame): + # Check if there could be labels + var = pd.DataFrame(index=data.columns) + else: + # Create them otherwise + var = pd.DataFrame(index=pd.RangeIndex(data.shape[1]).astype(str)) + else: + var = getattr(adata, "var" if axis == 0 else "obs") + + # It's all coming together + result = AnnData(layers=layers, obs=new_label_df, var=var) + + if axis == 1: + return result.T + else: + return result + + +@singledispatch +def _aggregate( + data, + by: pd.Categorical, + func: AggType | Iterable[AggType], + *, + mask: NDArray[np.bool_] | None = None, + dof: int = 1, +): + msg = f"Data type {type(data)} not supported for aggregation" + raise NotImplementedError(msg) + + +@_aggregate.register(pd.DataFrame) +def aggregate_df(data, by, func, *, mask=None, dof=1): + return _aggregate(data.values, by, func, mask=mask, dof=dof) + + +@_aggregate.register(np.ndarray) +@_register_union(_aggregate, CSBase) +def aggregate_array( + data: Array, + by: pd.Categorical, + func: AggType | Iterable[AggType], + *, + mask: NDArray[np.bool_] | None = None, + dof: int = 1, +) -> dict[AggType, np.ndarray]: + groupby = Aggregate(groupby=by, data=data, mask=mask) + result = {} + + funcs = set([func] if isinstance(func, str) else func) + if unknown := funcs - get_literal_vals(AggType): + msg = f"func {unknown} is not one of {get_literal_vals(AggType)}" + raise ValueError(msg) + + if "sum" in funcs: # sum is calculated separately from the rest + agg = groupby.sum() + result["sum"] = agg + # here and below for count, if var is present, these can be calculate alongside var + if "mean" in funcs and "var" not in funcs: + agg = groupby.mean() + result["mean"] = agg + if "count_nonzero" in funcs: + result["count_nonzero"] = groupby.count_nonzero() + if "var" in funcs: + mean_, var_ = groupby.mean_var(dof) + result["var"] = var_ + if "mean" in funcs: + result["mean"] = mean_ + if "median" in funcs: + agg = groupby.median() + result["median"] = agg + return result + + +def _combine_categories( + label_df: pd.DataFrame, cols: Collection[str] | str +) -> tuple[pd.Categorical, pd.DataFrame]: + """Return both the result categories and a dataframe labelling each row.""" + from itertools import product + + if isinstance(cols, str): + cols = [cols] + + df = pd.DataFrame( + {c: pd.Categorical(label_df[c]).remove_unused_categories() for c in cols}, + ) + n_categories = [len(df[c].cat.categories) for c in cols] + + # It's like np.concatenate([x for x in product(*[range(n) for n in n_categories])]) + code_combinations = np.indices(n_categories).reshape(len(n_categories), -1) + result_categories = pd.Index([ + "_".join(map(str, x)) for x in product(*[df[c].cat.categories for c in cols]) + ]) + + # Dataframe with unique combination of categories for each row + new_label_df = pd.DataFrame( + { + c: pd.Categorical.from_codes(code_combinations[i], df[c].cat.categories) + for i, c in enumerate(cols) + }, + index=result_categories, + ) + + # Calculating result codes + factors = np.ones(len(cols) + 1, dtype=np.int32) # First factor needs to be 1 + np.cumprod(n_categories[::-1], out=factors[1:]) + factors = factors[:-1][::-1] + + code_array = np.zeros((len(cols), df.shape[0]), dtype=np.int32) + for i, c in enumerate(cols): + code_array[i] = df[c].cat.codes + code_array *= factors[:, None] + + result_categorical = pd.Categorical.from_codes( + code_array.sum(axis=0), categories=result_categories + ) + + # Filter unused categories + result_categorical = result_categorical.remove_unused_categories() + new_label_df = new_label_df.loc[result_categorical.categories] + + return result_categorical, new_label_df + + +def sparse_indicator( + categorical: pd.Categorical, + *, + mask: NDArray[np.bool_] | None = None, + weight: NDArray[np.floating] | None = None, +) -> sparse.coo_matrix: + if mask is not None and weight is None: + weight = mask.astype(np.float32) + elif mask is not None and weight is not None: + weight = mask * weight + elif mask is None and weight is None: + weight = np.broadcast_to(1.0, len(categorical)) + a = sparse.coo_matrix( + (weight, (categorical.codes, np.arange(len(categorical)))), + shape=(len(categorical.categories), len(categorical)), + ) + return a diff --git a/.venv/lib/python3.11/site-packages/scanpy/get/get.py b/.venv/lib/python3.11/site-packages/scanpy/get/get.py new file mode 100644 index 0000000000000000000000000000000000000000..3304d1ca882025256cef578911064f14f779338c --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/get/get.py @@ -0,0 +1,537 @@ +"""Helper functions for accessing data.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, TypeVar + +import numpy as np +import pandas as pd +from anndata import AnnData +from numpy.typing import NDArray +from packaging.version import Version + +from .._compat import CSBase, pkg_version + +if TYPE_CHECKING: + from collections.abc import Collection, Iterable + from typing import Any, Literal + + from anndata._core.sparse_dataset import BaseCompressedSparseDataset + from anndata._core.views import ArrayView + + from .._compat import DaskArray + + +# -------------------------------------------------------------------------------- +# Plotting data helpers +# -------------------------------------------------------------------------------- + + +# TODO: implement diffxpy method, make singledispatch +def rank_genes_groups_df( # noqa: PLR0912 + adata: AnnData, + group: str | Iterable[str] | None, + *, + key: str = "rank_genes_groups", + pval_cutoff: float | None = None, + log2fc_min: float | None = None, + log2fc_max: float | None = None, + gene_symbols: str | None = None, +) -> pd.DataFrame: + """Get :func:`scanpy.tl.rank_genes_groups` results in the form of a :class:`~pandas.DataFrame`. + + Params + ------ + adata + Object to get results from. + group + Which group (as in :func:`scanpy.tl.rank_genes_groups`'s `groupby` + argument) to return results from. Can be a list. All groups are + returned if groups is `None`. + key + Key differential expression groups were stored under. + pval_cutoff + Return only adjusted p-values below the cutoff. + log2fc_min + Minimum logfc to return. + log2fc_max + Maximum logfc to return. + gene_symbols + Column name in `.var` DataFrame that stores gene symbols. Specifying + this will add that column to the returned dataframe. + + Example + ------- + >>> import scanpy as sc + >>> pbmc = sc.datasets.pbmc68k_reduced() + >>> sc.tl.rank_genes_groups(pbmc, groupby="louvain", use_raw=True) + >>> dedf = sc.get.rank_genes_groups_df(pbmc, group="0") + + """ + if isinstance(group, str): + group = [group] + if group is None: + group = list(adata.uns[key]["names"].dtype.names) + method = adata.uns[key]["params"]["method"] + if method == "logreg": + colnames = ["names", "scores"] + else: + colnames = ["names", "scores", "logfoldchanges", "pvals", "pvals_adj"] + + d = [pd.DataFrame(adata.uns[key][c])[group] for c in colnames] + d = pd.concat(d, axis=1, names=[None, "group"], keys=colnames) + if pkg_version("pandas") >= Version("2.1"): + d = d.stack(level=1, future_stack=True).reset_index() + else: + d = d.stack(level=1).reset_index() + d["group"] = pd.Categorical(d["group"], categories=group) + d = d.sort_values(["group", "level_0"]).drop(columns="level_0") + + if method != "logreg": + if pval_cutoff is not None: + d = d[d["pvals_adj"] < pval_cutoff] + if log2fc_min is not None: + d = d[d["logfoldchanges"] > log2fc_min] + if log2fc_max is not None: + d = d[d["logfoldchanges"] < log2fc_max] + if gene_symbols is not None: + d = d.join(adata.var[gene_symbols], on="names") + + for pts, name in {"pts": "pct_nz_group", "pts_rest": "pct_nz_reference"}.items(): + if pts in adata.uns[key]: + pts_df = ( + adata.uns[key][pts][group] + .rename_axis(index="names") + .reset_index() + .melt(id_vars="names", var_name="group", value_name=name) + ) + d = d.merge(pts_df) + + # remove group column for backward compat if len(group) == 1 + if len(group) == 1: + d.drop(columns="group", inplace=True) + + return d.reset_index(drop=True) + + +def _check_indices( + dim_df: pd.DataFrame, + alt_index: pd.Index, + *, + dim: Literal["obs", "var"], + keys: Iterable[str], + alias_index: pd.Index | None = None, + use_raw: bool = False, +) -> tuple[list[str], list[str], list[str]]: + """Check indices for `obs_df` and `var_df`.""" + alt_repr = "adata.raw" if use_raw else "adata" + + alt_dim = ("obs", "var")[dim == "obs"] + + alias_name = None + if alias_index is not None: + alt_names = pd.Series(alt_index, index=alias_index) + alias_name = alias_index.name + alt_search_repr = f"{alt_dim}['{alias_name}']" + else: + alt_names = pd.Series(alt_index, index=alt_index) + alt_search_repr = f"{alt_dim}_names" + + col_keys = [] + index_keys = [] + index_aliases = [] + not_found = [] + + # check that adata.obs does not contain duplicated columns + # if duplicated columns names are present, they will + # be further duplicated when selecting them. + if not dim_df.columns.is_unique: + dup_cols = dim_df.columns[dim_df.columns.duplicated()].tolist() + msg = ( + f"adata.{dim} contains duplicated columns. Please rename or remove " + "these columns first.\n`" + f"Duplicated columns {dup_cols}" + ) + raise ValueError(msg) + + if not alt_index.is_unique: + msg = ( + f"{alt_repr}.{alt_dim}_names contains duplicated items\n" + f"Please rename these {alt_dim} names first for example using " + f"`adata.{alt_dim}_names_make_unique()`" + ) + raise ValueError(msg) + + # use only unique keys, otherwise duplicated keys will + # further duplicate when reordering the keys later in the function + for key in dict.fromkeys(keys): + if key in dim_df.columns: + col_keys.append(key) + if key in alt_names.index: + msg = f"The key {key!r} is found in both adata.{dim} and {alt_repr}.{alt_search_repr}." + raise KeyError(msg) + elif key in alt_names.index: + val = alt_names[key] + if isinstance(val, pd.Series): + # while var_names must be unique, adata.var[gene_symbols] does not + # It's still ambiguous to refer to a duplicated entry though. + assert alias_index is not None + msg = f"Found duplicate entries for {key!r} in {alt_repr}.{alt_search_repr}." + raise KeyError(msg) + index_keys.append(val) + index_aliases.append(key) + else: + not_found.append(key) + if len(not_found) > 0: + msg = ( + f"Could not find keys {not_found!r} in columns of `adata.{dim}` or in" + f" {alt_repr}.{alt_search_repr}." + ) + raise KeyError(msg) + + return col_keys, index_keys, index_aliases + + +def _get_array_values( + x, + /, + dim_names: pd.Index, + keys: Iterable[str], + *, + axis: Literal[0, 1], + backed: bool, +): + # TODO: This should be made easier on the anndata side + mutable_idxer = [slice(None), slice(None)] + idx = dim_names.get_indexer(keys) + + # for backed AnnData is important that the indices are ordered + if backed: + idx_order = np.argsort(idx) + rev_idxer = mutable_idxer.copy() + mutable_idxer[axis] = idx[idx_order] + rev_idxer[axis] = np.argsort(idx_order) + matrix = x[tuple(mutable_idxer)][tuple(rev_idxer)] + else: + mutable_idxer[axis] = idx + matrix = x[tuple(mutable_idxer)] + + if isinstance(matrix, CSBase): + matrix = matrix.toarray() + + return matrix + + +def obs_df( + adata: AnnData, + keys: Collection[str] = (), + obsm_keys: Iterable[tuple[str, int]] = (), + *, + layer: str | None = None, + gene_symbols: str | None = None, + use_raw: bool = False, +) -> pd.DataFrame: + """Return values for observations in adata. + + Params + ------ + adata + AnnData object to get values from. + keys + Keys from either `.var_names`, `.var[gene_symbols]`, or `.obs.columns`. + obsm_keys + Tuples of `(key from obsm, column index of obsm[key])`. + layer + Layer of `adata` to use as expression values. + gene_symbols + Column of `adata.var` to search for `keys` in. + use_raw + Whether to get expression values from `adata.raw`. + + Returns + ------- + A dataframe with `adata.obs_names` as index, and values specified by `keys` + and `obsm_keys`. + + Examples + -------- + Getting value for plotting: + + >>> import scanpy as sc + >>> pbmc = sc.datasets.pbmc68k_reduced() + >>> plotdf = sc.get.obs_df( + ... pbmc, keys=["CD8B", "n_genes"], obsm_keys=[("X_umap", 0), ("X_umap", 1)] + ... ) + >>> plotdf.columns + Index(['CD8B', 'n_genes', 'X_umap-0', 'X_umap-1'], dtype='object') + >>> plotdf.plot.scatter("X_umap-0", "X_umap-1", c="CD8B") # doctest: +SKIP + + + Calculating mean expression for marker genes by cluster: + + >>> pbmc = sc.datasets.pbmc68k_reduced() + >>> marker_genes = ["CD79A", "MS4A1", "CD8A", "CD8B", "LYZ"] + >>> genedf = sc.get.obs_df(pbmc, keys=["louvain", *marker_genes]) + >>> grouped = genedf.groupby("louvain", observed=True) + >>> mean, var = grouped.mean(), grouped.var() + + """ + if isinstance(keys, str): + keys = [keys] + if use_raw: + assert layer is None, ( + "Cannot specify use_raw=True and a layer at the same time." + ) + var = adata.raw.var + else: + var = adata.var + alias_index = pd.Index(var[gene_symbols]) if gene_symbols is not None else None + + obs_cols, var_idx_keys, var_symbols = _check_indices( + adata.obs, + var.index, + dim="obs", + keys=keys, + alias_index=alias_index, + use_raw=use_raw, + ) + + # Make df + df = pd.DataFrame(index=adata.obs_names) + + # add var values + if len(var_idx_keys) > 0: + matrix = _get_array_values( + _get_obs_rep(adata, layer=layer, use_raw=use_raw), + var.index, + var_idx_keys, + axis=1, + backed=adata.isbacked, + ) + df = pd.concat( + [df, pd.DataFrame(matrix, columns=var_symbols, index=adata.obs_names)], + axis=1, + ) + + # add obs values + if len(obs_cols) > 0: + df = pd.concat([df, adata.obs[obs_cols]], axis=1) + + # reorder columns to given order (including duplicates keys if present) + if keys: + df = df[keys] + + for k, idx in obsm_keys: + added_k = f"{k}-{idx}" + val = adata.obsm[k] + if isinstance(val, np.ndarray): + df[added_k] = np.ravel(val[:, idx]) + elif isinstance(val, CSBase): + df[added_k] = np.ravel(val[:, idx].toarray()) + elif isinstance(val, pd.DataFrame): + df[added_k] = val.loc[:, idx] + + return df + + +def var_df( + adata: AnnData, + keys: Collection[str] = (), + varm_keys: Iterable[tuple[str, int]] = (), + *, + layer: str | None = None, +) -> pd.DataFrame: + """Return values for observations in adata. + + Params + ------ + adata + AnnData object to get values from. + keys + Keys from either `.obs_names`, or `.var.columns`. + varm_keys + Tuples of `(key from varm, column index of varm[key])`. + layer + Layer of `adata` to use as expression values. + + Returns + ------- + A dataframe with `adata.var_names` as index, and values specified by `keys` + and `varm_keys`. + + """ + # Argument handling + if isinstance(keys, str): + keys = [keys] + var_cols, obs_idx_keys, _ = _check_indices( + adata.var, adata.obs_names, dim="var", keys=keys + ) + + # initialize df + df = pd.DataFrame(index=adata.var.index) + + if len(obs_idx_keys) > 0: + matrix = _get_array_values( + _get_obs_rep(adata, layer=layer), + adata.obs_names, + obs_idx_keys, + axis=0, + backed=adata.isbacked, + ).T + df = pd.concat( + [df, pd.DataFrame(matrix, columns=obs_idx_keys, index=adata.var_names)], + axis=1, + ) + + # add obs values + if len(var_cols) > 0: + df = pd.concat([df, adata.var[var_cols]], axis=1) + + # reorder columns to given order + if keys: + df = df[keys] + + for k, idx in varm_keys: + added_k = f"{k}-{idx}" + val = adata.varm[k] + if isinstance(val, np.ndarray): + df[added_k] = np.ravel(val[:, idx]) + elif isinstance(val, CSBase): + df[added_k] = np.ravel(val[:, idx].toarray()) + elif isinstance(val, pd.DataFrame): + df[added_k] = val.loc[:, idx] + return df + + +def _get_obs_rep( + adata: AnnData, + *, + use_raw: bool = False, + layer: str | None = None, + obsm: str | None = None, + obsp: str | None = None, +) -> ( + np.ndarray | CSBase | pd.DataFrame | ArrayView | BaseCompressedSparseDataset | None +): + """Choose array aligned with obs annotation.""" + # https://github.com/scverse/scanpy/issues/1546 + if not isinstance(use_raw, bool): + msg = f"use_raw expected to be bool, was {type(use_raw)}." + raise TypeError(msg) + + is_layer = layer is not None + is_raw = use_raw is not False + is_obsm = obsm is not None + is_obsp = obsp is not None + choices_made = sum((is_layer, is_raw, is_obsm, is_obsp)) + assert choices_made in {0, 1} + if choices_made == 0: + return adata.X + if is_layer: + return adata.layers[layer] + if use_raw: + return adata.raw.X + if is_obsm: + return adata.obsm[obsm] + if is_obsp: + return adata.obsp[obsp] + msg = ( + "That was unexpected. Please report this bug at:\n\n\t" + "https://github.com/scverse/scanpy/issues" + ) + raise AssertionError(msg) + + +def _set_obs_rep( + adata: AnnData, + val: Any, + *, + use_raw: bool = False, + layer: str | None = None, + obsm: str | None = None, + obsp: str | None = None, +): + """Set value for observation rep.""" + is_layer = layer is not None + is_raw = use_raw is not False + is_obsm = obsm is not None + is_obsp = obsp is not None + choices_made = sum((is_layer, is_raw, is_obsm, is_obsp)) + assert choices_made <= 1 + if choices_made == 0: + adata.X = val + elif is_layer: + adata.layers[layer] = val + elif use_raw: + adata.raw.X = val + elif is_obsm: + adata.obsm[obsm] = val + elif is_obsp: + adata.obsp[obsp] = val + else: + msg = ( + "That was unexpected. Please report this bug at:\n\n" + "\thttps://github.com/scverse/scanpy/issues" + ) + raise AssertionError(msg) + + +M = TypeVar("M", bound=NDArray[np.bool_] | NDArray[np.floating] | pd.Series | None) + + +def _check_mask( + data: AnnData | np.ndarray | CSBase | DaskArray, + mask: str | M, + dim: Literal["obs", "var"], + *, + allow_probabilities: bool = False, +) -> M: # Could also be a series, but should be one or the other + """Validate mask argument. + + Params + ------ + data + Annotated data matrix or numpy array. + mask + Mask (or probabilities if `allow_probabilities=True`). + Either an appropriatley sized array, or name of a column. + dim + The dimension being masked. + allow_probabilities + Whether to allow probabilities as `mask` + """ + if mask is None: + return mask + desc = "mask/probabilities" if allow_probabilities else "mask" + + if isinstance(mask, str): + if not isinstance(data, AnnData): + msg = f"Cannot refer to {desc} with string without providing anndata object as argument" + raise ValueError(msg) + + annot: pd.DataFrame = getattr(data, dim) + if mask not in annot.columns: + msg = ( + f"Did not find `adata.{dim}[{mask!r}]`. " + f"Either add the {desc} first to `adata.{dim}`" + f"or consider using the {desc} argument with an array." + ) + raise ValueError(msg) + mask_array = annot[mask].to_numpy() + else: + if len(mask) != data.shape[0 if dim == "obs" else 1]: + msg = f"The shape of the {desc} do not match the data." + raise ValueError(msg) + mask_array = mask + + is_bool = pd.api.types.is_bool_dtype(mask_array.dtype) + if not allow_probabilities and not is_bool: + msg = "Mask array must be boolean." + raise ValueError(msg) + elif allow_probabilities and not ( + is_bool or pd.api.types.is_float_dtype(mask_array.dtype) + ): + msg = f"{desc} array must be boolean or floating point." + raise ValueError(msg) + + return mask_array diff --git a/.venv/lib/python3.11/site-packages/scanpy/logging.py b/.venv/lib/python3.11/site-packages/scanpy/logging.py new file mode 100644 index 0000000000000000000000000000000000000000..059523070289e2b78d12875cc2dadc486ed95de8 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/logging.py @@ -0,0 +1,246 @@ +"""Logging and Profiling.""" + +from __future__ import annotations + +import logging +import sys +from datetime import datetime, timedelta, timezone +from functools import partial, update_wrapper +from importlib.metadata import version +from logging import CRITICAL, DEBUG, ERROR, INFO, WARNING +from typing import TYPE_CHECKING, overload + +import anndata.logging + +if TYPE_CHECKING: + from typing import IO + + from session_info2 import SessionInfo + + from ._settings import SettingsMeta + + +# This is currently the only documented API +__all__ = ["print_versions"] + +HINT = (INFO + DEBUG) // 2 +logging.addLevelName(HINT, "HINT") + + +class _RootLogger(logging.RootLogger): + def __init__(self, level): + super().__init__(level) + self.propagate = False + _RootLogger.manager = logging.Manager(self) + + def log( + self, + level: int, + msg: str, + *, + extra: dict | None = None, + time: datetime | None = None, + deep: str | None = None, + ) -> datetime: + from ._settings import settings + + now = datetime.now(timezone.utc) + time_passed: timedelta = None if time is None else now - time + extra = { + **(extra or {}), + "deep": deep if settings.verbosity.level < level else None, + "time_passed": time_passed, + } + super().log(level, msg, extra=extra) + return now + + def critical(self, msg, *, time=None, deep=None, extra=None) -> datetime: + return self.log(CRITICAL, msg, time=time, deep=deep, extra=extra) + + def error(self, msg, *, time=None, deep=None, extra=None) -> datetime: + return self.log(ERROR, msg, time=time, deep=deep, extra=extra) + + def warning(self, msg, *, time=None, deep=None, extra=None) -> datetime: + return self.log(WARNING, msg, time=time, deep=deep, extra=extra) + + def info(self, msg, *, time=None, deep=None, extra=None) -> datetime: + return self.log(INFO, msg, time=time, deep=deep, extra=extra) + + def hint(self, msg, *, time=None, deep=None, extra=None) -> datetime: + return self.log(HINT, msg, time=time, deep=deep, extra=extra) + + def debug(self, msg, *, time=None, deep=None, extra=None) -> datetime: + return self.log(DEBUG, msg, time=time, deep=deep, extra=extra) + + +def _set_log_file(settings: SettingsMeta) -> None: + file = settings.logfile + name = settings.logpath + root = settings._root_logger + h = logging.StreamHandler(file) if name is None else logging.FileHandler(name) + h.setFormatter(_LogFormatter()) + h.setLevel(root.level) + for handler in list(root.handlers): + root.removeHandler(handler) + root.addHandler(h) + + +def _set_log_level(settings: SettingsMeta, level: int) -> None: + root = settings._root_logger + root.setLevel(level) + for h in list(root.handlers): + h.setLevel(level) + + +class _LogFormatter(logging.Formatter): + def __init__( + self, fmt="{levelname}: {message}", datefmt="%Y-%m-%d %H:%M", style="{" + ): + super().__init__(fmt, datefmt, style) + + def format(self, record: logging.LogRecord): + format_orig = self._style._fmt + if record.levelno == INFO: + self._style._fmt = "{message}" + elif record.levelno == HINT: + self._style._fmt = "--> {message}" + elif record.levelno == DEBUG: + self._style._fmt = " {message}" + if record.time_passed: + # strip microseconds + if record.time_passed.microseconds: + record.time_passed = timedelta( + seconds=int(record.time_passed.total_seconds()) + ) + if "{time_passed}" in record.msg: + record.msg = record.msg.replace( + "{time_passed}", str(record.time_passed) + ) + else: + self._style._fmt += " ({time_passed})" + if record.deep: + record.msg = f"{record.msg}: {record.deep}" + result = logging.Formatter.format(self, record) + self._style._fmt = format_orig + return result + + +print_memory_usage = anndata.logging.print_memory_usage +get_memory_usage = anndata.logging.get_memory_usage + + +@overload +def print_header(*, file: None = None) -> SessionInfo: ... +@overload +def print_header(*, file: IO[str]) -> None: ... +def print_header(*, file: IO[str] | None = None): + """Versions that might influence the numerical results. + + Matplotlib and Seaborn are excluded from this. + + Parameters + ---------- + file + Optional path for dependency output. + + """ + from session_info2 import session_info + + sinfo = session_info(os=True, cpu=True, gpu=True, dependencies=True) + + if file is not None: + print(sinfo, file=file) + return + + return sinfo + + +def print_versions() -> SessionInfo: + """Alias for `print_header`. + + .. deprecated:: 1.11.0 + + Use :func:`print_header` instead. + """ + return print_header() + + +def print_version_and_date(*, file=None): + """Print small version and date header. + + Useful for starting a notebook so you see when you started working. + + Parameters + ---------- + file + Optional path for output. + + """ + if file is None: + file = sys.stdout + print( + f"Running Scanpy {version('scanpy')}, on {datetime.now():%Y-%m-%d %H:%M}.", + file=file, + ) + + +def _copy_docs_and_signature(fn): + return partial(update_wrapper, wrapped=fn, assigned=["__doc__", "__annotations__"]) + + +def error( + msg: str, + *, + time: datetime | None = None, + deep: str | None = None, + extra: dict | None = None, +) -> datetime: + """Log message with specific level and return current time. + + Parameters + ---------- + msg + Message to display. + time + A time in the past. If this is passed, the time difference from then + to now is appended to `msg` as ` (HH:MM:SS)`. + If `msg` contains `{time_passed}`, the time difference is instead + inserted at that position. + deep + If the current verbosity is higher than the log function’s level, + this gets displayed as well + extra + Additional values you can specify in `msg` like `{time_passed}`. + + """ + from ._settings import settings + + return settings._root_logger.error(msg, time=time, deep=deep, extra=extra) + + +@_copy_docs_and_signature(error) +def warning(msg, *, time=None, deep=None, extra=None) -> datetime: + from ._settings import settings + + return settings._root_logger.warning(msg, time=time, deep=deep, extra=extra) + + +@_copy_docs_and_signature(error) +def info(msg, *, time=None, deep=None, extra=None) -> datetime: + from ._settings import settings + + return settings._root_logger.info(msg, time=time, deep=deep, extra=extra) + + +@_copy_docs_and_signature(error) +def hint(msg, *, time=None, deep=None, extra=None) -> datetime: + from ._settings import settings + + return settings._root_logger.hint(msg, time=time, deep=deep, extra=extra) + + +@_copy_docs_and_signature(error) +def debug(msg, *, time=None, deep=None, extra=None) -> datetime: + from ._settings import settings + + return settings._root_logger.debug(msg, time=time, deep=deep, extra=extra) diff --git a/.venv/lib/python3.11/site-packages/scanpy/metrics/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/metrics/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9cdf82bdf25aab78e3b627e2d49a3b196e937157 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/metrics/__init__.py @@ -0,0 +1,9 @@ +"""Metrics.""" + +from __future__ import annotations + +from ._gearys_c import gearys_c +from ._metrics import confusion_matrix +from ._morans_i import morans_i + +__all__ = ["confusion_matrix", "gearys_c", "morans_i"] diff --git a/.venv/lib/python3.11/site-packages/scanpy/metrics/_common.py b/.venv/lib/python3.11/site-packages/scanpy/metrics/_common.py new file mode 100644 index 0000000000000000000000000000000000000000..5f4d3a78ea4602d3c42e3bc25eb5fe6161f846e0 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/metrics/_common.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +import warnings +from abc import ABC, abstractmethod +from dataclasses import InitVar, dataclass, field +from functools import singledispatch +from typing import TYPE_CHECKING, ClassVar, overload + +import numpy as np +import pandas as pd +from scipy import sparse + +from .._compat import CSRBase, DaskArray, SpBase, _register_union, fullname + +if TYPE_CHECKING: + from typing import NoReturn, TypeVar + + from anndata import AnnData + from numpy.typing import NDArray + + T_NonSparse = TypeVar("T_NonSparse", bound=NDArray | DaskArray) + V = TypeVar("V", bound=NDArray | CSRBase) + + _Vals = NDArray | SpBase | DaskArray | pd.DataFrame | pd.Series + + +__all__ = ["_SparseMetric", "_get_graph"] + + +@dataclass +class _SparseMetric(ABC): + graph: CSRBase + vals: InitVar[_Vals] + _vals: NDArray | CSRBase | DaskArray = field(init=False) + + name: ClassVar[str] + + def __post_init__(self, vals: _Vals) -> None: + assert isinstance(type(self).name, str) + assert self.graph.shape[0] == self.graph.shape[1], ( + "`g` should be a square adjacency matrix" + ) + self.graph = self.graph.astype(np.float64, copy=False) + self._vals = _resolve_vals(vals) + + @abstractmethod + def mtx(self, vals_het: NDArray | CSRBase, /) -> NDArray: + """Calculate metric when ``._vals`` is a 2D matrix (on an easier to handle version of ``._vals``).""" + + @abstractmethod + def vec(self) -> np.float64: + """Calculate metric when ``._vals`` is a 1D vector.""" + + def __call__(self) -> np.ndarray: + match self._vals, self._vals.ndim: + case _, 2 if isinstance(self._vals, CSRBase | np.ndarray): + assert self.graph.shape[0] == self._vals.shape[1] + vals_het, idxer, full_result = _vals_heterogeneous(self._vals) + result = self.mtx(vals_het.astype(np.float64, copy=False)) + full_result[idxer] = result + return full_result + case np.ndarray(), 1: + assert self.graph.shape[0] == self._vals.shape[0] + return self.vec() + case _, _: + msg = ( + f"{self.name} metric not implemented for vals of type " + f"{fullname(type(self._vals))} and ndim {self._vals.ndim}." + ) + raise NotImplementedError(msg) + + +def _get_graph(adata: AnnData, *, use_graph: str | None = None) -> CSRBase: + if use_graph is not None: + raise NotImplementedError() + # Fix for anndata<0.7 + if hasattr(adata, "obsp") and "connectivities" in adata.obsp: + return adata.obsp["connectivities"] + elif "neighbors" in adata.uns: + return adata.uns["neighbors"]["connectivities"] + else: + msg = "Must run neighbors first." + raise ValueError(msg) + + +@overload +def _resolve_vals(val: T_NonSparse) -> T_NonSparse: ... +@overload +def _resolve_vals(val: SpBase) -> CSRBase: ... +@overload +def _resolve_vals(val: pd.DataFrame | pd.Series) -> NDArray: ... + + +@singledispatch +def _resolve_vals(val: object) -> NoReturn: + msg = f"Unsupported type {type(val)}" + raise TypeError(msg) + + +@_resolve_vals.register(np.ndarray) +@_register_union(_resolve_vals, CSRBase) +@_resolve_vals.register(DaskArray) +def _( + val: np.ndarray | CSRBase | DaskArray, +) -> np.ndarray | CSRBase | DaskArray: + return val + + +@_register_union(_resolve_vals, SpBase) +def _(val: SpBase) -> CSRBase: + return sparse.csr_matrix(val) # noqa: TID251 + + +@_resolve_vals.register(pd.DataFrame) +@_resolve_vals.register(pd.Series) +def _(val: pd.DataFrame | pd.Series) -> NDArray: + return val.to_numpy() + + +def _vals_heterogeneous( + vals: V, +) -> tuple[V, NDArray[np.bool_] | slice, NDArray[np.float64]]: + """Check that values wont cause issues in computation. + + Returns new set of vals, and indexer to put values back into result. + + For details on why this is neccesary, see: + https://github.com/scverse/scanpy/issues/1806 + """ + from scanpy._utils import is_constant + + full_result = np.empty(vals.shape[0], dtype=np.float64) + full_result.fill(np.nan) + idxer = ~is_constant(vals, axis=1) + if idxer.all(): + idxer = slice(None) + else: + warnings.warn( + f"{len(idxer) - idxer.sum()} variables were constant, will return nan for these.", + UserWarning, + stacklevel=3, + ) + return vals[idxer], idxer, full_result diff --git a/.venv/lib/python3.11/site-packages/scanpy/metrics/_gearys_c.py b/.venv/lib/python3.11/site-packages/scanpy/metrics/_gearys_c.py new file mode 100644 index 0000000000000000000000000000000000000000..4bb4c4b15b42be8c493b0ebd8e97a7a3f1f0530b --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/metrics/_gearys_c.py @@ -0,0 +1,277 @@ +"""Geary's C autocorrelation.""" + +from __future__ import annotations + +from functools import singledispatch +from typing import TYPE_CHECKING, cast + +import numba +import numpy as np + +from .._compat import CSRBase, _register_union, njit +from ..get import _get_obs_rep +from ._common import _get_graph, _SparseMetric + +if TYPE_CHECKING: + from anndata import AnnData + from numpy.typing import NDArray + + from ._common import _Vals + + +@singledispatch +def gearys_c( + adata_or_graph: AnnData | CSRBase, + /, + vals: _Vals | None = None, + *, + use_graph: str | None = None, + layer: str | None = None, + obsm: str | None = None, + obsp: str | None = None, + use_raw: bool = False, +) -> np.ndarray | float: + r"""Calculate `Geary's C `_. + + Specifically as used by `VISION `_. + + Geary's C is a measure of autocorrelation for some measure on a graph. This + can be to whether measures are correlated between neighboring cells. Lower + values indicate greater correlation. + + .. math:: + + C = + \frac{ + (N - 1)\sum_{i,j} w_{i,j} (x_i - x_j)^2 + }{ + 2W \sum_i (x_i - \bar{x})^2 + } + + Params + ------ + adata_or_graph + AnnData object containing a graph (see ``use_graph``) or the graph itself. + See the examples for more info. + vals + Values to calculate Geary's C for. If this is two dimensional, should + be of shape `(n_features, n_cells)`. Otherwise should be of shape + `(n_cells,)`. This matrix can be selected from elements of the anndata + object by using key word arguments: `layer`, `obsm`, `obsp`, or + `use_raw`. + use_graph + Key to use for graph in anndata object. If not provided, default + neighbors connectivities will be used instead. + layer + Key for `adata.layers` to choose `vals`. + obsm + Key for `adata.obsm` to choose `vals`. + obsp + Key for `adata.obsp` to choose `vals`. + use_raw + Whether to use `adata.raw.X` for `vals`. + + Returns + ------- + If vals is two dimensional, returns a 1 dimensional ndarray array. Returns + a scalar if `vals` is 1d. + + Examples + -------- + Calculate Geary’s C for each components of a dimensionality reduction: + + .. code:: python + + import scanpy as sc, numpy as np + + pbmc = sc.datasets.pbmc68k_processed() + pc_c = sc.metrics.gearys_c(pbmc, obsm="X_pca") + + It's equivalent to call the function directly on the underlying arrays: + + .. code:: python + + alt = sc.metrics.gearys_c(pbmc.obsp["connectivities"], pbmc.obsm["X_pca"].T) + np.testing.assert_array_equal(pc_c, alt) + + """ + adata = cast("AnnData", adata_or_graph) + g = _get_graph(adata, use_graph=use_graph) + if vals is None: + vals = _get_obs_rep(adata, use_raw=use_raw, layer=layer, obsm=obsm, obsp=obsp).T + return gearys_c(g, vals) + + +@_register_union(gearys_c, CSRBase) +def _gearys_c(graph: CSRBase, /, vals: _Vals) -> NDArray: + return _GearysC(graph, vals)() + + +class _GearysC(_SparseMetric): + name = "Geary’s C" + + def mtx(self, vals_het: NDArray | CSRBase, /) -> NDArray: + g_parts = (self.graph.data, self.graph.indices, self.graph.indptr) + if isinstance(vals_het, np.ndarray): + return _gearys_c_mtx(*g_parts, vals_het) + v_parts = (vals_het.data, vals_het.indices, vals_het.indptr) + return _gearys_c_mtx_csr(*g_parts, *v_parts, vals_het.shape) + + def vec(self) -> np.float64: + w = self.graph.data.sum() + g_parts = (self.graph.data, self.graph.indices, self.graph.indptr) + return _gearys_c_vec_w(*g_parts, self._vals, w) + + +############################################################################### +# Calculation +############################################################################### +# Some notes on the implementation: +# * This could be phrased as tensor multiplication. However that does not get +# parallelized, which boosts performance almost linearly with cores. +# * Due to the umap setting the default threading backend, a parallel numba +# function that calls another parallel numba function can get stuck. This +# ends up meaning code re-use will be limited until umap 0.4. +# See: https://github.com/lmcinnes/umap/issues/306 +# * There can be a fair amount of numerical instability here (big reductions), +# so data is cast to float64. Removing these casts/ conversion will cause the +# tests to fail. + + +@njit +def _gearys_c_vec_w( + data: np.ndarray, + indices: np.ndarray, + indptr: np.ndarray, + x: np.ndarray, + w: np.float64, +) -> np.float64: + n = len(indptr) - 1 + x = x.astype(np.float64) + x_bar = x.mean() + + total = 0.0 + for i in numba.prange(n): + s = slice(indptr[i], indptr[i + 1]) + i_indices = indices[s] + i_data = data[s] + total += np.sum(i_data * ((x[i] - x[i_indices]) ** 2)) + + numer = (n - 1) * total + denom = 2 * w * ((x - x_bar) ** 2).sum() + return numer / denom + + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Inner functions (per element C) +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# For calling gearys_c on collections. +# TODO: These are faster if we can compile them in parallel mode. However, +# `workqueue` does not allow nested functions to be parallelized. +# Additionally, there are currently problems with numba's compiler around +# parallelization of this code: +# https://github.com/numba/numba/issues/6774#issuecomment-788789663 + + +@numba.njit(cache=True, parallel=False) # noqa: TID251 +def _gearys_c_inner_sparse_x_densevec( + g_data: np.ndarray, + g_indices: np.ndarray, + g_indptr: np.ndarray, + x: np.ndarray, + w: np.float64, +) -> np.float64: + x_bar = x.mean() + total = 0.0 + n = len(x) + for i in numba.prange(n): + s = slice(g_indptr[i], g_indptr[i + 1]) + i_indices = g_indices[s] + i_data = g_data[s] + total += np.sum(i_data * ((x[i] - x[i_indices]) ** 2)) + numer = (n - 1) * total + denom = 2 * w * ((x - x_bar) ** 2).sum() + return numer / denom + + +@numba.njit(cache=True, parallel=False) # noqa: TID251 +def _gearys_c_inner_sparse_x_sparsevec( # noqa: PLR0917 + g_data: np.ndarray, + g_indices: np.ndarray, + g_indptr: np.ndarray, + x_data: np.ndarray, + x_indices: np.ndarray, + n: int, + w: np.float64, +) -> np.float64: + x = np.zeros(n, dtype=np.float64) + x[x_indices] = x_data + x_bar = np.sum(x_data) / n + total = 0.0 + n = len(x) + for i in numba.prange(n): + s = slice(g_indptr[i], g_indptr[i + 1]) + i_indices = g_indices[s] + i_data = g_data[s] + total += np.sum(i_data * ((x[i] - x[i_indices]) ** 2)) + numer = (n - 1) * total + # Expanded from 2 * W * ((x_k - x_k_bar) ** 2).sum(), but uses sparsity + # to skip some calculations + # fmt: off + denom = ( + 2 * w + * ( + np.sum(x_data ** 2) + - np.sum(x_data * x_bar * 2) + + (x_bar ** 2) * n + ) + ) + # fmt: on + return numer / denom + + +@njit +def _gearys_c_mtx( + g_data: np.ndarray, + g_indices: np.ndarray, + g_indptr: np.ndarray, + x: np.ndarray, +) -> np.ndarray: + m, n = x.shape + assert n == len(g_indptr) - 1 + w = g_data.sum() + out = np.zeros(m, dtype=np.float64) + for k in numba.prange(m): + x_vec = x[k, :].astype(np.float64) + out[k] = _gearys_c_inner_sparse_x_densevec( + g_data, g_indices, g_indptr, x_vec, w + ) + return out + + +@njit +def _gearys_c_mtx_csr( # noqa: PLR0917 + g_data: np.ndarray, + g_indices: np.ndarray, + g_indptr: np.ndarray, + x_data: np.ndarray, + x_indices: np.ndarray, + x_indptr: np.ndarray, + x_shape: tuple, +) -> np.ndarray: + m, n = x_shape + w = g_data.sum() + out = np.zeros(m, dtype=np.float64) + x_data_list = np.split(x_data, x_indptr[1:-1]) + x_indices_list = np.split(x_indices, x_indptr[1:-1]) + for k in numba.prange(m): + out[k] = _gearys_c_inner_sparse_x_sparsevec( + g_data, + g_indices, + g_indptr, + x_data_list[k], + x_indices_list[k], + n, + w, + ) + return out diff --git a/.venv/lib/python3.11/site-packages/scanpy/metrics/_metrics.py b/.venv/lib/python3.11/site-packages/scanpy/metrics/_metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..c24cfe9fd1da63a9e8491ddee8757fedbbd5cff4 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/metrics/_metrics.py @@ -0,0 +1,91 @@ +"""Metrics which don't quite deserve their own file.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd +from natsort import natsorted +from pandas.api.types import CategoricalDtype + +if TYPE_CHECKING: + from collections.abc import Sequence + + +def confusion_matrix( + orig: pd.Series | np.ndarray | Sequence, + new: pd.Series | np.ndarray | Sequence, + data: pd.DataFrame | None = None, + *, + normalize: bool = True, +) -> pd.DataFrame: + """Given an original and new set of labels, create a labelled confusion matrix. + + Parameters `orig` and `new` can either be entries in data or categorical arrays + of the same size. + + Params + ------ + orig + Original labels. + new + New labels. + data + Optional dataframe to fill entries from. + normalize + Should the confusion matrix be normalized? + + + Examples + -------- + + .. plot:: + + import scanpy as sc; import seaborn as sns + pbmc = sc.datasets.pbmc68k_reduced() + cmtx = sc.metrics.confusion_matrix("bulk_labels", "louvain", pbmc.obs) + sns.heatmap(cmtx) + + """ + from sklearn.metrics import confusion_matrix as _confusion_matrix + + if data is not None: + if isinstance(orig, str): + orig = data[orig] + if isinstance(new, str): + new = data[new] + + # Coercing so I don't have to deal with it later + orig, new = pd.Series(orig), pd.Series(new) + assert len(orig) == len(new) + + unique_labels = pd.unique(np.concatenate((orig.values, new.values))) + + # Compute + mtx = _confusion_matrix(orig, new, labels=unique_labels) + if normalize: + sums = mtx.sum(axis=1)[:, np.newaxis] + mtx = np.divide(mtx, sums, where=sums != 0) + + # Label + orig_name = "Original labels" if orig.name is None else orig.name + new_name = "New Labels" if new.name is None else new.name + df = pd.DataFrame( + mtx, + index=pd.Index(unique_labels, name=orig_name), + columns=pd.Index(unique_labels, name=new_name), + ) + + # Filter + if isinstance(orig.dtype, CategoricalDtype): + orig_idx = pd.Series(orig).cat.categories + else: + orig_idx = natsorted(pd.unique(orig)) + if isinstance(new.dtype, CategoricalDtype): + new_idx = pd.Series(new).cat.categories + else: + new_idx = natsorted(pd.unique(new)) + df = df.loc[np.array(orig_idx), np.array(new_idx)] + + return df diff --git a/.venv/lib/python3.11/site-packages/scanpy/metrics/_morans_i.py b/.venv/lib/python3.11/site-packages/scanpy/metrics/_morans_i.py new file mode 100644 index 0000000000000000000000000000000000000000..f94e96a0cb1a69df2420211359d7c24091a3d841 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/metrics/_morans_i.py @@ -0,0 +1,209 @@ +"""Moran's I global spatial autocorrelation.""" + +from __future__ import annotations + +from functools import singledispatch +from typing import TYPE_CHECKING, cast + +import numba +import numpy as np + +from .._compat import CSRBase, _register_union, njit +from ..get import _get_obs_rep +from ._common import _get_graph, _SparseMetric + +if TYPE_CHECKING: + from anndata import AnnData + from numpy.typing import NDArray + + from ._common import _Vals + + +@singledispatch +def morans_i( + adata_or_graph: AnnData | CSRBase, + /, + vals: _Vals | None = None, + *, + use_graph: str | None = None, + layer: str | None = None, + obsm: str | None = None, + obsp: str | None = None, + use_raw: bool = False, +) -> np.ndarray | float: + r"""Calculate Moran’s I Global Autocorrelation Statistic. + + Moran’s I is a global autocorrelation statistic for some measure on a graph. It is commonly used in + spatial data analysis to assess autocorrelation on a 2D grid. It is closely related to Geary's C, + but not identical. More info can be found `here `_. + + .. math:: + + I = + \frac{ + N \sum_{i, j} w_{i, j} z_{i} z_{j} + }{ + S_{0} \sum_{i} z_{i}^{2} + } + + Params + ------ + adata_or_graph + AnnData object containing a graph (see ``use_graph``) or the graph itself. + See the examples for more info. + vals + Values to calculate Moran's I for. If this is two dimensional, should + be of shape `(n_features, n_cells)`. Otherwise should be of shape + `(n_cells,)`. This matrix can be selected from elements of the anndata + object by using key word arguments: `layer`, `obsm`, `obsp`, or + `use_raw`. + use_graph + Key to use for graph in anndata object. If not provided, default + neighbors connectivities will be used instead. + layer + Key for `adata.layers` to choose `vals`. + obsm + Key for `adata.obsm` to choose `vals`. + obsp + Key for `adata.obsp` to choose `vals`. + use_raw + Whether to use `adata.raw.X` for `vals`. + + Returns + ------- + If vals is two dimensional, returns a 1 dimensional ndarray array. Returns + a scalar if `vals` is 1d. + + Examples + -------- + Calculate Moran’s I for each components of a dimensionality reduction: + + .. code:: python + + import scanpy as sc, numpy as np + + pbmc = sc.datasets.pbmc68k_processed() + pc_c = sc.metrics.morans_i(pbmc, obsm="X_pca") + + It's equivalent to call the function directly on the underlying arrays: + + .. code:: python + + alt = sc.metrics.morans_i(pbmc.obsp["connectivities"], pbmc.obsm["X_pca"].T) + np.testing.assert_array_equal(pc_c, alt) + + """ + adata = cast("AnnData", adata_or_graph) + g = _get_graph(adata, use_graph=use_graph) + if vals is None: + vals = _get_obs_rep(adata, use_raw=use_raw, layer=layer, obsm=obsm, obsp=obsp).T + return morans_i(g, vals) + + +@_register_union(morans_i, CSRBase) +def _morans_i(graph: CSRBase, /, vals: _Vals) -> NDArray: + return _MoransI(graph, vals)() + + +class _MoransI(_SparseMetric): + name = "Moran’s I" + + def mtx(self, vals_het: NDArray | CSRBase, /) -> NDArray: + g_parts = (self.graph.data, self.graph.indices, self.graph.indptr) + if isinstance(vals_het, np.ndarray): + return _morans_i_mtx(*g_parts, vals_het) + v_parts = (vals_het.data, vals_het.indices, vals_het.indptr) + return _morans_i_mtx_csr(*g_parts, *v_parts, vals_het.shape) + + def vec(self) -> np.float64: + w = self.graph.data.sum() + g_parts = (self.graph.data, self.graph.indices, self.graph.indptr) + return _morans_i_vec_w(*g_parts, self._vals, w) + + +############################################################################### +# Calculation +############################################################################### +# This is done in a very similar way to gearys_c. See notes there for details. + + +@numba.njit(cache=True, parallel=False) # noqa: TID251 +def _morans_i_vec_w( + g_data: np.ndarray, + g_indices: np.ndarray, + g_indptr: np.ndarray, + x: np.ndarray, + w: np.float64, +) -> np.float64: + z = x - x.mean() + z2ss = (z * z).sum() + n = len(x) + inum = 0.0 + + for i in numba.prange(n): + s = slice(g_indptr[i], g_indptr[i + 1]) + i_indices = g_indices[s] + i_data = g_data[s] + inum += (i_data * z[i_indices]).sum() * z[i] + + return len(x) / w * inum / z2ss + + +@numba.njit(cache=True, parallel=False) # noqa: TID251 +def _morans_i_vec_w_sparse( # noqa: PLR0917 + g_data: np.ndarray, + g_indices: np.ndarray, + g_indptr: np.ndarray, + x_data: np.ndarray, + x_indices: np.ndarray, + n: int, + w: np.float64, +) -> np.float64: + x = np.zeros(n, dtype=x_data.dtype) + x[x_indices] = x_data + return _morans_i_vec_w(g_data, g_indices, g_indptr, x, w) + + +@njit +def _morans_i_mtx( + g_data: np.ndarray, + g_indices: np.ndarray, + g_indptr: np.ndarray, + x: np.ndarray, +) -> np.ndarray: + m, n = x.shape + assert n == len(g_indptr) - 1 + w = g_data.sum() + out = np.zeros(m, dtype=np.float64) + for k in numba.prange(m): + x_vec = x[k, :] + out[k] = _morans_i_vec_w(g_data, g_indices, g_indptr, x_vec, w) + return out + + +@njit +def _morans_i_mtx_csr( # noqa: PLR0917 + g_data: np.ndarray, + g_indices: np.ndarray, + g_indptr: np.ndarray, + x_data: np.ndarray, + x_indices: np.ndarray, + x_indptr: np.ndarray, + x_shape: tuple, +) -> np.ndarray: + m, n = x_shape + w = g_data.sum() + out = np.zeros(m, dtype=np.float64) + x_data_list = np.split(x_data, x_indptr[1:-1]) + x_indices_list = np.split(x_indices, x_indptr[1:-1]) + for k in numba.prange(m): + out[k] = _morans_i_vec_w_sparse( + g_data, + g_indices, + g_indptr, + x_data_list[k], + x_indices_list[k], + n, + w, + ) + return out diff --git a/.venv/lib/python3.11/site-packages/scanpy/neighbors/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/neighbors/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..02c5b2767b7d4f8c367e20c7ec0c25f5d8c5eb9b --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/neighbors/__init__.py @@ -0,0 +1,916 @@ +"""Functions and classes for computing nearest neighbors.""" + +from __future__ import annotations + +import contextlib +from collections.abc import Mapping +from textwrap import indent +from types import MappingProxyType +from typing import TYPE_CHECKING, NamedTuple, TypedDict +from warnings import warn + +import numpy as np +import scipy +from scipy import sparse +from sklearn.utils import check_random_state + +from .. import _utils +from .. import logging as logg +from .._compat import CSBase, CSRBase, SpBase, old_positionals +from .._settings import settings +from .._utils import NeighborsView, _doc_params, get_literal_vals +from . import _connectivity +from ._common import ( + _get_indices_distances_from_sparse_matrix, + _get_sparse_matrix_from_indices_distances, +) +from ._doc import doc_n_pcs, doc_use_rep +from ._types import _KnownTransformer, _Method + +if TYPE_CHECKING: + from collections.abc import Callable, MutableMapping + from typing import Any, Literal, NotRequired + + from anndata import AnnData + from igraph import Graph + + from .._utils.random import _LegacyRandom + from ._types import KnnTransformerLike, _Metric, _MetricFn + + +RPForestDict = Mapping[str, Mapping[str, np.ndarray]] + +N_DCS = 15 # default number of diffusion components +# Backwards compat, constants should be defined in only one place. +N_PCS = settings.N_PCS + + +class KwdsForTransformer(TypedDict): + """Keyword arguments passed to a _KnownTransformer. + + IMPORTANT: when changing the parameters set here, + update the “*ignored*” part in the parameter docs! + """ + + n_neighbors: int + metric: _Metric | _MetricFn + metric_params: Mapping[str, Any] + random_state: _LegacyRandom + + +class NeighborsParams(TypedDict): # noqa: D101 + n_neighbors: int + method: _Method + random_state: _LegacyRandom + metric: _Metric | _MetricFn + metric_kwds: NotRequired[Mapping[str, Any]] + use_rep: NotRequired[str] + n_pcs: NotRequired[int] + + +@_doc_params(n_pcs=doc_n_pcs, use_rep=doc_use_rep) +def neighbors( # noqa: PLR0913 + adata: AnnData, + n_neighbors: int = 15, + n_pcs: int | None = None, + *, + use_rep: str | None = None, + knn: bool = True, + method: _Method = "umap", + transformer: KnnTransformerLike | _KnownTransformer | None = None, + metric: _Metric | _MetricFn = "euclidean", + metric_kwds: Mapping[str, Any] = MappingProxyType({}), + random_state: _LegacyRandom = 0, + key_added: str | None = None, + copy: bool = False, +) -> AnnData | None: + """Compute the nearest neighbors distance matrix and a neighborhood graph of observations :cite:p:`McInnes2018`. + + The neighbor search efficiency of this heavily relies on UMAP :cite:p:`McInnes2018`, + which also provides a method for estimating connectivities of data points - + the connectivity of the manifold (`method=='umap'`). If `method=='gauss'`, + connectivities are computed according to :cite:t:`Coifman2005`, in the adaption of + :cite:t:`Haghverdi2016`. + + Parameters + ---------- + adata + Annotated data matrix. + n_neighbors + The size of local neighborhood (in terms of number of neighboring data + points) used for manifold approximation. Larger values result in more + global views of the manifold, while smaller values result in more local + data being preserved. In general values should be in the range 2 to 100. + If `knn` is `True`, number of nearest neighbors to be searched. If `knn` + is `False`, a Gaussian kernel width is set to the distance of the + `n_neighbors` neighbor. + + *ignored if ``transformer`` is an instance.* + {n_pcs} + {use_rep} + knn + If `True`, use a hard threshold to restrict the number of neighbors to + `n_neighbors`, that is, consider a knn graph. Otherwise, use a Gaussian + Kernel to assign low weights to neighbors more distant than the + `n_neighbors` nearest neighbor. + method + Use 'umap' :cite:p:`McInnes2018` or 'gauss' (Gauss kernel following :cite:t:`Coifman2005` + with adaptive width :cite:t:`Haghverdi2016`) for computing connectivities. + transformer + Approximate kNN search implementation following the API of + :class:`~sklearn.neighbors.KNeighborsTransformer`. + See :doc:`/how-to/knn-transformers` for more details. + Also accepts the following known options: + + `None` (the default) + Behavior depends on data size. + For small data, we will calculate exact kNN, otherwise we use + :class:`~pynndescent.pynndescent_.PyNNDescentTransformer` + `'pynndescent'` + :class:`~pynndescent.pynndescent_.PyNNDescentTransformer` + `'rapids'` + A transformer based on :class:`cuml.neighbors.NearestNeighbors`. + + .. deprecated:: 1.10.0 + Use :func:`rapids_singlecell.pp.neighbors` instead. + metric + A known metric’s name or a callable that returns a distance. + + *ignored if ``transformer`` is an instance.* + metric_kwds + Options for the metric. + + *ignored if ``transformer`` is an instance.* + random_state + A numpy random seed. + + *ignored if ``transformer`` is an instance.* + key_added + If not specified, the neighbors data is stored in `.uns['neighbors']`, + distances and connectivities are stored in `.obsp['distances']` and + `.obsp['connectivities']` respectively. + If specified, the neighbors data is added to .uns[key_added], + distances are stored in `.obsp[key_added+'_distances']` and + connectivities in `.obsp[key_added+'_connectivities']`. + copy + Return a copy instead of writing to adata. + + Returns + ------- + Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following fields: + + `adata.obsp['distances' | key_added+'_distances']` : :class:`scipy.sparse.csr_matrix` (dtype `float`) + Distance matrix of the nearest neighbors search. Each row (cell) has `n_neighbors`-1 non-zero entries. These are the distances to their `n_neighbors`-1 nearest neighbors (excluding the cell itself). + `adata.obsp['connectivities' | key_added+'_connectivities']` : :class:`scipy.sparse._csr.csr_matrix` (dtype `float`) + Weighted adjacency matrix of the neighborhood graph of data + points. Weights should be interpreted as connectivities. + `adata.uns['neighbors' | key_added]` : :class:`dict` + neighbors parameters. + + Examples + -------- + >>> import scanpy as sc + >>> adata = sc.datasets.pbmc68k_reduced() + >>> # Basic usage + >>> sc.pp.neighbors(adata, 20, metric="cosine") + >>> # Provide your own transformer for more control and flexibility + >>> from sklearn.neighbors import KNeighborsTransformer + >>> transformer = KNeighborsTransformer( + ... n_neighbors=10, metric="manhattan", algorithm="kd_tree" + ... ) + >>> sc.pp.neighbors(adata, transformer=transformer) + >>> # now you can e.g. access the index: `transformer._tree` + + See Also + -------- + :doc:`/how-to/knn-transformers` + + """ + start = logg.info("computing neighbors") + adata = adata.copy() if copy else adata + if adata.is_view: # we shouldn't need this here... + adata._init_as_actual(adata.copy()) + neighbors = Neighbors(adata) + neighbors.compute_neighbors( + n_neighbors, + n_pcs=n_pcs, + use_rep=use_rep, + knn=knn, + method=method, + transformer=transformer, + metric=metric, + metric_kwds=metric_kwds, + random_state=random_state, + ) + + if key_added is None: + key_added = "neighbors" + conns_key = "connectivities" + dists_key = "distances" + else: + conns_key = f"{key_added}_connectivities" + dists_key = f"{key_added}_distances" + + adata.uns[key_added] = {} + + neighbors_dict = adata.uns[key_added] + + neighbors_dict["connectivities_key"] = conns_key + neighbors_dict["distances_key"] = dists_key + + neighbors_dict["params"] = NeighborsParams( + n_neighbors=neighbors.n_neighbors, + method=method, + random_state=random_state, + metric=metric, + ) + if metric_kwds: + neighbors_dict["params"]["metric_kwds"] = metric_kwds + if use_rep is not None: + neighbors_dict["params"]["use_rep"] = use_rep + if n_pcs is not None: + neighbors_dict["params"]["n_pcs"] = n_pcs + + adata.obsp[dists_key] = neighbors.distances + adata.obsp[conns_key] = neighbors.connectivities + + if neighbors.rp_forest is not None: + neighbors_dict["rp_forest"] = neighbors.rp_forest + logg.info( + " finished", + time=start, + deep=( + f"added to `.uns[{key_added!r}]`\n" + f" `.obsp[{dists_key!r}]`, distances for each pair of neighbors\n" + f" `.obsp[{conns_key!r}]`, weighted adjacency matrix" + ), + ) + return adata if copy else None + + +class FlatTree(NamedTuple): # noqa: D101 + hyperplanes: None + offsets: None + children: None + indices: None + + +def _backwards_compat_get_full_x_diffmap(adata: AnnData) -> np.ndarray: + if "X_diffmap0" in adata.obs: + return np.c_[adata.obs["X_diffmap0"].values[:, None], adata.obsm["X_diffmap"]] + else: + return adata.obsm["X_diffmap"] + + +def _backwards_compat_get_full_eval(adata: AnnData): + if "X_diffmap0" in adata.obs: + return np.r_[1, adata.uns["diffmap_evals"]] + else: + return adata.uns["diffmap_evals"] + + +def _make_forest_dict(forest): + d = {} + props = ("hyperplanes", "offsets", "children", "indices") + for prop in props: + d[prop] = {} + sizes = np.fromiter( + (getattr(tree, prop).shape[0] for tree in forest), dtype=int + ) + d[prop]["start"] = np.zeros_like(sizes) + if prop == "offsets": + dims = sizes.sum() + else: + dims = (sizes.sum(), getattr(forest[0], prop).shape[1]) + dtype = getattr(forest[0], prop).dtype + dat = np.empty(dims, dtype=dtype) + start = 0 + for i, size in enumerate(sizes): + d[prop]["start"][i] = start + end = start + size + dat[start:end] = getattr(forest[i], prop) + start = end + d[prop]["data"] = dat + return d + + +class OnFlySymMatrix: + """Emulate a matrix where elements are calculated on the fly.""" + + def __init__( + self, + get_row: Callable[[Any], np.ndarray], + shape: tuple[int, int], + *, + rows: MutableMapping[Any, np.ndarray] | None = None, + restrict_array: np.ndarray | None = None, + ): + self.get_row = get_row + self.shape = shape + self.rows = {} if rows is None else rows + self.restrict_array = restrict_array # restrict the array to a subset + + def __getitem__(self, index): # noqa: D105 + if isinstance(index, int | np.integer): + if self.restrict_array is None: + glob_index = index + else: + # map the index back to the global index + glob_index = self.restrict_array[index] + if glob_index not in self.rows: + self.rows[glob_index] = self.get_row(glob_index) + row = self.rows[glob_index] + if self.restrict_array is None: + return row + else: + return row[self.restrict_array] + else: + if self.restrict_array is None: + glob_index_0, glob_index_1 = index + else: + glob_index_0 = self.restrict_array[index[0]] + glob_index_1 = self.restrict_array[index[1]] + if glob_index_0 not in self.rows: + self.rows[glob_index_0] = self.get_row(glob_index_0) + return self.rows[glob_index_0][glob_index_1] + + def restrict(self, index_array): + """Generate a view restricted to a subset of indices.""" + new_shape = index_array.shape[0], index_array.shape[0] + return OnFlySymMatrix( + self.get_row, new_shape, rows=self.rows, restrict_array=index_array + ) + + +class Neighbors: + """Data represented as graph of nearest neighbors. + + Represent a data matrix as a graph of nearest neighbor relations (edges) + among data points (nodes). + + Parameters + ---------- + adata + Annotated data object. + n_dcs + Number of diffusion components to use. + neighbors_key + Where to look in `.uns` and `.obsp` for neighbors data + + """ + + @old_positionals("n_dcs", "neighbors_key") + def __init__( # noqa: PLR0912, PLR0915 + self, + adata: AnnData, + *, + n_dcs: int | None = None, + neighbors_key: str | None = None, + ): + self._adata = adata + self._init_iroot() + # use the graph in adata + info_str = "" + self.knn: bool | None = None + self._distances: np.ndarray | CSRBase | None = None + self._connectivities: np.ndarray | CSRBase | None = None + self._transitions_sym: np.ndarray | CSRBase | None = None + self._number_connected_components: int | None = None + self._rp_forest: RPForestDict | None = None + if neighbors_key is None: + neighbors_key = "neighbors" + if neighbors_key in adata.uns: + neighbors = NeighborsView(adata, neighbors_key) + if "distances" in neighbors: + self.knn = isinstance(neighbors["distances"], CSBase) + self._distances = neighbors["distances"] + if "connectivities" in neighbors: + self.knn = isinstance(neighbors["connectivities"], CSBase) + self._connectivities = neighbors["connectivities"] + if "rp_forest" in neighbors: + self._rp_forest = neighbors["rp_forest"] + if "params" in neighbors: + self.n_neighbors = neighbors["params"]["n_neighbors"] + else: + + def count_nonzero(a: np.ndarray | CSRBase) -> int: + return ( + a.count_nonzero() + if isinstance(a, CSRBase) + else np.count_nonzero(a) + ) + + # estimating n_neighbors + if self._connectivities is None: + self.n_neighbors = int( + count_nonzero(self._distances) / self._distances.shape[0] + ) + else: + self.n_neighbors = int( + count_nonzero(self._connectivities) + / self._connectivities.shape[0] + / 2 + ) + info_str += "`.distances` `.connectivities` " + self._number_connected_components = 1 + if isinstance(self._connectivities, CSBase): + from scipy.sparse.csgraph import connected_components + + self._connected_components = connected_components(self._connectivities) + self._number_connected_components = self._connected_components[0] + if "X_diffmap" in adata.obsm: + self._eigen_values = _backwards_compat_get_full_eval(adata) + self._eigen_basis = _backwards_compat_get_full_x_diffmap(adata) + if n_dcs is not None: + if n_dcs > len(self._eigen_values): + msg = ( + f"Cannot instantiate using `n_dcs`={n_dcs}. " + "Compute diffmap/spectrum with more components first." + ) + raise ValueError(msg) + self._eigen_values = self._eigen_values[:n_dcs] + self._eigen_basis = self._eigen_basis[:, :n_dcs] + self.n_dcs = len(self._eigen_values) + info_str += "`.eigen_values` `.eigen_basis` `.distances_dpt`" + else: + self._eigen_values = None + self._eigen_basis = None + self.n_dcs = None + if info_str != "": + logg.debug(f" initialized {info_str}") + + @property + def rp_forest(self) -> RPForestDict | None: + """PyNNDescent index.""" + return self._rp_forest + + @property + def distances(self) -> np.ndarray | CSRBase | None: + """Distances between data points (sparse matrix).""" + return self._distances + + @property + def connectivities(self) -> np.ndarray | CSRBase | None: + """Connectivities between data points (sparse matrix).""" + return self._connectivities + + @property + def transitions(self) -> np.ndarray | CSRBase: + """Transition matrix (sparse matrix). + + Is conjugate to the symmetrized transition matrix via:: + + self.transitions = self.Z * self.transitions_sym / self.Z + + where ``self.Z`` is the diagonal matrix storing the normalization of the + underlying kernel matrix. + + Notes + ----- + This has not been tested, in contrast to `transitions_sym`. + + """ + z_inv = ( + self.Z.power(-1) + if isinstance(self.Z, SpBase) # can be DIA matrix + else np.diag(1.0 / np.diag(self.Z)) + ) + return self.Z @ self.transitions_sym @ z_inv + + @property + def transitions_sym(self) -> np.ndarray | CSRBase | None: + """Symmetrized transition matrix (sparse matrix). + + Is conjugate to the transition matrix via:: + + self.transitions_sym = self.Z / self.transitions * self.Z + + where ``self.Z`` is the diagonal matrix storing the normalization of the + underlying kernel matrix. + """ + return self._transitions_sym + + @property + def eigen_values(self) -> np.ndarray: + """Eigen values of transition matrix.""" + return self._eigen_values + + @property + def eigen_basis(self) -> np.ndarray: + """Eigen basis of transition matrix.""" + return self._eigen_basis + + @property + def distances_dpt(self) -> OnFlySymMatrix: + """DPT distances. + + This is yields :cite:p:`Haghverdi2016`, Eq. 15 from the supplement with the + extensions of :cite:p:`Wolf2019`, supplement on random-walk based distance + measures. + """ + return OnFlySymMatrix(self._get_dpt_row, shape=self._adata.shape) + + def to_igraph(self) -> Graph: + """Generate igraph from connectiviies.""" + return _utils.get_igraph_from_adjacency(self.connectivities) + + @_doc_params(n_pcs=doc_n_pcs, use_rep=doc_use_rep) + def compute_neighbors( # noqa: PLR0912 + self, + n_neighbors: int = 30, + n_pcs: int | None = None, + *, + use_rep: str | None = None, + knn: bool = True, + method: _Method | None = "umap", + transformer: KnnTransformerLike | _KnownTransformer | None = None, + metric: _Metric | _MetricFn = "euclidean", + metric_kwds: Mapping[str, Any] = MappingProxyType({}), + random_state: _LegacyRandom = 0, + ) -> None: + """Compute distances and connectivities of neighbors. + + Parameters + ---------- + n_neighbors + Use this number of nearest neighbors. + {n_pcs} + {use_rep} + knn + Restrict result to `n_neighbors` nearest neighbors. + method + See :func:`scanpy.pp.neighbors`. + If `None`, skip calculating connectivities. + + Returns + ------- + Writes sparse graph attributes `.distances` and, + if `method` is not `None`, `.connectivities`. + + """ + from ..tools._utils import _choose_representation + + start_neighbors = logg.debug("computing neighbors") + if transformer is not None and not isinstance(transformer, str): + n_neighbors = transformer.get_params()["n_neighbors"] + elif n_neighbors > self._adata.shape[0]: # very small datasets + n_neighbors = 1 + int(0.5 * self._adata.shape[0]) + logg.warning(f"n_obs too small: adjusting to `n_neighbors = {n_neighbors}`") + + # default keyword arguments when `transformer` is not an instance + transformer_kwds_default = KwdsForTransformer( + n_neighbors=n_neighbors, + metric=metric, + metric_params=metric_kwds, # most use _params, not _kwds + random_state=random_state, + ) + method, transformer, shortcut = self._handle_transformer( + method, transformer, knn=knn, kwds=transformer_kwds_default + ) + + if self._adata.shape[0] >= 10000 and not knn: + logg.warning("Using high n_obs without `knn=True` takes a lot of memory...") + # do not use the cached rp_forest + self._rp_forest = None + self.n_neighbors = n_neighbors + self.knn = knn + x = _choose_representation(self._adata, use_rep=use_rep, n_pcs=n_pcs) + self._distances = transformer.fit_transform(x) + knn_indices, knn_distances = _get_indices_distances_from_sparse_matrix( + self._distances, n_neighbors + ) + if shortcut: + # self._distances is a sparse matrix with a diag of 1, fix that + self._distances[np.diag_indices_from(self.distances)] = 0 + if knn: # remove too far away entries in self._distances + self._distances = _get_sparse_matrix_from_indices_distances( + knn_indices, knn_distances, keep_self=False + ) + else: # convert to dense + self._distances = self._distances.toarray() + if index := getattr(transformer, "index_", None): + from pynndescent import NNDescent + + if isinstance(index, NNDescent): + # very cautious here + # TODO catch the correct exception + with contextlib.suppress(Exception): + self._rp_forest = _make_forest_dict(index) + start_connect = logg.debug("computed neighbors", time=start_neighbors) + + if method == "umap": + self._connectivities = _connectivity.umap( + knn_indices, + knn_distances, + n_obs=self._adata.shape[0], + n_neighbors=self.n_neighbors, + ) + elif method == "gauss": + self._connectivities = _connectivity.gauss( + self._distances, self.n_neighbors, knn=self.knn + ) + elif method is not None: + msg = f"{method!r} should have been coerced in _handle_transform_args" + raise AssertionError(msg) + self._number_connected_components = 1 + if isinstance(self._connectivities, CSBase): + from scipy.sparse.csgraph import connected_components + + self._connected_components = connected_components(self._connectivities) + self._number_connected_components = self._connected_components[0] + if method is not None: + logg.debug("computed connectivities", time=start_connect) + + def _handle_transformer( + self, + method: _Method | Literal["gauss"] | None, + transformer: KnnTransformerLike | _KnownTransformer | None, + *, + knn: bool, + kwds: KwdsForTransformer, + ) -> tuple[_Method | None, KnnTransformerLike, bool]: + """Return effective `method` and transformer. + + `method` will be coerced to `'gauss'` or `'umap'`. + `transformer` is coerced from a str or instance to an instance class. + + If `transformer` is `None` and there are few data points, + `transformer` will be set to a brute force + :class:`~sklearn.neighbors.KNeighborsTransformer`. + + If `transformer` is `None` and there are many data points, + `transformer` will be set like `umap` does (i.e. to a + ~`pynndescent.PyNNDescentTransformer` with custom `n_trees` and `n_iter`). + """ + # legacy logic + use_dense_distances = ( + kwds["metric"] == "euclidean" and self._adata.n_obs < 8192 + ) or not knn + shortcut = transformer == "sklearn" or ( + transformer is None and (use_dense_distances or self._adata.n_obs < 4096) + ) + + # Coerce `method` to 'gauss' or 'umap' + if method == "rapids": + if transformer is not None: + msg = "Can’t specify both `method = 'rapids'` and `transformer`." + raise ValueError(msg) + method = "umap" + transformer = "rapids" + elif ( + method not in (methods := get_literal_vals(_Method)) and method is not None + ): + msg = f"`method` needs to be one of {methods}." + raise ValueError(msg) + + # Validate `knn` + conn_method = method if method in {"gauss", None} else "umap" + if not knn and not (conn_method == "gauss" and transformer is None): + # “knn=False” seems to be only intended for method “gauss” + msg = f"`method = {method!r} only with `knn = True`." + raise ValueError(msg) + + # Coerce `transformer` to an instance + if shortcut: + from sklearn.neighbors import KNeighborsTransformer + + assert transformer in {None, "sklearn"} + n_neighbors = self._adata.n_obs - 1 + if knn: # only obey n_neighbors arg if knn set + n_neighbors = min(n_neighbors, kwds["n_neighbors"]) + transformer = KNeighborsTransformer( + algorithm="brute", + n_jobs=settings.n_jobs, + n_neighbors=n_neighbors, + metric=kwds["metric"], + metric_params=dict(kwds["metric_params"]), # needs dict + # no random_state + ) + elif transformer is None or transformer == "pynndescent": + from pynndescent import PyNNDescentTransformer + + kwds = kwds.copy() + kwds["metric_kwds"] = kwds.pop("metric_params") + if transformer is None: + # Use defaults from UMAP’s `nearest_neighbors` function + kwds.update( + n_jobs=settings.n_jobs, + n_trees=min(64, 5 + round((self._adata.n_obs) ** 0.5 / 20.0)), + n_iters=max(5, round(np.log2(self._adata.n_obs))), + ) + transformer = PyNNDescentTransformer(**kwds) + elif transformer == "rapids": + msg = ( + "`transformer='rapids'` is deprecated. " + "Use `rapids_singlecell.tl.neighbors` instead." + ) + warn(msg, FutureWarning, stacklevel=3) + from scanpy.neighbors._backends.rapids import RapidsKNNTransformer + + transformer = RapidsKNNTransformer(**kwds) + elif isinstance(transformer, str): + msg = ( + f"Unknown transformer: {transformer}. " + f"Try passing a class or one of {get_literal_vals(_KnownTransformer)}" + ) + raise ValueError(msg) + # else `transformer` is probably an instance + return conn_method, transformer, shortcut + + @old_positionals("density_normalize") + def compute_transitions(self, *, density_normalize: bool = True): + """Compute transition matrix. + + Parameters + ---------- + density_normalize + The density rescaling of Coifman and Lafon (2006): Then only the + geometry of the data matters, not the sampled density. + + Returns + ------- + Makes attributes `.transitions_sym` and `.transitions` available. + + """ + start = logg.info("computing transitions") + conn = self._connectivities + # density normalization as of Coifman et al. (2005) + # ensures that kernel matrix is independent of sampling density + if density_normalize: + # dens[i] is an estimate for the sampling density at point i + # it's also the degree of the underlying graph + dens = np.asarray(conn.sum(axis=0)) + if not isinstance(conn, CSBase): + dens = np.diag(1.0 / dens) + else: + dens = sparse.spdiags(1.0 / dens, 0, conn.shape[0], conn.shape[0]) + conn_norm = dens @ conn @ dens + else: + conn_norm = conn + + # z[i] is the square root of the row sum of conn_norm + z = np.sqrt(np.asarray(conn_norm.sum(axis=0))) + if not isinstance(conn_norm, CSBase): + self.Z = np.diag(1.0 / z) + else: + self.Z = sparse.spdiags(1.0 / z, 0, conn_norm.shape[0], conn_norm.shape[0]) + self._transitions_sym = self.Z @ conn_norm @ self.Z + logg.info(" finished", time=start) + + def compute_eigen( + self, + *, + n_comps: int = 15, + sym: bool | None = None, + sort: Literal["decrease", "increase"] = "decrease", + random_state: _LegacyRandom = 0, + ): + """Compute eigen decomposition of transition matrix. + + Parameters + ---------- + n_comps + Number of eigenvalues/vectors to be computed, set `n_comps = 0` if + you need all eigenvectors. + sym + Instead of computing the eigendecomposition of the assymetric + transition matrix, computed the eigendecomposition of the symmetric + Ktilde matrix. + random_state + A numpy random seed + + Returns + ------- + Writes the following attributes. + + eigen_values : :class:`~numpy.ndarray` + Eigenvalues of transition matrix. + eigen_basis : :class:`~numpy.ndarray` + Matrix of eigenvectors (stored in columns). `.eigen_basis` is + projection of data matrix on right eigenvectors, that is, the + projection on the diffusion components. these are simply the + components of the right eigenvectors and can directly be used for + plotting. + + """ + np.set_printoptions(precision=10) + if self._transitions_sym is None: + msg = "Run `.compute_transitions` first." + raise ValueError(msg) + matrix = self._transitions_sym + # compute the spectrum + if n_comps == 0: + evals, evecs = scipy.linalg.eigh(matrix) + else: + n_comps = min(matrix.shape[0] - 1, n_comps) + # ncv = max(2 * n_comps + 1, int(np.sqrt(matrix.shape[0]))) + ncv = None + which = "LM" if sort == "decrease" else "SM" + # it pays off to increase the stability with a bit more precision + matrix = matrix.astype(np.float64) + + # Setting the random initial vector + random_state = check_random_state(random_state) + v0 = random_state.standard_normal(matrix.shape[0]) + evals, evecs = sparse.linalg.eigsh( + matrix, k=n_comps, which=which, ncv=ncv, v0=v0 + ) + evals, evecs = evals.astype(np.float32), evecs.astype(np.float32) + if sort == "decrease": + evals = evals[::-1] + evecs = evecs[:, ::-1] + logg.info(f" eigenvalues of transition matrix\n{indent(str(evals), ' ')}") + if self._number_connected_components > len(evals) / 2: + logg.warning("Transition matrix has many disconnected components!") + self._eigen_values = evals + self._eigen_basis = evecs + + def _init_iroot(self): + self.iroot = None + # set iroot directly + if "iroot" in self._adata.uns: + if self._adata.uns["iroot"] >= self._adata.n_obs: + msg = ( + f"Root cell index {self._adata.uns['iroot']} does not " + f"exist for {self._adata.n_obs} samples. It’s ignored." + ) + logg.warning(msg) + else: + self.iroot = self._adata.uns["iroot"] + return + # set iroot via xroot + xroot = None + if "xroot" in self._adata.uns: + xroot = self._adata.uns["xroot"] + elif "xroot" in self._adata.var: + xroot = self._adata.var["xroot"] + # see whether we can set self.iroot using the full data matrix + if xroot is not None and xroot.size == self._adata.shape[1]: + self._set_iroot_via_xroot(xroot) + + def _get_dpt_row(self, i: int) -> np.ndarray: + mask = None + if self._number_connected_components > 1: + label = self._connected_components[1][i] + mask = self._connected_components[1] == label + row = sum( + ( + self.eigen_values[j] + / (1 - self.eigen_values[j]) + * (self.eigen_basis[i, j] - self.eigen_basis[:, j]) + ) + ** 2 + # account for float32 precision + for j in range(self.eigen_values.size) + if self.eigen_values[j] < 0.9994 + ) + # thanks to Marius Lange for pointing Alex to this: + # we will likely remove the contributions from the stationary state below when making + # backwards compat breaking changes, they originate from an early implementation in 2015 + # they never seem to have deteriorated results, but also other distance measures (see e.g. + # PAGA paper) don't have it, which makes sense + row += sum( + (self.eigen_basis[i, k] - self.eigen_basis[:, k]) ** 2 + for k in range(self.eigen_values.size) + if self.eigen_values[k] >= 0.9994 + ) + if mask is not None: + row[~mask] = np.inf + return np.sqrt(row) + + def _set_pseudotime(self): + """Return pseudotime with respect to root point.""" + self.pseudotime = self.distances_dpt[self.iroot].copy() + self.pseudotime /= np.max(self.pseudotime[self.pseudotime < np.inf]) + + def _set_iroot_via_xroot(self, xroot: np.ndarray): + """Determine the index of the root cell. + + Given an expression vector, find the observation index that is closest + to this vector. + + Parameters + ---------- + xroot + Vector that marks the root cell, the vector storing the initial + condition, only relevant for computing pseudotime. + + """ + if self._adata.shape[1] != xroot.size: + msg = "The root vector you provided does not have the correct dimension." + raise ValueError(msg) + # this is the squared distance + dsqroot = 1e10 + iroot = 0 + for i in range(self._adata.shape[0]): + diff = self._adata.X[i, :] - xroot + dsq = diff @ diff + if dsq < dsqroot: + dsqroot = dsq + iroot = i + if np.sqrt(dsqroot) < 1e-10: + break + logg.debug(f"setting root index to {iroot}") + if self.iroot is not None and iroot != self.iroot: + logg.warning(f"Changing index of iroot from {self.iroot} to {iroot}.") + self.iroot = iroot diff --git a/.venv/lib/python3.11/site-packages/scanpy/neighbors/_backends/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/neighbors/_backends/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/.venv/lib/python3.11/site-packages/scanpy/neighbors/_backends/_common.py b/.venv/lib/python3.11/site-packages/scanpy/neighbors/_backends/_common.py new file mode 100644 index 0000000000000000000000000000000000000000..7c34fa078016c148fc8b303e7b2e46f8c1229c31 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/neighbors/_backends/_common.py @@ -0,0 +1,11 @@ +from __future__ import annotations + + +class TransformerChecksMixin: + def _transform_checks(self, x, /, *fitted_props, **check_params): + from sklearn.utils.validation import check_is_fitted + + if x is not None: + x = self._validate_data(x, reset=False, **check_params) + check_is_fitted(self, *fitted_props) + return x diff --git a/.venv/lib/python3.11/site-packages/scanpy/neighbors/_backends/rapids.py b/.venv/lib/python3.11/site-packages/scanpy/neighbors/_backends/rapids.py new file mode 100644 index 0000000000000000000000000000000000000000..b8a073a68ca0d39c2c4bcc72e0046e3be70e9ad4 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/neighbors/_backends/rapids.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.exceptions import NotFittedError +from sklearn.utils.validation import check_is_fitted + +from ..._settings import settings +from ._common import TransformerChecksMixin + +if TYPE_CHECKING: + from collections.abc import Mapping + from typing import Any, Literal + + from numpy.typing import ArrayLike + + from ..._compat import CSRBase + + _Algorithm = Literal["rbc", "brute", "ivfflat", "ivfpq"] + _Metric = Literal[ + "l1", + "cityblock", + "taxicab", + "manhattan", + "euclidean", + "l2", + "braycurtis", + "canberra", + "minkowski", + "chebyshev", + "jensenshannon", + "cosine", + "correlation", + ] + + +class RapidsKNNTransformer(TransformerChecksMixin, TransformerMixin, BaseEstimator): + """Compute nearest neighbors using RAPIDS cuml. + + See :class:`cuml.neighbors.NearestNeighbors`. + """ + + def __init__( + self, + *, + handle=None, + algorithm: _Algorithm | Literal["auto"] = "auto", + n_neighbors: int, + metric: _Metric = "euclidean", + p: int = 2, + algo_params: Mapping[str, Any] | None = None, + metric_params: Mapping[str, Any] | None = None, + random_state=None, + ) -> None: + from cuml.neighbors import NearestNeighbors + + self.n_neighbors = n_neighbors + self.metric = metric + self.p = p + self.nn = NearestNeighbors( + n_neighbors=n_neighbors, + # https://docs.rapids.ai/api/cuml/nightly/api/#verbosity-levels + verbose=settings.verbosity + 2, + handle=handle, + algorithm=algorithm, + metric=metric, + p=p, + algo_params=algo_params, + metric_params=metric_params, + output_type="input", # could also be None to respect global setting + ) + + def __sklearn_is_fitted__(self) -> bool: + try: + check_is_fitted(self.nn) + except NotFittedError: + return False + else: + return True + + def fit(self, x: ArrayLike, /, y: Any = None) -> RapidsKNNTransformer: + """Index data for knn search.""" + x_contiguous = np.ascontiguousarray(x, dtype=np.float32) + self.nn.fit(x_contiguous) + return self + + def transform(self, x: ArrayLike, /) -> CSRBase: + """Perform knn search on the index.""" + self._transform_checks(x) + x_contiguous = np.ascontiguousarray(x, dtype=np.float32) + return self.nn.kneighbors_graph(x_contiguous, mode="distance") + + def _more_tags(self) -> dict[str, Any]: + """See :label:`sklearn:estimator_tags`.""" + return { + "requires_y": False, + "preserves_dtype": [np.float32], + "non_deterministic": True, + } diff --git a/.venv/lib/python3.11/site-packages/scanpy/neighbors/_common.py b/.venv/lib/python3.11/site-packages/scanpy/neighbors/_common.py new file mode 100644 index 0000000000000000000000000000000000000000..c30d724afbcdc4fd4802cf189449c3ac664c54e9 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/neighbors/_common.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from warnings import warn + +import numpy as np +from scipy import sparse + +from .._utils.compute.is_constant import is_constant + +if TYPE_CHECKING: + from numpy.typing import NDArray + + from .._compat import CSRBase + + +def _has_self_column( + indices: NDArray[np.int32 | np.int64], + distances: NDArray[np.float32 | np.float64], +) -> bool: + # some algorithms have some messed up reordering. + return (indices[:, 0] == np.arange(indices.shape[0])).any() + + +def _remove_self_column( + indices: NDArray[np.int32 | np.int64], + distances: NDArray[np.float32 | np.float64], +) -> tuple[NDArray[np.int32 | np.int64], NDArray[np.float32 | np.float64]]: + if not _has_self_column(indices, distances): + msg = "The first neighbor should be the cell itself." + raise AssertionError(msg) + return indices[:, 1:], distances[:, 1:] + + +def _get_sparse_matrix_from_indices_distances( + indices: NDArray[np.int32 | np.int64], + distances: NDArray[np.float32 | np.float64], + *, + keep_self: bool, +) -> CSRBase: + """Create a sparse matrix from a pair of indices and distances. + + If keep_self=False, it verifies that the first column is the cell itself, + then removes it from the explicitly stored zeroes. + + Duplicates in the data are kept as explicitly stored zeroes. + """ + # instead of calling .eliminate_zeros() on our sparse matrix, + # we manually handle the nearest neighbor being the cell itself. + # This allows us to use _ind_dist_shortcut even when the data has duplicates. + if not keep_self: + indices, distances = _remove_self_column(indices, distances) + indptr = np.arange(0, np.prod(indices.shape) + 1, indices.shape[1]) + return sparse.csr_matrix( # noqa: TID251 + ( + distances.copy().ravel(), # copy the data, otherwise strange behavior here + indices.copy().ravel(), + indptr, + ), + shape=(indices.shape[0],) * 2, + ) + + +def _get_indices_distances_from_dense_matrix( + d: NDArray[np.float32 | np.float64], /, n_neighbors: int +): + sample_range = np.arange(d.shape[0])[:, None] + indices = np.argpartition(d, n_neighbors - 1, axis=1)[:, :n_neighbors] + indices = indices[sample_range, np.argsort(d[sample_range, indices])] + distances = d[sample_range, indices] + return indices, distances + + +def _get_indices_distances_from_sparse_matrix( + d: CSRBase, /, n_neighbors: int +) -> tuple[NDArray[np.int32 | np.int64], NDArray[np.float32 | np.float64]]: + """Get indices and distances from a sparse matrix. + + Makes sure that for both of the returned matrices: + 1. the first column corresponds to the cell itself as nearest neighbor. + 2. the number of neighbors (`.shape[1]`) is restricted to `n_neighbors`. + """ + if (shortcut := _ind_dist_shortcut(d)) is not None: + indices, distances = shortcut + else: + indices, distances = _ind_dist_slow(d, n_neighbors) + + # handle RAPIDS style indices_distances lacking the self-column + if not _has_self_column(indices, distances): + indices = np.hstack([np.arange(indices.shape[0])[:, None], indices]) + distances = np.hstack([np.zeros(distances.shape[0])[:, None], distances]) + + # If using the shortcut or adding the self column resulted in too many neighbors, + # restrict the output matrices to the correct size + if indices.shape[1] > n_neighbors: + indices, distances = indices[:, :n_neighbors], distances[:, :n_neighbors] + + return indices, distances + + +def _ind_dist_slow( + d: CSRBase, /, n_neighbors: int +) -> tuple[NDArray[np.int32 | np.int64], NDArray[np.float32 | np.float64]]: + indices = np.zeros((d.shape[0], n_neighbors), dtype=int) + distances = np.zeros((d.shape[0], n_neighbors), dtype=d.dtype) + n_neighbors_m1 = n_neighbors - 1 + for i in range(indices.shape[0]): + neighbors = d[i].nonzero() # 'true' and 'spurious' zeros + indices[i, 0] = i + distances[i, 0] = 0 + # account for the fact that there might be more than n_neighbors + # due to an approximate search + # [the point itself was not detected as its own neighbor during the search] + if len(neighbors[1]) > n_neighbors_m1: + sorted_indices = np.argsort(d[i][neighbors].A1)[:n_neighbors_m1] + indices[i, 1:] = neighbors[1][sorted_indices] + distances[i, 1:] = d[i][ + neighbors[0][sorted_indices], neighbors[1][sorted_indices] + ] + else: + indices[i, 1:] = neighbors[1] + distances[i, 1:] = d[i][neighbors] + return indices, distances + + +def _ind_dist_shortcut( + d: CSRBase, / +) -> tuple[NDArray[np.int32 | np.int64], NDArray[np.float32 | np.float64]] | None: + """Shortcut for scipy or RAPIDS style distance matrices.""" + # Check if each row has the correct number of entries + nnzs = d.getnnz(axis=1) + if not is_constant(nnzs): + msg = ( + "Sparse matrix has no constant number of neighbors per row. " + "Cannot efficiently get indices and distances." + ) + # 4: caller -> 3: `Neighbors.compute_neighbors` -> 2: `_get_indices_distances_from_sparse_matrix` -> 1: here + warn(msg, category=RuntimeWarning, stacklevel=4) + return None + n_obs, n_neighbors = d.shape[0], int(nnzs[0]) + return ( + d.indices.reshape(n_obs, n_neighbors), + d.data.reshape(n_obs, n_neighbors), + ) diff --git a/.venv/lib/python3.11/site-packages/scanpy/neighbors/_connectivity.py b/.venv/lib/python3.11/site-packages/scanpy/neighbors/_connectivity.py new file mode 100644 index 0000000000000000000000000000000000000000..d616f9e8da7950064aae745d6d80a1cc379622b1 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/neighbors/_connectivity.py @@ -0,0 +1,138 @@ +from __future__ import annotations + +import warnings +from typing import TypeVar + +import numpy as np +from numpy.typing import NDArray +from scipy import sparse + +from .._compat import CSRBase +from ._common import ( + _get_indices_distances_from_dense_matrix, + _get_indices_distances_from_sparse_matrix, +) + +D = TypeVar("D", NDArray[np.float32], CSRBase) + + +def gauss(distances: D, n_neighbors: int, *, knn: bool) -> D: # noqa: PLR0912 + """Derive gaussian connectivities between data points from their distances. + + Parameters + ---------- + distances + The input matrix of distances between data points. + n_neighbors + The number of nearest neighbors to consider. + knn + Specify if the distances have been restricted to k nearest neighbors. + + """ + # init distances + if isinstance(distances, CSRBase): + d_sq = distances.power(2) + indices, distances_sq = _get_indices_distances_from_sparse_matrix( + d_sq, n_neighbors + ) + else: + assert isinstance(distances, np.ndarray) + d_sq = np.power(distances, 2) + indices, distances_sq = _get_indices_distances_from_dense_matrix( + d_sq, n_neighbors + ) + + # exclude the first point, the 0th neighbor + indices = indices[:, 1:] + distances_sq = distances_sq[:, 1:] + + # choose sigma, the heuristic here doesn't seem to make much of a difference, + # but is used to reproduce the figures of Haghverdi et al. (2016) + if isinstance(distances, CSRBase): + # as the distances are not sorted + # we have decay within the n_neighbors first neighbors + sigmas_sq = np.median(distances_sq, axis=1) + else: + # the last item is already in its sorted position through argpartition + # we have decay beyond the n_neighbors neighbors + sigmas_sq = distances_sq[:, -1] / 4 + sigmas = np.sqrt(sigmas_sq) + + # compute the symmetric weight matrix + if not isinstance(distances, CSRBase): + num = 2 * np.multiply.outer(sigmas, sigmas) + den = np.add.outer(sigmas_sq, sigmas_sq) + w = np.sqrt(num / den) * np.exp(-d_sq / den) + # make the weight matrix sparse + if not knn: + mask = w > 1e-14 + w[~mask] = 0 + else: + # restrict number of neighbors to ~k + # build a symmetric mask + mask = np.zeros(d_sq.shape, dtype=bool) + for i, row in enumerate(indices): + mask[i, row] = True + for j in row: + if i not in set(indices[j]): + w[j, i] = w[i, j] + mask[j, i] = True + # set all entries that are not nearest neighbors to zero + w[~mask] = 0 + else: + assert isinstance(d_sq, CSRBase) + # need to copy the distance matrix here; what follows is inplace + w = d_sq.copy() + for i in range(len(d_sq.indptr[:-1])): + row = d_sq.indices[d_sq.indptr[i] : d_sq.indptr[i + 1]] + num = 2 * sigmas[i] * sigmas[row] + den = sigmas_sq[i] + sigmas_sq[row] + w.data[d_sq.indptr[i] : d_sq.indptr[i + 1]] = np.sqrt(num / den) * np.exp( + -d_sq.data[d_sq.indptr[i] : d_sq.indptr[i + 1]] / den + ) + w = w.tolil() + for i, row in enumerate(indices): + for j in row: + if i not in set(indices[j]): + w[j, i] = w[i, j] + w = w.tocsr() + + return w + + +def umap( + knn_indices: NDArray[np.int32 | np.int64], + knn_dists: NDArray[np.float32 | np.float64], + *, + n_obs: int, + n_neighbors: int, + set_op_mix_ratio: float = 1.0, + local_connectivity: float = 1.0, +) -> CSRBase: + """Wrap for `umap.fuzzy_simplicial_set` :cite:p:`McInnes2018`. + + Given a set of data X, a neighborhood size, and a measure of distance + compute the fuzzy simplicial set (here represented as a fuzzy graph in + the form of a sparse matrix) associated to the data. This is done by + locally approximating geodesic distance at each point, creating a fuzzy + simplicial set for each such point, and then combining all the local + fuzzy simplicial sets into a global one via a fuzzy union. + """ + with warnings.catch_warnings(): + # umap 0.5.0 + warnings.filterwarnings("ignore", message=r"Tensorflow not installed") + from umap.umap_ import fuzzy_simplicial_set + + x = sparse.coo_matrix((n_obs, 1)) + connectivities, _sigmas, _rhos = fuzzy_simplicial_set( + x, + n_neighbors, + None, + None, + knn_indices=knn_indices, + knn_dists=knn_dists, + set_op_mix_ratio=set_op_mix_ratio, + local_connectivity=local_connectivity, + ) + + return connectivities.tocsr() diff --git a/.venv/lib/python3.11/site-packages/scanpy/neighbors/_doc.py b/.venv/lib/python3.11/site-packages/scanpy/neighbors/_doc.py new file mode 100644 index 0000000000000000000000000000000000000000..ad14654343d6591dae589e1c6ae6a9b51fec899e --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/neighbors/_doc.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +doc_use_rep = """\ +use_rep + Use the indicated representation. `'X'` or any key for `.obsm` is valid. + If `None`, the representation is chosen automatically: + For `.n_vars` < :attr:`~scanpy.settings.N_PCS` (default: 50), `.X` is used, otherwise 'X_pca' is used. + If 'X_pca' is not present, it’s computed with default parameters or `n_pcs` if present.\ +""" + +doc_n_pcs = """\ +n_pcs + Use this many PCs. If `n_pcs==0` use `.X` if `use_rep is None`.\ +""" diff --git a/.venv/lib/python3.11/site-packages/scanpy/neighbors/_types.py b/.venv/lib/python3.11/site-packages/scanpy/neighbors/_types.py new file mode 100644 index 0000000000000000000000000000000000000000..7c52ae56e6c8c72d118fa9e2a9193253bec27bd2 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/neighbors/_types.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from collections.abc import Callable +from typing import TYPE_CHECKING, Literal, Protocol + +import numpy as np + +if TYPE_CHECKING: + from typing import Any, Self + + from .._compat import CSRBase + + +# These two are used with get_literal_vals elsewhere +_Method = Literal["umap", "gauss"] +_KnownTransformer = Literal["pynndescent", "sklearn", "rapids"] + +# sphinx-autodoc-typehints can’t transitively import types from if TYPE_CHECKING blocks, +# so these four needs to be importable + +_MetricFn = Callable[[np.ndarray, np.ndarray], float] +# from sklearn.metrics.pairwise_distances.__doc__: +_MetricSparseCapable = Literal[ + "cityblock", "cosine", "euclidean", "l1", "l2", "manhattan" +] +_MetricScipySpatial = Literal[ + "braycurtis", + "canberra", + "chebyshev", + "correlation", + "dice", + "hamming", + "jaccard", + "kulsinski", + "mahalanobis", + "minkowski", + "rogerstanimoto", + "russellrao", + "seuclidean", + "sokalmichener", + "sokalsneath", + "sqeuclidean", + "yule", +] +_Metric = _MetricSparseCapable | _MetricScipySpatial + + +class KnnTransformerLike(Protocol): + """See :class:`~sklearn.neighbors.KNeighborsTransformer`.""" + + def fit(self, x, /, y: None = None): ... + def transform(self, x, /) -> CSRBase: ... + + # from TransformerMixin + def fit_transform(self, x, /, y: None = None) -> CSRBase: ... + + # from BaseEstimator + def get_params(self, *, deep: bool = True) -> dict[str, Any]: ... + def set_params(self, **params: Any) -> Self: ... diff --git a/.venv/lib/python3.11/site-packages/scanpy/plotting/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/plotting/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b1c4e7832d8047657a9c467ad02dedd00cc89567 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/plotting/__init__.py @@ -0,0 +1,110 @@ +"""Plotting functions and classes.""" + +from __future__ import annotations + +from .._compat import deprecated +from . import palettes +from ._anndata import ( + clustermap, + correlation_matrix, + dendrogram, + heatmap, + ranking, + scatter, + tracksplot, + violin, +) +from ._dotplot import DotPlot, dotplot +from ._matrixplot import MatrixPlot, matrixplot +from ._preprocessing import filter_genes_dispersion, highly_variable_genes +from ._qc import highest_expr_genes +from ._rcmod import set_rcParams_defaults, set_rcParams_scanpy +from ._scrublet import scrublet_score_distribution +from ._stacked_violin import StackedViolin, stacked_violin +from ._tools import ( + dpt_groups_pseudotime, + dpt_timeseries, + embedding_density, + pca_loadings, + pca_overview, + pca_scatter, + pca_variance_ratio, + rank_genes_groups, + rank_genes_groups_dotplot, + rank_genes_groups_heatmap, + rank_genes_groups_matrixplot, + rank_genes_groups_stacked_violin, + rank_genes_groups_tracksplot, + rank_genes_groups_violin, + sim, +) +from ._tools.paga import ( + paga, + paga_adjacency, # noqa: F401 + paga_compare, + paga_path, +) +from ._tools.scatterplots import ( + diffmap, + draw_graph, + embedding, + pca, + spatial, + tsne, + umap, +) +from ._utils import matrix, timeseries, timeseries_as_heatmap, timeseries_subplot + +__all__ = [ + "DotPlot", + "MatrixPlot", + "StackedViolin", + "clustermap", + "correlation_matrix", + "dendrogram", + "diffmap", + "dotplot", + "dpt_groups_pseudotime", + "dpt_timeseries", + "draw_graph", + "embedding", + "embedding_density", + "filter_genes_dispersion", + "heatmap", + "highest_expr_genes", + "highly_variable_genes", + "matrix", + "matrixplot", + "paga", + "paga_compare", + "paga_path", + "palettes", + "pca", + "pca_loadings", + "pca_overview", + "pca_scatter", + "pca_variance_ratio", + "rank_genes_groups", + "rank_genes_groups_dotplot", + "rank_genes_groups_heatmap", + "rank_genes_groups_matrixplot", + "rank_genes_groups_stacked_violin", + "rank_genes_groups_tracksplot", + "rank_genes_groups_violin", + "ranking", + "scatter", + "scrublet_score_distribution", + "set_rcParams_defaults", + "set_rcParams_scanpy", + "sim", + "spatial", + "stacked_violin", + "tracksplot", + "tsne", + "umap", + "violin", +] + +timeseries = deprecated("Use `dpt_timeseries`.")(timeseries) +timeseries_as_heatmap = deprecated("Use `dpt_timeseries`.")(timeseries_as_heatmap) +timeseries_subplot = deprecated("Use `dpt_timeseries`.")(timeseries_subplot) diff --git a/.venv/lib/python3.11/site-packages/scanpy/plotting/_anndata.py b/.venv/lib/python3.11/site-packages/scanpy/plotting/_anndata.py new file mode 100644 index 0000000000000000000000000000000000000000..2cf40adcdf70c1f16f5919c134c0a7e3f391a157 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/plotting/_anndata.py @@ -0,0 +1,2688 @@ +"""Plotting functions for AnnData.""" + +from __future__ import annotations + +from collections import OrderedDict +from collections.abc import Collection, Mapping, Sequence +from itertools import pairwise, product +from types import NoneType +from typing import TYPE_CHECKING, NamedTuple, TypedDict, cast + +import numpy as np +import pandas as pd +from matplotlib import colormaps, gridspec, patheffects, rcParams +from matplotlib import pyplot as plt +from matplotlib.colors import is_color_like +from pandas.api.types import CategoricalDtype, is_numeric_dtype + +from .. import get +from .. import logging as logg +from .._compat import CSBase, old_positionals +from .._settings import settings +from .._utils import ( + _check_use_raw, + _doc_params, + _empty, + get_literal_vals, + sanitize_anndata, +) +from . import _utils +from ._docs import ( + doc_common_plot_args, + doc_scatter_basic, + doc_show_save_ax, + doc_vboundnorm, +) +from ._utils import ( + _deprecated_scale, + _dk, + check_colornorm, + scatter_base, + scatter_group, + setup_axes, +) + +if TYPE_CHECKING: + from collections.abc import Iterable + from typing import Literal, Self + + from anndata import AnnData + from cycler import Cycler + from matplotlib.axes import Axes + from matplotlib.colors import Colormap, ListedColormap, Normalize + from numpy.typing import NDArray + from seaborn import FacetGrid + from seaborn.matrix import ClusterGrid + + from .._utils import Empty + from ._utils import ( + ColorLike, + DensityNorm, + _FontSize, + _FontWeight, + _LegendLoc, + ) + + # TODO: is that all? + _Basis = Literal["pca", "tsne", "umap", "diffmap", "draw_graph_fr"] + _VarNames = str | Sequence[str] + + +class VarGroups(NamedTuple): + labels: Sequence[str] + """Var labels.""" + positions: Sequence[tuple[int, int]] + """Var positions. + + Each item in the list should contain the start and end position that the bracket should cover. + Eg. `[(0, 4), (5, 8)]` means that there are two brackets, + one for the var_names (eg genes) in positions 0-4 and other for positions 5-8 + """ + + @classmethod + def validate( + cls, labels: Sequence[str] | None, positions: Sequence[tuple[int, int]] | None + ) -> Self | None: + if labels is None and positions is None: + return None + if labels is None or positions is None: + msg = ( + "If var_group_labels or var_group_positions are given, " + "both have to be given." + ) + raise ValueError(msg) + if len(labels) != len(positions): + msg = ( + "var_group_labels and var_group_positions must have the same length. " + f"Got {len(labels)=} and {len(positions)=}." + ) + raise ValueError(msg) + return None if len(labels) == 0 else cls(labels, positions) + + +@old_positionals( + "color", + "use_raw", + "layers", + "sort_order", + "alpha", + "basis", + "groups", + "components", + "projection", + "legend_loc", + "legend_fontsize", + "legend_fontweight", + "legend_fontoutline", + "color_map", + # 17 positionals are enough for backwards compatibility +) +@_doc_params(scatter_temp=doc_scatter_basic, show_save_ax=doc_show_save_ax) +def scatter( # noqa: PLR0913 + adata: AnnData, + x: str | None = None, + y: str | None = None, + *, + color: str | ColorLike | Collection[str | ColorLike] | None = None, + use_raw: bool | None = None, + layers: str | Collection[str] | None = None, + sort_order: bool = True, + alpha: float | None = None, + basis: _Basis | None = None, + groups: str | Iterable[str] | None = None, + components: str | Collection[str] | None = None, + projection: Literal["2d", "3d"] = "2d", + legend_loc: _LegendLoc | None = "right margin", + legend_fontsize: float | _FontSize | None = None, + legend_fontweight: int | _FontWeight | None = None, + legend_fontoutline: float | None = None, + color_map: str | Colormap | None = None, + palette: Cycler | ListedColormap | ColorLike | Sequence[ColorLike] | None = None, + frameon: bool | None = None, + right_margin: float | None = None, + left_margin: float | None = None, + size: float | None = None, + marker: str | Sequence[str] = ".", + title: str | Collection[str] | None = None, + show: bool | None = None, + save: str | bool | None = None, + ax: Axes | None = None, +) -> Axes | list[Axes] | None: + """Scatter plot along observations or variables axes. + + Color the plot using annotations of observations (`.obs`), variables + (`.var`) or expression of genes (`.var_names`). + + Parameters + ---------- + adata + Annotated data matrix. + x + x coordinate. + y + y coordinate. + color + Keys for annotations of observations/cells or variables/genes, + or a hex color specification, e.g., + `'ann1'`, `'#fe57a1'`, or `['ann1', 'ann2']`. + use_raw + Whether to use `raw` attribute of `adata`. Defaults to `True` if `.raw` is present. + layers + Use the `layers` attribute of `adata` if present: specify the layer for + `x`, `y` and `color`. If `layers` is a string, then it is expanded to + `(layers, layers, layers)`. + basis + String that denotes a plotting tool that computed coordinates. + {scatter_temp} + {show_save_ax} + + Returns + ------- + If `show==False` a :class:`~matplotlib.axes.Axes` or a list of it. + + """ + # color can be a obs column name or a matplotlib color specification (or a collection thereof) + if color is not None: + color = cast( + "Collection[str | ColorLike]", + [color] if isinstance(color, str) or is_color_like(color) else color, + ) + args = locals() + + if basis is not None: + return _scatter_obs(**args) + if x is None or y is None: + msg = "Either provide a `basis` or `x` and `y`." + raise ValueError(msg) + if _check_if_annotations(adata, "obs", x=x, y=y, colors=color, use_raw=use_raw): + return _scatter_obs(**args) + if _check_if_annotations(adata, "var", x=x, y=y, colors=color, use_raw=use_raw): + args_t = {**args, "adata": adata.T} + axs = _scatter_obs(**args_t) + # store .uns annotations that were added to the new adata object + adata.uns = args_t["adata"].uns + return axs + msg = ( + "`x`, `y`, and potential `color` inputs must all " + "come from either `.obs` or `.var`" + ) + raise ValueError(msg) + + +def _check_if_annotations( + adata: AnnData, + axis_name: Literal["obs", "var"], + *, + x: str | None = None, + y: str | None = None, + colors: Collection[str | ColorLike] | None = None, + use_raw: bool | None = None, +) -> bool: + """Check if `x`, `y`, and `colors` are annotations of `adata`. + + In the case of `colors`, valid matplotlib colors are also accepted. + + If `axis_name` is `obs`, checks in `adata.obs.columns` and `adata.var_names`, + if `axis_name` is `var`, checks in `adata.var.columns` and `adata.obs_names`. + """ + annotations: pd.Index[str] = getattr(adata, axis_name).columns + other_ax_obj = ( + adata.raw if _check_use_raw(adata, use_raw) and axis_name == "obs" else adata + ) + names: pd.Index[str] = getattr( + other_ax_obj, "var" if axis_name == "obs" else "obs" + ).index + + def is_annotation(needle: pd.Index) -> NDArray[np.bool_]: + return needle.isin({None}) | needle.isin(annotations) | needle.isin(names) + + if not is_annotation(pd.Index([x, y])).all(): + return False + + color_idx = pd.Index(colors if colors is not None else []) + # Colors are valid + color_valid: NDArray[np.bool_] = np.fromiter( + map(is_color_like, color_idx), dtype=np.bool_, count=len(color_idx) + ) + # Annotation names are valid too + color_valid[~color_valid] = is_annotation(color_idx[~color_valid]) + return bool(color_valid.all()) + + +def _scatter_obs( # noqa: PLR0912, PLR0913, PLR0915 + *, + adata: AnnData, + x: str | None = None, + y: str | None = None, + color: Collection[str | ColorLike] | None = None, + use_raw: bool | None = None, + layers: str | Collection[str] | None = None, + sort_order: bool = True, + alpha: float | None = None, + basis: _Basis | None = None, + groups: str | Iterable[str] | None = None, + components: str | Collection[str] | None = None, + projection: Literal["2d", "3d"] = "2d", + legend_loc: _LegendLoc | None = "right margin", + legend_fontsize: float | _FontSize | None = None, + legend_fontweight: int | _FontWeight | None = None, + legend_fontoutline: float | None = None, + color_map: str | Colormap | None = None, + palette: Cycler | ListedColormap | ColorLike | Sequence[ColorLike] | None = None, + frameon: bool | None = None, + right_margin: float | None = None, + left_margin: float | None = None, + size: float | None = None, + marker: str | Sequence[str] = ".", + title: str | Collection[str] | None = None, + show: bool | None = None, + save: str | bool | None = None, + ax: Axes | None = None, +) -> Axes | list[Axes] | None: + """See docstring of scatter.""" + sanitize_anndata(adata) + + use_raw = _check_use_raw(adata, use_raw) + + # Process layers + if layers in ["X", None] or (isinstance(layers, str) and layers in adata.layers): + layers = (layers, layers, layers) + elif isinstance(layers, Collection) and len(layers) == 3: + layers = tuple(layers) + for layer in layers: + if layer not in adata.layers and layer not in ["X", None]: + msg = ( + "`layers` should have elements that are " + "either None or in adata.layers.keys()." + ) + raise ValueError(msg) + else: + msg = ( + "`layers` should be a string or a collection of strings " + f"with length 3, had value '{layers}'" + ) + raise ValueError(msg) + if use_raw and layers not in [("X", "X", "X"), (None, None, None)]: + msg = "`use_raw` must be `False` if layers are used." + raise ValueError(msg) + + if legend_loc not in (valid_legend_locs := get_literal_vals(_utils._LegendLoc)): + msg = f"Invalid `legend_loc`, need to be one of: {valid_legend_locs}." + raise ValueError(msg) + if components is None: + components = "1,2" if "2d" in projection else "1,2,3" + if isinstance(components, str): + components = components.split(",") + components = np.array(components).astype(int) - 1 + keys = ["grey"] if color is None else color + if title is not None and isinstance(title, str): + title = [title] + highlights = adata.uns.get("highlights", []) + if basis is not None: + try: + # ignore the '0th' diffusion component + if basis == "diffmap": + components += 1 + xy = adata.obsm["X_" + basis][:, components] + # correct the component vector for use in labeling etc. + if basis == "diffmap": + components -= 1 + except KeyError: + msg = f"compute coordinates using visualization tool {basis} first" + raise KeyError(msg) from None + elif x is not None and y is not None: + if use_raw: + if x in adata.obs.columns: + x_arr = adata.obs_vector(x) + else: + x_arr = adata.raw.obs_vector(x) + if y in adata.obs.columns: + y_arr = adata.obs_vector(y) + else: + y_arr = adata.raw.obs_vector(y) + else: + x_arr = adata.obs_vector(x, layer=layers[0]) + y_arr = adata.obs_vector(y, layer=layers[1]) + + xy = np.c_[x_arr, y_arr] + else: + msg = "Either provide a `basis` or `x` and `y`." + raise ValueError(msg) + + if size is None: + n = xy.shape[0] + size = 120000 / n + + if legend_fontsize is None: + legend_fontsize = rcParams["legend.fontsize"] + + if isinstance(palette, Sequence) and not isinstance(palette, str): + palettes = palette if not is_color_like(palette[0]) else [palette] + else: + palettes = [palette for _ in range(len(keys))] + palettes = [_utils.default_palette(palette) for palette in palettes] + + if basis is not None: + component_name = ( + "DC" + if basis == "diffmap" + else "tSNE" + if basis == "tsne" + else "UMAP" + if basis == "umap" + else "PC" + if basis == "pca" + else "TriMap" + if basis == "trimap" + else basis.replace("draw_graph_", "").upper() + if "draw_graph" in basis + else basis + ) + else: + component_name = None + axis_labels = (x, y) if component_name is None else None + show_ticks = component_name is None + + # generate the colors + color_ids: list[np.ndarray | ColorLike] = [] + categoricals = [] + colorbars = [] + for ikey, key in enumerate(keys): + c = "white" + categorical = False # by default, assume continuous or flat color + colorbar = None + # test whether we have categorial or continuous annotation + if key in adata.obs: + if isinstance(adata.obs[key].dtype, CategoricalDtype): + categorical = True + else: + c = adata.obs[key].to_numpy() + # coloring according to gene expression + elif use_raw and adata.raw is not None and key in adata.raw.var_names: + c = adata.raw.obs_vector(key) + elif key in adata.var_names: + c = adata.obs_vector(key, layer=layers[2]) + elif is_color_like(key): # a flat color + c = key + colorbar = False + else: + msg = ( + f"key {key!r} is invalid! pass valid observation annotation, " + f"one of {adata.obs.columns.tolist()} or a gene name {adata.var_names}" + ) + raise ValueError(msg) + if colorbar is None: + colorbar = not categorical + colorbars.append(colorbar) + if categorical: + categoricals.append(ikey) + color_ids.append(c) + + if right_margin is None and len(categoricals) > 0 and legend_loc == "right margin": + right_margin = 0.5 + if title is None and keys[0] is not None: + title = [ + key.replace("_", " ") if not is_color_like(key) else "" for key in keys + ] + + axs: list[Axes] = scatter_base( + xy, + title=title, + alpha=alpha, + component_name=component_name, + axis_labels=axis_labels, + component_indexnames=components + 1, + projection=projection, + colors=color_ids, + highlights=highlights, + colorbars=colorbars, + right_margin=right_margin, + left_margin=left_margin, + sizes=[size for _ in keys], + markers=marker, + color_map=color_map, + show_ticks=show_ticks, + ax=ax, + ) + + def add_centroid(centroids, name, xy, mask) -> None: + xy_mask = xy[mask] + if xy_mask.shape[0] == 0: + return + median = np.median(xy_mask, axis=0) + i = np.argmin(np.sum(np.abs(xy_mask - median), axis=1)) + centroids[name] = xy_mask[i] + + # loop over all categorical annotation and plot it + for ikey, pal in zip(categoricals, palettes, strict=False): + key = keys[ikey] + _utils.add_colors_for_categorical_sample_annotation( + adata, key, palette=pal, force_update_colors=palette is not None + ) + # actually plot the groups + mask_remaining = np.ones(xy.shape[0], dtype=bool) + centroids = {} + if groups is None: + for iname, name in enumerate(adata.obs[key].cat.categories): + if name not in settings.categories_to_ignore: + mask = scatter_group( + axs[ikey], + key, + iname, + adata, + xy, + projection=projection, + size=size, + alpha=alpha, + marker=marker, + ) + mask_remaining[mask] = False + if legend_loc.startswith("on data"): + add_centroid(centroids, name, xy, mask) + else: + groups = [groups] if isinstance(groups, str) else groups + for name in groups: + if name not in set(adata.obs[key].cat.categories): + msg = ( + f"{name!r} is invalid! specify valid name, " + f"one of {adata.obs[key].cat.categories}" + ) + raise ValueError(msg) + else: + iname = np.flatnonzero( + adata.obs[key].cat.categories.values == name + )[0] + mask = scatter_group( + axs[ikey], + key, + iname, + adata, + xy, + projection=projection, + size=size, + alpha=alpha, + marker=marker, + ) + if legend_loc.startswith("on data"): + add_centroid(centroids, name, xy, mask) + mask_remaining[mask] = False + if mask_remaining.sum() > 0: + data = [xy[mask_remaining, 0], xy[mask_remaining, 1]] + if projection == "3d": + data.append(xy[mask_remaining, 2]) + axs[ikey].scatter( + *data, + marker=marker, + c="lightgrey", + s=size, + edgecolors="none", + zorder=-1, + ) + legend = None + if legend_loc.startswith("on data"): + if legend_fontweight is None: + legend_fontweight = "bold" + if legend_fontoutline is not None: + path_effect = [ + patheffects.withStroke(linewidth=legend_fontoutline, foreground="w") + ] + else: + path_effect = None + for name, pos in centroids.items(): + axs[ikey].text( + pos[0], + pos[1], + name, + weight=legend_fontweight, + verticalalignment="center", + horizontalalignment="center", + fontsize=legend_fontsize, + path_effects=path_effect, + ) + + all_pos = np.zeros((len(adata.obs[key].cat.categories), 2)) + for iname, name in enumerate(adata.obs[key].cat.categories): + all_pos[iname] = centroids.get(name, [np.nan, np.nan]) + if legend_loc == "on data export": + filename = settings.writedir / "pos.csv" + logg.warning(f"exporting label positions to {filename}") + settings.writedir.mkdir(parents=True, exist_ok=True) + np.savetxt(filename, all_pos, delimiter=",") + elif legend_loc == "right margin": + legend = axs[ikey].legend( + frameon=False, + loc="center left", + bbox_to_anchor=(1, 0.5), + ncol=( + 1 + if len(adata.obs[key].cat.categories) <= 14 + else 2 + if len(adata.obs[key].cat.categories) <= 30 + else 3 + ), + fontsize=legend_fontsize, + ) + elif legend_loc != "none": + legend = axs[ikey].legend( + frameon=False, loc=legend_loc, fontsize=legend_fontsize + ) + if legend is not None: + for handle in legend.legend_handles: + handle.set_sizes([300.0]) + + # draw a frame around the scatter + frameon = settings._frameon if frameon is None else frameon + if not frameon and x is None and y is None: + for ax_ in axs: + ax_.set_xlabel("") + ax_.set_ylabel("") + ax_.set_frame_on(False) + + show = settings.autoshow if show is None else show + _utils.savefig_or_show("scatter" if basis is None else basis, show=show, save=save) + if show: + return None + if len(keys) > 1: + return axs + return axs[0] + + +@old_positionals( + "dictionary", + "indices", + "labels", + "color", + "n_points", + "log", + "include_lowest", + "show", +) +def ranking( # noqa: PLR0912, PLR0913 + adata: AnnData, + attr: Literal["var", "obs", "uns", "varm", "obsm"], + keys: str | Sequence[str], + *, + dictionary: str | None = None, + indices: Sequence[int] | None = None, + labels: str | Sequence[str] | None = None, + color: ColorLike = "black", + n_points: int = 30, + log: bool = False, + include_lowest: bool = False, + show: bool | None = None, +) -> gridspec.GridSpec | None: + """Plot rankings. + + See, for example, how this is used in pl.pca_loadings. + + Parameters + ---------- + adata + The data. + attr + The attribute of AnnData that contains the score. + keys + The scores to look up an array from the attribute of adata. + + Returns + ------- + Returns matplotlib gridspec with access to the axes. + + """ + if isinstance(keys, str) and indices is not None: + scores = getattr(adata, attr)[keys][:, indices] + keys = [f"{keys[:-1]}{i + 1}" for i in indices] + elif dictionary is None: + scores = getattr(adata, attr)[keys] + else: + scores = getattr(adata, attr)[dictionary][keys] + n_panels = len(keys) if isinstance(keys, list) else 1 + if n_panels == 1: + scores, keys = scores[:, None], [keys] + if log: + scores = np.log(scores) + if labels is None: + labels = ( + adata.var_names + if attr in {"var", "varm"} + else np.arange(scores.shape[0]).astype(str) + ) + if isinstance(labels, str): + labels = [labels + str(i + 1) for i in range(scores.shape[0])] + if n_panels <= 5: + n_rows, n_cols = 1, n_panels + else: + n_rows, n_cols = 2, int(n_panels / 2 + 0.5) + _ = plt.figure( + figsize=( + n_cols * rcParams["figure.figsize"][0], + n_rows * rcParams["figure.figsize"][1], + ) + ) + left, bottom = 0.2 / n_cols, 0.13 / n_rows + gs = gridspec.GridSpec( + wspace=0.2, + nrows=n_rows, + ncols=n_cols, + left=left, + bottom=bottom, + right=1 - (n_cols - 1) * left - 0.01 / n_cols, + top=1 - (n_rows - 1) * bottom - 0.1 / n_rows, + ) + for iscore, score in enumerate(scores.T): + plt.subplot(gs[iscore]) + order_scores = np.argsort(score)[::-1] + if not include_lowest: + indices = order_scores[: n_points + 1] + else: + indices = order_scores[: n_points // 2] + neg_indices = order_scores[-(n_points - (n_points // 2)) :] + txt_args = dict( + color=color, + rotation="vertical", + verticalalignment="bottom", + horizontalalignment="center", + fontsize=8, + ) + for ig, g in enumerate(indices): + plt.text(ig, score[g], labels[g], **txt_args) + if include_lowest: + score_mid = (score[g] + score[neg_indices[0]]) / 2 + if (len(indices) + len(neg_indices)) < len(order_scores): + plt.text(len(indices), score_mid, "⋮", **txt_args) + for ig, g in enumerate(neg_indices): + plt.text(ig + len(indices) + 2, score[g], labels[g], **txt_args) + else: + for ig, g in enumerate(neg_indices): + plt.text(ig + len(indices), score[g], labels[g], **txt_args) + plt.xticks([]) + plt.title(keys[iscore].replace("_", " ")) + if n_panels <= 5 or iscore > n_cols: + plt.xlabel("ranking") + plt.xlim(-0.9, n_points + 0.9 + (1 if include_lowest else 0)) + score_min, score_max = ( + np.min(score[neg_indices if include_lowest else indices]), + np.max(score[indices]), + ) + plt.ylim( + (0.95 if score_min > 0 else 1.05) * score_min, + (1.05 if score_max > 0 else 0.95) * score_max, + ) + show = settings.autoshow if show is None else show + if show: + return None + return gs + + +@old_positionals( + "log", + "use_raw", + "stripplot", + "jitter", + "size", + "layer", + "scale", + "order", + "multi_panel", + "xlabel", + "ylabel", + "rotation", + "show", + "save", + "ax", +) +@_doc_params(show_save_ax=doc_show_save_ax) +def violin( # noqa: PLR0912, PLR0913, PLR0915 + adata: AnnData, + keys: str | Sequence[str], + groupby: str | None = None, + *, + log: bool = False, + use_raw: bool | None = None, + stripplot: bool = True, + jitter: float | bool = True, + size: int = 1, + layer: str | None = None, + density_norm: DensityNorm = "width", + order: Sequence[str] | None = None, + multi_panel: bool | None = None, + xlabel: str = "", + ylabel: str | Sequence[str] | None = None, + rotation: float | None = None, + show: bool | None = None, + save: bool | str | None = None, + ax: Axes | None = None, + # deprecatd + scale: DensityNorm | Empty = _empty, + **kwds, +) -> Axes | FacetGrid | None: + """Violin plot. + + Wraps :func:`seaborn.violinplot` for :class:`~anndata.AnnData`. + + Parameters + ---------- + adata + Annotated data matrix. + keys + Keys for accessing variables of `.var_names` or fields of `.obs`. + groupby + The key of the observation grouping to consider. + log + Plot on logarithmic axis. + use_raw + Whether to use `raw` attribute of `adata`. Defaults to `True` if `.raw` is present. + stripplot + Add a stripplot on top of the violin plot. + See :func:`~seaborn.stripplot`. + jitter + Add jitter to the stripplot (only when stripplot is True) + See :func:`~seaborn.stripplot`. + size + Size of the jitter points. + layer + Name of the AnnData object layer that wants to be plotted. By + default adata.raw.X is plotted. If `use_raw=False` is set, + then `adata.X` is plotted. If `layer` is set to a valid layer name, + then the layer is plotted. `layer` takes precedence over `use_raw`. + density_norm + The method used to scale the width of each violin. + If 'width' (the default), each violin will have the same width. + If 'area', each violin will have the same area. + If 'count', a violin’s width corresponds to the number of observations. + order + Order in which to show the categories. + multi_panel + Display keys in multiple panels also when `groupby is not None`. + xlabel + Label of the x axis. Defaults to `groupby` if `rotation` is `None`, + otherwise, no label is shown. + ylabel + Label of the y axis. If `None` and `groupby` is `None`, defaults + to `'value'`. If `None` and `groubpy` is not `None`, defaults to `keys`. + rotation + Rotation of xtick labels. + {show_save_ax} + **kwds + Are passed to :func:`~seaborn.violinplot`. + + Returns + ------- + A :class:`~matplotlib.axes.Axes` object if `ax` is `None` else `None`. + + Examples + -------- + + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc68k_reduced() + sc.pl.violin(adata, keys='S_score') + + Plot by category. Rotate x-axis labels so that they do not overlap. + + .. plot:: + :context: close-figs + + sc.pl.violin(adata, keys='S_score', groupby='bulk_labels', rotation=90) + + Set order of categories to be plotted or select specific categories to be plotted. + + .. plot:: + :context: close-figs + + groupby_order = ['CD34+', 'CD19+ B'] + sc.pl.violin(adata, keys='S_score', groupby='bulk_labels', rotation=90, + order=groupby_order) + + Plot multiple keys. + + .. plot:: + :context: close-figs + + sc.pl.violin(adata, keys=['S_score', 'G2M_score'], groupby='bulk_labels', + rotation=90) + + For large datasets consider omitting the overlaid scatter plot. + + .. plot:: + :context: close-figs + + sc.pl.violin(adata, keys='S_score', stripplot=False) + + .. currentmodule:: scanpy + + See Also + -------- + pl.stacked_violin + + """ + import seaborn as sns # Slow import, only import if called + + sanitize_anndata(adata) + use_raw = _check_use_raw(adata, use_raw) + if isinstance(keys, str): + keys = [keys] + keys = list(OrderedDict.fromkeys(keys)) # remove duplicates, preserving the order + density_norm = _deprecated_scale(density_norm, scale, default="width") + del scale + + if isinstance(ylabel, str | NoneType): + ylabel = [ylabel] * (1 if groupby is None else len(keys)) + if groupby is None: + if len(ylabel) != 1: + msg = f"Expected number of y-labels to be `1`, found `{len(ylabel)}`." + raise ValueError(msg) + elif len(ylabel) != len(keys): + msg = f"Expected number of y-labels to be `{len(keys)}`, found `{len(ylabel)}`." + raise ValueError(msg) + + if groupby is not None: + obs_df = get.obs_df(adata, keys=[groupby, *keys], layer=layer, use_raw=use_raw) + if kwds.get("palette") is None: + if not isinstance(adata.obs[groupby].dtype, CategoricalDtype): + msg = ( + f"The column `adata.obs[{groupby!r}]` needs to be categorical, " + f"but is of dtype {adata.obs[groupby].dtype}." + ) + raise ValueError(msg) + _utils.add_colors_for_categorical_sample_annotation(adata, groupby) + kwds["hue"] = groupby + kwds["palette"] = dict( + zip( + obs_df[groupby].cat.categories, + adata.uns[f"{groupby}_colors"], + strict=True, + ) + ) + else: + obs_df = get.obs_df(adata, keys=keys, layer=layer, use_raw=use_raw) + if groupby is None: + obs_tidy = pd.melt(obs_df, value_vars=keys) + x = "variable" + ys = ["value"] + else: + obs_tidy = obs_df + x = groupby + ys = keys + + if multi_panel and groupby is None and len(ys) == 1: + # This is a quick and dirty way for adapting scales across several + # keys if groupby is None. + y = ys[0] + + g: sns.axisgrid.FacetGrid = sns.catplot( + y=y, + data=obs_tidy, + kind="violin", + density_norm=density_norm, + col=x, + col_order=keys, + sharey=False, + cut=0, + inner=None, + **kwds, + ) + + if stripplot: + grouped_df = obs_tidy.groupby(x, observed=True) + for ax_id, key in zip(range(g.axes.shape[1]), keys, strict=True): + sns.stripplot( + y=y, + data=grouped_df.get_group(key), + jitter=jitter, + size=size, + color="black", + ax=g.axes[0, ax_id], + ) + if log: + g.set(yscale="log") + g.set_titles(col_template="{col_name}").set_xlabels("") + if rotation is not None: + for ax_base in g.axes[0]: + ax_base.tick_params(axis="x", labelrotation=rotation) + else: + # set by default the violin plot cut=0 to limit the extend + # of the violin plot (see stacked_violin code) for more info. + kwds.setdefault("cut", 0) + kwds.setdefault("inner") + + if ax is None: + axs, _, _, _ = setup_axes( + ax, + panels=["x"] if groupby is None else keys, + show_ticks=True, + right_margin=0.3, + ) + else: + axs = [ax] + for ax_base, y, ylab in zip(axs, ys, ylabel, strict=True): + sns.violinplot( + x=x, + y=y, + data=obs_tidy, + order=order, + orient="vertical", + density_norm=density_norm, + ax=ax_base, + **kwds, + ) + if stripplot: + sns.stripplot( + x=x, + y=y, + data=obs_tidy, + order=order, + jitter=jitter, + color="black", + size=size, + ax=ax_base, + ) + if xlabel == "" and groupby is not None and rotation is None: + xlabel = groupby.replace("_", " ") + ax_base.set_xlabel(xlabel) + if ylab is not None: + ax_base.set_ylabel(ylab) + if log: + ax_base.set_yscale("log") + if rotation is not None: + ax_base.tick_params(axis="x", labelrotation=rotation) + show = settings.autoshow if show is None else show + _utils.savefig_or_show("violin", show=show, save=save) + if show: + return None + if multi_panel and groupby is None and len(ys) == 1: + return g + if len(axs) == 1: + return axs[0] + return axs + + +@old_positionals("use_raw", "show", "save") +@_doc_params(show_save_ax=doc_show_save_ax) +def clustermap( + adata: AnnData, + obs_keys: str | None = None, + *, + use_raw: bool | None = None, + show: bool | None = None, + save: bool | str | None = None, + **kwds, +) -> ClusterGrid | None: + """Hierarchically-clustered heatmap. + + Wraps :func:`seaborn.clustermap` for :class:`~anndata.AnnData`. + + Parameters + ---------- + adata + Annotated data matrix. + obs_keys + Categorical annotation to plot with a different color map. + Currently, only a single key is supported. + use_raw + Whether to use `raw` attribute of `adata`. Defaults to `True` if `.raw` is present. + {show_save_ax} + **kwds + Keyword arguments passed to :func:`~seaborn.clustermap`. + + Returns + ------- + If `show` is `False`, a :class:`~seaborn.matrix.ClusterGrid` object + (see :func:`~seaborn.clustermap`). + + Examples + -------- + + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.krumsiek11() + sc.pl.clustermap(adata) + + .. plot:: + :context: close-figs + + sc.pl.clustermap(adata, obs_keys='cell_type') + + """ + import seaborn as sns # Slow import, only import if called + + if not isinstance(obs_keys, str | NoneType): + msg = "Currently, only a single key is supported." + raise ValueError(msg) + sanitize_anndata(adata) + use_raw = _check_use_raw(adata, use_raw) + x = adata.raw.X if use_raw else adata.X + if isinstance(x, CSBase): + x = x.toarray() + df = pd.DataFrame(x, index=adata.obs_names, columns=adata.var_names) + if obs_keys is not None: + row_colors = adata.obs[obs_keys] + _utils.add_colors_for_categorical_sample_annotation(adata, obs_keys) + # do this more efficiently... just a quick solution + lut = dict( + zip(row_colors.cat.categories, adata.uns[f"{obs_keys}_colors"], strict=True) + ) + row_colors = adata.obs[obs_keys].map(lut) + g = sns.clustermap(df, row_colors=row_colors.values, **kwds) + else: + g = sns.clustermap(df, **kwds) + show = settings.autoshow if show is None else show + _utils.savefig_or_show("clustermap", show=show, save=save) + if show: + plt.show() + return None + return g + + +@old_positionals( + "use_raw", + "log", + "num_categories", + "dendrogram", + "gene_symbols", + "var_group_positions", + "var_group_labels", + "var_group_rotation", + "layer", + "standard_scale", + "swap_axes", + "show_gene_labels", + "show", + "save", + "figsize", + "vmin", + "vmax", + "vcenter", + "norm", +) +@_doc_params( + vminmax=doc_vboundnorm, + show_save_ax=doc_show_save_ax, + common_plot_args=doc_common_plot_args, +) +def heatmap( # noqa: PLR0912, PLR0913, PLR0915 + adata: AnnData, + var_names: _VarNames | Mapping[str, _VarNames], + groupby: str | Sequence[str], + *, + use_raw: bool | None = None, + log: bool = False, + num_categories: int = 7, + dendrogram: bool | str = False, + gene_symbols: str | None = None, + var_group_positions: Sequence[tuple[int, int]] | None = None, + var_group_labels: Sequence[str] | None = None, + var_group_rotation: float | None = None, + layer: str | None = None, + standard_scale: Literal["var", "obs"] | None = None, + swap_axes: bool = False, + show_gene_labels: bool | None = None, + show: bool | None = None, + save: str | bool | None = None, + figsize: tuple[float, float] | None = None, + vmin: float | None = None, + vmax: float | None = None, + vcenter: float | None = None, + norm: Normalize | None = None, + **kwds, +) -> dict[str, Axes] | None: + """Heatmap of the expression values of genes. + + If `groupby` is given, the heatmap is ordered by the respective group. For + example, a list of marker genes can be plotted, ordered by clustering. If + the `groupby` observation annotation is not categorical the observation + annotation is turned into a categorical by binning the data into the number + specified in `num_categories`. + + Parameters + ---------- + {common_plot_args} + standard_scale + Whether or not to standardize that dimension between 0 and 1, meaning for each variable or observation, + subtract the minimum and divide each by its maximum. + swap_axes + By default, the x axis contains `var_names` (e.g. genes) and the y axis the `groupby` + categories (if any). By setting `swap_axes` then x are the `groupby` categories and y the `var_names`. + show_gene_labels + By default gene labels are shown when there are 50 or less genes. Otherwise the labels are removed. + {show_save_ax} + {vminmax} + **kwds + Are passed to :func:`matplotlib.pyplot.imshow`. + + Returns + ------- + Dict of :class:`~matplotlib.axes.Axes` + + Examples + -------- + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc68k_reduced() + markers = ['C1QA', 'PSAP', 'CD79A', 'CD79B', 'CST3', 'LYZ'] + sc.pl.heatmap(adata, markers, groupby='bulk_labels', swap_axes=True) + + .. currentmodule:: scanpy + + See Also + -------- + pl.rank_genes_groups_heatmap + tl.rank_genes_groups + + """ + var_names, var_groups = _check_var_names_type( + var_names, var_group_labels, var_group_positions + ) + del var_group_labels, var_group_positions + + categories, obs_tidy = _prepare_dataframe( + adata, + var_names, + groupby, + use_raw=use_raw, + log=log, + num_categories=num_categories, + gene_symbols=gene_symbols, + layer=layer, + ) + + # check if var_group_labels are a subset of categories: + var_groups_subset_of_groupby = var_groups is not None and set( + var_groups.labels + ).issubset(categories) + + if standard_scale == "obs": + obs_tidy = obs_tidy.sub(obs_tidy.min(1), axis=0) + obs_tidy = obs_tidy.div(obs_tidy.max(1), axis=0).fillna(0) + elif standard_scale == "var": + obs_tidy -= obs_tidy.min(0) + obs_tidy = (obs_tidy / obs_tidy.max(0)).fillna(0) + elif standard_scale is None: + pass + else: + logg.warning("Unknown type for standard_scale, ignored") + + if groupby is None or len(categories) <= 1: + categorical = False + # dendrogram can only be computed between groupby categories + dendrogram = False + else: + categorical = True + # get categories colors + if isinstance(groupby, str) and isinstance( + adata.obs[groupby].dtype, CategoricalDtype + ): + # saved category colors only work when groupby is valid adata.obs + # categorical column. When groupby is a numerical column + # or when groupby is a list of columns the colors are assigned on the fly, + # which may create inconsistencies in multiple runs that require sorting + # of the categories (eg. when dendrogram is plotted). + if f"{groupby}_colors" not in adata.uns: + # if colors are not found, assign a new palette + # and save it using the same code for embeddings + from ._tools.scatterplots import _get_palette + + _get_palette(adata, groupby) + groupby_colors = adata.uns[f"{groupby}_colors"] + else: + # this case happen when adata.obs[groupby] is numeric + # the values are converted into a category on the fly + groupby_colors = None + + if dendrogram: + dendro_data = _reorder_categories_after_dendrogram( + adata, + groupby, + dendrogram_key=_dk(dendrogram), + var_names=var_names, + var_groups=var_groups, + categories=categories, + ) + + var_groups = dendro_data["var_groups"] + + # reorder obs_tidy + if dendro_data["var_names_idx_ordered"] is not None: + obs_tidy = obs_tidy.iloc[:, dendro_data["var_names_idx_ordered"]] + var_names = [var_names[x] for x in dendro_data["var_names_idx_ordered"]] + + obs_tidy.index = obs_tidy.index.reorder_categories( + [categories[x] for x in dendro_data["categories_idx_ordered"]], + ordered=True, + ) + + # reorder groupby colors + if groupby_colors is not None: + groupby_colors = [ + groupby_colors[x] for x in dendro_data["categories_idx_ordered"] + ] + + if show_gene_labels is None: + if len(var_names) <= 50: + show_gene_labels = True + else: + show_gene_labels = False + logg.warning( + "Gene labels are not shown when more than 50 genes are visualized. " + "To show gene labels set `show_gene_labels=True`" + ) + if categorical: + obs_tidy = obs_tidy.sort_index() + + colorbar_width = 0.2 + norm = check_colornorm(vmin, vmax, vcenter, norm) + + if not swap_axes: + # define a layout of 2 rows x 4 columns + # first row is for 'brackets' (if no brackets needed, the height of this row + # is zero) second row is for main content. This second row is divided into + # three axes: + # first ax is for the categories defined by `groupby` + # second ax is for the heatmap + # third ax is for the dendrogram + # fourth ax is for colorbar + + dendro_width = 1 if dendrogram else 0 + groupby_width = 0.2 if categorical else 0 + if figsize is None: + height = 6 + heatmap_width = len(var_names) * 0.3 if show_gene_labels else 8 + width = heatmap_width + dendro_width + groupby_width + else: + width, height = figsize + heatmap_width = width - (dendro_width + groupby_width) + + # add some space in case 'brackets' want to be plotted on top of the image + height_ratios = (0 if var_groups is None else 0.15, height) + width_ratios = ( + groupby_width, + heatmap_width, + dendro_width, + colorbar_width, + ) + + fig = plt.figure(figsize=(width, height)) + axs = gridspec.GridSpec( + nrows=2, + ncols=4, + width_ratios=width_ratios, + wspace=0.15 / width, + hspace=0.13 / height, + height_ratios=height_ratios, + ) + + heatmap_ax = fig.add_subplot(axs[1, 1]) + kwds.setdefault("interpolation", "nearest") + im = heatmap_ax.imshow(obs_tidy.values, aspect="auto", norm=norm, **kwds) + + heatmap_ax.set_ylim(obs_tidy.shape[0] - 0.5, -0.5) + heatmap_ax.set_xlim(-0.5, obs_tidy.shape[1] - 0.5) + heatmap_ax.tick_params(axis="y", left=False, labelleft=False) + heatmap_ax.set_ylabel("") + heatmap_ax.grid(visible=False) + + if show_gene_labels: + heatmap_ax.tick_params(axis="x", labelsize="small") + heatmap_ax.set_xticks(np.arange(len(var_names))) + heatmap_ax.set_xticklabels(var_names, rotation=90) + else: + heatmap_ax.tick_params(axis="x", labelbottom=False, bottom=False) + # plot colorbar + _plot_colorbar(im, fig, axs[1, 3]) + + if categorical: + groupby_ax = fig.add_subplot(axs[1, 0]) + label2code, ticks, _labels, groupby_cmap, norm = ( + _plot_categories_as_colorblocks( + groupby_ax, obs_tidy, colors=groupby_colors, orientation="left" + ) + ) + + # add lines to main heatmap + line_positions = ( + np.cumsum(obs_tidy.index.value_counts(sort=False))[:-1] - 0.5 + ) + heatmap_ax.hlines( + line_positions, + -0.5, + len(var_names) - 0.5, + lw=1, + color="black", + zorder=10, + clip_on=False, + ) + + if dendrogram: + dendro_ax = fig.add_subplot(axs[1, 2], sharey=heatmap_ax) + _plot_dendrogram( + dendro_ax, adata, groupby, dendrogram_key=_dk(dendrogram), ticks=ticks + ) + + # plot group legends on top of heatmap_ax (if given) + if var_groups is not None: + gene_groups_ax = fig.add_subplot(axs[0, 1], sharex=heatmap_ax) + _plot_var_groups_brackets( + gene_groups_ax, + var_groups=var_groups, + rotation=var_group_rotation, + left_adjustment=-0.3, + right_adjustment=0.3, + ) + + # swap axes case + else: + # define a layout of 3 rows x 3 columns + # The first row is for the dendrogram (if not dendrogram height is zero) + # second row is for main content. This col is divided into three axes: + # first ax is for the heatmap + # second ax is for 'brackets' if any (othwerise width is zero) + # third ax is for colorbar + + dendro_height = 0.8 if dendrogram else 0 + groupby_height = 0.13 if categorical else 0 + if figsize is None: + heatmap_height = len(var_names) * 0.18 if show_gene_labels else 4 + width = 10 + height = heatmap_height + dendro_height + groupby_height + else: + width, height = figsize + heatmap_height = height - (dendro_height + groupby_height) + + height_ratios = (dendro_height, heatmap_height, groupby_height) + # add some space in case 'brackets' want to be plotted on top of the image + width_ratios = (width, 0 if var_groups is None else 0.14, colorbar_width) + + fig = plt.figure(figsize=(width, height)) + axs = gridspec.GridSpec( + nrows=3, + ncols=3, + wspace=0.25 / width, + hspace=0.3 / height, + width_ratios=width_ratios, + height_ratios=height_ratios, + ) + + # plot heatmap + heatmap_ax = fig.add_subplot(axs[1, 0]) + + kwds.setdefault("interpolation", "nearest") + im = heatmap_ax.imshow(obs_tidy.T.values, aspect="auto", norm=norm, **kwds) + heatmap_ax.set_xlim(0 - 0.5, obs_tidy.shape[0] - 0.5) + heatmap_ax.set_ylim(obs_tidy.shape[1] - 0.5, -0.5) + heatmap_ax.tick_params(axis="x", bottom=False, labelbottom=False) + heatmap_ax.set_xlabel("") + heatmap_ax.grid(visible=False) + if show_gene_labels: + heatmap_ax.tick_params(axis="y", labelsize="small", length=1) + heatmap_ax.set_yticks(np.arange(len(var_names))) + heatmap_ax.set_yticklabels(var_names, rotation=0) + else: + heatmap_ax.tick_params(axis="y", labelleft=False, left=False) + + if categorical: + groupby_ax = fig.add_subplot(axs[2, 0]) + label2code, ticks, _labels, groupby_cmap, norm = ( + _plot_categories_as_colorblocks( + groupby_ax, obs_tidy, colors=groupby_colors, orientation="bottom" + ) + ) + # add lines to main heatmap + line_positions = ( + np.cumsum(obs_tidy.index.value_counts(sort=False))[:-1] - 0.5 + ) + heatmap_ax.vlines( + line_positions, + -0.5, + len(var_names) - 0.5, + lw=1, + color="black", + zorder=10, + clip_on=False, + ) + + if dendrogram: + dendro_ax = fig.add_subplot(axs[0, 0], sharex=heatmap_ax) + _plot_dendrogram( + dendro_ax, + adata, + groupby, + dendrogram_key=_dk(dendrogram), + ticks=ticks, + orientation="top", + ) + + # plot group legends next to the heatmap_ax (if given) + if var_groups is not None: + gene_groups_ax = fig.add_subplot(axs[1, 1]) + arr = [] + for idx, (label, pos) in enumerate(zip(*var_groups, strict=True)): + label_code = label2code[label] if var_groups_subset_of_groupby else idx + arr += [label_code] * (pos[1] + 1 - pos[0]) + gene_groups_ax.imshow( + np.array([arr]).T, aspect="auto", cmap=groupby_cmap, norm=norm + ) + gene_groups_ax.axis("off") + + # plot colorbar + _plot_colorbar(im, fig, axs[1, 2]) + + return_ax_dict = {"heatmap_ax": heatmap_ax} + if categorical: + return_ax_dict["groupby_ax"] = groupby_ax + if dendrogram: + return_ax_dict["dendrogram_ax"] = dendro_ax + if var_groups is not None: + return_ax_dict["gene_groups_ax"] = gene_groups_ax + + _utils.savefig_or_show("heatmap", show=show, save=save) + show = settings.autoshow if show is None else show + if show: + return None + return return_ax_dict + + +@old_positionals( + "use_raw", + "log", + "dendrogram", + "gene_symbols", + "var_group_positions", + "var_group_labels", + "layer", + "show", + "save", + "figsize", +) +@_doc_params(show_save_ax=doc_show_save_ax, common_plot_args=doc_common_plot_args) +def tracksplot( # noqa: PLR0912, PLR0913, PLR0915 + adata: AnnData, + var_names: _VarNames | Mapping[str, _VarNames], + groupby: str, + *, + use_raw: bool | None = None, + log: bool = False, + dendrogram: bool | str = False, + gene_symbols: str | None = None, + var_group_positions: Sequence[tuple[int, int]] | None = None, + var_group_labels: Sequence[str] | None = None, + layer: str | None = None, + show: bool | None = None, + save: str | bool | None = None, + figsize: tuple[float, float] | None = None, + **kwds, +) -> dict[str, Axes] | None: + """Compact plot of expression of a list of genes. + + In this type of plot each var_name is plotted as a filled line plot where the + y values correspond to the var_name values and x is each of the cells. Best results + are obtained when using raw counts that are not log. + + `groupby` is required to sort and order the values using the respective group + and should be a categorical value. + + Parameters + ---------- + {common_plot_args} + {show_save_ax} + **kwds + Are passed to :func:`~seaborn.heatmap`. + + Returns + ------- + A list of :class:`~matplotlib.axes.Axes`. + + Examples + -------- + Using var_names as list: + + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc68k_reduced() + markers = ['C1QA', 'PSAP', 'CD79A', 'CD79B', 'CST3', 'LYZ'] + sc.pl.tracksplot(adata, markers, groupby='bulk_labels', dendrogram=True) + + Using var_names as dict: + + .. plot:: + :context: close-figs + + markers = {{'T-cell': 'CD3D', 'B-cell': 'CD79A', 'myeloid': 'CST3'}} + sc.pl.tracksplot(adata, markers, groupby='bulk_labels', dendrogram=True) + + .. currentmodule:: scanpy + + See Also + -------- + pl.rank_genes_groups_tracksplot: to plot marker genes identified using the :func:`~scanpy.tl.rank_genes_groups` function. + + """ + if groupby not in adata.obs or adata.obs[groupby].dtype.name != "category": + msg = ( + "groupby has to be a valid categorical observation. " + f"Given value: {groupby}, valid categorical observations: " + f"{[x for x in adata.obs if adata.obs[x].dtype.name == 'category']}" + ) + raise ValueError(msg) + + var_names, var_groups = _check_var_names_type( + var_names, var_group_labels, var_group_positions + ) + del var_group_labels, var_group_positions + + categories, obs_tidy = _prepare_dataframe( + adata, + var_names, + groupby, + use_raw=use_raw, + log=log, + num_categories=None, # TODO: fix this line + gene_symbols=gene_symbols, + layer=layer, + ) + + # get categories colors: + if f"{groupby}_colors" not in adata.uns: + from ._utils import _set_default_colors_for_categorical_obs + + _set_default_colors_for_categorical_obs(adata, groupby) + groupby_colors = adata.uns[f"{groupby}_colors"] + + if dendrogram: + # compute dendrogram if needed and reorder + # rows and columns to match leaves order. + dendro_data = _reorder_categories_after_dendrogram( + adata, + groupby, + dendrogram_key=_dk(dendrogram), + var_names=var_names, + var_groups=var_groups, + categories=categories, + ) + # reorder obs_tidy + if dendro_data["var_names_idx_ordered"] is not None: + obs_tidy = obs_tidy.iloc[:, dendro_data["var_names_idx_ordered"]] + var_names = [var_names[x] for x in dendro_data["var_names_idx_ordered"]] + + obs_tidy.index = obs_tidy.index.reorder_categories( + [categories[x] for x in dendro_data["categories_idx_ordered"]], + ordered=True, + ) + categories = [categories[x] for x in dendro_data["categories_idx_ordered"]] + + groupby_colors = [ + groupby_colors[x] for x in dendro_data["categories_idx_ordered"] + ] + + obs_tidy = obs_tidy.sort_index() + + # obtain the start and end of each category and make + # a list of ranges that will be used to plot a different + # color + cumsum = [0, *np.cumsum(obs_tidy.index.value_counts(sort=False)).tolist()] + x_values = list(pairwise(cumsum)) + + dendro_height = 1 if dendrogram else 0 + + groupby_height = 0.24 + # +2 because of dendrogram on top and categories at bottom + num_rows = len(var_names) + 2 + if figsize is None: + width = 12 + track_height = 0.25 + else: + width, height = figsize + track_height = (height - (dendro_height + groupby_height)) / len(var_names) + + height_ratios = [dendro_height] + [track_height] * len(var_names) + [groupby_height] + height = sum(height_ratios) + + obs_tidy = obs_tidy.T + + fig = plt.figure(figsize=(width, height)) + axs = gridspec.GridSpec( + ncols=2, + nrows=num_rows, + wspace=1.0 / width, + hspace=0, + height_ratios=height_ratios, + width_ratios=[width, 0.14], + ) + axs_list = [] + first_ax = None + for idx, var in enumerate(var_names): + ax_idx = idx + 1 # this is because of the dendrogram + if first_ax is None: + ax = fig.add_subplot(axs[ax_idx, 0]) + first_ax = ax + else: + ax = fig.add_subplot(axs[ax_idx, 0], sharex=first_ax) + axs_list.append(ax) + for cat_idx, _category in enumerate(categories): + x_start, x_end = x_values[cat_idx] + ax.fill_between( + range(x_start, x_end), + 0, + obs_tidy.iloc[idx, x_start:x_end], + lw=0.1, + color=groupby_colors[cat_idx], + ) + + # remove the xticks labels except for the last processed plot. + # Because the plots share the x axis it is redundant and less compact + # to plot the axis for each plot + if idx < len(var_names) - 1: + ax.tick_params(labelbottom=False, labeltop=False, bottom=False, top=False) + ax.set_xlabel("") + if log: + ax.set_yscale("log") + ax.spines["left"].set_visible(False) + ax.spines["top"].set_visible(False) + ax.spines["bottom"].set_visible(False) + ax.grid(visible=False) + _ymin, ymax = ax.get_ylim() + ymax = int(ymax) + ax.set_yticks([ymax]) + ax.set_yticklabels([str(ymax)], ha="left", va="top") + ax.spines["right"].set_position(("axes", 1.01)) + ax.tick_params( + axis="y", + labelsize="x-small", + right=True, + left=False, + length=2, + which="both", + labelright=True, + labelleft=False, + direction="in", + ) + ax.set_ylabel(var, rotation=0, fontsize="small", ha="right", va="bottom") + ax.yaxis.set_label_coords(-0.005, 0.1) + ax.set_xlim(0, x_end) + ax.tick_params(axis="x", bottom=False, labelbottom=False) + + # the ax to plot the groupby categories is split to add a small space + # between the rest of the plot and the categories + axs2 = gridspec.GridSpecFromSubplotSpec( + 2, 1, subplot_spec=axs[num_rows - 1, 0], height_ratios=[1, 1] + ) + + groupby_ax = fig.add_subplot(axs2[1]) + + _label2code, ticks, _labels, groupby_cmap, norm = _plot_categories_as_colorblocks( + groupby_ax, obs_tidy.T, colors=groupby_colors, orientation="bottom" + ) + # add lines to plot + overlay_ax = fig.add_subplot(axs[1:-1, 0], sharex=first_ax) + line_positions = np.cumsum(obs_tidy.T.index.value_counts(sort=False))[:-1] + overlay_ax.vlines(line_positions, 0, 1, lw=0.5, linestyle="--") + overlay_ax.axis("off") + overlay_ax.set_ylim(0, 1) + + if dendrogram: + dendro_ax = fig.add_subplot(axs[0], sharex=first_ax) + _plot_dendrogram( + dendro_ax, + adata, + groupby, + dendrogram_key=_dk(dendrogram), + orientation="top", + ticks=ticks, + ) + + if var_groups is not None: + gene_groups_ax = fig.add_subplot(axs[1:-1, 1]) + arr = [] + for idx, pos in enumerate(var_groups.positions): + arr += [idx] * (pos[1] + 1 - pos[0]) + + gene_groups_ax.imshow( + np.array([arr]).T, aspect="auto", cmap=groupby_cmap, norm=norm + ) + gene_groups_ax.axis("off") + + return_ax_dict = {"track_axes": axs_list, "groupby_ax": groupby_ax} + if dendrogram: + return_ax_dict["dendrogram_ax"] = dendro_ax + if var_groups is not None: + return_ax_dict["gene_groups_ax"] = gene_groups_ax + + _utils.savefig_or_show("tracksplot", show=show, save=save) + show = settings.autoshow if show is None else show + if show: + return None + return return_ax_dict + + +@_doc_params(show_save_ax=doc_show_save_ax) +def dendrogram( + adata: AnnData, + groupby: str, + *, + dendrogram_key: str | None = None, + orientation: Literal["top", "bottom", "left", "right"] = "top", + remove_labels: bool = False, + show: bool | None = None, + save: str | bool | None = None, + ax: Axes | None = None, +) -> Axes: + """Plot a dendrogram of the categories defined in `groupby`. + + See :func:`~scanpy.tl.dendrogram`. + + Parameters + ---------- + adata + Annotated data matrix. + groupby + Categorical data column used to create the dendrogram + dendrogram_key + Key under with the dendrogram information was stored. + By default the dendrogram information is stored under + `.uns[f'dendrogram_{{groupby}}']`. + orientation + Origin of the tree. Will grow into the opposite direction. + remove_labels + Don’t draw labels. Used e.g. by :func:`scanpy.pl.matrixplot` + to annotate matrix columns/rows. + {show_save_ax} + + Returns + ------- + :class:`matplotlib.axes.Axes` + + Examples + -------- + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc68k_reduced() + sc.tl.dendrogram(adata, 'bulk_labels') + sc.pl.dendrogram(adata, 'bulk_labels') + + .. currentmodule:: scanpy + + """ + if ax is None: + _, ax = plt.subplots() + _plot_dendrogram( + ax, + adata, + groupby, + dendrogram_key=dendrogram_key, + remove_labels=remove_labels, + orientation=orientation, + ) + _utils.savefig_or_show("dendrogram", show=show, save=save) + return ax + + +@old_positionals( + "show_correlation_numbers", + "dendrogram", + "figsize", + "show", + "save", + "ax", + "vmin", + "vmax", + "vcenter", + "norm", +) +@_doc_params(show_save_ax=doc_show_save_ax, vminmax=doc_vboundnorm) +def correlation_matrix( # noqa: PLR0912, PLR0913, PLR0915 + adata: AnnData, + groupby: str, + *, + show_correlation_numbers: bool = False, + dendrogram: bool | str | None = None, + figsize: tuple[float, float] | None = None, + show: bool | None = None, + save: str | bool | None = None, + ax: Axes | None = None, + vmin: float | None = None, + vmax: float | None = None, + vcenter: float | None = None, + norm: Normalize | None = None, + **kwds, +) -> list[Axes] | None: + """Plot the correlation matrix computed as part of :func:`scanpy.tl.dendrogram`. + + Parameters + ---------- + adata + groupby + Categorical data column used to create the dendrogram + show_correlation_numbers + If `show_correlation=True`, plot the correlation on top of each cell. + dendrogram + If True or a valid dendrogram key, a dendrogram based on the + hierarchical clustering between the `groupby` categories is added. + The dendrogram is computed using :func:`scanpy.tl.dendrogram`. + If `tl.dendrogram` has not been called previously, + the function is called with default parameters. + figsize + By default a figure size that aims to produce a squared correlation + matrix plot is used. Format is (width, height) + {show_save_ax} + {vminmax} + **kwds + Only if `show_correlation` is True: + Are passed to :func:`matplotlib.pyplot.pcolormesh` when plotting the + correlation heatmap. `cmap` can be used to change the color palette. + + Returns + ------- + If `show=False`, returns a list of :class:`matplotlib.axes.Axes` objects. + + Examples + -------- + >>> import scanpy as sc + >>> adata = sc.datasets.pbmc68k_reduced() + >>> sc.tl.dendrogram(adata, "bulk_labels") + >>> sc.pl.correlation_matrix(adata, "bulk_labels") + + """ + dendrogram_key = _get_dendrogram_key(adata, _dk(dendrogram), groupby) + + index = adata.uns[dendrogram_key]["categories_idx_ordered"] + corr_matrix = adata.uns[dendrogram_key]["correlation_matrix"] + # reorder matrix columns according to the dendrogram + if dendrogram is None: + dendrogram = ax is None + if dendrogram: + if ax is not None: + msg = "Can only plot dendrogram when not plotting to an axis" + raise ValueError(msg) + assert (len(index)) == corr_matrix.shape[0] + corr_matrix = corr_matrix[index, :] + corr_matrix = corr_matrix[:, index] + labels = list(adata.obs[groupby].cat.categories) + labels = np.array(labels).astype("str")[index] + else: + labels = adata.obs[groupby].cat.categories + num_rows = corr_matrix.shape[0] + colorbar_height = 0.2 + dendrogram_width = 1.8 if dendrogram else 0 + if figsize is None: + corr_matrix_height = num_rows * 0.6 + height = corr_matrix_height + colorbar_height + width = corr_matrix_height + dendrogram_width + else: + width, height = figsize + corr_matrix_height = height - colorbar_height + + fig = plt.figure(figsize=(width, height)) if ax is None else None + # layout with 2 rows and 2 columns: + # row 1: dendrogram + correlation matrix + # row 2: nothing + colormap bar (horizontal) + gs = gridspec.GridSpec( + nrows=2, + ncols=2, + width_ratios=[dendrogram_width, corr_matrix_height], + height_ratios=[corr_matrix_height, colorbar_height], + wspace=0.01, + hspace=0.05, + ) + + axs = [] + corr_matrix_ax = fig.add_subplot(gs[1]) if ax is None else ax + if dendrogram: + dendro_ax = fig.add_subplot(gs[0], sharey=corr_matrix_ax) + _plot_dendrogram( + dendro_ax, + adata, + groupby, + dendrogram_key=dendrogram_key, + remove_labels=True, + orientation="left", + ticks=np.arange(corr_matrix.shape[0]) + 0.5, + ) + axs.append(dendro_ax) + # define some default pcolormesh parameters + if "edgecolors" not in kwds: + if corr_matrix.shape[0] > 30: + # when there are too many rows it is better to remove + # the black lines surrounding the boxes in the heatmap + kwds["edgecolors"] = "none" + else: + kwds["edgecolors"] = "black" + kwds.setdefault("linewidth", 0.01) + if vmax is None and vmin is None and norm is None: + vmax = 1 + vmin = -1 + norm = check_colornorm(vmin, vmax, vcenter, norm) + if "cmap" not in kwds: + # by default use a divergent color map + kwds["cmap"] = "bwr" + + img_mat = corr_matrix_ax.pcolormesh(corr_matrix, norm=norm, **kwds) + corr_matrix_ax.set_xlim(0, num_rows) + corr_matrix_ax.set_ylim(0, num_rows) + + corr_matrix_ax.yaxis.tick_right() + corr_matrix_ax.set_yticks(np.arange(corr_matrix.shape[0]) + 0.5) + corr_matrix_ax.set_yticklabels(labels) + + corr_matrix_ax.xaxis.set_tick_params(labeltop=True) + corr_matrix_ax.xaxis.set_tick_params(labelbottom=False) + corr_matrix_ax.set_xticks(np.arange(corr_matrix.shape[0]) + 0.5) + corr_matrix_ax.set_xticklabels(labels, rotation=45, ha="left") + + for ax_name in "xy": + corr_matrix_ax.tick_params(axis=ax_name, which="both", bottom=False, top=False) + + if show_correlation_numbers: + for row, col in product(range(num_rows), repeat=2): + corr_matrix_ax.text( + row + 0.5, + col + 0.5, + f"{corr_matrix[row, col]:.2f}", + ha="center", + va="center", + ) + + axs.append(corr_matrix_ax) + + if ax is None: # Plot colorbar + colormap_ax = fig.add_subplot(gs[3]) + cobar = plt.colorbar(img_mat, cax=colormap_ax, orientation="horizontal") + cobar.solids.set_edgecolor("face") + axs.append(colormap_ax) + + show = settings.autoshow if show is None else show + _utils.savefig_or_show("correlation_matrix", show=show, save=save) + if ax is not None or show: + return None + return axs + + +def _prepare_dataframe( # noqa: PLR0912 + adata: AnnData, + var_names: _VarNames | Mapping[str, _VarNames], + groupby: str | Sequence[str] | None = None, + *, + use_raw: bool | None = None, + log: bool = False, + num_categories: int = 7, + layer: str | None = None, + gene_symbols: str | None = None, +) -> tuple[Sequence[str], pd.DataFrame]: + """Prepare a data frame of categories (`groupby`) × `var_names`. + + Parameters + ---------- + adata + Annotated data matrix. + var_names + `var_names` should be a valid subset of `adata.var_names`. + groupby + The key of the observation grouping to consider. It is expected that + groupby is a categorical. If groupby is not a categorical observation, + it would be subdivided into `num_categories`. + use_raw + Whether to use `raw` attribute of `adata`. Defaults to `True` if `.raw` is present. + log + Use the log of the values. + layer + AnnData layer to use. Takes precedence over `use_raw` + num_categories + Only used if groupby observation is not categorical. This value + determines the number of groups into which the groupby observation + should be subdivided. + gene_symbols + Key for field in .var that stores gene symbols. + + Returns + ------- + Tuple of `pandas.DataFrame` and list of categories. + + """ + sanitize_anndata(adata) + use_raw = _check_use_raw(adata, use_raw, layer=layer) + if isinstance(var_names, str): + var_names = [var_names] + + groupby_index = None + if groupby is not None: + if isinstance(groupby, str): + # if not a list, turn into a list + groupby = [groupby] + for group in groupby: + if group not in [*adata.obs, adata.obs.index.name]: + if adata.obs.index.name is not None: + msg = f' or index name "{adata.obs.index.name}"' + else: + msg = "" + msg = ( + "groupby has to be a valid observation. " + f"Given {group}, is not in observations: " + f"{adata.obs.columns.tolist()} {msg}" + ) + raise ValueError(msg) + if group in adata.obs.columns and group == adata.obs.index.name: + msg = ( + f"Given group {group} is both and index and a column level, " + "which is ambiguous." + ) + raise ValueError(msg) + if group == adata.obs.index.name: + groupby_index = group + if groupby_index is not None: + # obs_tidy contains adata.obs.index + # and does not need to be given + groupby = groupby.copy() # copy to not modify user passed parameter + groupby.remove(groupby_index) + keys = list(groupby) + list(np.unique(var_names)) + obs_tidy = get.obs_df( + adata, keys=keys, layer=layer, use_raw=use_raw, gene_symbols=gene_symbols + ) + assert np.all(np.array(keys) == np.array(obs_tidy.columns)) + + if groupby_index is not None: + # reset index to treat all columns the same way. + obs_tidy.reset_index(inplace=True) + groupby.append(groupby_index) + + if groupby is None: + categorical = pd.Series(np.repeat("", len(obs_tidy))).astype("category") + elif len(groupby) == 1 and is_numeric_dtype(obs_tidy[groupby[0]]): + # if the groupby column is not categorical, turn it into one + # by subdividing into `num_categories` categories + categorical = pd.cut(obs_tidy[groupby[0]], num_categories) + elif len(groupby) == 1: + categorical = obs_tidy[groupby[0]].astype("category") + categorical.name = groupby[0] + else: + # join the groupby values using "_" to make a new 'category' + categorical = obs_tidy[groupby].apply("_".join, axis=1).astype("category") + categorical.name = "_".join(groupby) + + # preserve category order + from itertools import product + + order = { + "_".join(k): idx + for idx, k in enumerate( + product(*(obs_tidy[g].cat.categories for g in groupby)) + ) + } + categorical = categorical.cat.reorder_categories( + sorted(categorical.cat.categories, key=lambda x: order[x]) + ) + obs_tidy = obs_tidy[var_names].set_index(categorical) + categories = obs_tidy.index.categories + + if log: + obs_tidy = np.log1p(obs_tidy) + + return categories, obs_tidy + + +def _plot_var_groups_brackets( + var_groups_ax: Axes, + *, + var_groups: VarGroups, + left_adjustment: float = -0.3, + right_adjustment: float = 0.3, + rotation: float | None = None, + orientation: Literal["top", "right"] = "top", + wide: bool = False, +) -> None: + """Draw brackets that represent groups of genes on the give axis. + + For best results, this axis is located on top of an image whose + x axis contains gene names. + + The `var_groups_ax` should share the x axis with the main ax. + + E.g: `var_groups_ax=fig.add_subplot(axs[0, 0], sharex=dot_ax)` + + This function is used by dotplot, heatmap etc. + + Parameters + ---------- + var_groups_ax + In this axis the gene marks are drawn + var_groups + Group labels and positions + left_adjustment + adjustment to plot the bracket start slightly before or after the first gene position. + If the value is negative the start is moved before. + right_adjustment + adjustment to plot the bracket end slightly before or after the last gene position + If the value is negative the start is moved before. + rotation + rotation degrees for the labels. If not given, small labels (<4 characters) are not + rotated, otherwise, they are rotated 90 degrees + orientation + location of the brackets. Either `top` or `right` + + Returns + ------- + None + + """ + from matplotlib import patches + from matplotlib.path import Path + + # get the 'brackets' coordinates as lists of start and end positions + left = [x[0] + left_adjustment for x in var_groups.positions] + right = [x[1] + right_adjustment for x in var_groups.positions] + + # verts and codes are used by PathPatch to make the brackets + verts = [] + codes = [] + if orientation == "top": + # rotate labels if any of them is longer than 4 characters + if rotation is None: + rotation = 90 if max([len(x) for x in var_groups.labels]) > 4 else 0 + for idx in range(len(left)): + verts.append((left[idx], 0)) # lower-left + verts.append((left[idx], 0.6)) # upper-left + verts.append((right[idx], 0.6)) # upper-right + verts.append((right[idx], 0)) # lower-right + + codes.append(Path.MOVETO) + codes.append(Path.LINETO) + codes.append(Path.LINETO) + codes.append(Path.LINETO) + + group_x_center = left[idx] + float(right[idx] - left[idx]) / 2 + var_groups_ax.text( + group_x_center, + 1.1, + var_groups.labels[idx], + ha="center", + va="bottom", + rotation=rotation, + ) + else: + top = left + bottom = right + for idx in range(len(top)): + verts.append((0, top[idx])) # upper-left + verts.append((0.4 if wide else 0.15, top[idx])) # upper-right + verts.append((0.4 if wide else 0.15, bottom[idx])) # lower-right + verts.append((0, bottom[idx])) # lower-left + + codes.append(Path.MOVETO) + codes.append(Path.LINETO) + codes.append(Path.LINETO) + codes.append(Path.LINETO) + + diff = bottom[idx] - top[idx] + group_y_center = top[idx] + float(diff) / 2 + # cut label to fit available space + label = ( + f"{var_groups.labels[idx][: int(diff * 2)]}." + if diff * 2 < len(var_groups.labels[idx]) + else var_groups.labels[idx] + ) + var_groups_ax.text( + 1.1 if wide else 0.6, + group_y_center, + label, + ha="right", + va="center", + rotation=270, + fontsize="small", + ) + + path = Path(verts, codes) + + patch = patches.PathPatch(path, facecolor="none", lw=1.5) + + var_groups_ax.add_patch(patch) + var_groups_ax.grid(visible=False) + var_groups_ax.axis("off") + # remove y ticks + var_groups_ax.tick_params(axis="y", left=False, labelleft=False) + # remove x ticks and labels + var_groups_ax.tick_params(axis="x", bottom=False, labelbottom=False, labeltop=False) + + +class _ReorderCats(TypedDict): + categories_idx_ordered: Sequence[int] + categories_ordered: Sequence[str] + var_names_idx_ordered: Sequence[int] | None + var_names_ordered: Sequence[str] | None + var_groups: VarGroups | None + + +def _reorder_categories_after_dendrogram( + adata: AnnData, + groupby: str | Sequence[str], + *, + dendrogram_key: str | None, + var_names: Sequence[str], + var_groups: VarGroups | None, + categories: Sequence[str], +) -> _ReorderCats: + """Reorder the the groupby observations based on the dendrogram results. + + The function checks if a dendrogram has already been precomputed. + If not, `sc.tl.dendrogram` is run with default parameters. + + The results found in `.uns[dendrogram_key]` are used to reorder `var_groups`. + """ + if isinstance(groupby, str): + groupby = [groupby] + + dendro_info = adata.uns[ + _get_dendrogram_key(adata, dendrogram_key, groupby, validate_groupby=True) + ] + + if categories is None: + categories = adata.obs[dendro_info["groupby"]].cat.categories + + # order of groupby categories + categories_idx_ordered = dendro_info["categories_idx_ordered"] + categories_ordered = dendro_info["categories_ordered"] + + if len(categories) != len(categories_idx_ordered): + msg = ( + "Incompatible observations. Dendrogram data has " + f"{len(categories_idx_ordered)} categories but current groupby " + f"observation {groupby!r} contains {len(categories)} categories. " + "Most likely the underlying groupby observation changed after the " + "initial computation of `sc.tl.dendrogram`. " + "Please run `sc.tl.dendrogram` again.'" + ) + raise ValueError(msg) + + # reorder var_groups (if any) + if var_groups is None: + var_names_idx_ordered = None + elif set(var_groups.labels) == set(categories): + positions_ordered = [] + labels_ordered = [] + position_start = 0 + var_names_idx_ordered = [] + for cat_name in categories_ordered: + idx = var_groups.labels.index(cat_name) + position = var_groups.positions[idx] + _var_names = var_names[position[0] : position[1] + 1] + var_names_idx_ordered.extend(range(position[0], position[1] + 1)) + positions_ordered.append(( + position_start, + position_start + len(_var_names) - 1, + )) + position_start += len(_var_names) + labels_ordered.append(var_groups.labels[idx]) + var_groups = VarGroups(labels_ordered, positions_ordered) + else: + logg.warning( + "Groups are not reordered because the `groupby` categories " + "and the `var_group_labels` are different.\n" + f"categories: {_format_first_three_categories(categories)}\n" + f"var_group_labels: {_format_first_three_categories(var_groups.labels)}" + ) + var_names_idx_ordered = list(range(len(var_names))) + + if var_names_idx_ordered is not None: + var_names_ordered = [var_names[x] for x in var_names_idx_ordered] + else: + var_names_ordered = None + + return _ReorderCats( + categories_idx_ordered=categories_idx_ordered, + categories_ordered=dendro_info["categories_ordered"], + var_names_idx_ordered=var_names_idx_ordered, + var_names_ordered=var_names_ordered, + var_groups=var_groups, + ) + + +def _format_first_three_categories(categories): + """Clean up warning message.""" + categories = list(categories) + if len(categories) > 3: + categories = [*categories[:3], "etc."] + return ", ".join(categories) + + +def _get_dendrogram_key( + adata: AnnData, + dendrogram_key: str | None, + groupby: str | Sequence[str], + *, + validate_groupby: bool = False, +) -> str: + # the `dendrogram_key` can be a bool an NoneType or the name of the + # dendrogram key. By default the name of the dendrogram key is 'dendrogram' + if dendrogram_key is None: + if isinstance(groupby, str): + dendrogram_key = f"dendrogram_{groupby}" + elif isinstance(groupby, Sequence): + dendrogram_key = f"dendrogram_{'_'.join(groupby)}" + else: + msg = f"groupby has wrong type: {type(groupby).__name__}." + raise AssertionError(msg) + + if dendrogram_key not in adata.uns: + from ..tools._dendrogram import dendrogram + + logg.warning( + f"dendrogram data not found (using key={dendrogram_key}). " + "Running `sc.tl.dendrogram` with default parameters. For fine " + "tuning it is recommended to run `sc.tl.dendrogram` independently." + ) + dendrogram(adata, groupby, key_added=dendrogram_key) + + if "dendrogram_info" not in adata.uns[dendrogram_key]: + msg = ( + f"The given dendrogram key ({dendrogram_key!r}) does not contain " + "valid dendrogram information." + ) + raise ValueError(msg) + + if validate_groupby: + existing_groupby = adata.uns[dendrogram_key]["groupby"] + if groupby != existing_groupby: + msg = ( + "Incompatible observations. The precomputed dendrogram contains " + f"information for the observation: {groupby!r} while the plot is " + f"made for the observation: {existing_groupby!r}. " + "Please run `sc.tl.dendrogram` using the right observation.'" + ) + raise ValueError(msg) + + return dendrogram_key + + +def _plot_dendrogram( # noqa: PLR0915 + dendro_ax: Axes, + adata: AnnData, + groupby: str | Sequence[str], + *, + dendrogram_key: str | None = None, + orientation: Literal["top", "bottom", "left", "right"] = "right", + remove_labels: bool = True, + ticks: Collection[float] | None = None, +): + """Plot a dendrogram on the given ax. + + Uses the precomputed dendrogram information stored in `.uns[dendrogram_key]`. + """ + dendrogram_key = _get_dendrogram_key(adata, dendrogram_key, groupby) + + def translate_pos(pos_list, new_ticks, old_ticks): + """Transform the dendrogram coordinates to a given new position. + + The xlabel_pos and orig_ticks should be of the same length. + + This is mostly done for the heatmap case, where the position of the + dendrogram leaves needs to be adjusted depending on the category size. + + Parameters + ---------- + pos_list + list of dendrogram positions that should be translated + new_ticks + sorted list of goal tick positions (e.g. [0,1,2,3] ) + old_ticks + sorted list of original tick positions (e.g. [5, 15, 25, 35]), + This list is usually the default position used by + `scipy.cluster.hierarchy.dendrogram`. + + Returns + ------- + translated list of positions + + Examples + -------- + >>> translate_pos( + ... [5, 15, 20, 21], + ... [0, 1, 2, 3], + ... [5, 15, 25, 35], + ... ) + [0, 1, 1.5, 1.6] + + """ + # of given coordinates. + + if not isinstance(old_ticks, list): + # assume that the list is a numpy array + old_ticks = old_ticks.tolist() + new_xs = [] + for x_val in pos_list: + if x_val in old_ticks: + new_x_val = new_ticks[old_ticks.index(x_val)] + else: + # find smaller and bigger indices + idx_next = np.searchsorted(old_ticks, x_val, side="left") + idx_prev = idx_next - 1 + old_min = old_ticks[idx_prev] + old_max = old_ticks[idx_next] + new_min = new_ticks[idx_prev] + new_max = new_ticks[idx_next] + new_x_val = ((x_val - old_min) / (old_max - old_min)) * ( + new_max - new_min + ) + new_min + new_xs.append(new_x_val) + return new_xs + + dendro_info = adata.uns[dendrogram_key]["dendrogram_info"] + leaves = dendro_info["ivl"] + icoord = np.array(dendro_info["icoord"]) + dcoord = np.array(dendro_info["dcoord"]) + + orig_ticks = np.arange(5, len(leaves) * 10 + 5, 10).astype(float) + # check that ticks has the same length as orig_ticks + if ticks is not None and len(orig_ticks) != len(ticks): + logg.warning( + "ticks argument does not have the same size as orig_ticks. " + "The argument will be ignored" + ) + ticks = None + + for xs, ys in zip(icoord, dcoord, strict=True): + if ticks is not None: + xs = translate_pos(xs, ticks, orig_ticks) # noqa: PLW2901 + if orientation in ["right", "left"]: + xs, ys = ys, xs # noqa: PLW2901 + dendro_ax.plot(xs, ys, color="#555555") + + dendro_ax.tick_params(bottom=False, top=False, left=False, right=False) + ticks = ticks if ticks is not None else orig_ticks + if orientation in ["right", "left"]: + dendro_ax.set_yticks(ticks) + dendro_ax.set_yticklabels(leaves, fontsize="small", rotation=0) + dendro_ax.tick_params(labelbottom=False, labeltop=False) + if orientation == "left": + xmin, xmax = dendro_ax.get_xlim() + dendro_ax.set_xlim(xmax, xmin) + dendro_ax.tick_params(labelleft=False, labelright=True) + else: + dendro_ax.set_xticks(ticks) + dendro_ax.set_xticklabels(leaves, fontsize="small", rotation=90) + dendro_ax.tick_params(labelleft=False, labelright=False) + if orientation == "bottom": + ymin, ymax = dendro_ax.get_ylim() + dendro_ax.set_ylim(ymax, ymin) + dendro_ax.tick_params(labeltop=True, labelbottom=False) + + if remove_labels: + dendro_ax.tick_params( + labelbottom=False, labeltop=False, labelleft=False, labelright=False + ) + + dendro_ax.grid(visible=False) + + dendro_ax.spines["right"].set_visible(False) + dendro_ax.spines["top"].set_visible(False) + dendro_ax.spines["left"].set_visible(False) + dendro_ax.spines["bottom"].set_visible(False) + + +def _plot_categories_as_colorblocks( + groupby_ax: Axes, + obs_tidy: pd.DataFrame, + colors=None, + orientation: Literal["top", "bottom", "left", "right"] = "left", + cmap_name: str = "tab20", +): + """Plot categories as colored blocks. + + If orientation is 'left', the categories are plotted vertically, + otherwise they are plotted horizontally. + + Parameters + ---------- + groupby_ax + obs_tidy + colors + Sequence of valid color names to use for each category. + orientation + cmap_name + Name of colormap to use, in case colors is None + + Returns + ------- + ticks position, labels, colormap + + """ + groupby = obs_tidy.index.name + from matplotlib.colors import BoundaryNorm, ListedColormap + + if colors is None: + groupby_cmap = colormaps.get_cmap(cmap_name) + else: + groupby_cmap = ListedColormap(colors, f"{groupby}_cmap") + norm = BoundaryNorm(np.arange(groupby_cmap.N + 1) - 0.5, groupby_cmap.N) + + # determine groupby label positions such that they appear + # centered next/below to the color code rectangle assigned to the category + value_sum = 0 + ticks = [] # list of centered position of the labels + labels = [] + label2code = {} # dictionary of numerical values asigned to each label + for code, (label, value) in enumerate( + obs_tidy.index.value_counts(sort=False).items() + ): + ticks.append(value_sum + (value / 2)) + labels.append(label) + value_sum += value + label2code[label] = code + + groupby_ax.grid(visible=False) + + if orientation == "left": + groupby_ax.imshow( + np.array([[label2code[lab] for lab in obs_tidy.index]]).T, + aspect="auto", + cmap=groupby_cmap, + norm=norm, + ) + if len(labels) > 1: + groupby_ax.set_yticks(ticks) + groupby_ax.set_yticklabels(labels) + + # remove y ticks + groupby_ax.tick_params(axis="y", left=False, labelsize="small") + # remove x ticks and labels + groupby_ax.tick_params(axis="x", bottom=False, labelbottom=False) + + # remove surrounding lines + groupby_ax.spines["right"].set_visible(False) + groupby_ax.spines["top"].set_visible(False) + groupby_ax.spines["left"].set_visible(False) + groupby_ax.spines["bottom"].set_visible(False) + + groupby_ax.set_ylabel(groupby) + else: + groupby_ax.imshow( + np.array([[label2code[lab] for lab in obs_tidy.index]]), + aspect="auto", + cmap=groupby_cmap, + norm=norm, + ) + if len(labels) > 1: + groupby_ax.set_xticks(ticks) + # if the labels are small do not rotate them + rotation = 0 if max(len(str(x)) for x in labels) < 3 else 90 + groupby_ax.set_xticklabels(labels, rotation=rotation) + + # remove x ticks + groupby_ax.tick_params(axis="x", bottom=False, labelsize="small") + # remove y ticks and labels + groupby_ax.tick_params(axis="y", left=False, labelleft=False) + + # remove surrounding lines + groupby_ax.spines["right"].set_visible(False) + groupby_ax.spines["top"].set_visible(False) + groupby_ax.spines["left"].set_visible(False) + groupby_ax.spines["bottom"].set_visible(False) + + groupby_ax.set_xlabel(groupby) + + return label2code, ticks, labels, groupby_cmap, norm + + +def _plot_colorbar(mappable, fig, subplot_spec, max_cbar_height: float = 4.0): + """Plot a vertical color bar based on mappable. + + The height of the colorbar is min(figure-height, max_cmap_height). + + Parameters + ---------- + mappable + The image to which the colorbar applies. + fig + The figure object + subplot_spec + The gridspec subplot. Eg. axs[1,2] + max_cbar_height + The maximum colorbar height + + Returns + ------- + color bar ax + + """ + _width, height = fig.get_size_inches() + if height > max_cbar_height: + # to make the colorbar shorter, the + # ax is split and the lower portion is used. + axs2 = gridspec.GridSpecFromSubplotSpec( + 2, + 1, + subplot_spec=subplot_spec, + height_ratios=[height - max_cbar_height, max_cbar_height], + ) + heatmap_cbar_ax = fig.add_subplot(axs2[1]) + else: + heatmap_cbar_ax = fig.add_subplot(subplot_spec) + plt.colorbar(mappable, cax=heatmap_cbar_ax) + return heatmap_cbar_ax + + +def _check_var_names_type( + var_names: _VarNames | Mapping[str, _VarNames], + var_group_labels: Sequence[str] | None = None, + var_group_positions: Sequence[tuple[int, int]] | None = None, +) -> tuple[Sequence[str], VarGroups | None]: + """If var_names is a dict, set the `var_group_labels` and `var_group_positions`. + + Returns + ------- + var_names, var_groups + + """ + from ._baseplot_class import _var_groups + + if isinstance(var_names, Mapping): + return _var_groups(var_names) + + if isinstance(var_names, str): + var_names = [var_names] + return var_names, VarGroups.validate(var_group_labels, var_group_positions) diff --git a/.venv/lib/python3.11/site-packages/scanpy/plotting/_baseplot_class.py b/.venv/lib/python3.11/site-packages/scanpy/plotting/_baseplot_class.py new file mode 100644 index 0000000000000000000000000000000000000000..b64351050932e5ade4644cae59a85650ff04bb4b --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/plotting/_baseplot_class.py @@ -0,0 +1,938 @@ +"""BasePlot for dotplot, matrixplot and stacked_violin.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, NamedTuple +from warnings import warn + +import numpy as np +from matplotlib import colormaps, gridspec +from matplotlib import pyplot as plt + +from .. import logging as logg +from .._compat import old_positionals +from .._utils import _empty +from ._anndata import ( + VarGroups, + _plot_dendrogram, + _plot_var_groups_brackets, + _prepare_dataframe, + _reorder_categories_after_dendrogram, +) +from ._utils import check_colornorm, make_grid_spec + +if TYPE_CHECKING: + from collections.abc import Sequence + from typing import Literal, Self + + import pandas as pd + from anndata import AnnData + from matplotlib.axes import Axes + from matplotlib.colors import Colormap, Normalize + + from .._utils import Empty + from ._utils import ColorLike, _AxesSubplot + + _VarNames = str | Sequence[str] + + +class VBoundNorm(NamedTuple): + vmin: float | None + vmax: float | None + vcenter: float | None + norm: Normalize | None + + +doc_common_groupby_plot_args = """\ +title + Title for the figure +colorbar_title + Title for the color bar. New line character (\\n) can be used. +cmap + String denoting matplotlib color map. +standard_scale + Whether or not to standardize the given dimension between 0 and 1, meaning for + each variable or group, subtract the minimum and divide each by its maximum. +swap_axes + By default, the x axis contains `var_names` (e.g. genes) and the y axis + the `groupby` categories. By setting `swap_axes` then x are the + `groupby` categories and y the `var_names`. +return_fig + Returns :class:`DotPlot` object. Useful for fine-tuning + the plot. Takes precedence over `show=False`. +""" + + +class BasePlot: + """Generic class for the visualization of AnnData categories and selected `var` (features or genes). + + Takes care of the visual location of a main plot, additional plots + in the margins (e.g. dendrogram, margin totals) and legends. Also + understand how to adapt the visual parameter if the plot is rotated + + Classed based on BasePlot implement their own _mainplot() method. + + The BasePlot works by method chaining. For example: + BasePlot(adata, ...).legend(title='legend').style(cmap='binary').show() + """ + + DEFAULT_SAVE_PREFIX = "baseplot_" + MIN_FIGURE_HEIGHT = 2.5 + DEFAULT_CATEGORY_HEIGHT = 0.35 + DEFAULT_CATEGORY_WIDTH = 0.37 + + # gridspec parameter. Sets the space between mainplot, dendrogram and legend + DEFAULT_WSPACE = 0 + + DEFAULT_COLORMAP = "winter" + DEFAULT_LEGENDS_WIDTH = 1.5 + DEFAULT_COLOR_LEGEND_TITLE = "Expression\nlevel in group" + + MAX_NUM_CATEGORIES = 500 # maximum number of categories allowed to be plotted + + var_groups: VarGroups | None + + @old_positionals( + "use_raw", + "log", + "num_categories", + "categories_order", + "title", + "figsize", + "gene_symbols", + "var_group_positions", + "var_group_labels", + "var_group_rotation", + "layer", + "ax", + "vmin", + "vmax", + "vcenter", + "norm", + ) + def __init__( # noqa: PLR0913 + self, + adata: AnnData, + var_names: _VarNames | Mapping[str, _VarNames], + groupby: str | Sequence[str], + *, + use_raw: bool | None = None, + log: bool = False, + num_categories: int = 7, + categories_order: Sequence[str] | None = None, + title: str | None = None, + figsize: tuple[float, float] | None = None, + gene_symbols: str | None = None, + var_group_labels: Sequence[str] | None = None, + var_group_positions: Sequence[tuple[int, int]] | None = None, + var_group_rotation: float | None = None, + layer: str | None = None, + ax: _AxesSubplot | None = None, + vmin: float | None = None, + vmax: float | None = None, + vcenter: float | None = None, + norm: Normalize | None = None, + **kwds, + ): + self.var_names, self.var_groups = _var_groups(var_names) + if self.var_groups is None: + self.var_groups = VarGroups.validate(var_group_labels, var_group_positions) + elif var_group_labels is not None or var_group_positions is not None: + msg = "var_group_labels and var_group_positions cannot be set if var_names is a dict" + raise TypeError(msg) + del var_group_labels, var_group_positions + self.var_group_rotation = var_group_rotation + self.width, self.height = figsize if figsize is not None else (None, None) + + self.categories, self.obs_tidy = _prepare_dataframe( + adata, + self.var_names, + groupby, + use_raw=use_raw, + log=log, + num_categories=num_categories, + layer=layer, + gene_symbols=gene_symbols, + ) + if len(self.categories) > self.MAX_NUM_CATEGORIES: + warn( + f"Over {self.MAX_NUM_CATEGORIES} categories found. " + "Plot would be very large.", + UserWarning, + stacklevel=2, + ) + + if categories_order is not None and ( + set(self.obs_tidy.index.categories) != set(categories_order) + ): + logg.error( + "Please check that the categories given by " + "the `order` parameter match the categories that " + "want to be reordered.\n\n" + "Mismatch: " + f"{set(self.obs_tidy.index.categories).difference(categories_order)}\n\n" + f"Given order categories: {categories_order}\n\n" + f"{groupby} categories: {list(self.obs_tidy.index.categories)}\n" + ) + return + + self.adata = adata + self.groupby = [groupby] if isinstance(groupby, str) else groupby + self.log = log + self.kwds = kwds + + self.vboundnorm = VBoundNorm(vmin=vmin, vmax=vmax, vcenter=vcenter, norm=norm) + + # set default values for legend + self.color_legend_title = self.DEFAULT_COLOR_LEGEND_TITLE + self.legends_width = self.DEFAULT_LEGENDS_WIDTH + + # set style defaults + self.cmap = self.DEFAULT_COLORMAP + + # style default parameters + self.are_axes_swapped = False + self.categories_order = categories_order + self.var_names_idx_order = None + + self.wspace = self.DEFAULT_WSPACE + + # minimum height required for legends to plot properly + self.min_figure_height = self.MIN_FIGURE_HEIGHT + + self.fig_title = title + + self.group_extra_size = 0 + self.plot_group_extra = None + # after .render() is called the fig value is assigned and ax_dict + # contains a dictionary of the axes used in the plot + self.fig = None + self.ax_dict = None + self.ax = ax + + @old_positionals("swap_axes") + def swap_axes(self, *, swap_axes: bool | None = True) -> Self: + """Plot a transposed image. + + By default, the x axis contains `var_names` (e.g. genes) and the y + axis the `groupby` categories. By setting `swap_axes` then x are + the `groupby` categories and y the `var_names`. + + Parameters + ---------- + swap_axes + Boolean to turn on (True) or off (False) 'swap_axes'. Default True + + + Returns + ------- + Returns `self` for method chaining. + + """ + self.DEFAULT_CATEGORY_HEIGHT, self.DEFAULT_CATEGORY_WIDTH = ( + self.DEFAULT_CATEGORY_WIDTH, + self.DEFAULT_CATEGORY_HEIGHT, + ) + + self.are_axes_swapped = swap_axes + return self + + @old_positionals("show", "dendrogram_key", "size") + def add_dendrogram( + self, + *, + show: bool | None = True, + dendrogram_key: str | None = None, + size: float | None = 0.8, + ) -> Self: + r"""Show dendrogram based on the hierarchical clustering between the `groupby` categories. + + Categories are reordered to match the dendrogram order. + + The dendrogram information is computed using :func:`scanpy.tl.dendrogram`. + If `sc.tl.dendrogram` has not been called previously the function is called + with default parameters. + + The dendrogram is by default shown on the right side of the plot or on top + if the axes are swapped. + + `var_names` are reordered to produce a more pleasing output if: + * The data contains `var_groups` + * the `var_groups` match the categories. + + The previous conditions happen by default when using Plot + to show the results from :func:`~scanpy.tl.rank_genes_groups` (aka gene markers), by + calling `scanpy.tl.rank_genes_groups_(plot_name)`. + + + Parameters + ---------- + show + Boolean to turn on (True) or off (False) 'add_dendrogram' + dendrogram_key + Needed if `sc.tl.dendrogram` saved the dendrogram using a key different + than the default name. + size + size of the dendrogram. Corresponds to width when dendrogram shown on + the right of the plot, or height when shown on top. The unit is the same + as in matplotlib (inches). + + Returns + ------- + Returns `self` for method chaining. + + + Examples + -------- + >>> import scanpy as sc + >>> adata = sc.datasets.pbmc68k_reduced() + >>> markers = {"T-cell": "CD3D", "B-cell": "CD79A", "myeloid": "CST3"} + >>> plot = sc.pl._baseplot_class.BasePlot( + ... adata, markers, groupby="bulk_labels" + ... ).add_dendrogram() + >>> plot.plot_group_extra # doctest: +NORMALIZE_WHITESPACE + {'kind': 'dendrogram', + 'width': 0.8, + 'dendrogram_key': None, + 'dendrogram_ticks': array([0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5])} + + """ + if not show: + self.plot_group_extra = None + return self + + if self.groupby is None or len(self.categories) <= 2: + # dendrogram can only be computed between groupby categories + logg.warning( + "Dendrogram not added. Dendrogram is added only " + "when the number of categories to plot > 2" + ) + return self + + self.group_extra_size = size + + # to correctly plot the dendrogram the categories need to be ordered + # according to the dendrogram ordering. + self._reorder_categories_after_dendrogram(dendrogram_key) + + dendro_ticks = np.arange(len(self.categories)) + 0.5 + + self.group_extra_size = size + self.plot_group_extra = { + "kind": "dendrogram", + "width": size, + "dendrogram_key": dendrogram_key, + "dendrogram_ticks": dendro_ticks, + } + return self + + @old_positionals("show", "sort", "size", "color") + def add_totals( + self, + *, + show: bool | None = True, + sort: Literal["ascending", "descending"] | None = None, + size: float | None = 0.8, + color: ColorLike | Sequence[ColorLike] | None = None, + ) -> Self: + r"""Show barplot for the number of cells in in `groupby` category. + + The barplot is by default shown on the right side of the plot or on top + if the axes are swapped. + + + Parameters + ---------- + show + Boolean to turn on (True) or off (False) 'add_totals' + sort + Set to either 'ascending' or 'descending' to reorder the categories + by cell number + size + size of the barplot. Corresponds to width when shown on + the right of the plot, or height when shown on top. The unit is the same + as in matplotlib (inches). + color + Color for the bar plots or list of colors for each of the bar plots. + By default, each bar plot uses the colors assigned in + `adata.uns[{groupby}_colors]`. + + + Returns + ------- + Returns `self` for method chaining. + + + Examples + -------- + >>> import scanpy as sc + >>> adata = sc.datasets.pbmc68k_reduced() + >>> markers = {"T-cell": "CD3D", "B-cell": "CD79A", "myeloid": "CST3"} + >>> plot = sc.pl._baseplot_class.BasePlot( + ... adata, markers, groupby="bulk_labels" + ... ).add_totals() + >>> plot.plot_group_extra["counts_df"] # doctest: +SKIP + bulk_labels + CD4+/CD25 T Reg 68 + CD4+/CD45RA+/CD25- Naive T 8 + CD4+/CD45RO+ Memory 19 + CD8+ Cytotoxic T 54 + CD8+/CD45RA+ Naive Cytotoxic 43 + CD14+ Monocyte 129 + CD19+ B 95 + CD34+ 13 + CD56+ NK 31 + Dendritic 240 + Name: count, dtype: int64 + + """ + self.group_extra_size = size + + if not show: + # hide totals + self.plot_group_extra = None + self.group_extra_size = 0 + return self + + _sort = sort is not None + _ascending = sort == "ascending" + counts_df = self.obs_tidy.index.value_counts(sort=_sort, ascending=_ascending) + + if _sort: + self.categories_order = counts_df.index + + self.plot_group_extra = { + "kind": "group_totals", + "width": size, + "sort": sort, + "counts_df": counts_df, + "color": color, + } + return self + + @old_positionals("cmap") + def style(self, *, cmap: Colormap | str | None | Empty = _empty) -> Self: + r"""Set visual style parameters. + + Parameters + ---------- + cmap + Matplotlib color map, specified by name or directly. + If ``None``, use :obj:`matplotlib.rcParams`\ ``["image.cmap"]`` + + Returns + ------- + Returns `self` for method chaining. + + """ + if cmap is not _empty: + self.cmap = cmap + return self + + @old_positionals("show", "title", "width") + def legend( + self, + *, + show: bool | None = True, + title: str | None = DEFAULT_COLOR_LEGEND_TITLE, + width: float | None = DEFAULT_LEGENDS_WIDTH, + ) -> Self: + r"""Configure legend parameters. + + Parameters + ---------- + show + Set to 'False' to hide the default plot of the legend. This sets the + legend width to zero which will result in a wider main plot. + title + Legend title. Appears on top of the color bar. Use ``\n`` to add line breaks. + width + Width of the legend. The unit is the same as in matplotlib (inches) + + Returns + ------- + Returns `self` for method chaining. + + + Examples + -------- + Set legend title: + + >>> import scanpy as sc + >>> adata = sc.datasets.pbmc68k_reduced() + >>> markers = {'T-cell': 'CD3D', 'B-cell': 'CD79A', 'myeloid': 'CST3'} + >>> dp = sc.pl._baseplot_class.BasePlot(adata, markers, groupby='bulk_labels') \ + ... .legend(title='log(UMI counts + 1)') + >>> dp.color_legend_title + 'log(UMI counts + 1)' + + """ + if not show: + # turn of legends by setting width to 0 + self.legends_width = 0 + else: + self.color_legend_title = title + self.legends_width = width + + return self + + def get_axes(self) -> dict[str, Axes]: + if self.ax_dict is None: + self.make_figure() + return self.ax_dict + + def _plot_totals( + self, total_barplot_ax: Axes, orientation: Literal["top", "right"] + ): + """Make the bar plot for totals.""" + params = self.plot_group_extra + counts_df: pd.DataFrame = params["counts_df"] + if self.categories_order is not None: + counts_df = counts_df.loc[self.categories_order] + if params["color"] is None: + color = self.adata.uns.get(f"{self.groupby}_colors", "salmon") + else: + color = params["color"] + + if orientation == "top": + counts_df.plot( + kind="bar", + color=color, + position=0.5, + ax=total_barplot_ax, + edgecolor="black", + width=0.65, + ) + # add numbers to the top of the bars + max_y = max([p.get_height() for p in total_barplot_ax.patches]) + + for p in total_barplot_ax.patches: + p.set_x(p.get_x() + 0.5) + if p.get_height() >= 1000: + display_number = f"{np.round(p.get_height() / 1000, decimals=1)}k" + else: + display_number = np.round(p.get_height(), decimals=1) + total_barplot_ax.annotate( + display_number, + (p.get_x() + p.get_width() / 2.0, (p.get_height() + max_y * 0.05)), + ha="center", + va="top", + xytext=(0, 10), + fontsize="x-small", + textcoords="offset points", + ) + # for k in total_barplot_ax.spines.keys(): + # total_barplot_ax.spines[k].set_visible(False) + total_barplot_ax.set_ylim(0, max_y * 1.4) + + elif orientation == "right": + counts_df.plot( + kind="barh", + color=color, + position=-0.3, + ax=total_barplot_ax, + edgecolor="black", + width=0.65, + ) + + # add numbers to the right of the bars + max_x = max([p.get_width() for p in total_barplot_ax.patches]) + for p in total_barplot_ax.patches: + if p.get_width() >= 1000: + display_number = f"{np.round(p.get_width() / 1000, decimals=1)}k" + else: + display_number = np.round(p.get_width(), decimals=1) + total_barplot_ax.annotate( + display_number, + ((p.get_width()), p.get_y() + p.get_height()), + ha="center", + va="top", + xytext=(10, 10), + fontsize="x-small", + textcoords="offset points", + ) + total_barplot_ax.set_xlim(0, max_x * 1.4) + + total_barplot_ax.grid(visible=False) + total_barplot_ax.axis("off") + + def _plot_colorbar(self, color_legend_ax: Axes, normalize) -> None: + """Plot a horizontal colorbar given the ax an normalize values. + + Parameters + ---------- + color_legend_ax + normalize + + Returns + ------- + `None`, updates color_legend_ax + + """ + cmap = colormaps.get_cmap(self.cmap) + + import matplotlib.colorbar + from matplotlib.cm import ScalarMappable + + mappable = ScalarMappable(norm=normalize, cmap=cmap) + + matplotlib.colorbar.Colorbar( + color_legend_ax, mappable=mappable, orientation="horizontal" + ) + + color_legend_ax.set_title(self.color_legend_title, fontsize="small") + + color_legend_ax.xaxis.set_tick_params(labelsize="small") + + def _plot_legend(self, legend_ax, return_ax_dict, normalize): + # to maintain the fixed height size of the legends, a + # spacer of variable height is added at top and bottom. + # The structure for the legends is: + # first row: variable space to keep the first rows of the same size + # second row: size legend + + legend_height = self.min_figure_height * 0.08 + height_ratios = [ + self.height - legend_height, + legend_height, + ] + fig, legend_gs = make_grid_spec( + legend_ax, nrows=2, ncols=1, height_ratios=height_ratios + ) + + color_legend_ax = fig.add_subplot(legend_gs[1]) + + self._plot_colorbar(color_legend_ax, normalize) + return_ax_dict["color_legend_ax"] = color_legend_ax + + def _mainplot(self, ax: Axes): + y_labels = self.categories + x_labels = self.var_names + + if self.var_names_idx_order is not None: + x_labels = [x_labels[x] for x in self.var_names_idx_order] + + if self.categories_order is not None: + y_labels = self.categories_order + + if self.are_axes_swapped: + x_labels, y_labels = y_labels, x_labels + ax.set_xlabel(self.groupby) + else: + ax.set_ylabel(self.groupby) + + y_ticks = np.arange(len(y_labels)) + 0.5 + ax.set_yticks(y_ticks) + ax.set_yticklabels(y_labels) + + x_ticks = np.arange(len(x_labels)) + 0.5 + ax.set_xticks(x_ticks) + ax.set_xticklabels(x_labels, rotation=90, ha="center", minor=False) + + ax.tick_params(axis="both", labelsize="small") + ax.grid(visible=False) + + # to be consistent with the heatmap plot, is better to + # invert the order of the y-axis, such that the first group is on + # top + ax.set_ylim(len(y_labels), 0) + ax.set_xlim(0, len(x_labels)) + + return check_colornorm( + self.vboundnorm.vmin, + self.vboundnorm.vmax, + self.vboundnorm.vcenter, + self.vboundnorm.norm, + ) + + def make_figure(self) -> None: # noqa: PLR0912, PLR0915 + r"""Render the image but does not call :func:`matplotlib.pyplot.show`. + + Useful when several plots are put together into one figure. + + See Also + -------- + `show()`: Renders and shows the plot. + `savefig()`: Saves the plot. + + Examples + -------- + >>> import scanpy as sc + >>> import matplotlib.pyplot as plt + >>> adata = sc.datasets.pbmc68k_reduced() + >>> markers = ['C1QA', 'PSAP', 'CD79A', 'CD79B', 'CST3', 'LYZ'] + >>> fig, (ax0, ax1) = plt.subplots(1, 2) + >>> sc.pl.MatrixPlot(adata, markers, groupby='bulk_labels', ax=ax0) \ + ... .style(cmap='Blues', edge_color='none').make_figure() + >>> sc.pl.DotPlot(adata, markers, groupby='bulk_labels', ax=ax1).make_figure() + + """ + category_height = self.DEFAULT_CATEGORY_HEIGHT + category_width = self.DEFAULT_CATEGORY_WIDTH + + if self.height is None: + mainplot_height = len(self.categories) * category_height + mainplot_width = ( + len(self.var_names) * category_width + self.group_extra_size + ) + if self.are_axes_swapped: + mainplot_height, mainplot_width = mainplot_width, mainplot_height + + height = mainplot_height + 1 # +1 for labels + + # if the number of categories is small use + # a larger height, otherwise the legends do not fit + self.height = max([self.min_figure_height, height]) + self.width = mainplot_width + self.legends_width + else: + self.min_figure_height = self.height + mainplot_height = self.height + + mainplot_width = self.width - (self.legends_width + self.group_extra_size) + + return_ax_dict = {} + # define a layout of 1 rows x 2 columns + # first ax is for the main figure. + # second ax is to plot legends + legends_width_spacer = 0.7 / self.width + + self.fig, gs = make_grid_spec( + self.ax or (self.width, self.height), + nrows=1, + ncols=2, + wspace=legends_width_spacer, + width_ratios=[mainplot_width + self.group_extra_size, self.legends_width], + ) + + if self.var_groups: + # add some space in case 'brackets' want to be plotted on top of the image + if self.are_axes_swapped: + var_groups_height = category_height + else: + var_groups_height = category_height / 2 + + else: + var_groups_height = 0 + + mainplot_width = mainplot_width - self.group_extra_size + spacer_height = self.height - var_groups_height - mainplot_height + if not self.are_axes_swapped: + height_ratios = [spacer_height, var_groups_height, mainplot_height] + width_ratios = [mainplot_width, self.group_extra_size] + + else: + height_ratios = [spacer_height, self.group_extra_size, mainplot_height] + width_ratios = [mainplot_width, var_groups_height] + # gridspec is the same but rows and columns are swapped + + if self.fig_title is not None and self.fig_title.strip() != "": + # for the figure title use the ax that contains + # all the main graphical elements (main plot, dendrogram etc) + # otherwise the title may overlay with the figure. + # also, this puts the title centered on the main figure and not + # centered between the main figure and the legends + _ax = self.fig.add_subplot(gs[0, 0]) + _ax.axis("off") + _ax.set_title(self.fig_title) + + # the main plot is divided into three rows and two columns + # first row is an spacer that is adjusted in case the + # legends need more height than the main plot + # second row is for brackets (if needed), + # third row is for mainplot and dendrogram/totals (legend goes in gs[0,1] + # defined earlier) + mainplot_gs = gridspec.GridSpecFromSubplotSpec( + nrows=3, + ncols=2, + wspace=self.wspace, + hspace=0.0, + subplot_spec=gs[0, 0], + width_ratios=width_ratios, + height_ratios=height_ratios, + ) + main_ax = self.fig.add_subplot(mainplot_gs[2, 0]) + return_ax_dict["mainplot_ax"] = main_ax + if not self.are_axes_swapped: + if self.plot_group_extra is not None: + group_extra_ax = self.fig.add_subplot(mainplot_gs[2, 1], sharey=main_ax) + group_extra_orientation = "right" + if self.var_groups: + gene_groups_ax = self.fig.add_subplot(mainplot_gs[1, 0], sharex=main_ax) + var_group_orientation = "top" + else: + if self.plot_group_extra: + group_extra_ax = self.fig.add_subplot(mainplot_gs[1, 0], sharex=main_ax) + group_extra_orientation = "top" + if self.var_groups: + gene_groups_ax = self.fig.add_subplot(mainplot_gs[2, 1], sharey=main_ax) + var_group_orientation = "right" + + if self.plot_group_extra is not None: + if self.plot_group_extra["kind"] == "dendrogram": + _plot_dendrogram( + group_extra_ax, + self.adata, + self.groupby, + dendrogram_key=self.plot_group_extra["dendrogram_key"], + ticks=self.plot_group_extra["dendrogram_ticks"], + orientation=group_extra_orientation, + ) + if self.plot_group_extra["kind"] == "group_totals": + self._plot_totals(group_extra_ax, group_extra_orientation) + + return_ax_dict["group_extra_ax"] = group_extra_ax + + # plot group legends on top or left of main_ax (if given) + if self.var_groups: + _plot_var_groups_brackets( + gene_groups_ax, + var_groups=self.var_groups, + rotation=self.var_group_rotation, + left_adjustment=0.2, + right_adjustment=0.7, + orientation=var_group_orientation, + wide=True, + ) + return_ax_dict["gene_group_ax"] = gene_groups_ax + + # plot the mainplot + normalize = self._mainplot(main_ax) + + # code from pandas.plot in add_totals adds + # minor ticks that need to be removed + main_ax.yaxis.set_tick_params(which="minor", left=False, right=False) + main_ax.xaxis.set_tick_params(which="minor", top=False, bottom=False, length=0) + main_ax.set_zorder(100) + if self.legends_width > 0: + legend_ax = self.fig.add_subplot(gs[0, 1]) + self._plot_legend(legend_ax, return_ax_dict, normalize) + + self.ax_dict = return_ax_dict + + @old_positionals("return_axes") + def show(self, *, return_axes: bool | None = None) -> dict[str, Axes] | None: + """Show the figure. + + Parameters + ---------- + return_axes + If true return a dictionary with the figure axes. When return_axes is true + then :func:`matplotlib.pyplot.show` is not called. + + Returns + ------- + If `return_axes=True`: Dict of :class:`matplotlib.axes.Axes`. The dict key + indicates the type of ax (eg. `mainplot_ax`) + + See Also + -------- + `render()`: Renders the plot but does not call :func:`matplotlib.pyplot.show` + `savefig()`: Saves the plot. + + Examples + -------- + >>> import scanpy as sc + >>> adata = sc.datasets.pbmc68k_reduced() + >>> markers = ["C1QA", "PSAP", "CD79A", "CD79B", "CST3", "LYZ"] + >>> sc.pl._baseplot_class.BasePlot(adata, markers, groupby="bulk_labels").show() + + """ + self.make_figure() + + if return_axes: + return self.ax_dict + else: + plt.show() + + def savefig(self, filename: str, bbox_inches: str | None = "tight", **kwargs): + """Save the current figure. + + Parameters + ---------- + filename + Figure filename. Figure *format* is taken from the file ending unless + the parameter `format` is given. + bbox_inches + By default is set to 'tight' to avoid cropping of the legends. + kwargs + Passed to :func:`matplotlib.pyplot.savefig` + + See Also + -------- + `render()`: Renders the plot but does not call :func:`matplotlib.pyplot.show` + `show()`: Renders and shows the plot + + Examples + -------- + >>> import scanpy as sc + >>> adata = sc.datasets.pbmc68k_reduced() + >>> markers = ["C1QA", "PSAP", "CD79A", "CD79B", "CST3", "LYZ"] + >>> sc.pl._baseplot_class.BasePlot( + ... adata, markers, groupby="bulk_labels" + ... ).savefig("plot.pdf") + + """ + self.make_figure() + plt.savefig(filename, bbox_inches=bbox_inches, **kwargs) + + def _reorder_categories_after_dendrogram(self, dendrogram_key: str | None) -> None: + """Reorder the the groupby observations based on the dendrogram results. + + The function checks if a dendrogram has already been precomputed. + If not, `sc.tl.dendrogram` is run with default parameters. + + The results found in `.uns[dendrogram_key]` are used to reorder + `var_group_labels` and `var_group_positions`. + + + Returns + ------- + `None`, internally updates + `categories_idx_ordered`, `var_group_names_idx_ordered`, + `var_group_labels`, `var_group_positions`, and `var_groups` + + """ + rv = _reorder_categories_after_dendrogram( + self.adata, + self.groupby, + dendrogram_key=dendrogram_key, + var_names=self.var_names, + var_groups=self.var_groups, + categories=self.categories, + ) + + self.categories_idx_ordered = rv["categories_idx_ordered"] + self.categories_order = rv["categories_ordered"] + self.var_names_idx_order = rv["var_names_idx_ordered"] + self.var_names_ordered = rv["var_names_ordered"] + self.var_groups = rv["var_groups"] + + +def _var_groups( + var_names: _VarNames | Mapping[str, _VarNames], +) -> tuple[Sequence[str], VarGroups | None]: + """Normalize var_names. + + If it’s a mapping, also return var_group_labels and var_group_positions. + """ + if not isinstance(var_names, Mapping): + var_names = [var_names] if isinstance(var_names, str) else var_names + return var_names, None + if len(var_names) == 0: + return [], None + + var_group_labels: list[str] = [] + var_names_seq: list[str] = [] + var_group_positions: list[tuple[int, int]] = [] + for label, vars in var_names.items(): + vars_list = [vars] if isinstance(vars, str) else vars + start = len(var_names_seq) + # use list() in case var_list is a numpy array or pandas series + var_names_seq.extend(list(vars_list)) + var_group_labels.append(label) + var_group_positions.append((start, start + len(vars_list) - 1)) + if not var_names_seq: + msg = "No valid var_names were passed." + raise ValueError(msg) + return var_names_seq, VarGroups(var_group_labels, var_group_positions) diff --git a/.venv/lib/python3.11/site-packages/scanpy/plotting/_docs.py b/.venv/lib/python3.11/site-packages/scanpy/plotting/_docs.py new file mode 100644 index 0000000000000000000000000000000000000000..55c4c690c7e96fce8d83299c12f9c8387a7a43bd --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/plotting/_docs.py @@ -0,0 +1,320 @@ +"""Shared docstrings for plotting function parameters.""" + +from __future__ import annotations + +doc_adata_color_etc = """\ +adata + Annotated data matrix. +color + Keys for annotations of observations/cells or variables/genes, e.g., + `'ann1'` or `['ann1', 'ann2']`. +gene_symbols + Column name in `.var` DataFrame that stores gene symbols. By default `var_names` + refer to the index column of the `.var` DataFrame. Setting this option allows + alternative names to be used. +use_raw + Use `.raw` attribute of `adata` for coloring with gene expression. If `None`, + defaults to `True` if `layer` isn't provided and `adata.raw` is present. +layer + Name of the AnnData object layer that wants to be plotted. By default + adata.raw.X is plotted. If `use_raw=False` is set, then `adata.X` is plotted. + If `layer` is set to a valid layer name, then the layer is plotted. `layer` + takes precedence over `use_raw`.\ +""" + +doc_edges_arrows = """\ +edges + Show edges. +edges_width + Width of edges. +edges_color + Color of edges. See :func:`~networkx.drawing.nx_pylab.draw_networkx_edges`. +neighbors_key + Where to look for neighbors connectivities. + If not specified, this looks .obsp['connectivities'] for connectivities + (default storage place for pp.neighbors). + If specified, this looks + .obsp[.uns[neighbors_key]['connectivities_key']] for connectivities. +arrows + Show arrows (deprecated in favour of `scvelo.pl.velocity_embedding`). +arrows_kwds + Passed to :meth:`~matplotlib.axes.Axes.quiver`\ +""" + +doc_cm_palette = """\ +color_map + Color map to use for continous variables. Can be a name or a + :class:`~matplotlib.colors.Colormap` instance (e.g. `"magma`", `"viridis"` + or `mpl.cm.cividis`), see :meth:`~matplotlib.cm.ColormapRegistry.get_cmap`. + If `None`, the value of `mpl.rcParams["image.cmap"]` is used. + The default `color_map` can be set using :func:`~scanpy.set_figure_params`. +palette + Colors to use for plotting categorical annotation groups. + The palette can be a valid :class:`~matplotlib.colors.ListedColormap` name + (`'Set2'`, `'tab20'`, …), a :class:`~cycler.Cycler` object, a dict mapping + categories to colors, or a sequence of colors. Colors must be valid to + matplotlib. (see :func:`~matplotlib.colors.is_color_like`). + If `None`, `mpl.rcParams["axes.prop_cycle"]` is used unless the categorical + variable already has colors stored in `adata.uns["{var}_colors"]`. + If provided, values of `adata.uns["{var}_colors"]` will be set.\ +""" + +# Docs for pl.scatter +doc_scatter_basic = f"""\ +sort_order + For continuous annotations used as color parameter, plot data points + with higher values on top of others. +groups + Restrict to a few categories in categorical observation annotation. + The default is not to restrict to any groups. +dimensions + 0-indexed dimensions of the embedding to plot as integers. E.g. [(0, 1), (1, 2)]. + Unlike `components`, this argument is used in the same way as `colors`, e.g. is + used to specify a single plot at a time. Will eventually replace the components + argument. +components + For instance, `['1,2', '2,3']`. To plot all available components use + `components='all'`. +projection + Projection of plot (default: `'2d'`). +legend_loc + Location of legend, either `'on data'`, `'right margin'`, `None`, + or a valid keyword for the `loc` parameter of :class:`~matplotlib.legend.Legend`. +legend_fontsize + Numeric size in pt or string describing the size. + See :meth:`~matplotlib.text.Text.set_fontsize`. +legend_fontweight + Legend font weight. A numeric value in range 0-1000 or a string. + Defaults to `'bold'` if `legend_loc == 'on data'`, otherwise to `'normal'`. + See :meth:`~matplotlib.text.Text.set_fontweight`. +legend_fontoutline + Line width of the legend font outline in pt. Draws a white outline using + the path effect :class:`~matplotlib.patheffects.withStroke`. +colorbar_loc + Where to place the colorbar for continous variables. If `None`, no colorbar + is added. +size + Point size. If `None`, is automatically computed as 120000 / n_cells. + Can be a sequence containing the size for each cell. The order should be + the same as in adata.obs. +{doc_cm_palette} +na_color + Color to use for null or masked values. Can be anything matplotlib accepts as a + color. Used for all points if `color=None`. +na_in_legend + If there are missing values, whether they get an entry in the legend. Currently + only implemented for categorical legends. +frameon + Draw a frame around the scatter plot. Defaults to value set in + :func:`~scanpy.set_figure_params`, defaults to `True`. +title + Provide title for panels either as string or list of strings, + e.g. `['title1', 'title2', ...]`. +""" + +doc_vbound_percentile = """\ +vmin + The value representing the lower limit of the color scale. Values smaller than vmin are plotted + with the same color as vmin. vmin can be a number, a string, a function or `None`. If + vmin is a string and has the format `pN`, this is interpreted as a vmin=percentile(N). + For example vmin='p1.5' is interpreted as the 1.5 percentile. If vmin is function, then + vmin is interpreted as the return value of the function over the list of values to plot. + For example to set vmin tp the mean of the values to plot, `def my_vmin(values): return + np.mean(values)` and then set `vmin=my_vmin`. If vmin is None (default) an automatic + minimum value is used as defined by matplotlib `scatter` function. When making multiple + plots, vmin can be a list of values, one for each plot. For example `vmin=[0.1, 'p1', None, my_vmin]` +vmax + The value representing the upper limit of the color scale. The format is the same as for `vmin`. +vcenter + The value representing the center of the color scale. Useful for diverging colormaps. + The format is the same as for `vmin`. + Example: ``sc.pl.umap(adata, color='TREM2', vcenter='p50', cmap='RdBu_r')``\ +""" + +doc_vboundnorm = """\ +vmin + The value representing the lower limit of the color scale. Values smaller than vmin are plotted + with the same color as vmin. +vmax + The value representing the upper limit of the color scale. Values larger than vmax are plotted + with the same color as vmax. +vcenter + The value representing the center of the color scale. Useful for diverging colormaps. +norm + Custom color normalization object from matplotlib. See :ref:`colormapnorms` for details.\ +""" + +doc_outline = """\ +add_outline + If set to True, this will add a thin border around groups of dots. In some situations + this can enhance the aesthetics of the resulting image +outline_color + Tuple with two valid color names used to adjust the add_outline. The first color is the + border color (default: black), while the second color is a gap color between the + border color and the scatter dot (default: white). +outline_width + Tuple with two width numbers used to adjust the outline. The first value is the width + of the border color as a fraction of the scatter dot size (default: 0.3). The second value is + width of the gap color (default: 0.05).\ +""" + +doc_panels = """\ +ncols + Number of panels per row. +wspace + Adjust the width of the space between multiple panels. +hspace + Adjust the height of the space between multiple panels. +return_fig + Return the matplotlib figure.\ +""" + +# Docs for pl.pca, pl.tsne, … (everything in _tools.scatterplots) +doc_scatter_embedding = f"""\ +{doc_scatter_basic} +{doc_vbound_percentile} +{doc_outline} +{doc_panels} +kwargs + Arguments to pass to :func:`matplotlib.pyplot.scatter`, + for instance: the maximum and minimum values (e.g. `vmin=-2, vmax=5`).\ +""" + +doc_show_save = """\ +show + Show the plot, do not return axis. +save + If `True` or a `str`, save the figure. + A string is appended to the default filename. + Infer the filetype if ending on {`'.pdf'`, `'.png'`, `'.svg'`}.\ +""" + +doc_show_save_ax = f"""\ +{doc_show_save} +ax + A matplotlib axes object. Only works if plotting a single component.\ +""" + +doc_common_plot_args = """\ +adata + Annotated data matrix. +var_names + `var_names` should be a valid subset of `adata.var_names`. + If `var_names` is a mapping, then the key is used as label + to group the values (see `var_group_labels`). The mapping values + should be sequences of valid `adata.var_names`. In this + case either coloring or 'brackets' are used for the grouping + of var names depending on the plot. When `var_names` is a mapping, + then the `var_group_labels` and `var_group_positions` are set. +groupby + The key of the observation grouping to consider. +use_raw + Use `raw` attribute of `adata` if present. +log + Plot on logarithmic axis. +num_categories + Only used if groupby observation is not categorical. This value + determines the number of groups into which the groupby observation + should be subdivided. +categories_order + Order in which to show the categories. Note: add_dendrogram or add_totals + can change the categories order. +figsize + Figure size when `multi_panel=True`. + Otherwise the `rcParam['figure.figsize]` value is used. + Format is (width, height) +dendrogram + If True or a valid dendrogram key, a dendrogram based on the hierarchical + clustering between the `groupby` categories is added. + The dendrogram information is computed using :func:`scanpy.tl.dendrogram`. + If `tl.dendrogram` has not been called previously the function is called + with default parameters. +gene_symbols + Column name in `.var` DataFrame that stores gene symbols. + By default `var_names` refer to the index column of the `.var` DataFrame. + Setting this option allows alternative names to be used. +var_group_positions + Use this parameter to highlight groups of `var_names`. + This will draw a 'bracket' or a color block between the given start and end + positions. If the parameter `var_group_labels` is set, the corresponding + labels are added on top/left. E.g. `var_group_positions=[(4,10)]` + will add a bracket between the fourth `var_name` and the tenth `var_name`. + By giving more positions, more brackets/color blocks are drawn. +var_group_labels + Labels for each of the `var_group_positions` that want to be highlighted. +var_group_rotation + Label rotation degrees. + By default, labels larger than 4 characters are rotated 90 degrees. +layer + Name of the AnnData object layer that wants to be plotted. By default adata.raw.X is plotted. + If `use_raw=False` is set, then `adata.X` is plotted. If `layer` is set to a valid layer name, + then the layer is plotted. `layer` takes precedence over `use_raw`.\ +""" + +doc_rank_genes_groups_plot_args = """\ +adata + Annotated data matrix. +groups + The groups for which to show the gene ranking. +n_genes + Number of genes to show. This can be a negative number to show for + example the down regulated genes. eg: num_genes=-10. Is ignored if + `gene_names` is passed. +gene_symbols + Column name in `.var` DataFrame that stores gene symbols. By default `var_names` + refer to the index column of the `.var` DataFrame. Setting this option allows + alternative names to be used. +groupby + The key of the observation grouping to consider. By default, + the groupby is chosen from the rank genes groups parameter but + other groupby options can be used. It is expected that + groupby is a categorical. If groupby is not a categorical observation, + it would be subdivided into `num_categories` (see :func:`~scanpy.pl.dotplot`). +min_logfoldchange + Value to filter genes in groups if their logfoldchange is less than the + min_logfoldchange +key + Key used to store the ranking results in `adata.uns`.\ +""" + +doc_rank_genes_groups_values_to_plot = """\ +values_to_plot + Instead of the mean gene value, plot the values computed by `sc.rank_genes_groups`. + The options are: ['scores', 'logfoldchanges', 'pvals', 'pvals_adj', + 'log10_pvals', 'log10_pvals_adj']. When plotting logfoldchanges a divergent + colormap is recommended. See examples below. +var_names + Genes to plot. Sometimes is useful to pass a specific list of var names (e.g. genes) + to check their fold changes or p-values, instead of the top/bottom genes. The + var_names could be a dictionary or a list as in :func:`~scanpy.pl.dotplot` or + :func:`~scanpy.pl.matrixplot`. See examples below.\ +""" + +doc_scatter_spatial = """\ +library_id + library_id for Visium data, e.g. key in `adata.uns["spatial"]`. +img_key + Key for image data, used to get `img` and `scale_factor` from `"images"` + and `"scalefactors"` entires for this library. To use spatial coordinates, + but not plot an image, pass `img_key=None`. +img + image data to plot, overrides `img_key`. +scale_factor + Scaling factor used to map from coordinate space to pixel space. + Found by default if `library_id` and `img_key` can be resolved. + Otherwise defaults to `1.`. +spot_size + Diameter of spot (in coordinate space) for each point. Diameter + in pixels of the spots will be `size * spot_size * scale_factor`. + This argument is required if it cannot be resolved from library info. +crop_coord + Coordinates to use for cropping the image (left, right, top, bottom). + These coordinates are expected to be in pixel space (same as `basis`) + and will be transformed by `scale_factor`. + If not provided, image is automatically cropped to bounds of `basis`, + plus a border. +alpha_img + Alpha value for image. +bw + Plot image data in gray scale.\ +""" diff --git a/.venv/lib/python3.11/site-packages/scanpy/plotting/_dotplot.py b/.venv/lib/python3.11/site-packages/scanpy/plotting/_dotplot.py new file mode 100644 index 0000000000000000000000000000000000000000..8c15574a1d7da81042236231220b5be960408a09 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/plotting/_dotplot.py @@ -0,0 +1,1031 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +from matplotlib import colormaps + +from .. import logging as logg +from .._compat import old_positionals +from .._settings import settings +from .._utils import _doc_params, _empty +from ._baseplot_class import BasePlot, doc_common_groupby_plot_args +from ._docs import doc_common_plot_args, doc_show_save_ax, doc_vboundnorm +from ._utils import ( + _dk, + check_colornorm, + fix_kwds, + make_grid_spec, + savefig_or_show, +) + +if TYPE_CHECKING: + from collections.abc import Mapping, Sequence + from typing import Literal, Self + + import pandas as pd + from anndata import AnnData + from matplotlib.axes import Axes + from matplotlib.colors import Colormap, Normalize + + from .._utils import Empty + from ._baseplot_class import _VarNames + from ._utils import ColorLike, _AxesSubplot + + +@_doc_params(common_plot_args=doc_common_plot_args) +class DotPlot(BasePlot): + """Allows the visualization of two values that are encoded as dot size and color. + + The size usually represents the fraction of cells (obs) + that have a non-zero value for genes (var). + + For each var_name and each `groupby` category a dot is plotted. + Each dot represents two values: mean expression within each category + (visualized by color) and fraction of cells expressing the `var_name` in the + category (visualized by the size of the dot). If `groupby` is not given, + the dotplot assumes that all data belongs to a single category. + + .. note:: + A gene is considered expressed if the expression value in the `adata` (or + `adata.raw`) is above the specified threshold which is zero by default. + + An example of dotplot usage is to visualize, for multiple marker genes, + the mean value and the percentage of cells expressing the gene + across multiple clusters. + + Parameters + ---------- + {common_plot_args} + title + Title for the figure + expression_cutoff + Expression cutoff that is used for binarizing the gene expression and + determining the fraction of cells expressing given genes. A gene is + expressed only if the expression value is greater than this threshold. + mean_only_expressed + If True, gene expression is averaged only over the cells + expressing the given genes. + standard_scale + Whether or not to standardize that dimension between 0 and 1, + meaning for each variable or group, + subtract the minimum and divide each by its maximum. + kwds + Are passed to :func:`matplotlib.pyplot.scatter`. + + See Also + -------- + :func:`~scanpy.pl.dotplot`: Simpler way to call DotPlot but with less options. + :func:`~scanpy.pl.rank_genes_groups_dotplot`: to plot marker + genes identified using the :func:`~scanpy.tl.rank_genes_groups` function. + + Examples + -------- + >>> import scanpy as sc + >>> adata = sc.datasets.pbmc68k_reduced() + >>> markers = ["C1QA", "PSAP", "CD79A", "CD79B", "CST3", "LYZ"] + >>> sc.pl.DotPlot(adata, markers, groupby="bulk_labels").show() + + Using var_names as dict: + + >>> markers = {{"T-cell": "CD3D", "B-cell": "CD79A", "myeloid": "CST3"}} + >>> sc.pl.DotPlot(adata, markers, groupby="bulk_labels").show() + + """ + + DEFAULT_SAVE_PREFIX = "dotplot_" + # default style parameters + DEFAULT_COLORMAP = "Reds" + DEFAULT_COLOR_ON = "dot" + DEFAULT_DOT_MAX = None + DEFAULT_DOT_MIN = None + DEFAULT_SMALLEST_DOT = 0.0 + DEFAULT_LARGEST_DOT = 200.0 + DEFAULT_DOT_EDGECOLOR = "black" + DEFAULT_DOT_EDGELW = 0.2 + DEFAULT_SIZE_EXPONENT = 1.5 + + # default legend parameters + DEFAULT_SIZE_LEGEND_TITLE = "Fraction of cells\nin group (%)" + DEFAULT_COLOR_LEGEND_TITLE = "Mean expression\nin group" + DEFAULT_LEGENDS_WIDTH = 1.5 # inches + DEFAULT_PLOT_X_PADDING = 0.8 # a unit is the distance between two x-axis ticks + DEFAULT_PLOT_Y_PADDING = 1.0 # a unit is the distance between two y-axis ticks + + @old_positionals( + "use_raw", + "log", + "num_categories", + "categories_order", + "title", + "figsize", + "gene_symbols", + "var_group_positions", + "var_group_labels", + "var_group_rotation", + "layer", + "expression_cutoff", + "mean_only_expressed", + "standard_scale", + "dot_color_df", + "dot_size_df", + "ax", + "vmin", + "vmax", + "vcenter", + "norm", + ) + def __init__( # noqa: PLR0913 + self, + adata: AnnData, + var_names: _VarNames | Mapping[str, _VarNames], + groupby: str | Sequence[str], + *, + use_raw: bool | None = None, + log: bool = False, + num_categories: int = 7, + categories_order: Sequence[str] | None = None, + title: str | None = None, + figsize: tuple[float, float] | None = None, + gene_symbols: str | None = None, + var_group_positions: Sequence[tuple[int, int]] | None = None, + var_group_labels: Sequence[str] | None = None, + var_group_rotation: float | None = None, + layer: str | None = None, + expression_cutoff: float = 0.0, + mean_only_expressed: bool = False, + standard_scale: Literal["var", "group"] | None = None, + dot_color_df: pd.DataFrame | None = None, + dot_size_df: pd.DataFrame | None = None, + ax: _AxesSubplot | None = None, + vmin: float | None = None, + vmax: float | None = None, + vcenter: float | None = None, + norm: Normalize | None = None, + **kwds, + ) -> None: + BasePlot.__init__( + self, + adata, + var_names, + groupby, + use_raw=use_raw, + log=log, + num_categories=num_categories, + categories_order=categories_order, + title=title, + figsize=figsize, + gene_symbols=gene_symbols, + var_group_positions=var_group_positions, + var_group_labels=var_group_labels, + var_group_rotation=var_group_rotation, + layer=layer, + ax=ax, + vmin=vmin, + vmax=vmax, + vcenter=vcenter, + norm=norm, + **kwds, + ) + + # for if category defined by groupby (if any) compute for each var_name + # 1. the fraction of cells in the category having a value >expression_cutoff + # 2. the mean value over the category + + # 1. compute fraction of cells having value > expression_cutoff + # transform obs_tidy into boolean matrix using the expression_cutoff + obs_bool = self.obs_tidy > expression_cutoff + + # compute the sum per group which in the boolean matrix this is the number + # of values >expression_cutoff, and divide the result by the total number of + # values in the group (given by `count()`) + if dot_size_df is None: + dot_size_df = ( + obs_bool.groupby(level=0, observed=True).sum() + / obs_bool.groupby(level=0, observed=True).count() + ) + + if dot_color_df is None: + # 2. compute mean expression value value + if mean_only_expressed: + dot_color_df = ( + self.obs_tidy.mask(~obs_bool) + .groupby(level=0, observed=True) + .mean() + .fillna(0) + ) + else: + dot_color_df = self.obs_tidy.groupby(level=0, observed=True).mean() + + if standard_scale == "group": + dot_color_df = dot_color_df.sub(dot_color_df.min(1), axis=0) + dot_color_df = dot_color_df.div(dot_color_df.max(1), axis=0).fillna(0) + elif standard_scale == "var": + dot_color_df -= dot_color_df.min(0) + dot_color_df = (dot_color_df / dot_color_df.max(0)).fillna(0) + elif standard_scale is None: + pass + else: + logg.warning("Unknown type for standard_scale, ignored") + else: + # check that both matrices have the same shape + if dot_color_df.shape != dot_size_df.shape: + logg.error( + "the given dot_color_df data frame has a different shape than " + "the data frame used for the dot size. Both data frames need " + "to have the same index and columns" + ) + + # Because genes (columns) can be duplicated (e.g. when the + # same gene is reported as marker gene in two clusters) + # they need to be removed first, + # otherwise, the duplicated genes are further duplicated when reordering + # Eg. A df with columns ['a', 'b', 'a'] after reordering columns + # with df[['a', 'a', 'b']], results in a df with columns: + # ['a', 'a', 'a', 'a', 'b'] + + unique_var_names, unique_idx = np.unique( + dot_color_df.columns, return_index=True + ) + # remove duplicate columns + if len(unique_var_names) != len(self.var_names): + dot_color_df = dot_color_df.iloc[:, unique_idx] + + # get the same order for rows and columns in the dot_color_df + # using the order from the doc_size_df + dot_color_df = dot_color_df.loc[dot_size_df.index][dot_size_df.columns] + + self.dot_color_df, self.dot_size_df = ( + df.loc[ + categories_order if categories_order is not None else self.categories + ] + for df in (dot_color_df, dot_size_df) + ) + self.standard_scale = standard_scale + + # Set default style parameters + self.cmap = self.DEFAULT_COLORMAP + self.dot_max = self.DEFAULT_DOT_MAX + self.dot_min = self.DEFAULT_DOT_MIN + self.smallest_dot = self.DEFAULT_SMALLEST_DOT + self.largest_dot = self.DEFAULT_LARGEST_DOT + self.color_on = self.DEFAULT_COLOR_ON + self.size_exponent = self.DEFAULT_SIZE_EXPONENT + self.grid = False + self.plot_x_padding = self.DEFAULT_PLOT_X_PADDING + self.plot_y_padding = self.DEFAULT_PLOT_Y_PADDING + + self.dot_edge_color = self.DEFAULT_DOT_EDGECOLOR + self.dot_edge_lw = self.DEFAULT_DOT_EDGELW + + # set legend defaults + self.color_legend_title = self.DEFAULT_COLOR_LEGEND_TITLE + self.size_title = self.DEFAULT_SIZE_LEGEND_TITLE + self.legends_width = self.DEFAULT_LEGENDS_WIDTH + self.show_size_legend = True + self.show_colorbar = True + + @old_positionals( + "cmap", + "color_on", + "dot_max", + "dot_min", + "smallest_dot", + "largest_dot", + "dot_edge_color", + "dot_edge_lw", + "size_exponent", + "grid", + "x_padding", + "y_padding", + ) + def style( # noqa: PLR0913 + self, + *, + cmap: Colormap | str | None | Empty = _empty, + color_on: Literal["dot", "square"] | Empty = _empty, + dot_max: float | None | Empty = _empty, + dot_min: float | None | Empty = _empty, + smallest_dot: float | Empty = _empty, + largest_dot: float | Empty = _empty, + dot_edge_color: ColorLike | None | Empty = _empty, + dot_edge_lw: float | None | Empty = _empty, + size_exponent: float | Empty = _empty, + grid: bool | Empty = _empty, + x_padding: float | Empty = _empty, + y_padding: float | Empty = _empty, + ) -> Self: + r"""Modify plot visual parameters. + + Parameters + ---------- + cmap + String denoting matplotlib color map. + color_on + By default the color map is applied to the color of the ``"dot"``. + Optionally, the colormap can be applied to a ``"square"`` behind the dot, + in which case the dot is transparent and only the edge is shown. + dot_max + If ``None``, the maximum dot size is set to the maximum fraction value found (e.g. 0.6). + If given, the value should be a number between 0 and 1. + All fractions larger than dot_max are clipped to this value. + dot_min + If ``None``, the minimum dot size is set to 0. + If given, the value should be a number between 0 and 1. + All fractions smaller than dot_min are clipped to this value. + smallest_dot + All expression fractions with `dot_min` are plotted with this size. + largest_dot + All expression fractions with `dot_max` are plotted with this size. + dot_edge_color + Dot edge color. + When `color_on='dot'`, ``None`` means no edge. + When `color_on='square'`, ``None`` means that + the edge color is white for darker colors and black for lighter background square colors. + dot_edge_lw + Dot edge line width. + When `color_on='dot'`, ``None`` means no edge. + When `color_on='square'`, ``None`` means a line width of 1.5. + size_exponent + Dot size is computed as: + fraction ** size exponent and afterwards scaled to match the + `smallest_dot` and `largest_dot` size parameters. + Using a different size exponent changes the relative sizes of the dots + to each other. + grid + Set to true to show grid lines. By default grid lines are not shown. + Further configuration of the grid lines can be achieved directly on the + returned ax. + x_padding + Space between the plot left/right borders and the dots center. A unit + is the distance between the x ticks. Only applied when color_on = dot + y_padding + Space between the plot top/bottom borders and the dots center. A unit is + the distance between the y ticks. Only applied when color_on = dot + + Returns + ------- + :class:`~scanpy.pl.DotPlot` + + Examples + -------- + >>> import scanpy as sc + >>> adata = sc.datasets.pbmc68k_reduced() + >>> markers = ['C1QA', 'PSAP', 'CD79A', 'CD79B', 'CST3', 'LYZ'] + + Change color map and apply it to the square behind the dot + + >>> sc.pl.DotPlot(adata, markers, groupby='bulk_labels') \ + ... .style(cmap='RdBu_r', color_on='square').show() + + Add edge to dots and plot a grid + + >>> sc.pl.DotPlot(adata, markers, groupby='bulk_labels') \ + ... .style(dot_edge_color='black', dot_edge_lw=1, grid=True) \ + ... .show() + + """ + super().style(cmap=cmap) + + if dot_max is not _empty: + self.dot_max = dot_max + if dot_min is not _empty: + self.dot_min = dot_min + if smallest_dot is not _empty: + self.smallest_dot = smallest_dot + if largest_dot is not _empty: + self.largest_dot = largest_dot + if color_on is not _empty: + self.color_on = color_on + if size_exponent is not _empty: + self.size_exponent = size_exponent + if dot_edge_color is not _empty: + self.dot_edge_color = dot_edge_color + if dot_edge_lw is not _empty: + self.dot_edge_lw = dot_edge_lw + if grid is not _empty: + self.grid = grid + if x_padding is not _empty: + self.plot_x_padding = x_padding + if y_padding is not _empty: + self.plot_y_padding = y_padding + + return self + + @old_positionals( + "show", + "show_size_legend", + "show_colorbar", + "size_title", + "colorbar_title", + "width", + ) + def legend( + self, + *, + show: bool | None = True, + show_size_legend: bool | None = True, + show_colorbar: bool | None = True, + size_title: str | None = DEFAULT_SIZE_LEGEND_TITLE, + colorbar_title: str | None = DEFAULT_COLOR_LEGEND_TITLE, + width: float | None = DEFAULT_LEGENDS_WIDTH, + ) -> Self: + r"""Configure dot size and the colorbar legends. + + Parameters + ---------- + show + Set to `False` to hide the default plot of the legends. This sets the + legend width to zero, which will result in a wider main plot. + show_size_legend + Set to `False` to hide the dot size legend + show_colorbar + Set to `False` to hide the colorbar legend + size_title + Title for the dot size legend. Use ``\n`` to add line breaks. Appears on top + of dot sizes + colorbar_title + Title for the color bar. Use ``\n`` to add line breaks. Appears on top of the + color bar + width + Width of the legends area. The unit is the same as in matplotlib (inches). + + Returns + ------- + :class:`~scanpy.pl.DotPlot` + + Examples + -------- + Set color bar title: + + >>> import scanpy as sc + >>> adata = sc.datasets.pbmc68k_reduced() + >>> markers = {"T-cell": "CD3D", "B-cell": "CD79A", "myeloid": "CST3"} + >>> dp = sc.pl.DotPlot(adata, markers, groupby="bulk_labels") + >>> dp.legend(colorbar_title="log(UMI counts + 1)").show() + + """ + if not show: + # turn of legends by setting width to 0 + self.legends_width = 0 + else: + self.color_legend_title = colorbar_title + self.size_title = size_title + self.legends_width = width + self.show_size_legend = show_size_legend + self.show_colorbar = show_colorbar + + return self + + def _plot_size_legend(self, size_legend_ax: Axes): + # for the dot size legend, use step between dot_max and dot_min + # based on how different they are. + diff = self.dot_max - self.dot_min + if 0.3 < diff <= 0.6: + step = 0.1 + elif diff <= 0.3: + step = 0.05 + else: + step = 0.2 + # a descending range that is afterwards inverted is used + # to guarantee that dot_max is in the legend. + size_range = np.arange(self.dot_max, self.dot_min, step * -1)[::-1] + if self.dot_min != 0 or self.dot_max != 1: + dot_range = self.dot_max - self.dot_min + size_values = (size_range - self.dot_min) / dot_range + else: + size_values = size_range + + size = size_values**self.size_exponent + size = size * (self.largest_dot - self.smallest_dot) + self.smallest_dot + + # plot size bar + size_legend_ax.scatter( + np.arange(len(size)) + 0.5, + np.repeat(0, len(size)), + s=size, + color="gray", + edgecolor="black", + linewidth=self.dot_edge_lw, + zorder=100, + ) + size_legend_ax.set_xticks(np.arange(len(size)) + 0.5) + labels = [f"{np.round((x * 100), decimals=0).astype(int)}" for x in size_range] + size_legend_ax.set_xticklabels(labels, fontsize="small") + + # remove y ticks and labels + size_legend_ax.tick_params( + axis="y", left=False, labelleft=False, labelright=False + ) + + # remove surrounding lines + size_legend_ax.spines["right"].set_visible(False) + size_legend_ax.spines["top"].set_visible(False) + size_legend_ax.spines["left"].set_visible(False) + size_legend_ax.spines["bottom"].set_visible(False) + size_legend_ax.grid(visible=False) + + ymax = size_legend_ax.get_ylim()[1] + size_legend_ax.set_ylim(-1.05 - self.largest_dot * 0.003, 4) + size_legend_ax.set_title(self.size_title, y=ymax + 0.45, size="small") + + xmin, xmax = size_legend_ax.get_xlim() + size_legend_ax.set_xlim(xmin - 0.15, xmax + 0.5) + + def _plot_legend(self, legend_ax, return_ax_dict, normalize): + # to maintain the fixed height size of the legends, a + # spacer of variable height is added at the bottom. + # The structure for the legends is: + # first row: variable space to keep the other rows of + # the same size (avoid stretching) + # second row: legend for dot size + # third row: spacer to avoid color and size legend titles to overlap + # fourth row: colorbar + + cbar_legend_height = self.min_figure_height * 0.08 + size_legend_height = self.min_figure_height * 0.27 + spacer_height = self.min_figure_height * 0.3 + + height_ratios = [ + self.height - size_legend_height - cbar_legend_height - spacer_height, + size_legend_height, + spacer_height, + cbar_legend_height, + ] + fig, legend_gs = make_grid_spec( + legend_ax, nrows=4, ncols=1, height_ratios=height_ratios + ) + + if self.show_size_legend: + size_legend_ax = fig.add_subplot(legend_gs[1]) + self._plot_size_legend(size_legend_ax) + return_ax_dict["size_legend_ax"] = size_legend_ax + + if self.show_colorbar: + color_legend_ax = fig.add_subplot(legend_gs[3]) + + self._plot_colorbar(color_legend_ax, normalize) + return_ax_dict["color_legend_ax"] = color_legend_ax + + def _mainplot(self, ax: Axes): + # work on a copy of the dataframes. This is to avoid changes + # on the original data frames after repetitive calls to the + # DotPlot object, for example once with swap_axes and other without + + _color_df = self.dot_color_df.copy() + _size_df = self.dot_size_df.copy() + if self.var_names_idx_order is not None: + _color_df = _color_df.iloc[:, self.var_names_idx_order] + _size_df = _size_df.iloc[:, self.var_names_idx_order] + + if self.categories_order is not None: + _color_df = _color_df.loc[self.categories_order, :] + _size_df = _size_df.loc[self.categories_order, :] + + if self.are_axes_swapped: + _size_df = _size_df.T + _color_df = _color_df.T + self.cmap = self.kwds.pop("cmap", self.cmap) + + normalize, dot_min, dot_max = self._dotplot( + _size_df, + _color_df, + ax, + cmap=self.cmap, + color_on=self.color_on, + dot_max=self.dot_max, + dot_min=self.dot_min, + standard_scale=self.standard_scale, + edge_color=self.dot_edge_color, + edge_lw=self.dot_edge_lw, + smallest_dot=self.smallest_dot, + largest_dot=self.largest_dot, + size_exponent=self.size_exponent, + grid=self.grid, + x_padding=self.plot_x_padding, + y_padding=self.plot_y_padding, + vmin=self.vboundnorm.vmin, + vmax=self.vboundnorm.vmax, + vcenter=self.vboundnorm.vcenter, + norm=self.vboundnorm.norm, + **self.kwds, + ) + + self.dot_min, self.dot_max = dot_min, dot_max + return normalize + + @staticmethod + def _dotplot( # noqa: PLR0912, PLR0913, PLR0915 + dot_size: pd.DataFrame, + dot_color: pd.DataFrame, + dot_ax: Axes, + *, + cmap: Colormap | str | None, + color_on: Literal["dot", "square"], + dot_max: float | None, + dot_min: float | None, + standard_scale: Literal["var", "group"] | None, + smallest_dot: float, + largest_dot: float, + size_exponent: float, + edge_color: ColorLike | None, + edge_lw: float | None, + grid: bool, + x_padding: float, + y_padding: float, + vmin: float | None, + vmax: float | None, + vcenter: float | None, + norm: Normalize | None, + **kwds, + ): + """Make a *dot plot* given two data frames. + + One containing the dot size and other containing the dot color. + The indices and columns of the data frame are used to label the output image. + + The dots are plotted using :func:`matplotlib.pyplot.scatter`. Thus, additional + arguments can be passed. + + Parameters + ---------- + dot_size + Data frame containing the dot_size. + dot_color + Data frame containing the dot_color, should have the same, + shape, columns and indices as dot_size. + dot_ax + matplotlib axis + cmap + color_on + dot_max + dot_min + standard_scale + smallest_dot + edge_color + edge_lw + grid + x_padding + y_padding + See `style` + kwds + Are passed to :func:`matplotlib.pyplot.scatter`. + + Returns + ------- + matplotlib.colors.Normalize, dot_min, dot_max + + """ + assert dot_size.shape == dot_color.shape, ( + "please check that dot_size and dot_color dataframes have the same shape" + ) + + assert list(dot_size.index) == list(dot_color.index), ( + "please check that dot_size and dot_color dataframes have the same index" + ) + + assert list(dot_size.columns) == list(dot_color.columns), ( + "please check that the dot_size " + "and dot_color dataframes have the same columns" + ) + + if standard_scale == "group": + dot_color = dot_color.sub(dot_color.min(1), axis=0) + dot_color = dot_color.div(dot_color.max(1), axis=0).fillna(0) + elif standard_scale == "var": + dot_color -= dot_color.min(0) + dot_color = (dot_color / dot_color.max(0)).fillna(0) + elif standard_scale is None: + pass + + # make scatter plot in which + # x = var_names + # y = groupby category + # size = fraction + # color = mean expression + + # +0.5 in y and x to set the dot center at 0.5 multiples + # this facilitates dendrogram and totals alignment for + # matrixplot, dotplot and stackec_violin using the same coordinates. + y, x = np.indices(dot_color.shape) + y = y.flatten() + 0.5 + x = x.flatten() + 0.5 + frac = dot_size.values.flatten() + mean_flat = dot_color.values.flatten() + cmap = colormaps.get_cmap(cmap) + if dot_max is None: + dot_max = np.ceil(max(frac) * 10) / 10 + elif dot_max < 0 or dot_max > 1: + msg = "`dot_max` value has to be between 0 and 1" + raise ValueError(msg) + if dot_min is None: + dot_min = 0 + elif dot_min < 0 or dot_min > 1: + msg = "`dot_min` value has to be between 0 and 1" + raise ValueError(msg) + + if dot_min != 0 or dot_max != 1: + # clip frac between dot_min and dot_max + frac = np.clip(frac, dot_min, dot_max) + old_range = dot_max - dot_min + # re-scale frac between 0 and 1 + frac = (frac - dot_min) / old_range + + size = frac**size_exponent + # rescale size to match smallest_dot and largest_dot + size = size * (largest_dot - smallest_dot) + smallest_dot + normalize = check_colornorm(vmin, vmax, vcenter, norm) + + if color_on == "square": + if edge_color is None: + from seaborn.utils import relative_luminance + + # use either black or white for the edge color + # depending on the luminance of the background + # square color + edge_color = [] + for color_value in cmap(normalize(mean_flat)): + lum = relative_luminance(color_value) + edge_color.append(".15" if lum > 0.408 else "w") + + edge_lw = 1.5 if edge_lw is None else edge_lw + + # first make a heatmap similar to `sc.pl.matrixplot` + # (squares with the asigned colormap). Circles will be plotted + # on top + dot_ax.pcolor(dot_color.values, cmap=cmap, norm=normalize) + for axis in ["top", "bottom", "left", "right"]: + dot_ax.spines[axis].set_linewidth(1.5) + kwds = fix_kwds( + kwds, + s=size, + linewidth=edge_lw, + facecolor="none", + edgecolor=edge_color, + ) + dot_ax.scatter(x, y, **kwds) + else: + edge_color = "none" if edge_color is None else edge_color + edge_lw = 0.0 if edge_lw is None else edge_lw + + color = cmap(normalize(mean_flat)) + kwds = fix_kwds( + kwds, + s=size, + color=color, + linewidth=edge_lw, + edgecolor=edge_color, + ) + dot_ax.scatter(x, y, **kwds) + + y_ticks = np.arange(dot_color.shape[0]) + 0.5 + dot_ax.set_yticks(y_ticks) + dot_ax.set_yticklabels( + [dot_color.index[idx] for idx, _ in enumerate(y_ticks)], minor=False + ) + + x_ticks = np.arange(dot_color.shape[1]) + 0.5 + dot_ax.set_xticks(x_ticks) + dot_ax.set_xticklabels( + [dot_color.columns[idx] for idx, _ in enumerate(x_ticks)], + rotation=90, + ha="center", + minor=False, + ) + dot_ax.tick_params(axis="both", labelsize="small") + dot_ax.grid(visible=False) + + # to be consistent with the heatmap plot, is better to + # invert the order of the y-axis, such that the first group is on + # top + dot_ax.set_ylim(dot_color.shape[0], 0) + dot_ax.set_xlim(0, dot_color.shape[1]) + + if color_on == "dot": + # add padding to the x and y lims when the color is not in the square + # default y range goes from 0.5 to num cols + 0.5 + # and default x range goes from 0.5 to num rows + 0.5, thus + # the padding needs to be corrected. + x_padding = x_padding - 0.5 + y_padding = y_padding - 0.5 + dot_ax.set_ylim(dot_color.shape[0] + y_padding, -y_padding) + + dot_ax.set_xlim(-x_padding, dot_color.shape[1] + x_padding) + + if grid: + dot_ax.grid(visible=True, color="gray", linewidth=0.1) + dot_ax.set_axisbelow(True) + + return normalize, dot_min, dot_max + + +@old_positionals( + "use_raw", + "log", + "num_categories", + "expression_cutoff", + "mean_only_expressed", + "cmap", + "dot_max", + "dot_min", + "standard_scale", + "smallest_dot", + "title", + "colorbar_title", + "size_title", + # No need to have backwards compat for > 16 positional parameters +) +@_doc_params( + show_save_ax=doc_show_save_ax, + common_plot_args=doc_common_plot_args, + groupby_plots_args=doc_common_groupby_plot_args, + vminmax=doc_vboundnorm, +) +def dotplot( # noqa: PLR0913 + adata: AnnData, + var_names: _VarNames | Mapping[str, _VarNames], + groupby: str | Sequence[str], + *, + use_raw: bool | None = None, + log: bool = False, + num_categories: int = 7, + categories_order: Sequence[str] | None = None, + expression_cutoff: float = 0.0, + mean_only_expressed: bool = False, + standard_scale: Literal["var", "group"] | None = None, + title: str | None = None, + colorbar_title: str | None = DotPlot.DEFAULT_COLOR_LEGEND_TITLE, + size_title: str | None = DotPlot.DEFAULT_SIZE_LEGEND_TITLE, + figsize: tuple[float, float] | None = None, + dendrogram: bool | str = False, + gene_symbols: str | None = None, + var_group_positions: Sequence[tuple[int, int]] | None = None, + var_group_labels: Sequence[str] | None = None, + var_group_rotation: float | None = None, + layer: str | None = None, + swap_axes: bool | None = False, + dot_color_df: pd.DataFrame | None = None, + show: bool | None = None, + save: str | bool | None = None, + ax: _AxesSubplot | None = None, + return_fig: bool | None = False, + vmin: float | None = None, + vmax: float | None = None, + vcenter: float | None = None, + norm: Normalize | None = None, + # Style parameters + cmap: Colormap | str | None = DotPlot.DEFAULT_COLORMAP, + dot_max: float | None = DotPlot.DEFAULT_DOT_MAX, + dot_min: float | None = DotPlot.DEFAULT_DOT_MIN, + smallest_dot: float = DotPlot.DEFAULT_SMALLEST_DOT, + **kwds, +) -> DotPlot | dict | None: + r"""Make a *dot plot* of the expression values of `var_names`. + + For each var_name and each `groupby` category a dot is plotted. + Each dot represents two values: mean expression within each category + (visualized by color) and fraction of cells expressing the `var_name` in the + category (visualized by the size of the dot). If `groupby` is not given, + the dotplot assumes that all data belongs to a single category. + + .. note:: + A gene is considered expressed if the expression value in the `adata` (or + `adata.raw`) is above the specified threshold which is zero by default. + + An example of dotplot usage is to visualize, for multiple marker genes, + the mean value and the percentage of cells expressing the gene + across multiple clusters. + + This function provides a convenient interface to the :class:`~scanpy.pl.DotPlot` + class. If you need more flexibility, you should use :class:`~scanpy.pl.DotPlot` + directly. + + Parameters + ---------- + {common_plot_args} + {groupby_plots_args} + size_title + Title for the size legend. New line character (\n) can be used. + expression_cutoff + Expression cutoff that is used for binarizing the gene expression and + determining the fraction of cells expressing given genes. A gene is + expressed only if the expression value is greater than this threshold. + mean_only_expressed + If True, gene expression is averaged only over the cells + expressing the given genes. + dot_max + If ``None``, the maximum dot size is set to the maximum fraction value found + (e.g. 0.6). If given, the value should be a number between 0 and 1. + All fractions larger than dot_max are clipped to this value. + dot_min + If ``None``, the minimum dot size is set to 0. If given, + the value should be a number between 0 and 1. + All fractions smaller than dot_min are clipped to this value. + smallest_dot + All expression levels with `dot_min` are plotted with this size. + {show_save_ax} + {vminmax} + kwds + Are passed to :func:`matplotlib.pyplot.scatter`. + + Returns + ------- + If `return_fig` is `True`, returns a :class:`~scanpy.pl.DotPlot` object, + else if `show` is false, return axes dict + + See Also + -------- + :class:`~scanpy.pl.DotPlot`: The DotPlot class can be used to to control + several visual parameters not available in this function. + :func:`~scanpy.pl.rank_genes_groups_dotplot`: to plot marker genes + identified using the :func:`~scanpy.tl.rank_genes_groups` function. + + Examples + -------- + Create a dot plot using the given markers and the PBMC example dataset grouped by + the category 'bulk_labels'. + + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc68k_reduced() + markers = ['C1QA', 'PSAP', 'CD79A', 'CD79B', 'CST3', 'LYZ'] + sc.pl.dotplot(adata, markers, groupby='bulk_labels', dendrogram=True) + + Using var_names as dict: + + .. plot:: + :context: close-figs + + markers = {{'T-cell': 'CD3D', 'B-cell': 'CD79A', 'myeloid': 'CST3'}} + sc.pl.dotplot(adata, markers, groupby='bulk_labels', dendrogram=True) + + Get DotPlot object for fine tuning + + .. plot:: + :context: close-figs + + dp = sc.pl.dotplot(adata, markers, 'bulk_labels', return_fig=True) + dp.add_totals().style(dot_edge_color='black', dot_edge_lw=0.5).show() + + The axes used can be obtained using the get_axes() method + + .. code-block:: python + + axes_dict = dp.get_axes() + print(axes_dict) + + """ + # backwards compatibility: previous version of dotplot used `color_map` + # instead of `cmap` + cmap = kwds.pop("color_map", cmap) + + dp = DotPlot( + adata, + var_names, + groupby, + use_raw=use_raw, + log=log, + num_categories=num_categories, + categories_order=categories_order, + expression_cutoff=expression_cutoff, + mean_only_expressed=mean_only_expressed, + standard_scale=standard_scale, + title=title, + figsize=figsize, + gene_symbols=gene_symbols, + var_group_positions=var_group_positions, + var_group_labels=var_group_labels, + var_group_rotation=var_group_rotation, + layer=layer, + dot_color_df=dot_color_df, + ax=ax, + vmin=vmin, + vmax=vmax, + vcenter=vcenter, + norm=norm, + **kwds, + ) + + if dendrogram: + dp.add_dendrogram(dendrogram_key=_dk(dendrogram)) + if swap_axes: + dp.swap_axes() + + dp = dp.style( + cmap=cmap, + dot_max=dot_max, + dot_min=dot_min, + smallest_dot=smallest_dot, + dot_edge_lw=kwds.pop("linewidth", _empty), + ).legend(colorbar_title=colorbar_title, size_title=size_title) + + if return_fig: + return dp + else: + dp.make_figure() + savefig_or_show(DotPlot.DEFAULT_SAVE_PREFIX, show=show, save=save) + show = settings.autoshow if show is None else show + if not show: + return dp.get_axes() diff --git a/.venv/lib/python3.11/site-packages/scanpy/plotting/_matrixplot.py b/.venv/lib/python3.11/site-packages/scanpy/plotting/_matrixplot.py new file mode 100644 index 0000000000000000000000000000000000000000..e767e224cce44d8ce4ed08f0e926f0c94f32e6a2 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/plotting/_matrixplot.py @@ -0,0 +1,469 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +from matplotlib import colormaps, rcParams + +from .. import logging as logg +from .._compat import old_positionals +from .._settings import settings +from .._utils import _doc_params, _empty +from ._baseplot_class import BasePlot, doc_common_groupby_plot_args +from ._docs import ( + doc_common_plot_args, + doc_show_save_ax, + doc_vboundnorm, +) +from ._utils import _dk, check_colornorm, fix_kwds, savefig_or_show + +if TYPE_CHECKING: + from collections.abc import Mapping, Sequence + from typing import Literal, Self + + import pandas as pd + from anndata import AnnData + from matplotlib.axes import Axes + from matplotlib.colors import Colormap, Normalize + + from .._utils import Empty + from ._baseplot_class import _VarNames + from ._utils import ColorLike, _AxesSubplot + + +@_doc_params(common_plot_args=doc_common_plot_args) +class MatrixPlot(BasePlot): + """Allows the visualization of values using a color map. + + Parameters + ---------- + {common_plot_args} + title + Title for the figure. + expression_cutoff + Expression cutoff that is used for binarizing the gene expression and + determining the fraction of cells expressing given genes. A gene is + expressed only if the expression value is greater than this threshold. + mean_only_expressed + If True, gene expression is averaged only over the cells + expressing the given genes. + standard_scale + Whether or not to standardize that dimension between 0 and 1, + meaning for each variable or group, + subtract the minimum and divide each by its maximum. + values_df + Optionally, a dataframe with the values to plot can be given. The + index should be the grouby categories and the columns the genes names. + + kwds + Are passed to :func:`matplotlib.pyplot.scatter`. + + See Also + -------- + :func:`~scanpy.pl.matrixplot`: Simpler way to call MatrixPlot but with less options. + :func:`~scanpy.pl.rank_genes_groups_matrixplot`: to plot marker genes identified + using the :func:`~scanpy.tl.rank_genes_groups` function. + + Examples + -------- + Simple visualization of the average expression of a few genes grouped by + the category 'bulk_labels'. + + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc68k_reduced() + markers = ['C1QA', 'PSAP', 'CD79A', 'CD79B', 'CST3', 'LYZ'] + sc.pl.MatrixPlot(adata, markers, groupby='bulk_labels').show() + + Same visualization but passing var_names as dict, which adds a grouping of + the genes on top of the image: + + .. plot:: + :context: close-figs + + markers = {{'T-cell': 'CD3D', 'B-cell': 'CD79A', 'myeloid': 'CST3'}} + sc.pl.MatrixPlot(adata, markers, groupby='bulk_labels').show() + + """ + + DEFAULT_SAVE_PREFIX = "matrixplot_" + DEFAULT_COLOR_LEGEND_TITLE = "Mean expression\nin group" + + # default style parameters + DEFAULT_COLORMAP = rcParams["image.cmap"] + DEFAULT_EDGE_COLOR = "gray" + DEFAULT_EDGE_LW = 0.1 + + @old_positionals( + "use_raw", + "log", + "num_categories", + "categories_order", + "title", + "figsize", + "gene_symbols", + "var_group_positions", + "var_group_labels", + "var_group_rotation", + "layer", + "standard_scale", + "ax", + "values_df", + "vmin", + "vmax", + "vcenter", + "norm", + ) + def __init__( # noqa: PLR0913 + self, + adata: AnnData, + var_names: _VarNames | Mapping[str, _VarNames], + groupby: str | Sequence[str], + *, + use_raw: bool | None = None, + log: bool = False, + num_categories: int = 7, + categories_order: Sequence[str] | None = None, + title: str | None = None, + figsize: tuple[float, float] | None = None, + gene_symbols: str | None = None, + var_group_positions: Sequence[tuple[int, int]] | None = None, + var_group_labels: Sequence[str] | None = None, + var_group_rotation: float | None = None, + layer: str | None = None, + standard_scale: Literal["var", "group"] | None = None, + ax: _AxesSubplot | None = None, + values_df: pd.DataFrame | None = None, + vmin: float | None = None, + vmax: float | None = None, + vcenter: float | None = None, + norm: Normalize | None = None, + **kwds, + ): + BasePlot.__init__( + self, + adata, + var_names, + groupby, + use_raw=use_raw, + log=log, + num_categories=num_categories, + categories_order=categories_order, + title=title, + figsize=figsize, + gene_symbols=gene_symbols, + var_group_positions=var_group_positions, + var_group_labels=var_group_labels, + var_group_rotation=var_group_rotation, + layer=layer, + ax=ax, + vmin=vmin, + vmax=vmax, + vcenter=vcenter, + norm=norm, + **kwds, + ) + + if values_df is None: + # compute mean value + values_df = ( + self.obs_tidy.groupby(level=0, observed=True) + .mean() + .loc[ + self.categories_order + if self.categories_order is not None + else self.categories + ] + ) + + if standard_scale == "group": + values_df = values_df.sub(values_df.min(1), axis=0) + values_df = values_df.div(values_df.max(1), axis=0).fillna(0) + elif standard_scale == "var": + values_df -= values_df.min(0) + values_df = (values_df / values_df.max(0)).fillna(0) + elif standard_scale is None: + pass + else: + logg.warning("Unknown type for standard_scale, ignored") + + self.values_df = values_df + + self.cmap = self.DEFAULT_COLORMAP + self.edge_color = self.DEFAULT_EDGE_COLOR + self.edge_lw = self.DEFAULT_EDGE_LW + + def style( + self, + cmap: Colormap | str | None | Empty = _empty, + edge_color: ColorLike | None | Empty = _empty, + edge_lw: float | None | Empty = _empty, + ) -> Self: + r"""Modify plot visual parameters. + + Parameters + ---------- + cmap + Matplotlib color map, specified by name or directly. + If ``None``, use :obj:`matplotlib.rcParams`\ ``["image.cmap"]`` + edge_color + Edge color between the squares of matrix plot. + If ``None``, use :obj:`matplotlib.rcParams`\ ``["patch.edgecolor"]`` + edge_lw + Edge line width. + If ``None``, use :obj:`matplotlib.rcParams`\ ``["lines.linewidth"]`` + + Returns + ------- + :class:`~scanpy.pl.MatrixPlot` + + Examples + -------- + + .. plot:: + :context: close-figs + + import scanpy as sc + + adata = sc.datasets.pbmc68k_reduced() + markers = ['C1QA', 'PSAP', 'CD79A', 'CD79B', 'CST3', 'LYZ'] + + Change color map and turn off edges: + + + .. plot:: + :context: close-figs + + ( + sc.pl.MatrixPlot(adata, markers, groupby='bulk_labels') + .style(cmap='Blues', edge_color='none') + .show() + ) + + """ + super().style(cmap=cmap) + + if edge_color is not _empty: + self.edge_color = edge_color + if edge_lw is not _empty: + self.edge_lw = edge_lw + + return self + + def _mainplot(self, ax: Axes): + # work on a copy of the dataframes. This is to avoid changes + # on the original data frames after repetitive calls to the + # MatrixPlot object, for example once with swap_axes and other without + + _color_df = self.values_df.copy() + if self.var_names_idx_order is not None: + _color_df = _color_df.iloc[:, self.var_names_idx_order] + + if self.categories_order is not None: + _color_df = _color_df.loc[self.categories_order, :] + + if self.are_axes_swapped: + _color_df = _color_df.T + cmap = colormaps.get_cmap(self.kwds.get("cmap", self.cmap)) + if "cmap" in self.kwds: + del self.kwds["cmap"] + normalize = check_colornorm( + self.vboundnorm.vmin, + self.vboundnorm.vmax, + self.vboundnorm.vcenter, + self.vboundnorm.norm, + ) + + for axis in ["top", "bottom", "left", "right"]: + ax.spines[axis].set_linewidth(1.5) + + kwds = fix_kwds( + self.kwds, + cmap=cmap, + edgecolor=self.edge_color, + linewidth=self.edge_lw, + norm=normalize, + ) + _ = ax.pcolor(_color_df, **kwds) + + y_labels = _color_df.index + x_labels = _color_df.columns + + y_ticks = np.arange(len(y_labels)) + 0.5 + ax.set_yticks(y_ticks) + ax.set_yticklabels(y_labels) + + x_ticks = np.arange(len(x_labels)) + 0.5 + ax.set_xticks(x_ticks) + ax.set_xticklabels(x_labels, rotation=90, ha="center", minor=False) + + ax.tick_params(axis="both", labelsize="small") + ax.grid(visible=False) + + # to be consistent with the heatmap plot, is better to + # invert the order of the y-axis, such that the first group is on + # top + ax.set_ylim(len(y_labels), 0) + ax.set_xlim(0, len(x_labels)) + + return normalize + + +@old_positionals( + "use_raw", + "log", + "num_categories", + "figsize", + "dendrogram", + "title", + "cmap", + "colorbar_title", + "gene_symbols", + "var_group_positions", + "var_group_labels", + "var_group_rotation", + "layer", + "standard_scale", + # 17 positionals are enough for backwards compatibility +) +@_doc_params( + show_save_ax=doc_show_save_ax, + common_plot_args=doc_common_plot_args, + groupby_plots_args=doc_common_groupby_plot_args, + vminmax=doc_vboundnorm, +) +def matrixplot( # noqa: PLR0913 + adata: AnnData, + var_names: _VarNames | Mapping[str, _VarNames], + groupby: str | Sequence[str], + *, + use_raw: bool | None = None, + log: bool = False, + num_categories: int = 7, + categories_order: Sequence[str] | None = None, + figsize: tuple[float, float] | None = None, + dendrogram: bool | str = False, + title: str | None = None, + cmap: Colormap | str | None = MatrixPlot.DEFAULT_COLORMAP, + colorbar_title: str | None = MatrixPlot.DEFAULT_COLOR_LEGEND_TITLE, + gene_symbols: str | None = None, + var_group_positions: Sequence[tuple[int, int]] | None = None, + var_group_labels: Sequence[str] | None = None, + var_group_rotation: float | None = None, + layer: str | None = None, + standard_scale: Literal["var", "group"] | None = None, + values_df: pd.DataFrame | None = None, + swap_axes: bool = False, + show: bool | None = None, + save: str | bool | None = None, + ax: _AxesSubplot | None = None, + return_fig: bool | None = False, + vmin: float | None = None, + vmax: float | None = None, + vcenter: float | None = None, + norm: Normalize | None = None, + **kwds, +) -> MatrixPlot | dict[str, Axes] | None: + """Create a heatmap of the mean expression values per group of each var_names. + + This function provides a convenient interface to the :class:`~scanpy.pl.MatrixPlot` + class. If you need more flexibility, you should use :class:`~scanpy.pl.MatrixPlot` + directly. + + Parameters + ---------- + {common_plot_args} + {groupby_plots_args} + {show_save_ax} + {vminmax} + kwds + Are passed to :func:`matplotlib.pyplot.pcolor`. + + Returns + ------- + If `return_fig` is `True`, returns a :class:`~scanpy.pl.MatrixPlot` object, + else if `show` is false, return axes dict + + See Also + -------- + :class:`~scanpy.pl.MatrixPlot`: The MatrixPlot class can be used to to control + several visual parameters not available in this function. + :func:`~scanpy.pl.rank_genes_groups_matrixplot`: to plot marker genes + identified using the :func:`~scanpy.tl.rank_genes_groups` function. + + Examples + -------- + + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc68k_reduced() + markers = ['C1QA', 'PSAP', 'CD79A', 'CD79B', 'CST3', 'LYZ'] + sc.pl.matrixplot(adata, markers, groupby='bulk_labels', dendrogram=True) + + Using var_names as dict: + + .. plot:: + :context: close-figs + + markers = {{'T-cell': 'CD3D', 'B-cell': 'CD79A', 'myeloid': 'CST3'}} + sc.pl.matrixplot(adata, markers, groupby='bulk_labels', dendrogram=True) + + Get Matrix object for fine tuning: + + .. plot:: + :context: close-figs + + mp = sc.pl.matrixplot(adata, markers, 'bulk_labels', return_fig=True) + mp.add_totals().style(edge_color='black').show() + + The axes used can be obtained using the get_axes() method + + .. plot:: + :context: close-figs + + axes_dict = mp.get_axes() + + """ + mp = MatrixPlot( + adata, + var_names, + groupby=groupby, + use_raw=use_raw, + log=log, + num_categories=num_categories, + categories_order=categories_order, + standard_scale=standard_scale, + title=title, + figsize=figsize, + gene_symbols=gene_symbols, + var_group_positions=var_group_positions, + var_group_labels=var_group_labels, + var_group_rotation=var_group_rotation, + layer=layer, + values_df=values_df, + ax=ax, + vmin=vmin, + vmax=vmax, + vcenter=vcenter, + norm=norm, + **kwds, + ) + + if dendrogram: + mp.add_dendrogram(dendrogram_key=_dk(dendrogram)) + if swap_axes: + mp.swap_axes() + + mp = mp.style(cmap=cmap).legend(title=colorbar_title) + if return_fig: + return mp + mp.make_figure() + savefig_or_show(MatrixPlot.DEFAULT_SAVE_PREFIX, show=show, save=save) + show = settings.autoshow if show is None else show + if show: + return None + return mp.get_axes() diff --git a/.venv/lib/python3.11/site-packages/scanpy/plotting/_preprocessing.py b/.venv/lib/python3.11/site-packages/scanpy/plotting/_preprocessing.py new file mode 100644 index 0000000000000000000000000000000000000000..fdcdb8ae71bcae1e0f798733d5c6f9db409f4125 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/plotting/_preprocessing.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +import numpy as np +import pandas as pd +from anndata import AnnData +from matplotlib import pyplot as plt +from matplotlib import rcParams + +from .._compat import deprecated, old_positionals +from .._settings import settings +from ._utils import savefig_or_show + +# -------------------------------------------------------------------------------- +# Plot result of preprocessing functions +# -------------------------------------------------------------------------------- + + +@old_positionals("log", "show", "save", "highly_variable_genes") +def highly_variable_genes( # noqa: PLR0912 + adata_or_result: AnnData | pd.DataFrame | np.recarray, + *, + log: bool = False, + show: bool | None = None, + save: bool | str | None = None, + highly_variable_genes: bool = True, +) -> None: + """Plot dispersions or normalized variance versus means for genes. + + Produces Supp. Fig. 5c of Zheng et al. (2017) and MeanVarPlot() and + VariableFeaturePlot() of Seurat. + + Parameters + ---------- + adata + Result of :func:`~scanpy.pp.highly_variable_genes`. + log + Plot on logarithmic axes. + show + Show the plot, do not return axis. + save + If `True` or a `str`, save the figure. + A string is appended to the default filename. + Infer the filetype if ending on {{`'.pdf'`, `'.png'`, `'.svg'`}}. + + """ + if isinstance(adata_or_result, AnnData): + result = adata_or_result.var + seurat_v3_flavor = adata_or_result.uns["hvg"]["flavor"] == "seurat_v3" + else: + result = adata_or_result + if isinstance(result, pd.DataFrame): + seurat_v3_flavor = "variances_norm" in result.columns + else: + seurat_v3_flavor = False + if highly_variable_genes: + gene_subset = result.highly_variable + else: + gene_subset = result.gene_subset + means = result.means + + if seurat_v3_flavor: + var_or_disp = result.variances + var_or_disp_norm = result.variances_norm + else: + var_or_disp = result.dispersions + var_or_disp_norm = result.dispersions_norm + size = rcParams["figure.figsize"] + plt.figure(figsize=(2 * size[0], size[1])) + plt.subplots_adjust(wspace=0.3) + for idx, d in enumerate([var_or_disp_norm, var_or_disp]): + plt.subplot(1, 2, idx + 1) + for label, color, mask in zip( + ["highly variable genes", "other genes"], + ["black", "grey"], + [gene_subset, ~gene_subset], + strict=True, + ): + if False: + means_, var_or_disps_ = np.log10(means[mask]), np.log10(d[mask]) + else: + means_, var_or_disps_ = means[mask], d[mask] + plt.scatter(means_, var_or_disps_, label=label, c=color, s=1) + if log: # there's a bug in autoscale + plt.xscale("log") + plt.yscale("log") + y_min = np.min(var_or_disp) + y_min = 0.95 * y_min if y_min > 0 else 1e-1 + plt.xlim(0.95 * np.min(means), 1.05 * np.max(means)) + plt.ylim(y_min, 1.05 * np.max(var_or_disp)) + if idx == 0: + plt.legend() + plt.xlabel(f"{'$log_{10}$ ' if False else ''}mean expressions of genes") + data_type = "dispersions" if not seurat_v3_flavor else "variances" + plt.ylabel( + f"{'$log_{10}$ ' if False else ''}{data_type} " + f"of genes ({'' if idx == 0 else 'not '}normalized)" + ) + + show = settings.autoshow if show is None else show + savefig_or_show("filter_genes_dispersion", show=show, save=save) + if show: + return None + return plt.gca() + + +# backwards compat +@deprecated("Use sc.pl.highly_variable_genes instead") +@old_positionals("log", "show", "save") +def filter_genes_dispersion( + result: np.recarray, + *, + log: bool = False, + show: bool | None = None, + save: bool | str | None = None, +) -> None: + """Plot dispersions versus means for genes. + + Produces Supp. Fig. 5c of Zheng et al. (2017) and MeanVarPlot() of Seurat. + + Parameters + ---------- + result + Result of :func:`~scanpy.pp.filter_genes_dispersion`. + log + Plot on logarithmic axes. + show + Show the plot, do not return axis. + save + If `True` or a `str`, save the figure. + A string is appended to the default filename. + Infer the filetype if ending on {{`'.pdf'`, `'.png'`, `'.svg'`}}. + + """ + highly_variable_genes( + result, log=log, show=show, save=save, highly_variable_genes=False + ) diff --git a/.venv/lib/python3.11/site-packages/scanpy/plotting/_qc.py b/.venv/lib/python3.11/site-packages/scanpy/plotting/_qc.py new file mode 100644 index 0000000000000000000000000000000000000000..439b22a88d98077a72c8fcf756a673e90288d5b0 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/plotting/_qc.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd +from matplotlib import pyplot as plt + +from .._compat import CSBase, old_positionals +from .._settings import settings +from .._utils import _doc_params +from ..preprocessing._normalization import normalize_total +from ._docs import doc_show_save_ax +from ._utils import savefig_or_show + +if TYPE_CHECKING: + from anndata import AnnData + from matplotlib.axes import Axes + + +@old_positionals("show", "save", "ax", "gene_symbols", "log") +@_doc_params(show_save_ax=doc_show_save_ax) +def highest_expr_genes( + adata: AnnData, + n_top: int = 30, + *, + layer: str | None = None, + gene_symbols: str | None = None, + log: bool = False, + show: bool | None = None, + save: str | bool | None = None, + ax: Axes | None = None, + **kwds, +): + """Fraction of counts assigned to each gene over all cells. + + Computes, for each gene, the fraction of counts assigned to that gene within + a cell. The `n_top` genes with the highest mean fraction over all cells are + plotted as boxplots. + + This plot is similar to the `scater` package function `plotHighestExprs(type + = "highest-expression")`, see `here + `__. Quoting + from there: + + *We expect to see the “usual suspects”, i.e., mitochondrial genes, actin, + ribosomal protein, MALAT1. A few spike-in transcripts may also be + present here, though if all of the spike-ins are in the top 50, it + suggests that too much spike-in RNA was added. A large number of + pseudo-genes or predicted genes may indicate problems with alignment.* + -- Davis McCarthy and Aaron Lun + + Parameters + ---------- + adata + Annotated data matrix. + n_top + Number of top + layer + Layer from which to pull data. + gene_symbols + Key for field in .var that stores gene symbols if you do not want to use .var_names. + log + Plot x-axis in log scale + {show_save_ax} + **kwds + Are passed to :func:`~seaborn.boxplot`. + + Returns + ------- + If `show==False` a :class:`~matplotlib.axes.Axes`. + + """ + import seaborn as sns # Slow import, only import if called + + # compute the percentage of each gene per cell + norm_dict = normalize_total(adata, target_sum=100, layer=layer, inplace=False) + + # identify the genes with the highest mean + if isinstance(norm_dict["X"], CSBase): + mean_percent = norm_dict["X"].mean(axis=0).A1 + top_idx = np.argsort(mean_percent)[::-1][:n_top] + counts_top_genes = norm_dict["X"][:, top_idx].toarray() + else: + mean_percent = norm_dict["X"].mean(axis=0) + top_idx = np.argsort(mean_percent)[::-1][:n_top] + counts_top_genes = norm_dict["X"][:, top_idx] + columns = ( + adata.var_names[top_idx] + if gene_symbols is None + else adata.var[gene_symbols].iloc[top_idx].astype("string") + ) + counts_top_genes = pd.DataFrame( + counts_top_genes, index=adata.obs_names, columns=columns + ) + + if not ax: + # figsize is hardcoded to produce a tall image. To change the fig size, + # a matplotlib.axes.Axes object needs to be passed. + height = (n_top * 0.2) + 1.5 + _fig, ax = plt.subplots(figsize=(5, height)) + sns.boxplot(data=counts_top_genes, orient="h", ax=ax, fliersize=1, **kwds) + ax.set_xlabel("% of total counts") + if log: + ax.set_xscale("log") + show = settings.autoshow if show is None else show + savefig_or_show("highest_expr_genes", show=show, save=save) + if show: + return None + return ax diff --git a/.venv/lib/python3.11/site-packages/scanpy/plotting/_rcmod.py b/.venv/lib/python3.11/site-packages/scanpy/plotting/_rcmod.py new file mode 100644 index 0000000000000000000000000000000000000000..62101ab626fbe72d0f76151315711dd5b86ef91e --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/plotting/_rcmod.py @@ -0,0 +1,71 @@ +"""Set the default matplotlib.rcParams.""" + +from __future__ import annotations + +import matplotlib as mpl +from cycler import cycler +from matplotlib import rcParams + +from . import palettes + + +def set_rcParams_scanpy(fontsize=14, color_map=None) -> None: # noqa: N802 + """Set matplotlib.rcParams to Scanpy defaults. + + Call this through :func:`scanpy.set_figure_params`. + """ + # figure + rcParams["figure.figsize"] = (4, 4) + rcParams["figure.subplot.left"] = 0.18 + rcParams["figure.subplot.right"] = 0.96 + rcParams["figure.subplot.bottom"] = 0.15 + rcParams["figure.subplot.top"] = 0.91 + + rcParams["lines.linewidth"] = 1.5 # the line width of the frame + rcParams["lines.markersize"] = 6 + rcParams["lines.markeredgewidth"] = 1 + + # font + rcParams["font.sans-serif"] = [ + "Arial", + "Helvetica", + "DejaVu Sans", + "Bitstream Vera Sans", + "sans-serif", + ] + rcParams["font.size"] = fontsize + rcParams["legend.fontsize"] = 0.92 * fontsize + rcParams["axes.titlesize"] = fontsize + rcParams["axes.labelsize"] = fontsize + + # legend + rcParams["legend.numpoints"] = 1 + rcParams["legend.scatterpoints"] = 1 + rcParams["legend.handlelength"] = 0.5 + rcParams["legend.handletextpad"] = 0.4 + + # color cycle + rcParams["axes.prop_cycle"] = cycler(color=palettes.default_20) + + # lines + rcParams["axes.linewidth"] = 0.8 + rcParams["axes.edgecolor"] = "black" + rcParams["axes.facecolor"] = "white" + + # ticks + rcParams["xtick.color"] = "k" + rcParams["ytick.color"] = "k" + rcParams["xtick.labelsize"] = fontsize + rcParams["ytick.labelsize"] = fontsize + + # axes grid + rcParams["axes.grid"] = True + rcParams["grid.color"] = ".8" + + # color map + rcParams["image.cmap"] = rcParams["image.cmap"] if color_map is None else color_map + + +def set_rcParams_defaults() -> None: # noqa: N802 + """Reset `matplotlib.rcParams` to defaults.""" + rcParams.update(mpl.rcParamsDefault) diff --git a/.venv/lib/python3.11/site-packages/scanpy/plotting/_scrublet.py b/.venv/lib/python3.11/site-packages/scanpy/plotting/_scrublet.py new file mode 100644 index 0000000000000000000000000000000000000000..1413a5711d8ecdd0420128d05ec5f1b9c178084a --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/plotting/_scrublet.py @@ -0,0 +1,164 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd +from matplotlib import pyplot as plt + +from .._compat import old_positionals +from ._utils import savefig_or_show + +if TYPE_CHECKING: + from collections.abc import Sequence + from typing import Literal + + from anndata import AnnData + from matplotlib.axes import Axes + from matplotlib.figure import Figure + + Scale = Literal["linear", "log", "symlog", "logit"] | str + + +@old_positionals( + "scale_hist_obs", "scale_hist_sim", "figsize", "return_fig", "show", "save" +) +def scrublet_score_distribution( + adata: AnnData, + *, + scale_hist_obs: Scale = "log", + scale_hist_sim: Scale = "linear", + figsize: tuple[float | int, float | int] = (8, 3), + return_fig: bool = False, + show: bool = True, + save: str | bool | None = None, +) -> Figure | Sequence[tuple[Axes, Axes]] | tuple[Axes, Axes] | None: + """Plot histogram of doublet scores for observed transcriptomes and simulated doublets. + + The histogram for simulated doublets is useful for determining the correct doublet + score threshold. + + Scrublet must have been run previously with the input object. + + Parameters + ---------- + adata + An AnnData object resulting from :func:`~scanpy.pp.scrublet`. + scale_hist_obs + Set y axis scale transformation in matplotlib for the plot of observed transcriptomes + scale_hist_sim + Set y axis scale transformation in matplotlib for the plot of simulated doublets + figsize + width, height + show + Show the plot, do not return axis. + save + If :data:`True` or a :class:`str`, save the figure. + A string is appended to the default filename. + Infer the filetype if ending on {`'.pdf'`, `'.png'`, `'.svg'`}. + + Returns + ------- + If ``return_fig`` is True, a :class:`~matplotlib.figure.Figure`. + If ``show==False`` a list of :class:`~matplotlib.axes.Axes`. + + See Also + -------- + :func:`~scanpy.pp.scrublet`: Main way of running Scrublet, runs + preprocessing, doublet simulation and calling. + :func:`~scanpy.pp.scrublet_simulate_doublets`: Run Scrublet's doublet + simulation separately for advanced usage. + + """ + if "scrublet" not in adata.uns: + msg = "Please run scrublet before trying to generate the scrublet plot." + raise ValueError(msg) + + # If batched_by is populated, then we know Scrublet was run over multiple batches + + if "batched_by" in adata.uns["scrublet"]: + batched_by = adata.uns["scrublet"]["batched_by"] + batches = adata.obs[batched_by].astype("category", copy=False) + n_batches = len(batches.cat.categories) + figsize = (figsize[0], figsize[1] * n_batches) + else: + batches = pd.Series( + np.broadcast_to(0, adata.n_obs), dtype="category", index=adata.obs_names + ) + n_batches = 1 + + fig, axs = plt.subplots(n_batches, 2, figsize=figsize) + + for idx, (batch_key, sub_obs) in enumerate( + adata.obs.groupby(batches, observed=True) + ): + obs_ax: Axes + sim_ax: Axes + # We'll need multiple rows if Scrublet was run in multiple batches + if "batched_by" in adata.uns["scrublet"]: + threshold = adata.uns["scrublet"]["batches"][batch_key].get( + "threshold", None + ) + doublet_scores_sim = adata.uns["scrublet"]["batches"][batch_key][ + "doublet_scores_sim" + ] + axis_lab_suffix = f" ({batch_key})" + obs_ax = axs[idx][0] + sim_ax = axs[idx][1] + + else: + threshold = adata.uns["scrublet"].get("threshold", None) + doublet_scores_sim = adata.uns["scrublet"]["doublet_scores_sim"] + axis_lab_suffix = "" + obs_ax = axs[0] + sim_ax = axs[1] + + # Make the plots + _plot_scores( + obs_ax, + sub_obs["doublet_score"], + scale=scale_hist_obs, + title=f"Observed transcriptomes {axis_lab_suffix}", + threshold=threshold, + ) + _plot_scores( + sim_ax, + doublet_scores_sim, + scale=scale_hist_sim, + title=f"Simulated doublets {axis_lab_suffix}", + threshold=threshold, + ) + + fig.tight_layout() + + savefig_or_show("scrublet_score_distribution", show=show, save=save) + if return_fig: + return fig + elif not show: + return axs + + +def _plot_scores( + ax: Axes, + scores: np.ndarray, + scale: Scale, + title: str, + threshold: float | None = None, +) -> None: + ax.hist( + scores, + np.linspace(0, 1, 50), + color="gray", + linewidth=0, + density=True, + ) + ax.set_yscale(scale) + yl = ax.get_ylim() + ax.set_ylim(yl) + + if threshold is not None: + ax.plot(threshold * np.ones(2), yl, c="black", linewidth=1) + + ax.set_title(title) + ax.set_xlabel("Doublet score") + ax.set_ylabel("Prob. density") diff --git a/.venv/lib/python3.11/site-packages/scanpy/plotting/_stacked_violin.py b/.venv/lib/python3.11/site-packages/scanpy/plotting/_stacked_violin.py new file mode 100644 index 0000000000000000000000000000000000000000..725494d9ab17df50ab046b15b4cde32e4d720ed8 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/plotting/_stacked_violin.py @@ -0,0 +1,857 @@ +from __future__ import annotations + +import warnings +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd +from matplotlib import colormaps +from matplotlib.colors import is_color_like +from packaging.version import Version + +from .. import logging as logg +from .._compat import old_positionals, pkg_version +from .._settings import settings +from .._utils import _doc_params, _empty +from ._baseplot_class import BasePlot, doc_common_groupby_plot_args +from ._docs import doc_common_plot_args, doc_show_save_ax, doc_vboundnorm +from ._utils import ( + _deprecated_scale, + _dk, + check_colornorm, + make_grid_spec, + savefig_or_show, +) + +if TYPE_CHECKING: + from collections.abc import Mapping, Sequence + from typing import Literal, Self + + from anndata import AnnData + from matplotlib.axes import Axes + from matplotlib.colors import Colormap, Normalize + + from .._utils import Empty + from ._baseplot_class import _VarNames + from ._utils import DensityNorm, _AxesSubplot + + +@_doc_params(common_plot_args=doc_common_plot_args) +class StackedViolin(BasePlot): + """Stacked violin plots. + + Makes a compact image composed of individual violin plots + (from :func:`~seaborn.violinplot`) stacked on top of each other. + Useful to visualize gene expression per cluster. + + Wraps :func:`seaborn.violinplot` for :class:`~anndata.AnnData`. + + Parameters + ---------- + {common_plot_args} + title + Title for the figure + stripplot + Add a stripplot on top of the violin plot. + See :func:`~seaborn.stripplot`. + jitter + Add jitter to the stripplot (only when stripplot is True) + See :func:`~seaborn.stripplot`. + size + Size of the jitter points. + order + Order in which to show the categories. Note: if `dendrogram=True` + the categories order will be given by the dendrogram and `order` + will be ignored. + density_norm + The method used to scale the width of each violin. + If 'width' (the default), each violin will have the same width. + If 'area', each violin will have the same area. + If 'count', a violin’s width corresponds to the number of observations. + row_palette + The row palette determines the colors to use for the stacked violins. + The value should be a valid seaborn or matplotlib palette name + (see :func:`~seaborn.color_palette`). + Alternatively, a single color name or hex value can be passed, + e.g. `'red'` or `'#cc33ff'`. + standard_scale + Whether or not to standardize a dimension between 0 and 1, + meaning for each variable or observation, + subtract the minimum and divide each by its maximum. + swap_axes + By default, the x axis contains `var_names` (e.g. genes) and the y axis + the `groupby` categories. By setting `swap_axes` then x are the `groupby` + categories and y the `var_names`. When swapping + axes var_group_positions are no longer used + kwds + Are passed to :func:`~seaborn.violinplot`. + + + See Also + -------- + :func:`~scanpy.pl.stacked_violin`: simpler way to call StackedViolin but with less + options. + :func:`~scanpy.pl.violin` and :func:`~scanpy.pl.rank_genes_groups_stacked_violin`: + to plot marker genes identified using :func:`~scanpy.tl.rank_genes_groups` + + Examples + -------- + >>> import scanpy as sc + >>> adata = sc.datasets.pbmc68k_reduced() + >>> markers = ["C1QA", "PSAP", "CD79A", "CD79B", "CST3", "LYZ"] + >>> sc.pl.StackedViolin( + ... adata, markers, groupby="bulk_labels", dendrogram=True + ... ) # doctest: +ELLIPSIS + + + Using var_names as dict: + + >>> markers = {{"T-cell": "CD3D", "B-cell": "CD79A", "myeloid": "CST3"}} + >>> sc.pl.StackedViolin( + ... adata, markers, groupby="bulk_labels", dendrogram=True + ... ) # doctest: +ELLIPSIS + + + """ + + DEFAULT_SAVE_PREFIX = "stacked_violin_" + DEFAULT_COLOR_LEGEND_TITLE = "Median expression\nin group" + + DEFAULT_COLORMAP = "Blues" + DEFAULT_STRIPPLOT = False + DEFAULT_JITTER = False + DEFAULT_JITTER_SIZE = 1 + DEFAULT_LINE_WIDTH = 0.2 + DEFAULT_ROW_PALETTE = None + DEFAULT_DENSITY_NORM: DensityNorm = "width" + DEFAULT_PLOT_YTICKLABELS = False + DEFAULT_YLIM = None + DEFAULT_PLOT_X_PADDING = 0.5 # a unit is the distance between two x-axis ticks + DEFAULT_PLOT_Y_PADDING = 0.5 # a unit is the distance between two y-axis ticks + + # set by default the violin plot cut=0 to limit the extend + # of the violin plot as this produces better plots that wont extend + # to negative values for example. From seaborn.violin documentation: + # + # cut: Distance, in units of bandwidth size, to extend the density past + # the extreme datapoints. Set to 0 to limit the violin range within + # the range of the observed data (i.e., to have the same effect as + # trim=True in ggplot. + DEFAULT_CUT = 0 + + # inner{“box”, “quartile”, “point”, “stick”, None} (Default seaborn: box) + # Representation of the datapoints in the violin interior. If box, draw a + # miniature boxplot. If quartiles, draw the quartiles of the distribution. + # If point or stick, show each underlying datapoint. Using + # None will draw unadorned violins. + DEFAULT_INNER = None + + # Called unconditionally when accessing an instance attribute: + def __getattribute__(self, name: str) -> object: + # If the user has set the deprecated version on the class, + # and our code accesses the new version from the instance, + # return the user-specified version instead and warn. + # This is done because class properties are hard to do. + if name == "DEFAULT_DENSITY_NORM" and hasattr(self, "DEFAULT_SCALE"): + msg = "Don’t set DEFAULT_SCALE, use DEFAULT_DENSITY_NORM instead" + warnings.warn(msg, FutureWarning, stacklevel=2) + return object.__getattribute__(self, "DEFAULT_SCALE") + return object.__getattribute__(self, name) + + @old_positionals( + "use_raw", + "log", + "num_categories", + "categories_order", + "title", + "figsize", + "gene_symbols", + "var_group_positions", + "var_group_labels", + "var_group_rotation", + "layer", + "standard_scale", + "ax", + "vmin", + "vmax", + "vcenter", + "norm", + ) + def __init__( # noqa: PLR0913 + self, + adata: AnnData, + var_names: _VarNames | Mapping[str, _VarNames], + groupby: str | Sequence[str], + *, + use_raw: bool | None = None, + log: bool = False, + num_categories: int = 7, + categories_order: Sequence[str] | None = None, + title: str | None = None, + figsize: tuple[float, float] | None = None, + gene_symbols: str | None = None, + var_group_positions: Sequence[tuple[int, int]] | None = None, + var_group_labels: Sequence[str] | None = None, + var_group_rotation: float | None = None, + layer: str | None = None, + standard_scale: Literal["var", "group"] | None = None, + ax: _AxesSubplot | None = None, + vmin: float | None = None, + vmax: float | None = None, + vcenter: float | None = None, + norm: Normalize | None = None, + **kwds, + ): + BasePlot.__init__( + self, + adata, + var_names, + groupby, + use_raw=use_raw, + log=log, + num_categories=num_categories, + categories_order=categories_order, + title=title, + figsize=figsize, + gene_symbols=gene_symbols, + var_group_positions=var_group_positions, + var_group_labels=var_group_labels, + var_group_rotation=var_group_rotation, + layer=layer, + ax=ax, + vmin=vmin, + vmax=vmax, + vcenter=vcenter, + norm=norm, + **kwds, + ) + + if standard_scale == "obs": + standard_scale = "group" + msg = "`standard_scale='obs'` is deprecated, use `standard_scale='group'` instead" + warnings.warn(msg, FutureWarning, stacklevel=2) + if standard_scale == "group": + self.obs_tidy = self.obs_tidy.sub(self.obs_tidy.min(1), axis=0) + self.obs_tidy = self.obs_tidy.div(self.obs_tidy.max(1), axis=0).fillna(0) + elif standard_scale == "var": + self.obs_tidy -= self.obs_tidy.min(0) + self.obs_tidy = (self.obs_tidy / self.obs_tidy.max(0)).fillna(0) + elif standard_scale is None: + pass + else: + logg.warning("Unknown type for standard_scale, ignored") + + # Set default style parameters + self.cmap = self.DEFAULT_COLORMAP + self.row_palette = self.DEFAULT_ROW_PALETTE + self.stripplot = self.DEFAULT_STRIPPLOT + self.jitter = self.DEFAULT_JITTER + self.jitter_size = self.DEFAULT_JITTER_SIZE + self.plot_yticklabels = self.DEFAULT_PLOT_YTICKLABELS + self.ylim = self.DEFAULT_YLIM + self.plot_x_padding = self.DEFAULT_PLOT_X_PADDING + self.plot_y_padding = self.DEFAULT_PLOT_Y_PADDING + + self.kwds.setdefault("cut", self.DEFAULT_CUT) + self.kwds.setdefault("inner", self.DEFAULT_INNER) + self.kwds.setdefault("linewidth", self.DEFAULT_LINE_WIDTH) + self.kwds.setdefault("density_norm", self.DEFAULT_DENSITY_NORM) + + @old_positionals( + "cmap", + "stripplot", + "jitter", + "jitter_size", + "linewidth", + "row_palette", + "density_norm", + "yticklabels", + "ylim", + "x_padding", + "y_padding", + ) + def style( # noqa: PLR0913 + self, + *, + cmap: Colormap | str | None | Empty = _empty, + stripplot: bool | Empty = _empty, + jitter: float | bool | Empty = _empty, + jitter_size: float | Empty = _empty, + linewidth: float | None | Empty = _empty, + row_palette: str | None | Empty = _empty, + density_norm: DensityNorm | Empty = _empty, + yticklabels: bool | Empty = _empty, + ylim: tuple[float, float] | None | Empty = _empty, + x_padding: float | Empty = _empty, + y_padding: float | Empty = _empty, + # deprecated + scale: DensityNorm | Empty = _empty, + ) -> Self: + r"""Modify plot visual parameters. + + Parameters + ---------- + cmap + Matplotlib color map, specified by name or directly. + If ``None``, use :obj:`matplotlib.rcParams`\ ``["image.cmap"]`` + stripplot + Add a stripplot on top of the violin plot. + See :func:`~seaborn.stripplot`. + jitter + Add jitter to the stripplot (only when stripplot is True) + See :func:`~seaborn.stripplot`. + jitter_size + Size of the jitter points. + linewidth + line width for the violin plots. + If None, use :obj:`matplotlib.rcParams`\ ``["lines.linewidth"]`` + row_palette + The row palette determines the colors to use for the stacked violins. + If ``None``, use :obj:`matplotlib.rcParams`\ ``["axes.prop_cycle"]`` + The value should be a valid seaborn or matplotlib palette name + (see :func:`~seaborn.color_palette`). + Alternatively, a single color name or hex value can be passed, + e.g. `'red'` or `'#cc33ff'`. + density_norm + The method used to scale the width of each violin. + If 'width' (the default), each violin will have the same width. + If 'area', each violin will have the same area. + If 'count', a violin’s width corresponds to the number of observations. + yticklabels + Set to true to view the y tick labels. + ylim + minimum and maximum values for the y-axis. + If not ``None``, all rows will have the same y-axis range. + Example: ``ylim=(0, 5)`` + x_padding + Space between the plot left/right borders and the violins. A unit + is the distance between the x ticks. + y_padding + Space between the plot top/bottom borders and the violins. A unit is + the distance between the y ticks. + + Returns + ------- + :class:`~scanpy.pl.StackedViolin` + + Examples + -------- + >>> import scanpy as sc + >>> adata = sc.datasets.pbmc68k_reduced() + >>> markers = ['C1QA', 'PSAP', 'CD79A', 'CD79B', 'CST3', 'LYZ'] + + Change color map and turn off edges + + >>> sc.pl.StackedViolin(adata, markers, groupby='bulk_labels') \ + ... .style(row_palette='Blues', linewidth=0).show() + + """ + super().style(cmap=cmap) + + if row_palette is not _empty: + self.row_palette = row_palette + self.kwds["color"] = self.row_palette + if stripplot is not _empty: + self.stripplot = stripplot + if jitter is not _empty: + self.jitter = jitter + if jitter_size is not _empty: + self.jitter_size = jitter_size + if yticklabels is not _empty: + self.plot_yticklabels = yticklabels + if self.plot_yticklabels: + # space needs to be added to avoid overlapping + # of labels and legend or dendrogram/totals. + self.wspace = 0.3 + else: + self.wspace = StackedViolin.DEFAULT_WSPACE + if ylim is not _empty: + self.ylim = ylim + if x_padding is not _empty: + self.plot_x_padding = x_padding + if y_padding is not _empty: + self.plot_y_padding = y_padding + if linewidth is not _empty: + self.kwds["linewidth"] = linewidth + if (density_norm := _deprecated_scale(density_norm, scale)) is not _empty: + self.kwds["density_norm"] = density_norm + + return self + + def _mainplot(self, ax: Axes): + # to make the stacked violin plots, the + # `ax` is subdivided horizontally and in each horizontal sub ax + # a seaborn violin plot is added. + + # work on a copy of the dataframes. This is to avoid changes + # on the original data frames after repetitive calls to the + # StackedViolin object, for example once with swap_axes and other without + _matrix = self.obs_tidy.copy() + + if self.var_names_idx_order is not None: + _matrix = _matrix.iloc[:, self.var_names_idx_order] + + # get mean values for color and transform to color values + # using colormap + _color_df = ( + _matrix.groupby(level=0, observed=True) + .median() + .loc[ + self.categories_order + if self.categories_order is not None + else self.categories + ] + ) + if self.are_axes_swapped: + _color_df = _color_df.T + + cmap = colormaps.get_cmap(self.kwds.pop("cmap", self.cmap)) + normalize = check_colornorm( + self.vboundnorm.vmin, + self.vboundnorm.vmax, + self.vboundnorm.vcenter, + self.vboundnorm.norm, + ) + colormap_array = cmap(normalize(_color_df.values)) + x_spacer_size = self.plot_x_padding + y_spacer_size = self.plot_y_padding + + # All columns should have a unique name, yet, frequently + # gene names are repeated in self.var_names, otherwise the + # violin plot will not distinguish those genes + _matrix.columns = [f"{x}_{idx}" for idx, x in enumerate(_matrix.columns)] + + # Ensure the categories axis is always ordered identically. + # If the axes are not swapped, the above _matrix.columns is used in the actual violin plot (i.e., unique names). + # If they are swapped, then use the same as the labels used below. + # Without this, `_make_rows_of_violinplots` does not know about the order of the categories in labels. + labels = _color_df.columns + x_axis_order = labels if self.are_axes_swapped else _matrix.columns + + self._make_rows_of_violinplots( + ax, + _matrix, + colormap_array, + _color_df, + x_spacer_size, + y_spacer_size, + x_axis_order, + ) + + # turn on axis for `ax` as this is turned off + # by make_grid_spec when the axis is subdivided earlier. + ax.set_frame_on(True) + ax.axis("on") + ax.patch.set_alpha(0.0) + + # add tick labels + ax.set_ylim(_color_df.shape[0] + y_spacer_size, -y_spacer_size) + ax.set_xlim(-x_spacer_size, _color_df.shape[1] + x_spacer_size) + + # 0.5 to position the ticks on the center of the violins + y_ticks = np.arange(_color_df.shape[0]) + 0.5 + ax.set_yticks(y_ticks) + ax.set_yticklabels( + [_color_df.index[idx] for idx, _ in enumerate(y_ticks)], minor=False + ) + + # 0.5 to position the ticks on the center of the violins + x_ticks = np.arange(_color_df.shape[1]) + 0.5 + ax.set_xticks(x_ticks) + ax.set_xticklabels(labels, minor=False, ha="center") + # rotate x tick labels if they are longer than 2 characters + if max([len(x) for x in labels]) > 2: + ax.tick_params(axis="x", labelrotation=90) + ax.tick_params(axis="both", labelsize="small") + ax.grid(visible=False) + + return normalize + + def _make_rows_of_violinplots( + self, + ax, + _matrix, + colormap_array, + _color_df, + x_spacer_size: float, + y_spacer_size: float, + x_axis_order, + ): + import seaborn as sns # Slow import, only import if called + + row_palette = self.kwds.pop("color", self.row_palette) + if row_palette is not None: + if is_color_like(row_palette): + row_colors = [row_palette] * _color_df.shape[0] + else: + row_colors = sns.color_palette(row_palette, n_colors=_color_df.shape[0]) + # when row_palette is used, there is no need for a legend + self.legends_width = 0.0 + else: + row_colors = [None] * _color_df.shape[0] + + # transform the dataframe into a dataframe having three columns: + # the categories name (from groupby), + # the gene name + # the expression value + # This format is convenient to aggregate per gene or per category + # while making the violin plots. + if pkg_version("pandas") >= Version("2.1"): + stack_kwargs = {"future_stack": True} + else: + stack_kwargs = {"dropna": False} + + df = ( + pd.DataFrame(_matrix.stack(**stack_kwargs)) + .reset_index() + .rename( + columns={ + "level_1": "genes", + _matrix.index.name: "categories", + 0: "values", + } + ) + ) + df["genes"] = ( + df["genes"].astype("category").cat.reorder_categories(_matrix.columns) + ) + df["categories"] = ( + df["categories"] + .astype("category") + .cat.reorder_categories(_matrix.index.categories) + ) + + # the ax need to be subdivided + # define a layout of nrows = len(categories) rows + # each row is one violin plot. + num_rows, num_cols = _color_df.shape + height_ratios = [y_spacer_size] + [1] * num_rows + [y_spacer_size] + width_ratios = [x_spacer_size] + [1] * num_cols + [x_spacer_size] + + fig, gs = make_grid_spec( + ax, + nrows=num_rows + 2, + ncols=num_cols + 2, + hspace=0.2 if self.plot_yticklabels else 0, + wspace=0, + height_ratios=height_ratios, + width_ratios=width_ratios, + ) + axs_list = [] + for idx, row_label in enumerate(_color_df.index): + row_ax = fig.add_subplot(gs[idx + 1, 1:-1]) + axs_list.append(row_ax) + + palette_colors = ( + list(colormap_array[idx, :]) if row_colors[idx] is None else None + ) + + if not self.are_axes_swapped: + x = "genes" + _df = df[df.categories == row_label] + else: + x = "categories" + # because of the renamed matrix columns here + # we need to use this instead of the 'row_label' + # (in _color_df the values are not renamed as those + # values will be used to label the ticks) + _df = df[df.genes == _matrix.columns[idx]] + + row_ax = sns.violinplot( + x=x, + y="values", + data=_df, + orient="vertical", + ax=row_ax, + # use a single `color`` if row_colors[idx] is defined + # else use the palette + hue=None if palette_colors is None else x, + palette=palette_colors, + color=row_colors[idx], + order=x_axis_order, + hue_order=x_axis_order, + **self.kwds, + ) + if self.stripplot: + row_ax = sns.stripplot( + x=x, + y="values", + data=_df, + jitter=self.jitter, + color="black", + size=self.jitter_size, + ax=row_ax, + ) + + self._setup_violin_axes_ticks(row_ax, num_cols) + + def _setup_violin_axes_ticks(self, row_ax: Axes, num_cols: int): + """Configure each of the violin plot axes ticks like remove or add labels etc.""" + # remove the default seaborn grids because in such a compact + # plot are unnecessary + + row_ax.grid(visible=False) + if self.ylim is not None: + row_ax.set_ylim(self.ylim) + if self.log: + row_ax.set_yscale("log") + + if self.plot_yticklabels: + for spine in ["top", "bottom", "left"]: + row_ax.spines[spine].set_visible(False) + + # make line a bit ticker to see the extend of the yaxis in the + # final plot + row_ax.spines["right"].set_linewidth(1.5) + row_ax.spines["right"].set_position(("data", num_cols)) + + row_ax.tick_params( + axis="y", + left=False, + right=True, + labelright=True, + labelleft=False, + labelsize="x-small", + ) + # use only the smallest and the largest y ticks + # and align the firts label on top of the tick and + # the second below the tick. This avoid overlapping + # of nearby ticks + from matplotlib import ticker + + # use MaxNLocator to set 2 ticks + row_ax.yaxis.set_major_locator( + ticker.MaxNLocator(nbins=2, steps=[1, 1.2, 10]) + ) + yticks = row_ax.get_yticks() + row_ax.set_yticks([yticks[0], yticks[-1]]) + ticklabels = row_ax.get_yticklabels() + ticklabels[0].set_va("bottom") + ticklabels[-1].set_va("top") + else: + row_ax.axis("off") + # remove labels + row_ax.set_yticklabels([]) + row_ax.tick_params(axis="y", left=False, right=False) + + row_ax.set_ylabel("") + + row_ax.set_xlabel("") + + row_ax.set_xticklabels([]) + row_ax.tick_params( + axis="x", bottom=False, top=False, labeltop=False, labelbottom=False + ) + + +@old_positionals( + "log", + "use_raw", + "num_categories", + "title", + "colorbar_title", + "figsize", + "dendrogram", + "gene_symbols", + "var_group_positions", + "var_group_labels", + "standard_scale", + "var_group_rotation", + "layer", + "stripplot", + # 17 positionals are enough for backwards compatibility +) +@_doc_params( + show_save_ax=doc_show_save_ax, + common_plot_args=doc_common_plot_args, + groupby_plots_args=doc_common_groupby_plot_args, + vminmax=doc_vboundnorm, +) +def stacked_violin( # noqa: PLR0913 + adata: AnnData, + var_names: _VarNames | Mapping[str, _VarNames], + groupby: str | Sequence[str], + *, + log: bool = False, + use_raw: bool | None = None, + num_categories: int = 7, + title: str | None = None, + colorbar_title: str | None = StackedViolin.DEFAULT_COLOR_LEGEND_TITLE, + figsize: tuple[float, float] | None = None, + dendrogram: bool | str = False, + gene_symbols: str | None = None, + var_group_positions: Sequence[tuple[int, int]] | None = None, + var_group_labels: Sequence[str] | None = None, + standard_scale: Literal["var", "group"] | None = None, + var_group_rotation: float | None = None, + layer: str | None = None, + categories_order: Sequence[str] | None = None, + swap_axes: bool = False, + show: bool | None = None, + save: bool | str | None = None, + return_fig: bool | None = False, + ax: _AxesSubplot | None = None, + vmin: float | None = None, + vmax: float | None = None, + vcenter: float | None = None, + norm: Normalize | None = None, + # Style options + cmap: Colormap | str | None = StackedViolin.DEFAULT_COLORMAP, + stripplot: bool = StackedViolin.DEFAULT_STRIPPLOT, + jitter: float | bool = StackedViolin.DEFAULT_JITTER, + size: float = StackedViolin.DEFAULT_JITTER_SIZE, + row_palette: str | None = StackedViolin.DEFAULT_ROW_PALETTE, + density_norm: DensityNorm | Empty = _empty, + yticklabels: bool = StackedViolin.DEFAULT_PLOT_YTICKLABELS, + # deprecated + order: Sequence[str] | None | Empty = _empty, + scale: DensityNorm | Empty = _empty, + **kwds, +) -> StackedViolin | dict | None: + """Stacked violin plots. + + Makes a compact image composed of individual violin plots + (from :func:`~seaborn.violinplot`) stacked on top of each other. + Useful to visualize gene expression per cluster. + + Wraps :func:`seaborn.violinplot` for :class:`~anndata.AnnData`. + + This function provides a convenient interface to the + :class:`~scanpy.pl.StackedViolin` class. If you need more flexibility, + you should use :class:`~scanpy.pl.StackedViolin` directly. + + Parameters + ---------- + {common_plot_args} + {groupby_plots_args} + stripplot + Add a stripplot on top of the violin plot. + See :func:`~seaborn.stripplot`. + jitter + Add jitter to the stripplot (only when stripplot is True) + See :func:`~seaborn.stripplot`. + size + Size of the jitter points. + density_norm + The method used to scale the width of each violin. + If 'width' (the default), each violin will have the same width. + If 'area', each violin will have the same area. + If 'count', a violin’s width corresponds to the number of observations. + yticklabels + Set to true to view the y tick labels. + row_palette + Be default, median values are mapped to the violin color using a + color map (see `cmap` argument). Alternatively, a 'row_palette` can + be given to color each violin plot row using a different colors. + The value should be a valid seaborn or matplotlib palette name + (see :func:`~seaborn.color_palette`). + Alternatively, a single color name or hex value can be passed, + e.g. `'red'` or `'#cc33ff'`. + {show_save_ax} + {vminmax} + **kwds + Are passed to :func:`~seaborn.violinplot`. + + Returns + ------- + If `return_fig` is `True`, returns a :class:`~scanpy.pl.StackedViolin` object, + else if `show` is false, return axes dict + + See Also + -------- + :class:`~scanpy.pl.StackedViolin`: The StackedViolin class can be used to to control + several visual parameters not available in this function. + :func:`~scanpy.pl.rank_genes_groups_stacked_violin` to plot marker genes identified + using the :func:`~scanpy.tl.rank_genes_groups` function. + + Examples + -------- + Visualization of violin plots of a few genes grouped by the category `bulk_labels`: + + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc68k_reduced() + markers = ['C1QA', 'PSAP', 'CD79A', 'CD79B', 'CST3', 'LYZ'] + sc.pl.stacked_violin(adata, markers, groupby='bulk_labels', dendrogram=True) + + Same visualization but passing var_names as dict, which adds a grouping of + the genes on top of the image: + + .. plot:: + :context: close-figs + + markers = {{'T-cell': 'CD3D', 'B-cell': 'CD79A', 'myeloid': 'CST3'}} + sc.pl.stacked_violin(adata, markers, groupby='bulk_labels', dendrogram=True) + + Get StackedViolin object for fine tuning + + .. plot:: + :context: close-figs + + vp = sc.pl.stacked_violin(adata, markers, 'bulk_labels', return_fig=True) + vp.add_totals().style(ylim=(0,5)).show() + + The axes used can be obtained using the get_axes() method: + + .. code-block:: python + + axes_dict = vp.get_axes() + print(axes_dict) + + """ + if order is not _empty: + msg = ( + "`order` is deprecated (and never worked for `stacked_violin`), " + "use categories_order instead" + ) + warnings.warn(msg, FutureWarning, stacklevel=2) + # no reason to set `categories_order` here, as `order` never worked. + + vp = StackedViolin( + adata, + var_names, + groupby=groupby, + use_raw=use_raw, + log=log, + num_categories=num_categories, + categories_order=categories_order, + standard_scale=standard_scale, + title=title, + figsize=figsize, + gene_symbols=gene_symbols, + var_group_positions=var_group_positions, + var_group_labels=var_group_labels, + var_group_rotation=var_group_rotation, + layer=layer, + ax=ax, + vmin=vmin, + vmax=vmax, + vcenter=vcenter, + norm=norm, + **kwds, + ) + + if dendrogram: + vp.add_dendrogram(dendrogram_key=_dk(dendrogram)) + if swap_axes: + vp.swap_axes() + vp = vp.style( + cmap=cmap, + stripplot=stripplot, + jitter=jitter, + jitter_size=size, + row_palette=row_palette, + density_norm=_deprecated_scale(density_norm, scale), + yticklabels=yticklabels, + linewidth=kwds.get("linewidth", _empty), + ).legend(title=colorbar_title) + if return_fig: + return vp + vp.make_figure() + savefig_or_show(StackedViolin.DEFAULT_SAVE_PREFIX, show=show, save=save) + show = settings.autoshow if show is None else show + if show: + return None + return vp.get_axes() diff --git a/.venv/lib/python3.11/site-packages/scanpy/plotting/_tools/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/plotting/_tools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..27ad54bc6ee0026e11517b6b47362782588694f3 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/plotting/_tools/__init__.py @@ -0,0 +1,1805 @@ +from __future__ import annotations + +import functools +import operator +from collections.abc import Mapping, Sequence +from copy import copy +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd +from matplotlib import colormaps, rcParams +from matplotlib import pyplot as plt + +from scanpy.get import obs_df + +from ... import logging as logg +from ..._compat import old_positionals +from ..._settings import settings +from ..._utils import _doc_params, _empty, sanitize_anndata +from ...get import rank_genes_groups_df +from .._anndata import ranking +from .._docs import ( + doc_cm_palette, + doc_panels, + doc_rank_genes_groups_plot_args, + doc_rank_genes_groups_values_to_plot, + doc_scatter_embedding, + doc_show_save, + doc_show_save_ax, + doc_vbound_percentile, +) +from .._utils import ( + _deprecated_scale, + savefig_or_show, + timeseries, + timeseries_as_heatmap, + timeseries_subplot, +) +from .scatterplots import _panel_grid, embedding, pca + +if TYPE_CHECKING: + from collections.abc import Iterable + from typing import Literal + + from anndata import AnnData + from cycler import Cycler + from matplotlib.axes import Axes + from matplotlib.colors import Colormap, Normalize + from matplotlib.figure import Figure + + from ..._utils import Empty + from .._baseplot_class import BasePlot + from .._utils import DensityNorm + +# ------------------------------------------------------------------------------ +# PCA +# ------------------------------------------------------------------------------ + + +@_doc_params(scatter_bulk=doc_scatter_embedding, show_save_ax=doc_show_save_ax) +def pca_overview(adata: AnnData, **params): + """Plot PCA results. + + The parameters are the ones of the scatter plot. Call pca_ranking separately + if you want to change the default settings. + + Parameters + ---------- + adata + Annotated data matrix. + color + Keys for observation/cell annotation either as list `["ann1", "ann2"]` or + string `"ann1,ann2,..."`. + use_raw + Use `raw` attribute of `adata` if present. + {scatter_bulk} + show + Show the plot, do not return axis. + save + If `True` or a `str`, save the figure. + A string is appended to the default filename. + Infer the filetype if ending on {{`'.pdf'`, `'.png'`, `'.svg'`}}. + + Examples + -------- + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc3k_processed() + sc.pl.pca_overview(adata, color="louvain") + + .. currentmodule:: scanpy + + See Also + -------- + pp.pca + + """ + show = params.pop("show", None) + pca(adata, **params, show=False) + pca_loadings(adata, show=False) + pca_variance_ratio(adata, show=show) + + +# backwards compat +pca_scatter = pca + + +@old_positionals("include_lowest", "n_points", "show", "save") +def pca_loadings( + adata: AnnData, + components: str | Sequence[int] | None = None, + *, + include_lowest: bool = True, + n_points: int | None = None, + show: bool | None = None, + save: str | bool | None = None, +): + """Rank genes according to contributions to PCs. + + Parameters + ---------- + adata + Annotated data matrix. + components + For example, ``'1,2,3'`` means ``[1, 2, 3]``, first, second, third + principal component. + include_lowest + Whether to show the variables with both highest and lowest loadings. + show + Show the plot, do not return axis. + n_points + Number of variables to plot for each component. + save + If `True` or a `str`, save the figure. + A string is appended to the default filename. + Infer the filetype if ending on {`'.pdf'`, `'.png'`, `'.svg'`}. + + Examples + -------- + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc3k_processed() + + Show first 3 components loadings + + .. plot:: + :context: close-figs + + sc.pl.pca_loadings(adata, components = '1,2,3') + + + """ + if components is None: + components = [1, 2, 3] + elif isinstance(components, str): + components = [int(x) for x in components.split(",")] + components = np.array(components) - 1 + + if np.any(components < 0): + msg = "Component indices must be greater than zero." + raise ValueError(msg) + + if n_points is None: + n_points = min(30, adata.n_vars) + elif adata.n_vars < n_points: + msg = f"Tried to plot {n_points} variables, but passed anndata only has {adata.n_vars}." + raise ValueError(msg) + + ranking( + adata, + "varm", + "PCs", + n_points=n_points, + indices=components, + include_lowest=include_lowest, + ) + savefig_or_show("pca_loadings", show=show, save=save) + + +@old_positionals("log", "show", "save") +def pca_variance_ratio( + adata: AnnData, + n_pcs: int = 30, + *, + log: bool = False, + show: bool | None = None, + save: bool | str | None = None, +): + """Plot the variance ratio. + + Parameters + ---------- + n_pcs + Number of PCs to show. + log + Plot on logarithmic scale.. + show + Show the plot, do not return axis. + save + If `True` or a `str`, save the figure. + A string is appended to the default filename. + Infer the filetype if ending on {`'.pdf'`, `'.png'`, `'.svg'`}. + + """ + ranking( + adata, + "uns", + "variance_ratio", + n_points=n_pcs, + dictionary="pca", + labels="PC", + log=log, + ) + savefig_or_show("pca_variance_ratio", show=show, save=save) + + +# ------------------------------------------------------------------------------ +# Subgroup identification and ordering – clustering, pseudotime, branching +# and tree inference tools +# ------------------------------------------------------------------------------ + + +@old_positionals("color_map", "show", "save", "as_heatmap", "marker") +def dpt_timeseries( + adata: AnnData, + *, + color_map: str | Colormap | None = None, + show: bool | None = None, + save: bool | None = None, + as_heatmap: bool = True, + marker: str | Sequence[str] = ".", +): + """Heatmap of pseudotime series. + + Parameters + ---------- + as_heatmap + Plot the timeseries as heatmap. + + """ + if adata.n_vars > 100: + logg.warning( + "Plotting more than 100 genes might take some while, " + "consider selecting only highly variable genes, for example." + ) + # only if number of genes is not too high + if as_heatmap: + # plot time series as heatmap, as in Haghverdi et al. (2016), Fig. 1d + timeseries_as_heatmap( + adata.X[adata.obs["dpt_order_indices"].values], + var_names=adata.var_names, + highlights_x=adata.uns["dpt_changepoints"], + color_map=color_map, + ) + else: + # plot time series as gene expression vs time + timeseries( + adata.X[adata.obs["dpt_order_indices"].values], + var_names=adata.var_names, + highlights_x=adata.uns["dpt_changepoints"], + xlim=[0, 1.3 * adata.X.shape[0]], + marker=marker, + ) + plt.xlabel("dpt order") + savefig_or_show("dpt_timeseries", save=save, show=show) + + +@old_positionals("color_map", "palette", "show", "save", "marker") +@_doc_params(cm_palette=doc_cm_palette, show_save=doc_show_save) +def dpt_groups_pseudotime( + adata: AnnData, + *, + color_map: str | Colormap | None = None, + palette: Sequence[str] | Cycler | None = None, + show: bool | None = None, + save: bool | str | None = None, + marker: str | Sequence[str] = ".", +): + """Plot groups and pseudotime. + + Parameters + ---------- + adata + Annotated data matrix. + {cm_palette} + {show_save} + marker + Marker style. See :mod:`~matplotlib.markers` for details. + + """ + _, (ax_grp, ax_ord) = plt.subplots(2, 1) + timeseries_subplot( + adata.obs["dpt_groups"].cat.codes, + time=adata.obs["dpt_order"].values, + color=np.asarray(adata.obs["dpt_groups"]), + highlights_x=adata.uns["dpt_changepoints"], + ylabel="dpt groups", + yticks=( + np.arange(len(adata.obs["dpt_groups"].cat.categories), dtype=int) + if len(adata.obs["dpt_groups"].cat.categories) < 5 + else None + ), + palette=palette, + ax=ax_grp, + marker=marker, + ) + timeseries_subplot( + adata.obs["dpt_pseudotime"].values, + time=adata.obs["dpt_order"].values, + color=adata.obs["dpt_pseudotime"].values, + xlabel="dpt order", + highlights_x=adata.uns["dpt_changepoints"], + ylabel="pseudotime", + yticks=[0, 1], + color_map=color_map, + ax=ax_ord, + marker=marker, + ) + savefig_or_show("dpt_groups_pseudotime", save=save, show=show) + + +@old_positionals( + "n_genes", + "gene_symbols", + "key", + "fontsize", + "ncols", + "sharey", + "show", + "save", + "ax", +) +@_doc_params(show_save_ax=doc_show_save_ax) +def rank_genes_groups( # noqa: PLR0912, PLR0913, PLR0915 + adata: AnnData, + groups: str | Sequence[str] | None = None, + *, + n_genes: int = 20, + gene_symbols: str | None = None, + key: str | None = "rank_genes_groups", + fontsize: int = 8, + ncols: int = 4, + sharey: bool = True, + show: bool | None = None, + save: bool | None = None, + ax: Axes | None = None, + **kwds, +) -> list[Axes] | None: + """Plot ranking of genes. + + Parameters + ---------- + adata + Annotated data matrix. + groups + The groups for which to show the gene ranking. + gene_symbols + Key for field in `.var` that stores gene symbols if you do not want to + use `.var_names`. + n_genes + Number of genes to show. + fontsize + Fontsize for gene names. + ncols + Number of panels shown per row. + sharey + Controls if the y-axis of each panels should be shared. But passing + `sharey=False`, each panel has its own y-axis range. + {show_save_ax} + + Returns + ------- + List of each group’s matplotlib axis or `None` if `show=True`. + + Examples + -------- + + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc68k_reduced() + sc.pl.rank_genes_groups(adata) + + + Plot top 10 genes (default 20 genes) + + .. plot:: + :context: close-figs + + sc.pl.rank_genes_groups(adata, n_genes=10) + + .. currentmodule:: scanpy + + See Also + -------- + tl.rank_genes_groups + + """ + n_panels_per_row = kwds.get("n_panels_per_row", ncols) + if n_genes < 1: + msg = ( + "Specifying a negative number for n_genes has not been implemented for " + f"this plot. Received {n_genes=!r}." + ) + raise NotImplementedError(msg) + + reference = str(adata.uns[key]["params"]["reference"]) + group_names = adata.uns[key]["names"].dtype.names if groups is None else groups + # one panel for each group + # set up the figure + n_panels_x = min(n_panels_per_row, len(group_names)) + n_panels_y = np.ceil(len(group_names) / n_panels_x).astype(int) + + from matplotlib import gridspec + + if ax is None or (sps := ax.get_subplotspec()) is None: + fig = ( + plt.figure( + figsize=( + n_panels_x * rcParams["figure.figsize"][0], + n_panels_y * rcParams["figure.figsize"][1], + ) + ) + if ax is None + else ax.get_figure() + ) + gs = gridspec.GridSpec(n_panels_y, n_panels_x, fig, wspace=0.22, hspace=0.3) + else: + fig = ax.get_figure() + gs = sps.subgridspec(n_panels_y, n_panels_x) + if fig is None: + msg = "passed ax has no associated figure" + raise RuntimeError(msg) + + axs: list[Axes] = [] + ymin = np.inf + ymax = -np.inf + for count, group_name in enumerate(group_names): + gene_names = adata.uns[key]["names"][group_name][:n_genes] + scores = adata.uns[key]["scores"][group_name][:n_genes] + + # Setting up axis, calculating y bounds + if sharey: + ymin = min(ymin, np.min(scores)) + ymax = max(ymax, np.max(scores)) + + axs.append(fig.add_subplot(gs[count], sharey=axs[0] if axs else None)) + else: + ymin = np.min(scores) + ymax = np.max(scores) + ymax += 0.3 * (ymax - ymin) + + axs.append(fig.add_subplot(gs[count])) + axs[-1].set_ylim(ymin, ymax) + + axs[-1].set_xlim(-0.9, n_genes - 0.1) + + # Mapping to gene_symbols + if gene_symbols is not None: + if adata.raw is not None and adata.uns[key]["params"]["use_raw"]: + gene_names = adata.raw.var[gene_symbols][gene_names] + else: + gene_names = adata.var[gene_symbols][gene_names] + + # Making labels + for ig, gene_name in enumerate(gene_names): + axs[-1].text( + ig, + scores[ig], + gene_name, + rotation="vertical", + verticalalignment="bottom", + horizontalalignment="center", + fontsize=fontsize, + ) + + axs[-1].set_title(f"{group_name} vs. {reference}") + if count >= n_panels_x * (n_panels_y - 1): + axs[-1].set_xlabel("ranking") + + # print the 'score' label only on the first panel per row. + if count % n_panels_x == 0: + axs[-1].set_ylabel("score") + + if sharey is True and axs: + ymax += 0.3 * (ymax - ymin) + axs[0].set_ylim(ymin, ymax) + + writekey = f"rank_genes_groups_{adata.uns[key]['params']['groupby']}" + savefig_or_show(writekey, show=show, save=save) + show = settings.autoshow if show is None else show + if show: + return None + return axs + + +def _fig_show_save_or_axes( + plot_obj: BasePlot, *, return_fig: bool, show: bool | None, save: bool | None +): + """Decides what to return.""" + if return_fig: + return plot_obj + plot_obj.make_figure() + savefig_or_show(plot_obj.DEFAULT_SAVE_PREFIX, show=show, save=save) + show = settings.autoshow if show is None else show + if show: + return None + return plot_obj.get_axes() + + +def _rank_genes_groups_plot( # noqa: PLR0912, PLR0913, PLR0915 + adata: AnnData, + plot_type: str = "heatmap", + *, + groups: str | Sequence[str] | None = None, + n_genes: int | None = None, + groupby: str | None = None, + values_to_plot: str | None = None, + var_names: Sequence[str] | Mapping[str, Sequence[str]] | None = None, + min_logfoldchange: float | None = None, + key: str | None = None, + show: bool | None = None, + save: bool | None = None, + return_fig: bool = False, + gene_symbols: str | None = None, + **kwds, +): + """Call the different `rank_genes_groups_*` plots.""" + if var_names is not None and n_genes is not None: + msg = ( + "The arguments n_genes and var_names are mutually exclusive. Please " + "select only one." + ) + raise ValueError(msg) + + if key is None: + key = "rank_genes_groups" + + if groupby is None: + groupby = str(adata.uns[key]["params"]["groupby"]) + group_names = adata.uns[key]["names"].dtype.names if groups is None else groups + + if var_names is not None: + if isinstance(var_names, Mapping): + # get a single list of all gene names in the dictionary + var_names_list = functools.reduce( + operator.iadd, [list(x) for x in var_names.values()], [] + ) + elif isinstance(var_names, str): + var_names_list = [var_names] + else: + var_names_list = var_names + else: + # set n_genes = 10 as default when none of the options is given + if n_genes is None: + n_genes = 10 + + # dict in which each group is the key and the n_genes are the values + var_names = {} + var_names_list = [] + for group in group_names: + df = rank_genes_groups_df( + adata, + group, + key=key, + gene_symbols=gene_symbols, + log2fc_min=min_logfoldchange, + ) + + if gene_symbols is not None: + df["names"] = df[gene_symbols] + + genes_list = df.names[df.names.notnull()].tolist() + + if len(genes_list) == 0: + logg.warning(f"No genes found for group {group}") + continue + genes_list = genes_list[n_genes:] if n_genes < 0 else genes_list[:n_genes] + var_names[group] = genes_list + var_names_list.extend(genes_list) + + # by default add dendrogram to plots + kwds.setdefault("dendrogram", True) + + if plot_type in ["dotplot", "matrixplot"]: + # these two types of plots can also + # show score, logfoldchange and pvalues, in general any value from rank + # genes groups + title = None + values_df = None + if values_to_plot is not None: + values_df = _get_values_to_plot( + adata, + values_to_plot, + var_names_list, + key=key, + gene_symbols=gene_symbols, + ) + title = values_to_plot + if values_to_plot == "logfoldchanges": + title = "log fold change" + else: + title = values_to_plot.replace("_", " ").replace("pvals", "p-value") + + if plot_type == "dotplot": + from .._dotplot import dotplot + + _pl = dotplot( + adata, + var_names, + groupby, + dot_color_df=values_df, + return_fig=True, + gene_symbols=gene_symbols, + **kwds, + ) + if title is not None and "colorbar_title" not in kwds: + _pl.legend(colorbar_title=title) + elif plot_type == "matrixplot": + from .._matrixplot import matrixplot + + _pl = matrixplot( + adata, + var_names, + groupby, + values_df=values_df, + return_fig=True, + gene_symbols=gene_symbols, + **kwds, + ) + + if title is not None and "colorbar_title" not in kwds: + _pl.legend(title=title) + + return _fig_show_save_or_axes(_pl, return_fig=return_fig, show=show, save=save) + + elif plot_type == "stacked_violin": + from .._stacked_violin import stacked_violin + + _pl = stacked_violin( + adata, + var_names, + groupby, + return_fig=True, + gene_symbols=gene_symbols, + **kwds, + ) + return _fig_show_save_or_axes(_pl, return_fig=return_fig, show=show, save=save) + elif plot_type == "heatmap": + from .._anndata import heatmap + + return heatmap( + adata, + var_names, + groupby, + show=show, + save=save, + gene_symbols=gene_symbols, + **kwds, + ) + + elif plot_type == "tracksplot": + from .._anndata import tracksplot + + return tracksplot( + adata, + var_names, + groupby, + show=show, + save=save, + gene_symbols=gene_symbols, + **kwds, + ) + + +@old_positionals( + "n_genes", + "groupby", + "gene_symbols", + "var_names", + "min_logfoldchange", + "key", + "show", + "save", +) +@_doc_params(params=doc_rank_genes_groups_plot_args, show_save_ax=doc_show_save_ax) +def rank_genes_groups_heatmap( + adata: AnnData, + groups: str | Sequence[str] | None = None, + *, + n_genes: int | None = None, + groupby: str | None = None, + gene_symbols: str | None = None, + var_names: Sequence[str] | Mapping[str, Sequence[str]] | None = None, + min_logfoldchange: float | None = None, + key: str | None = None, + show: bool | None = None, + save: bool | None = None, + **kwds, +): + """Plot ranking of genes using heatmap plot (see :func:`~scanpy.pl.heatmap`). + + Parameters + ---------- + {params} + {show_save_ax} + **kwds + Are passed to :func:`~scanpy.pl.heatmap`. + + Examples + -------- + + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc68k_reduced() + sc.tl.rank_genes_groups(adata, 'bulk_labels') + sc.pl.rank_genes_groups_heatmap(adata) + + Show gene names per group on the heatmap + + .. plot:: + :context: close-figs + + sc.pl.rank_genes_groups_heatmap(adata, show_gene_labels=True) + + Plot top 5 genes per group (default 10 genes) + + .. plot:: + :context: close-figs + + sc.pl.rank_genes_groups_heatmap(adata, n_genes=5, show_gene_labels=True) + + .. currentmodule:: scanpy + + See Also + -------- + tl.rank_genes_groups + tl.dendrogram + + """ + return _rank_genes_groups_plot( + adata, + plot_type="heatmap", + groups=groups, + n_genes=n_genes, + gene_symbols=gene_symbols, + groupby=groupby, + var_names=var_names, + key=key, + min_logfoldchange=min_logfoldchange, + show=show, + save=save, + **kwds, + ) + + +@old_positionals( + "n_genes", + "groupby", + "var_names", + "gene_symbols", + "min_logfoldchange", + "key", + "show", + "save", +) +@_doc_params(params=doc_rank_genes_groups_plot_args, show_save_ax=doc_show_save_ax) +def rank_genes_groups_tracksplot( + adata: AnnData, + groups: str | Sequence[str] | None = None, + *, + n_genes: int | None = None, + groupby: str | None = None, + var_names: Sequence[str] | Mapping[str, Sequence[str]] | None = None, + gene_symbols: str | None = None, + min_logfoldchange: float | None = None, + key: str | None = None, + show: bool | None = None, + save: bool | None = None, + **kwds, +): + """Plot ranking of genes using heatmap plot (see :func:`~scanpy.pl.heatmap`). + + Parameters + ---------- + {params} + {show_save_ax} + **kwds + Are passed to :func:`~scanpy.pl.tracksplot`. + + Examples + -------- + + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc68k_reduced() + sc.tl.rank_genes_groups(adata, 'bulk_labels') + sc.pl.rank_genes_groups_tracksplot(adata) + + """ + return _rank_genes_groups_plot( + adata, + plot_type="tracksplot", + groups=groups, + n_genes=n_genes, + var_names=var_names, + gene_symbols=gene_symbols, + groupby=groupby, + key=key, + min_logfoldchange=min_logfoldchange, + show=show, + save=save, + **kwds, + ) + + +@old_positionals( + "n_genes", + "groupby", + "values_to_plot", + "var_names", + "gene_symbols", + "min_logfoldchange", + "key", + "show", + "save", + "return_fig", +) +@_doc_params( + params=doc_rank_genes_groups_plot_args, + vals_to_plot=doc_rank_genes_groups_values_to_plot, + show_save_ax=doc_show_save_ax, +) +def rank_genes_groups_dotplot( # noqa: PLR0913 + adata: AnnData, + groups: str | Sequence[str] | None = None, + *, + n_genes: int | None = None, + groupby: str | None = None, + values_to_plot: Literal[ + "scores", + "logfoldchanges", + "pvals", + "pvals_adj", + "log10_pvals", + "log10_pvals_adj", + ] + | None = None, + var_names: Sequence[str] | Mapping[str, Sequence[str]] | None = None, + gene_symbols: str | None = None, + min_logfoldchange: float | None = None, + key: str | None = None, + show: bool | None = None, + save: bool | None = None, + return_fig: bool = False, + **kwds, +): + """Plot ranking of genes using dotplot plot (see :func:`~scanpy.pl.dotplot`). + + Parameters + ---------- + {params} + {vals_to_plot} + {show_save_ax} + return_fig + Returns :class:`DotPlot` object. Useful for fine-tuning + the plot. Takes precedence over `show=False`. + **kwds + Are passed to :func:`~scanpy.pl.dotplot`. + + Returns + ------- + If `return_fig` is `True`, returns a :class:`DotPlot` object, + else if `show` is false, return axes dict + + Examples + -------- + + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc68k_reduced() + sc.tl.rank_genes_groups(adata, 'bulk_labels', n_genes=adata.raw.shape[1]) + + Plot top 2 genes per group. + + .. plot:: + :context: close-figs + + sc.pl.rank_genes_groups_dotplot(adata,n_genes=2) + + Plot with scaled expressions for easier identification of differences. + + .. plot:: + :context: close-figs + + sc.pl.rank_genes_groups_dotplot(adata, n_genes=2, standard_scale='var') + + Plot `logfoldchanges` instead of gene expression. In this case a diverging colormap + like `bwr` or `seismic` works better. To center the colormap in zero, the minimum + and maximum values to plot are set to -4 and 4 respectively. + Also, only genes with a log fold change of 3 or more are shown. + + .. plot:: + :context: close-figs + + sc.pl.rank_genes_groups_dotplot( + adata, + n_genes=4, + values_to_plot="logfoldchanges", cmap='bwr', + vmin=-4, + vmax=4, + min_logfoldchange=3, + colorbar_title='log fold change' + ) + + Also, the last genes can be plotted. This can be useful to identify genes + that are lowly expressed in a group. For this `n_genes=-4` is used + + .. plot:: + :context: close-figs + + sc.pl.rank_genes_groups_dotplot( + adata, + n_genes=-4, + values_to_plot="logfoldchanges", + cmap='bwr', + vmin=-4, + vmax=4, + min_logfoldchange=3, + colorbar_title='log fold change', + ) + + A list specific genes can be given to check their log fold change. If a + dictionary, the dictionary keys will be added as labels in the plot. + + .. plot:: + :context: close-figs + + var_names = {{'T-cell': ['CD3D', 'CD3E', 'IL32'], + 'B-cell': ['CD79A', 'CD79B', 'MS4A1'], + 'myeloid': ['CST3', 'LYZ'] }} + sc.pl.rank_genes_groups_dotplot( + adata, + var_names=var_names, + values_to_plot="logfoldchanges", + cmap='bwr', + vmin=-4, + vmax=4, + min_logfoldchange=3, + colorbar_title='log fold change', + ) + + .. currentmodule:: scanpy + + See Also + -------- + tl.rank_genes_groups + + """ + return _rank_genes_groups_plot( + adata, + plot_type="dotplot", + groups=groups, + n_genes=n_genes, + groupby=groupby, + values_to_plot=values_to_plot, + var_names=var_names, + gene_symbols=gene_symbols, + key=key, + min_logfoldchange=min_logfoldchange, + show=show, + save=save, + return_fig=return_fig, + **kwds, + ) + + +@old_positionals("n_genes", "groupby", "gene_symbols") +@_doc_params(params=doc_rank_genes_groups_plot_args, show_save_ax=doc_show_save_ax) +def rank_genes_groups_stacked_violin( # noqa: PLR0913 + adata: AnnData, + groups: str | Sequence[str] | None = None, + *, + n_genes: int | None = None, + groupby: str | None = None, + gene_symbols: str | None = None, + var_names: Sequence[str] | Mapping[str, Sequence[str]] | None = None, + min_logfoldchange: float | None = None, + key: str | None = None, + show: bool | None = None, + save: bool | None = None, + return_fig: bool = False, + **kwds, +): + """Plot ranking of genes using stacked_violin plot. + + (See :func:`~scanpy.pl.stacked_violin`) + + Parameters + ---------- + {params} + {show_save_ax} + return_fig + Returns :class:`StackedViolin` object. Useful for fine-tuning + the plot. Takes precedence over `show=False`. + **kwds + Are passed to :func:`~scanpy.pl.stacked_violin`. + + Returns + ------- + If `return_fig` is `True`, returns a :class:`StackedViolin` object, + else if `show` is false, return axes dict + + Examples + -------- + >>> import scanpy as sc + >>> adata = sc.datasets.pbmc68k_reduced() + >>> sc.tl.rank_genes_groups(adata, "bulk_labels") + + >>> sc.pl.rank_genes_groups_stacked_violin( + ... adata, n_genes=4, min_logfoldchange=4, figsize=(8, 6) + ... ) + + """ + return _rank_genes_groups_plot( + adata, + plot_type="stacked_violin", + groups=groups, + n_genes=n_genes, + gene_symbols=gene_symbols, + groupby=groupby, + var_names=var_names, + key=key, + min_logfoldchange=min_logfoldchange, + show=show, + save=save, + return_fig=return_fig, + **kwds, + ) + + +@old_positionals( + "n_genes", + "groupby", + "values_to_plot", + "var_names", + "gene_symbols", + "min_logfoldchange", + "key", + "show", + "save", + "return_fig", +) +@_doc_params( + params=doc_rank_genes_groups_plot_args, + vals_to_plot=doc_rank_genes_groups_values_to_plot, + show_save_ax=doc_show_save_ax, +) +def rank_genes_groups_matrixplot( # noqa: PLR0913 + adata: AnnData, + groups: str | Sequence[str] | None = None, + *, + n_genes: int | None = None, + groupby: str | None = None, + values_to_plot: Literal[ + "scores", + "logfoldchanges", + "pvals", + "pvals_adj", + "log10_pvals", + "log10_pvals_adj", + ] + | None = None, + var_names: Sequence[str] | Mapping[str, Sequence[str]] | None = None, + gene_symbols: str | None = None, + min_logfoldchange: float | None = None, + key: str | None = None, + show: bool | None = None, + save: bool | None = None, + return_fig: bool = False, + **kwds, +): + """Plot ranking of genes using matrixplot plot (see :func:`~scanpy.pl.matrixplot`). + + Parameters + ---------- + {params} + {vals_to_plot} + {show_save_ax} + return_fig + Returns :class:`MatrixPlot` object. Useful for fine-tuning + the plot. Takes precedence over `show=False`. + **kwds + Are passed to :func:`~scanpy.pl.matrixplot`. + + Returns + ------- + If `return_fig` is `True`, returns a :class:`MatrixPlot` object, + else if `show` is false, return axes dict + + Examples + -------- + + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc68k_reduced() + sc.tl.rank_genes_groups(adata, 'bulk_labels', n_genes=adata.raw.shape[1]) + + Plot `logfoldchanges` instead of gene expression. In this case a diverging colormap + like `bwr` or `seismic` works better. To center the colormap in zero, the minimum + and maximum values to plot are set to -4 and 4 respectively. + Also, only genes with a log fold change of 3 or more are shown. + + + .. plot:: + :context: close-figs + + sc.pl.rank_genes_groups_matrixplot( + adata, + n_genes=4, + values_to_plot="logfoldchanges", + cmap='bwr', + vmin=-4, + vmax=4, + min_logfoldchange=3, + colorbar_title='log fold change', + ) + + Also, the last genes can be plotted. This can be useful to identify genes + that are lowly expressed in a group. For this `n_genes=-4` is used + + .. plot:: + :context: close-figs + + sc.pl.rank_genes_groups_matrixplot( + adata, + n_genes=-4, + values_to_plot="logfoldchanges", + cmap='bwr', + vmin=-4, + vmax=4, + min_logfoldchange=3, + colorbar_title='log fold change', + ) + + A list specific genes can be given to check their log fold change. If a + dictionary, the dictionary keys will be added as labels in the plot. + + .. plot:: + :context: close-figs + + var_names = {{"T-cell": ['CD3D', 'CD3E', 'IL32'], + 'B-cell': ['CD79A', 'CD79B', 'MS4A1'], + 'myeloid': ['CST3', 'LYZ'] }} + sc.pl.rank_genes_groups_matrixplot( + adata, + var_names=var_names, + values_to_plot="logfoldchanges", + cmap='bwr', + vmin=-4, + vmax=4, + min_logfoldchange=3, + colorbar_title='log fold change', + ) + + """ + return _rank_genes_groups_plot( + adata, + plot_type="matrixplot", + groups=groups, + n_genes=n_genes, + groupby=groupby, + values_to_plot=values_to_plot, + var_names=var_names, + gene_symbols=gene_symbols, + key=key, + min_logfoldchange=min_logfoldchange, + show=show, + save=save, + return_fig=return_fig, + **kwds, + ) + + +@old_positionals( + "n_genes", + "gene_names", + "gene_symbols", + "use_raw", + "key", + "split", + "density_norm", + "strip", + "jitter", + "size", + "ax", + "show", + "save", +) +@_doc_params(show_save_ax=doc_show_save_ax) +def rank_genes_groups_violin( # noqa: PLR0913 + adata: AnnData, + groups: Sequence[str] | None = None, + *, + n_genes: int = 20, + gene_names: Iterable[str] | None = None, + gene_symbols: str | None = None, + use_raw: bool | None = None, + key: str | None = None, + split: bool = True, + density_norm: DensityNorm = "width", + strip: bool = True, + jitter: float | bool = True, + size: int = 1, + ax: Axes | None = None, + show: bool | None = None, + save: bool | None = None, + # deprecated + scale: DensityNorm | Empty = _empty, +): + """Plot ranking of genes for all tested comparisons. + + Parameters + ---------- + adata + Annotated data matrix. + groups + List of group names. + n_genes + Number of genes to show. Is ignored if `gene_names` is passed. + gene_names + List of genes to plot. Is only useful if interested in a custom gene list, + which is not the result of :func:`scanpy.tl.rank_genes_groups`. + gene_symbols + Key for field in `.var` that stores gene symbols if you do not want to + use `.var_names` displayed in the plot. + use_raw + Use `raw` attribute of `adata` if present. Defaults to the value that + was used in :func:`~scanpy.tl.rank_genes_groups`. + split + Whether to split the violins or not. + density_norm + See :func:`~seaborn.violinplot`. + strip + Show a strip plot on top of the violin plot. + jitter + If set to 0, no points are drawn. See :func:`~seaborn.stripplot`. + size + Size of the jitter points. + {show_save_ax} + + """ + if key is None: + key = "rank_genes_groups" + groups_key = str(adata.uns[key]["params"]["groupby"]) + if use_raw is None: + use_raw = bool(adata.uns[key]["params"]["use_raw"]) + reference = str(adata.uns[key]["params"]["reference"]) + groups_names = adata.uns[key]["names"].dtype.names if groups is None else groups + if isinstance(groups_names, str): + groups_names = [groups_names] + density_norm = _deprecated_scale(density_norm, scale, default="width") + del scale + axs = [] + for group_name in groups_names: + if gene_names is None: + _gene_names = adata.uns[key]["names"][group_name][:n_genes] + else: + _gene_names = gene_names + if isinstance(_gene_names, np.ndarray): + _gene_names = _gene_names.tolist() + df = obs_df(adata, _gene_names, use_raw=use_raw, gene_symbols=gene_symbols) + new_gene_names = df.columns + df["hue"] = adata.obs[groups_key].astype(str).values + if reference == "rest": + df.loc[df["hue"] != group_name, "hue"] = "rest" + else: + df.loc[~df["hue"].isin([group_name, reference]), "hue"] = np.nan + df["hue"] = df["hue"].astype("category") + df_tidy = pd.melt(df, id_vars="hue", value_vars=new_gene_names) + x = "variable" + y = "value" + hue_order = [group_name, reference] + import seaborn as sns + + _ax = sns.violinplot( + x=x, + y=y, + data=df_tidy, + inner=None, + hue_order=hue_order, + hue="hue", + split=split, + density_norm=density_norm, + orient="vertical", + ax=ax, + ) + if strip: + _ax = sns.stripplot( + x=x, + y=y, + data=df_tidy, + hue="hue", + dodge=True, + hue_order=hue_order, + jitter=jitter, + palette="dark:black", + size=size, + ax=_ax, + ) + _ax.set_xlabel("genes") + _ax.set_title(f"{group_name} vs. {reference}") + _ax.legend_.remove() + _ax.set_ylabel("expression") + _ax.set_xticklabels(new_gene_names, rotation="vertical") + writekey = ( + f"rank_genes_groups_{adata.uns[key]['params']['groupby']}_{group_name}" + ) + savefig_or_show(writekey, show=show, save=save) + axs.append(_ax) + show = settings.autoshow if show is None else show + if show: + return None + return axs + + +@old_positionals("tmax_realization", "as_heatmap", "shuffle", "show", "save", "marker") +def sim( + adata: AnnData, + *, + tmax_realization: int | None = None, + as_heatmap: bool = False, + shuffle: bool = False, + show: bool | None = None, + save: bool | str | None = None, + marker: str | Sequence[str] = ".", +) -> None: + """Plot results of simulation. + + Parameters + ---------- + tmax_realization + Number of observations in one realization of the time series. The data matrix + adata.X consists in concatenated realizations. + as_heatmap + Plot the timeseries as heatmap. + shuffle + Shuffle the data. + show + Show the plot, do not return axis. + save + If `True` or a `str`, save the figure. + A string is appended to the default filename. + Infer the filetype if ending on {{`'.pdf'`, `'.png'`, `'.svg'`}}. + + """ + if tmax_realization is not None: + tmax = tmax_realization + elif "tmax_write" in adata.uns: + tmax = adata.uns["tmax_write"] + else: + tmax = adata.n_obs + n_realizations = adata.n_obs / tmax + if not shuffle: + if not as_heatmap: + timeseries( + adata.X, + var_names=adata.var_names, + xlim=[0, 1.25 * adata.n_obs], + highlights_x=np.arange(tmax, n_realizations * tmax, tmax), + xlabel="realizations", + marker=marker, + ) + else: + # plot time series as heatmap, as in Haghverdi et al. (2016), Fig. 1d + timeseries_as_heatmap( + adata.X, + var_names=adata.var_names, + highlights_x=np.arange(tmax, n_realizations * tmax, tmax), + ) + plt.xticks( + np.arange(0, n_realizations * tmax, tmax), + np.arange(n_realizations).astype(int) + 1, + ) + savefig_or_show("sim", save=save, show=show) + else: # shuffle data + np.random.seed(1) + rows = np.random.choice(adata.shape[0], size=adata.shape[0], replace=False) + x = adata[rows].X + timeseries( + x, + var_names=adata.var_names, + xlim=[0, 1.25 * adata.n_obs], + highlights_x=np.arange(tmax, n_realizations * tmax, tmax), + xlabel="index (arbitrary order)", + marker=marker, + ) + savefig_or_show("sim_shuffled", save=save, show=show) + + +@old_positionals( + "key", + "groupby", + "group", + "color_map", + "bg_dotsize", + "fg_dotsize", + "vmax", + "vmin", + "vcenter", + "norm", + "ncols", + "hspace", + "wspace", + "title", + "show", + "save", + "ax", + "return_fig", +) +@_doc_params( + vminmax=doc_vbound_percentile, panels=doc_panels, show_save_ax=doc_show_save_ax +) +def embedding_density( # noqa: PLR0912, PLR0913, PLR0915 + adata: AnnData, + basis: str = "umap", + *, + key: str | None = None, + groupby: str | None = None, + group: str | Sequence[str] | None = "all", + color_map: Colormap | str = "YlOrRd", + bg_dotsize: int | None = 80, + fg_dotsize: int | None = 180, + vmax: int | None = 1, + vmin: int | None = 0, + vcenter: int | None = None, + norm: Normalize | None = None, + ncols: int | None = 4, + hspace: float | None = 0.25, + wspace: None = None, + title: str | None = None, + show: bool | None = None, + save: bool | str | None = None, + ax: Axes | None = None, + return_fig: bool | None = None, + **kwargs, +) -> Figure | Axes | None: + """Plot the density of cells in an embedding (per condition). + + Plots the gaussian kernel density estimates (over condition) from the + `sc.tl.embedding_density()` output. + + This function was written by Sophie Tritschler and implemented into + Scanpy by Malte Luecken. + + Parameters + ---------- + adata + The annotated data matrix. + basis + The embedding over which the density was calculated. This embedded + representation should be found in `adata.obsm['X_[basis]']``. + key + Name of the `.obs` covariate that contains the density estimates. Alternatively, pass `groupby`. + groupby + Name of the condition used in `tl.embedding_density`. Alternatively, pass `key`. + group + The category in the categorical observation annotation to be plotted. + For example, 'G1' in the cell cycle 'phase' covariate. If all categories + are to be plotted use group='all' (default), If multiple categories + want to be plotted use a list (e.g.: ['G1', 'S']. If the overall density + wants to be ploted set group to 'None'. + color_map + Matplolib color map to use for density plotting. + bg_dotsize + Dot size for background data points not in the `group`. + fg_dotsize + Dot size for foreground data points in the `group`. + {vminmax} + {panels} + {show_save_ax} + + Examples + -------- + + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc68k_reduced() + sc.tl.umap(adata) + sc.tl.embedding_density(adata, basis='umap', groupby='phase') + + Plot all categories be default + + .. plot:: + :context: close-figs + + sc.pl.embedding_density(adata, basis='umap', key='umap_density_phase') + + Plot selected categories + + .. plot:: + :context: close-figs + + sc.pl.embedding_density( + adata, + basis='umap', + key='umap_density_phase', + group=['G1', 'S'], + ) + + .. currentmodule:: scanpy + + See Also + -------- + tl.embedding_density + + """ + sanitize_anndata(adata) + + # Test user inputs + basis = basis.lower() + + if basis == "fa": + basis = "draw_graph_fa" + + if key is not None and groupby is not None: + msg = "either pass key or groupby but not both" + raise ValueError(msg) + + if key is None: + key = "umap_density" + if groupby is not None: + key += f"_{groupby}" + + if f"X_{basis}" not in adata.obsm: + msg = ( + f"Cannot find the embedded representation `adata.obsm['X_{basis}']`. " + "Compute the embedding first." + ) + raise ValueError(msg) + + if key not in adata.obs or f"{key}_params" not in adata.uns: + msg = ( + "Please run `sc.tl.embedding_density()` first and specify the correct key." + ) + raise ValueError(msg) + + if "components" in kwargs: + logg.warning( + "Components were specified, but will be ignored. Only the " + "components used to calculate the density can be plotted." + ) + del kwargs["components"] + + components = adata.uns[f"{key}_params"]["components"] + groupby = adata.uns[f"{key}_params"]["covariate"] + + # turn group into a list if needed + if group == "all": + group = None if groupby is None else list(adata.obs[groupby].cat.categories) + elif isinstance(group, str): + group = [group] + + if group is None and groupby is not None: + msg = ( + "Densities were calculated over an `.obs` covariate. " + "Please specify a group from this covariate to plot." + ) + raise ValueError(msg) + + if group is not None and groupby is None: + logg.warning( + "value of 'group' is ignored because densities " + "were not calculated for an `.obs` covariate." + ) + group = None + + if np.min(adata.obs[key]) < 0 or np.max(adata.obs[key]) > 1: + msg = "Densities should be scaled between 0 and 1." + raise ValueError(msg) + + if wspace is None: + # try to set a wspace that is not too large or too small given the + # current figure size + wspace = 0.75 / rcParams["figure.figsize"][0] + 0.02 + + # Make the color map + if isinstance(color_map, str): + color_map = copy(colormaps.get_cmap(color_map)) + + color_map.set_over("black") + color_map.set_under("lightgray") + # a name to store the density values is needed. To avoid + # overwriting a user name a new random name is created + while True: + col_id = np.random.randint(1000, 10000) + density_col_name = f"_tmp_embedding_density_column_{col_id}_" + if density_col_name not in adata.obs.columns: + break + + # if group is set, then plot it using multiple panels + # (even if only one group is set) + if group is not None and not isinstance(group, str) and isinstance(group, Sequence): + if ax is not None: + msg = "Can only specify `ax` if no `group` sequence is given." + raise ValueError(msg) + fig, gs = _panel_grid(hspace, wspace, ncols, len(group)) + + axs = [] + for count, group_name in enumerate(group): + if group_name not in adata.obs[groupby].cat.categories: + msg = ( + "Please specify a group from the `.obs` category " + "over which the density was calculated. " + f"Invalid group name: {group_name}" + ) + raise ValueError(msg) + + ax = plt.subplot(gs[count]) + # Define plotting data + dot_sizes = np.ones(adata.n_obs) * bg_dotsize + group_mask = adata.obs[groupby] == group_name + dens_values = -np.ones(adata.n_obs) + dens_values[group_mask] = adata.obs[key][group_mask] + adata.obs[density_col_name] = dens_values + dot_sizes[group_mask] = np.ones(sum(group_mask)) * fg_dotsize + + _title = group_name if title is None else title + + ax = embedding( + adata, + basis, + dimensions=np.array(components) - 1, # Saved with 1 based indexing + color=density_col_name, + color_map=color_map, + size=dot_sizes, + vmax=vmax, + vmin=vmin, + vcenter=vcenter, + norm=norm, + save=False, + title=_title, + ax=ax, + show=False, + **kwargs, + ) + axs.append(ax) + + ax = axs + else: + dens_values = adata.obs[key] + dot_sizes = np.ones(adata.n_obs) * fg_dotsize + + adata.obs[density_col_name] = dens_values + + # Ensure title is blank as default + if title is None: + title = group if group is not None else "" + + # Plot the graph + fig_or_ax = embedding( + adata, + basis, + dimensions=np.array(components) - 1, # Saved with 1 based indexing + color=density_col_name, + color_map=color_map, + size=dot_sizes, + vmax=vmax, + vmin=vmin, + vcenter=vcenter, + norm=norm, + save=False, + show=False, + title=title, + ax=ax, + return_fig=return_fig, + **kwargs, + ) + if return_fig: + fig = fig_or_ax + else: + ax = fig_or_ax + + # remove temporary column name + adata.obs = adata.obs.drop(columns=[density_col_name]) + + if return_fig: + return fig + savefig_or_show(f"{key}_", show=show, save=save) + show = settings.autoshow if show is None else show + if show: + return None + return ax + + +def _get_values_to_plot( + adata, + values_to_plot: Literal[ + "scores", + "logfoldchanges", + "pvals", + "pvals_adj", + "log10_pvals", + "log10_pvals_adj", + ], + gene_names: Sequence[str], + *, + groups: Sequence[str] | None = None, + key: str | None = "rank_genes_groups", + gene_symbols: str | None = None, +): + """Prepare a dataframe to be plotted as dotplot or matrixplot. + + The specified `values_to_plot` stem from `rank_genes_groups`. + + The dataframe `index` are the given groups and the `columns` are the `gene_names`. + + (used by `rank_genes_groups_dotplot`) + + Parameters + ---------- + adata + values_to_plot + name of the value to plot + gene_names + gene names + groups + groupby categories + key + adata.uns key where the rank_genes_groups is stored. + By default 'rank_genes_groups' + gene_symbols + Key for field in .var that stores gene symbols. + + Returns + ------- + pandas DataFrame index=groups, columns=gene_names + + """ + valid_options = [ + "scores", + "logfoldchanges", + "pvals", + "pvals_adj", + "log10_pvals", + "log10_pvals_adj", + ] + if values_to_plot not in valid_options: + msg = f"given value_to_plot: '{values_to_plot}' is not valid. Valid options are {valid_options}" + raise ValueError(msg) + + values_df = None + check_done = False + if groups is None: + groups = adata.uns[key]["names"].dtype.names + if values_to_plot is not None: + df_list = [] + for group in groups: + df = rank_genes_groups_df(adata, group, key=key, gene_symbols=gene_symbols) + if gene_symbols is not None: + df["names"] = df[gene_symbols] + # check that all genes are present in the df as sc.tl.rank_genes_groups + # can be called with only top genes + if not check_done and df.shape[0] < adata.shape[1]: + message = ( + "Please run `sc.tl.rank_genes_groups` with " + "'n_genes=adata.shape[1]' to save all gene " + f"scores. Currently, only {df.shape[0]} " + "are found" + ) + logg.error(message) + raise ValueError(message) + df["group"] = group + df_list.append(df) + + values_df = pd.concat(df_list) + if values_to_plot.startswith("log10"): + column = values_to_plot.replace("log10_", "") + else: + column = values_to_plot + values_df = pd.pivot( + values_df, index="names", columns="group", values=column + ).fillna(1) + + if values_to_plot in ["log10_pvals", "log10_pvals_adj"]: + values_df = -1 * np.log10(values_df) + + values_df = values_df.loc[gene_names].T + + return values_df diff --git a/.venv/lib/python3.11/site-packages/scanpy/plotting/_tools/paga.py b/.venv/lib/python3.11/site-packages/scanpy/plotting/_tools/paga.py new file mode 100644 index 0000000000000000000000000000000000000000..c31ae94a7c352e28a798293c60d4616a92a2ef47 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/plotting/_tools/paga.py @@ -0,0 +1,1382 @@ +from __future__ import annotations + +import warnings +from collections.abc import Collection, Mapping, Sequence +from pathlib import Path +from types import MappingProxyType +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd +import scipy +from matplotlib import patheffects, rcParams, ticker +from matplotlib import pyplot as plt +from matplotlib.colors import is_color_like +from pandas.api.types import CategoricalDtype +from sklearn.utils import check_random_state + +from scanpy.tools._draw_graph import coerce_fa2_layout, fa2_positions + +from ... import _utils as _sc_utils +from ... import logging as logg +from ..._compat import CSBase, old_positionals +from ..._settings import settings +from .. import _utils +from .._utils import matrix + +if TYPE_CHECKING: + from typing import Any, Literal + + from anndata import AnnData + from matplotlib.axes import Axes + from matplotlib.colors import Colormap + + from ..._compat import SpBase + from ..._utils.random import _LegacyRandom + from ...tools._draw_graph import _Layout as _LayoutWithoutEqTree + from .._utils import _FontSize, _FontWeight, _LegendLoc + + _Layout = _LayoutWithoutEqTree | Literal["eq_tree"] + + +@old_positionals( + "edges", + "color", + "alpha", + "groups", + "components", + "projection", + "legend_loc", + "legend_fontsize", + "legend_fontweight", + "legend_fontoutline", + "color_map", + "palette", + "frameon", + "size", + "title", + "right_margin", + "left_margin", + "show", + "save", + "title_graph", + "groups_graph", +) +def paga_compare( # noqa: PLR0912, PLR0913 + adata: AnnData, + basis=None, + *, + edges=False, + color=None, + alpha=None, + groups=None, + components=None, + projection: Literal["2d", "3d"] = "2d", + legend_loc: _LegendLoc | None = "on data", + legend_fontsize: float | _FontSize | None = None, + legend_fontweight: int | _FontWeight = "bold", + legend_fontoutline=None, + color_map=None, + palette=None, + frameon=False, + size=None, + title=None, + right_margin=None, + left_margin=0.05, + show=None, + save=None, + title_graph=None, + groups_graph=None, + pos=None, + **paga_graph_params, +): + """Scatter and PAGA graph side-by-side. + + Consists in a scatter plot and the abstracted graph. See + :func:`~scanpy.pl.paga` for all related parameters. + + See :func:`~scanpy.pl.paga_path` for visualizing gene changes along paths + through the abstracted graph. + + Additional parameters are as follows. + + Parameters + ---------- + adata + Annotated data matrix. + kwds_scatter + Keywords for :func:`~scanpy.pl.scatter`. + kwds_paga + Keywords for :func:`~scanpy.pl.paga`. + + Returns + ------- + A list of :class:`~matplotlib.axes.Axes` if `show` is `False`. + + """ + axs, _, _, _ = _utils.setup_axes(panels=[0, 1], right_margin=right_margin) + if color is None: + color = adata.uns["paga"]["groups"] + suptitle = None # common title for entire figure + if title_graph is None: + suptitle = color if title is None else title + title, title_graph = "", "" + if basis is None: + if "X_draw_graph_fa" in adata.obsm: + basis = "draw_graph_fa" + elif "X_umap" in adata.obsm: + basis = "umap" + elif "X_tsne" in adata.obsm: + basis = "tsne" + elif "X_draw_graph_fr" in adata.obsm: + basis = "draw_graph_fr" + else: + basis = "umap" + + from .scatterplots import _components_to_dimensions, _get_basis, embedding + + embedding( + adata, + ax=axs[0], + basis=basis, + color=color, + edges=edges, + alpha=alpha, + groups=groups, + components=components, + legend_loc=legend_loc, + legend_fontsize=legend_fontsize, + legend_fontweight=legend_fontweight, + legend_fontoutline=legend_fontoutline, + color_map=color_map, + palette=palette, + frameon=frameon, + size=size, + title=title, + show=False, + save=False, + ) + + if pos is None: + if color == adata.uns["paga"]["groups"]: + # TODO: Use dimensions here + _basis = _get_basis(adata, basis) + dims = _components_to_dimensions( + components=components, dimensions=None, total_dims=_basis.shape[1] + )[0] + coords = _basis[:, dims] + pos = ( + pd.DataFrame(coords, columns=["x", "y"], index=adata.obs_names) + .groupby(adata.obs[color], observed=True) + .median() + .sort_index() + ).to_numpy() + else: + pos = adata.uns["paga"]["pos"] + xlim, ylim = axs[0].get_xlim(), axs[0].get_ylim() + axs[1].set_xlim(xlim) + axs[1].set_ylim(ylim) + if "labels" in paga_graph_params: + labels = paga_graph_params.pop("labels") + else: + labels = groups_graph + if legend_fontsize is not None: + paga_graph_params["fontsize"] = legend_fontsize + if legend_fontweight is not None: + paga_graph_params["fontweight"] = legend_fontweight + if legend_fontoutline is not None: + paga_graph_params["fontoutline"] = legend_fontoutline + paga( + adata, + ax=axs[1], + show=False, + save=False, + title=title_graph, + labels=labels, + colors=color, + frameon=frameon, + pos=pos, + **paga_graph_params, + ) + if suptitle is not None: + plt.suptitle(suptitle) + _utils.savefig_or_show("paga_compare", show=show, save=save) + if show: + return None + return axs + + +def _compute_pos( # noqa: PLR0912 + adjacency_solid: SpBase | np.ndarray, + *, + layout: _Layout | None = None, + random_state: _LegacyRandom = 0, + init_pos: np.ndarray | None = None, + adj_tree=None, + root: int = 0, + layout_kwds: Mapping[str, Any] = MappingProxyType({}), +): + import random + + import networkx as nx + + random_state = check_random_state(random_state) + + nx_g_solid = nx.Graph(adjacency_solid) + if layout is None: + layout = "fr" + layout = coerce_fa2_layout(layout) + if layout == "fa": + # np.random.seed(random_state) + if init_pos is None: + init_coords = random_state.random_sample((adjacency_solid.shape[0], 2)) + else: + init_coords = init_pos.copy() + pos_list = fa2_positions(adjacency_solid, init_coords, **layout_kwds) + pos = {n: (x, -y) for n, (x, y) in enumerate(pos_list)} + elif layout == "eq_tree": + nx_g_tree = nx.Graph(adj_tree) + pos = _utils.hierarchy_pos(nx_g_tree, root) + if len(pos) < adjacency_solid.shape[0]: + msg = ( + "This is a forest and not a single tree. " + "Try another `layout`, e.g., {'fr'}." + ) + raise ValueError(msg) + else: + # igraph layouts + random.seed(random_state.bytes(8)) + g = _sc_utils.get_igraph_from_adjacency(adjacency_solid) + if "rt" in layout: + g_tree = _sc_utils.get_igraph_from_adjacency(adj_tree) + pos_list = g_tree.layout( + layout, root=root if isinstance(root, list) else [root] + ).coords + elif layout == "circle": + pos_list = g.layout(layout).coords + else: + # I don't know why this is necessary + # np.random.seed(random_state) + if init_pos is None: + init_coords = random_state.random_sample(( + adjacency_solid.shape[0], + 2, + )).tolist() + else: + init_pos = init_pos.copy() + # this is a super-weird hack that is necessary as igraph’s + # layout function seems to do some strange stuff here + init_pos[:, 1] *= -1 + init_coords = init_pos.tolist() + try: + pos_list = g.layout( + layout, seed=init_coords, weights="weight", **layout_kwds + ).coords + except AttributeError: # hack for empty graphs... + pos_list = g.layout(layout, seed=init_coords, **layout_kwds).coords + pos = {n: (x, -y) for n, (x, y) in enumerate(pos_list)} + if len(pos) == 1: + pos[0] = (0.5, 0.5) + pos_array = np.array([pos[n] for count, n in enumerate(nx_g_solid)]) + return pos_array + + +@old_positionals( + "threshold", + "color", + "layout", + "layout_kwds", + "init_pos", + "root", + "labels", + "single_component", + "solid_edges", + "dashed_edges", + "transitions", + "fontsize", + "fontweight", + "fontoutline", + "text_kwds", + "node_size_scale", + # 17 positionals are enough for backwards compat +) +def paga( # noqa: PLR0912, PLR0913, PLR0915 + adata: AnnData, + *, + threshold: float | None = None, + color: str | Mapping[str | int, Mapping[Any, float]] | None = None, + layout: _Layout | None = None, + layout_kwds: Mapping[str, Any] = MappingProxyType({}), + init_pos: np.ndarray | None = None, + root: int | str | Sequence[int] | None = 0, + labels: str | Sequence[str] | Mapping[str, str] | None = None, + single_component: bool = False, + solid_edges: str = "connectivities", + dashed_edges: str | None = None, + transitions: str | None = None, + fontsize: int | None = None, + fontweight: str = "bold", + fontoutline: int | None = None, + text_kwds: Mapping[str, Any] = MappingProxyType({}), + node_size_scale: float = 1.0, + node_size_power: float = 0.5, + edge_width_scale: float = 1.0, + min_edge_width: float | None = None, + max_edge_width: float | None = None, + arrowsize: int = 30, + title: str | None = None, + left_margin: float = 0.01, + random_state: int | None = 0, + pos: np.ndarray | Path | str | None = None, + normalize_to_color: bool = False, + cmap: str | Colormap | None = None, + cax: Axes | None = None, + colorbar=None, # TODO: this seems to be unused + cb_kwds: Mapping[str, Any] = MappingProxyType({}), + frameon: bool | None = None, + add_pos: bool = True, + export_to_gexf: bool = False, + use_raw: bool = True, + colors=None, # backwards compat + groups=None, # backwards compat + plot: bool = True, + show: bool | None = None, + save: bool | str | None = None, + ax: Axes | None = None, +) -> Axes | list[Axes] | None: + r"""Plot the PAGA graph through thresholding low-connectivity edges. + + Compute a coarse-grained layout of the data. Reuse this by passing + `init_pos='paga'` to :func:`~scanpy.tl.umap` or + :func:`~scanpy.tl.draw_graph` and obtain embeddings with more meaningful + global topology :cite:p:`Wolf2019`. + + This uses ForceAtlas2 or igraph's layout algorithms for most layouts :cite:p:`Csardi2006`. + + Parameters + ---------- + adata + Annotated data matrix. + threshold + Do not draw edges for weights below this threshold. Set to 0 if you want + all edges. Discarding low-connectivity edges helps in getting a much + clearer picture of the graph. + color + Gene name or `obs` annotation defining the node colors. + Also plots the degree of the abstracted graph when + passing {`'degree_dashed'`, `'degree_solid'`}. + + Can be also used to visualize pie chart at each node in the following form: + `{: {: , ...}, ...}`. If the fractions + do not sum to 1, a new category called `'rest'` colored grey will be created. + labels + The node labels. If `None`, this defaults to the group labels stored in + the categorical for which :func:`~scanpy.tl.paga` has been computed. + pos + Two-column array-like storing the x and y coordinates for drawing. + Otherwise, path to a `.gdf` file that has been exported from Gephi or + a similar graph visualization software. + layout + Plotting layout that computes positions. + `'fa'` stands for “ForceAtlas2”, + `'fr'` stands for “Fruchterman-Reingold”, + `'rt'` stands for “Reingold-Tilford”, + `'eq_tree'` stands for “eqally spaced tree”. + All but `'fa'` and `'eq_tree'` are igraph layouts. + All other igraph layouts are also permitted. + See also parameter `pos` and :func:`~scanpy.tl.draw_graph`. + layout_kwds + Keywords for the layout. + init_pos + Two-column array storing the x and y coordinates for initializing the + layout. + random_state + For layouts with random initialization like `'fr'`, change this to use + different intial states for the optimization. If `None`, the initial + state is not reproducible. + root + If choosing a tree layout, this is the index of the root node or a list + of root node indices. If this is a non-empty vector then the supplied + node IDs are used as the roots of the trees (or a single tree if the + graph is connected). If this is `None` or an empty list, the root + vertices are automatically calculated based on topological sorting. + transitions + Key for `.uns['paga']` that specifies the matrix that stores the + arrows, for instance `'transitions_confidence'`. + solid_edges + Key for `.uns['paga']` that specifies the matrix that stores the edges + to be drawn solid black. + dashed_edges + Key for `.uns['paga']` that specifies the matrix that stores the edges + to be drawn dashed grey. If `None`, no dashed edges are drawn. + single_component + Restrict to largest connected component. + fontsize + Font size for node labels. + fontoutline + Width of the white outline around fonts. + text_kwds + Keywords for :meth:`~matplotlib.axes.Axes.text`. + node_size_scale + Increase or decrease the size of the nodes. + node_size_power + The power with which groups sizes influence the radius of the nodes. + edge_width_scale + Edge with scale in units of `rcParams['lines.linewidth']`. + min_edge_width + Min width of solid edges. + max_edge_width + Max width of solid and dashed edges. + arrowsize + For directed graphs, choose the size of the arrow head head's length and + width. See :py:class: `matplotlib.patches.FancyArrowPatch` for attribute + `mutation_scale` for more info. + export_to_gexf + Export to gexf format to be read by graph visualization programs such as + Gephi. + normalize_to_color + Whether to normalize categorical plots to `color` or the underlying + grouping. + cmap + The color map. + cax + A matplotlib axes object for a potential colorbar. + cb_kwds + Keyword arguments for :class:`~matplotlib.colorbar.Colorbar`, + for instance, `ticks`. + add_pos + Add the positions to `adata.uns['paga']`. + title + Provide a title. + frameon + Draw a frame around the PAGA graph. + plot + If `False`, do not create the figure, simply compute the layout. + save + If `True` or a `str`, save the figure. + A string is appended to the default filename. + Infer the filetype if ending on \{`'.pdf'`, `'.png'`, `'.svg'`\}. + ax + A matplotlib axes object. + + Returns + ------- + If `show==False`, one or more :class:`~matplotlib.axes.Axes` objects. + Adds `'pos'` to `adata.uns['paga']` if `add_pos` is `True`. + + Examples + -------- + + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc3k_processed() + sc.tl.paga(adata, groups='louvain') + sc.pl.paga(adata) + + You can increase node and edge sizes by specifying additional arguments. + + .. plot:: + :context: close-figs + + sc.pl.paga(adata, node_size_scale=10, edge_width_scale=2) + + Notes + ----- + When initializing the positions, note that – for some reason – igraph + mirrors coordinates along the x axis... that is, you should increase the + `maxiter` parameter by 1 if the layout is flipped. + + .. currentmodule:: scanpy + + See Also + -------- + tl.paga + pl.paga_compare + pl.paga_path + + """ + if groups is not None: # backwards compat + labels = groups + logg.warning("`groups` is deprecated in `pl.paga`: use `labels` instead") + if colors is None: + colors = color + + groups_key = adata.uns["paga"]["groups"] + + def is_flat(x): + has_one_per_category = isinstance(x, Collection) and len(x) == len( + adata.obs[groups_key].cat.categories + ) + return has_one_per_category or x is None or isinstance(x, str) + + if isinstance(colors, Mapping) and isinstance(colors[next(iter(colors))], Mapping): + # handle paga pie, remap string keys to integers + names_to_ixs = { + n: i for i, n in enumerate(adata.obs[groups_key].cat.categories) + } + colors = {names_to_ixs.get(n, n): v for n, v in colors.items()} + if is_flat(colors): + colors = [colors] + + if frameon is None: + frameon = settings._frameon + # labels is a list that contains no lists + if is_flat(labels): + labels = [labels for _ in range(len(colors))] + + if title is None and len(colors) > 1: + title = list(colors) + elif isinstance(title, str): + title = [title] * len(colors) + elif title is None: + title = [None] * len(colors) + + if colorbar is None: + var_names = adata.var_names if adata.raw is None else adata.raw.var_names + colorbars = [ + ( + (c in adata.obs and adata.obs[c].dtype.name != "category") + or (c in var_names) + ) + for c in colors + ] + else: + colorbars = [False for _ in colors] + + if isinstance(root, str): + if root not in labels: + msg = f"If `root` is a string, it needs to be one of {labels} not {root!r}." + raise ValueError(msg) + root = list(labels).index(root) + if isinstance(root, Sequence) and root[0] in labels: + root = [list(labels).index(r) for r in root] + + # define the adjacency matrices + adjacency_solid = adata.uns["paga"][solid_edges].copy() + adjacency_dashed = None + if threshold is None: + threshold = 0.01 # default threshold + if threshold > 0: + adjacency_solid.data[adjacency_solid.data < threshold] = 0 + adjacency_solid.eliminate_zeros() + if dashed_edges is not None: + adjacency_dashed = adata.uns["paga"][dashed_edges].copy() + if threshold > 0: + adjacency_dashed.data[adjacency_dashed.data < threshold] = 0 + adjacency_dashed.eliminate_zeros() + + # compute positions + if pos is None: + adj_tree = None + if layout in {"rt", "rt_circular", "eq_tree"}: + adj_tree = adata.uns["paga"]["connectivities_tree"] + pos = _compute_pos( + adjacency_solid, + layout=layout, + random_state=random_state, + init_pos=init_pos, + layout_kwds=layout_kwds, + adj_tree=adj_tree, + root=root, + ) + + if plot: + axs, panel_pos, draw_region_width, _figure_width = _utils.setup_axes( + ax, panels=colors, colorbars=colorbars + ) + + if len(colors) == 1 and not isinstance(axs, list): + axs = [axs] + + for icolor, c in enumerate(colors): + if title[icolor] is not None: + axs[icolor].set_title(title[icolor]) + sct = _paga_graph( + adata, + axs[icolor], + colors=colors if isinstance(colors, Mapping) else c, + solid_edges=solid_edges, + dashed_edges=dashed_edges, + transitions=transitions, + threshold=threshold, + adjacency_solid=adjacency_solid, + adjacency_dashed=adjacency_dashed, + root=root, + labels=labels[icolor], + fontsize=fontsize, + fontweight=fontweight, + fontoutline=fontoutline, + text_kwds=text_kwds, + node_size_scale=node_size_scale, + node_size_power=node_size_power, + edge_width_scale=edge_width_scale, + min_edge_width=min_edge_width, + max_edge_width=max_edge_width, + normalize_to_color=normalize_to_color, + frameon=frameon, + cmap=cmap, + colorbar=colorbars[icolor], + cb_kwds=cb_kwds, + use_raw=use_raw, + title=title[icolor], + export_to_gexf=export_to_gexf, + single_component=single_component, + arrowsize=arrowsize, + pos=pos, + ) + if colorbars[icolor]: + if cax is None: + bottom = panel_pos[0][0] + height = panel_pos[1][0] - bottom + width = 0.006 * draw_region_width / len(colors) + left = panel_pos[2][2 * icolor + 1] + 0.2 * width + rectangle = [left, bottom, width, height] + fig = plt.gcf() + ax_cb = fig.add_axes(rectangle) + else: + ax_cb = cax[icolor] + + _ = plt.colorbar( + sct, + format=ticker.FuncFormatter(_utils.ticks_formatter), + cax=ax_cb, + ) + if add_pos: + adata.uns["paga"]["pos"] = pos + logg.hint("added 'pos', the PAGA positions (adata.uns['paga'])") + + if not plot: + return None + _utils.savefig_or_show("paga", show=show, save=save) + if len(colors) == 1 and isinstance(axs, list): + axs = axs[0] + show = settings.autoshow if show is None else show + if show: + return None + return axs + + +def _paga_graph( # noqa: PLR0912, PLR0913, PLR0915 + adata, + ax, + *, + solid_edges=None, + dashed_edges=None, + adjacency_solid=None, + adjacency_dashed=None, + transitions=None, + threshold=None, + root=0, + colors=None, + labels=None, + fontsize=None, + fontweight=None, + fontoutline=None, + text_kwds: Mapping[str, Any] = MappingProxyType({}), + node_size_scale=1.0, + node_size_power=0.5, + edge_width_scale=1.0, + normalize_to_color="reference", + title=None, + pos=None, + cmap=None, + frameon=True, + min_edge_width=None, + max_edge_width=None, + export_to_gexf=False, + colorbar=None, + use_raw=True, + cb_kwds: Mapping[str, Any] = MappingProxyType({}), + single_component=False, + arrowsize=30, +): + import networkx as nx + + node_labels = labels # rename for clarity + if ( + node_labels is not None + and isinstance(node_labels, str) + and node_labels != adata.uns["paga"]["groups"] + ): + msg = ( + "Provide a list of group labels for the PAGA groups " + f"{adata.uns['paga']['groups']}, not {node_labels}." + ) + raise ValueError(msg) + groups_key = adata.uns["paga"]["groups"] + if node_labels is None: + node_labels = adata.obs[groups_key].cat.categories + + if (colors is None or colors == groups_key) and groups_key is not None: + if f"{groups_key}_colors" not in adata.uns or len( + adata.obs[groups_key].cat.categories + ) != len(adata.uns[f"{groups_key}_colors"]): + _utils.add_colors_for_categorical_sample_annotation(adata, groups_key) + colors = adata.uns[f"{groups_key}_colors"] + for iname, name in enumerate(adata.obs[groups_key].cat.categories): + if name in settings.categories_to_ignore: + colors[iname] = "grey" + + nx_g_solid = nx.Graph(adjacency_solid) + if dashed_edges is not None: + nx_g_dashed = nx.Graph(adjacency_dashed) + + # convert pos to array and dict + if not isinstance(pos, Path | str): + pos_array = pos + else: + pos = Path(pos) + if pos.suffix != ".gdf": + msg = ( + "Currently only supporting reading positions from .gdf files. " + "Consider generating them using, for instance, Gephi." + ) + raise ValueError(msg) + s = "" # read the node definition from the file + with pos.open() as f: + f.readline() + for line in f: + if line.startswith("edgedef>"): + break + s += line + from io import StringIO + + df = pd.read_csv(StringIO(s), header=-1) + pos_array = df[[4, 5]].values + + # convert to dictionary + pos = {n: [p[0], p[1]] for n, p in enumerate(pos_array)} + + # uniform color + if isinstance(colors, str) and is_color_like(colors): + colors = [colors for c in range(len(node_labels))] + + # color degree of the graph + if isinstance(colors, str) and colors.startswith("degree"): + # see also tools.paga.paga_degrees + if colors == "degree_dashed": + colors = [d for _, d in nx_g_dashed.degree(weight="weight")] + elif colors == "degree_solid": + colors = [d for _, d in nx_g_solid.degree(weight="weight")] + else: + msg = '`degree` either "degree_dashed" or "degree_solid".' + raise ValueError(msg) + colors = (np.array(colors) - np.min(colors)) / (np.max(colors) - np.min(colors)) + + # plot gene expression + var_names = adata.var_names if adata.raw is None else adata.raw.var_names + if isinstance(colors, str) and colors in var_names: + x_color = [] + cats = adata.obs[groups_key].cat.categories + for cat in cats: + subset = (cat == adata.obs[groups_key]).values + if adata.raw is not None and use_raw: + adata_gene = adata.raw[:, colors] + else: + adata_gene = adata[:, colors] + x_color.append(np.mean(adata_gene.X[subset])) + colors = x_color + + # plot continuous annotation + if ( + isinstance(colors, str) + and colors in adata.obs + and not isinstance(adata.obs[colors].dtype, CategoricalDtype) + ): + x_color = [] + cats = adata.obs[groups_key].cat.categories + for cat in cats: + subset = (cat == adata.obs[groups_key]).values + x_color.append(adata.obs.loc[subset, colors].mean()) + colors = x_color + + # plot categorical annotation + if ( + isinstance(colors, str) + and colors in adata.obs + and isinstance(adata.obs[colors].dtype, CategoricalDtype) + ): + _asso_names, asso_matrix = _sc_utils.compute_association_matrix_of_groups( + adata, + prediction=groups_key, + reference=colors, + normalization="reference" if normalize_to_color else "prediction", + ) + _utils.add_colors_for_categorical_sample_annotation(adata, colors) + asso_colors = _sc_utils.get_associated_colors_of_groups( + adata.uns[f"{colors}_colors"], asso_matrix + ) + colors = asso_colors + + if len(colors) != len(node_labels): + msg = ( + f"Expected `colors` to be of length `{len(node_labels)}`, " + f"found `{len(colors)}`." + ) + raise ValueError(msg) + + # count number of connected components + n_components, labels = scipy.sparse.csgraph.connected_components(adjacency_solid) + if n_components > 1 and not single_component: + logg.debug( + "Graph has more than a single connected component. " + "To restrict to this component, pass `single_component=True`." + ) + if n_components > 1 and single_component: + component_sizes = np.bincount(labels) + largest_component = np.where(component_sizes == component_sizes.max())[0][0] + adjacency_solid = adjacency_solid.tocsr()[labels == largest_component, :] + adjacency_solid = adjacency_solid.tocsc()[:, labels == largest_component] + colors = np.array(colors)[labels == largest_component] + node_labels = np.array(node_labels)[labels == largest_component] + cats_dropped = ( + adata.obs[groups_key].cat.categories[labels != largest_component].tolist() + ) + logg.info( + "Restricting graph to largest connected component by dropping categories\n" + f"{cats_dropped}" + ) + nx_g_solid = nx.Graph(adjacency_solid) + if dashed_edges is not None: + msg = "`single_component` only if `dashed_edges` is `None`." + raise ValueError(msg) + + # edge widths + base_edge_width = edge_width_scale * 5 * rcParams["lines.linewidth"] + + # draw dashed edges + if dashed_edges is not None: + widths = [x[-1]["weight"] for x in nx_g_dashed.edges(data=True)] + widths = base_edge_width * np.array(widths) + if max_edge_width is not None: + widths = np.clip(widths, None, max_edge_width) + nx.draw_networkx_edges( + nx_g_dashed, + pos, + ax=ax, + width=widths, + edge_color="grey", + style="dashed", + alpha=0.5, + ) + + # draw solid edges + if transitions is None: + widths = [x[-1]["weight"] for x in nx_g_solid.edges(data=True)] + widths = base_edge_width * np.array(widths) + if min_edge_width is not None or max_edge_width is not None: + widths = np.clip(widths, min_edge_width, max_edge_width) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + nx.draw_networkx_edges( + nx_g_solid, pos, ax=ax, width=widths, edge_color="black" + ) + # draw directed edges + else: + adjacency_transitions = adata.uns["paga"][transitions].copy() + if threshold is None: + threshold = 0.01 + adjacency_transitions.data[adjacency_transitions.data < threshold] = 0 + adjacency_transitions.eliminate_zeros() + g_dir = nx.DiGraph(adjacency_transitions.T) + widths = [x[-1]["weight"] for x in g_dir.edges(data=True)] + widths = base_edge_width * np.array(widths) + if min_edge_width is not None or max_edge_width is not None: + widths = np.clip(widths, min_edge_width, max_edge_width) + nx.draw_networkx_edges( + g_dir, pos, ax=ax, width=widths, edge_color="black", arrowsize=arrowsize + ) + + if export_to_gexf: + if isinstance(colors[0], tuple): + from matplotlib.colors import rgb2hex + + colors = [rgb2hex(c) for c in colors] + for count, _n in enumerate(nx_g_solid.nodes()): + nx_g_solid.node[count]["label"] = str(node_labels[count]) + nx_g_solid.node[count]["color"] = str(colors[count]) + nx_g_solid.node[count]["viz"] = dict( + position=dict( + x=1000 * pos[count][0], + y=1000 * pos[count][1], + z=0, + ) + ) + filename = settings.writedir / "paga_graph.gexf" + logg.warning(f"exporting to {filename}") + settings.writedir.mkdir(parents=True, exist_ok=True) + nx.write_gexf(nx_g_solid, settings.writedir / "paga_graph.gexf") + + ax.set_frame_on(frameon) + ax.set_xticks([]) + ax.set_yticks([]) + + # groups sizes + if groups_key is not None and f"{groups_key}_sizes" in adata.uns: + groups_sizes = adata.uns[f"{groups_key}_sizes"] + else: + groups_sizes = np.ones(len(node_labels)) + base_scale_scatter = 2000 + base_pie_size = ( + base_scale_scatter / (np.sqrt(adjacency_solid.shape[0]) + 10) * node_size_scale + ) + median_group_size = np.median(groups_sizes) + groups_sizes = base_pie_size * np.power( + groups_sizes / median_group_size, node_size_power + ) + + if fontsize is None: + fontsize = rcParams["legend.fontsize"] + if fontoutline is not None: + text_kwds = dict(text_kwds) + text_kwds["path_effects"] = [ + patheffects.withStroke(linewidth=fontoutline, foreground="w") + ] + # usual scatter plot + if not isinstance(colors[0], Mapping): + n_groups = len(pos_array) + sct = ax.scatter( + pos_array[:, 0], + pos_array[:, 1], + c=colors[:n_groups], + edgecolors="face", + s=groups_sizes, + cmap=cmap, + ) + for count, group in enumerate(node_labels): + ax.text( + pos_array[count, 0], + pos_array[count, 1], + group, + verticalalignment="center", + horizontalalignment="center", + size=fontsize, + fontweight=fontweight, + **text_kwds, + ) + # else pie chart plot + else: + for ix, (xx, yy) in enumerate( + zip(pos_array[:, 0], pos_array[:, 1], strict=True) + ): + if not isinstance(colors[ix], Mapping): + msg = ( + f"{colors[ix]} is neither a dict of valid " + "matplotlib colors nor a valid matplotlib color." + ) + raise ValueError(msg) + color_single = colors[ix].keys() + fracs = [colors[ix][c] for c in color_single] + total = sum(fracs) + + if total < 1: + color_single = list(color_single) + color_single.append("grey") + fracs.append(1 - sum(fracs)) + elif not np.isclose(total, 1): + msg = ( + f"Expected fractions for node `{ix}` to be " + f"close to 1, found `{total}`." + ) + raise ValueError(msg) + + cumsum = np.cumsum(fracs) + cumsum = cumsum / cumsum[-1] + cumsum = [0, *cumsum.tolist()] + + for r1, r2, color in zip( + cumsum[:-1], cumsum[1:], color_single, strict=True + ): + angles = np.linspace(2 * np.pi * r1, 2 * np.pi * r2, 20) + x = [0, *np.cos(angles).tolist()] + y = [0, *np.sin(angles).tolist()] + + xy = np.column_stack([x, y]) + s = np.abs(xy).max() + + sct = ax.scatter( + [xx], [yy], marker=xy, s=s**2 * groups_sizes[ix], color=color + ) + + if node_labels is not None: + ax.text( + xx, + yy, + node_labels[ix], + verticalalignment="center", + horizontalalignment="center", + size=fontsize, + fontweight=fontweight, + **text_kwds, + ) + + return sct + + +@old_positionals( + "use_raw", + "annotations", + "color_map", + "color_maps_annotations", + "palette_groups", + "n_avg", + "groups_key", + "xlim", + "title", + "left_margin", + "ytick_fontsize", + "title_fontsize", + "show_node_names", + "show_yticks", + "show_colorbar", + "legend_fontsize", + "legend_fontweight", + "normalize_to_zero_one", + "as_heatmap", + "return_data", + "show", + "save", + "ax", +) +def paga_path( # noqa: PLR0912, PLR0913, PLR0915 + adata: AnnData, + nodes: Sequence[str | int], + keys: Sequence[str], + *, + use_raw: bool = True, + annotations: Sequence[str] = ("dpt_pseudotime",), + color_map: str | Colormap | None = None, + color_maps_annotations: Mapping[str, str | Colormap] = MappingProxyType( + dict(dpt_pseudotime="Greys") + ), + palette_groups: Sequence[str] | None = None, + n_avg: int = 1, + groups_key: str | None = None, + xlim: tuple[int | None, int | None] = (None, None), + title: str | None = None, + left_margin=None, + ytick_fontsize: int | None = None, + title_fontsize: int | None = None, + show_node_names: bool = True, + show_yticks: bool = True, + show_colorbar: bool = True, + legend_fontsize: float | _FontSize | None = None, + legend_fontweight: int | _FontWeight | None = None, + normalize_to_zero_one: bool = False, + as_heatmap: bool = True, + return_data: bool = False, + show: bool | None = None, + save: bool | str | None = None, + ax: Axes | None = None, +) -> tuple[Axes, pd.DataFrame] | Axes | pd.DataFrame | None: + r"""Gene expression and annotation changes along paths in the abstracted graph. + + Parameters + ---------- + adata + An annotated data matrix. + nodes + A path through nodes of the abstracted graph, that is, names or indices + (within `.categories`) of groups that have been used to run PAGA. + keys + Either variables in `adata.var_names` or annotations in + `adata.obs`. They are plotted using `color_map`. + use_raw + Use `adata.raw` for retrieving gene expressions if it has been set. + annotations + Plot these keys with `color_maps_annotations`. Need to be keys for + `adata.obs`. + color_map + Matplotlib colormap. + color_maps_annotations + Color maps for plotting the annotations. Keys of the dictionary must + appear in `annotations`. + palette_groups + Ususally, use the same `sc.pl.palettes...` as used for coloring the + abstracted graph. + n_avg + Number of data points to include in computation of running average. + groups_key + Key of the grouping used to run PAGA. If `None`, defaults to + `adata.uns['paga']['groups']`. + as_heatmap + Plot the timeseries as heatmap. If not plotting as heatmap, + `annotations` have no effect. + show_node_names + Plot the node names on the nodes bar. + show_colorbar + Show the colorbar. + show_yticks + Show the y ticks. + normalize_to_zero_one + Shift and scale the running average to [0, 1] per gene. + return_data + Return the timeseries data in addition to the axes if `True`. + show + Show the plot, do not return axis. + save + If `True` or a `str`, save the figure. + A string is appended to the default filename. + Infer the filetype if ending on \{`'.pdf'`, `'.png'`, `'.svg'`\}. + ax + A matplotlib axes object. + + Returns + ------- + A :class:`~matplotlib.axes.Axes` object, if `ax` is `None`, else `None`. + If `return_data`, return the timeseries data in addition to an axes. + + """ + ax_was_none = ax is None + + if groups_key is None: + if "groups" not in adata.uns["paga"]: + msg = ( + "Pass the key of the grouping with which you ran PAGA, " + "using the parameter `groups_key`." + ) + raise KeyError(msg) + groups_key = adata.uns["paga"]["groups"] + groups_names = adata.obs[groups_key].cat.categories + + if "dpt_pseudotime" not in adata.obs.columns: + msg = ( + "`pl.paga_path` requires computation of a pseudotime `tl.dpt` " + "for ordering at single-cell resolution" + ) + raise ValueError(msg) + + if palette_groups is None: + _utils.add_colors_for_categorical_sample_annotation(adata, groups_key) + palette_groups = adata.uns[f"{groups_key}_colors"] + + def moving_average(a): + return _sc_utils.moving_average(a, n_avg) + + ax = plt.gca() if ax is None else ax + + xs = [] + x_tick_locs = [0] + x_tick_labels = [] + groups = [] + anno_dict = {anno: [] for anno in annotations} + if isinstance(nodes[0], str): + nodes_ints = [] + groups_names_set = set(groups_names) + for node in nodes: + if node not in groups_names_set: + msg = ( + f"Each node/group needs to be in {groups_names.tolist()} " + f"({groups_key=!r}) not {node!r}." + ) + raise ValueError(msg) + nodes_ints.append(groups_names.get_loc(node)) + nodes_strs = nodes + else: + nodes_ints = nodes + nodes_strs = [groups_names[node] for node in nodes] + + adata_x = adata + if use_raw and adata.raw is not None: + adata_x = adata.raw + + for ikey, key in enumerate(keys): + x = [] + for igroup, group in enumerate(nodes_ints): + idcs = np.arange(adata.n_obs)[ + adata.obs[groups_key].values == nodes_strs[igroup] + ] + if len(idcs) == 0: + msg = ( + "Did not find data points that match " + f"`adata.obs[{groups_key!r}].values == {str(group)!r}`. " + f"Check whether `adata.obs[{groups_key!r}]` " + "actually contains what you expect." + ) + raise ValueError(msg) + idcs_group = np.argsort( + adata.obs["dpt_pseudotime"].values[ + adata.obs[groups_key].values == nodes_strs[igroup] + ] + ) + idcs = idcs[idcs_group] + values = (adata.obs[key].values if key in adata.obs else adata_x[:, key].X)[ + idcs + ] + x += (values.toarray() if isinstance(values, CSBase) else values).tolist() + if ikey == 0: + groups += [group] * len(idcs) + x_tick_locs.append(len(x)) + for anno in annotations: + series = adata.obs[anno] + if isinstance(series.dtype, CategoricalDtype): + series = series.cat.codes + anno_dict[anno] += list(series.values[idcs]) + if n_avg > 1: + x = moving_average(x) + if ikey == 0: + for k in annotations: + if not isinstance(anno_dict[k][0], str): + anno_dict[k] = moving_average(anno_dict[k]) + if normalize_to_zero_one: + x -= np.min(x) + x /= np.max(x) + xs.append(x) + if not as_heatmap: + ax.plot(x[xlim[0] : xlim[1]], label=key) + if ikey == 0: + for group in nodes: + if len(groups_names) > 0 and group not in groups_names: + label = groups_names[group] + else: + label = group + x_tick_labels.append(label) + x = np.asarray(xs).squeeze() + if as_heatmap: + img = ax.imshow(x, aspect="auto", interpolation="nearest", cmap=color_map) + if show_yticks: + ax.set_yticks(range(len(x))) + ax.set_yticklabels(keys, fontsize=ytick_fontsize) + else: + ax.set_yticks([]) + ax.set_frame_on(False) + ax.set_xticks([]) + ax.tick_params(axis="both", which="both", length=0) + ax.grid(visible=False) + if show_colorbar: + plt.colorbar(img, ax=ax) + left_margin = 0.2 if left_margin is None else left_margin + plt.subplots_adjust(left=left_margin) + else: + left_margin = 0.4 if left_margin is None else left_margin + if len(keys) > 1: + plt.legend( + frameon=False, + loc="center left", + bbox_to_anchor=(-left_margin, 0.5), + fontsize=legend_fontsize, + ) + xlabel = groups_key + if not as_heatmap: + ax.set_xlabel(xlabel) + plt.yticks([]) + if len(keys) == 1: + plt.ylabel(f"{keys[0]} (a.u.)") + else: + import matplotlib.colors + + # groups bar + ax_bounds = ax.get_position().bounds + groups_axis = plt.axes(( + ax_bounds[0], + ax_bounds[1] - ax_bounds[3] / len(keys), + ax_bounds[2], + ax_bounds[3] / len(keys), + )) + groups = np.array(groups)[None, :] + groups_axis.imshow( + groups, + aspect="auto", + interpolation="nearest", + cmap=matplotlib.colors.ListedColormap( + # the following line doesn't work because of normalization + # adata.uns['paga_groups_colors']) + palette_groups[np.min(groups).astype(int) :], + N=int(np.max(groups) + 1 - np.min(groups)), + ), + ) + if show_yticks: + groups_axis.set_yticklabels(["", xlabel, ""], fontsize=ytick_fontsize) + else: + groups_axis.set_yticks([]) + groups_axis.set_frame_on(False) + if show_node_names: + ypos = (groups_axis.get_ylim()[1] + groups_axis.get_ylim()[0]) / 2 + x_tick_locs = _sc_utils.moving_average(x_tick_locs, n=2) + for loc, label in zip(x_tick_locs, x_tick_labels, strict=True): + font = dict(horizontalalignment="center", verticalalignment="center") + groups_axis.text(loc, ypos, label, fontdict=font) + groups_axis.set_xticks([]) + groups_axis.grid(visible=False) + groups_axis.tick_params(axis="both", which="both", length=0) + # further annotations + y_shift = ax_bounds[3] / len(keys) + for ianno, anno in enumerate(annotations): + if ianno > 0: + y_shift = ax_bounds[3] / len(keys) / 2 + anno_axis = plt.axes(( + ax_bounds[0], + ax_bounds[1] - (ianno + 2) * y_shift, + ax_bounds[2], + y_shift, + )) + arr = np.array(anno_dict[anno])[None, :] + if anno not in color_maps_annotations: + color_map_anno = ( + "Vega10" + if isinstance(adata.obs[anno].dtype, CategoricalDtype) + else "Greys" + ) + else: + color_map_anno = color_maps_annotations[anno] + img = anno_axis.imshow( + arr, + aspect="auto", + interpolation="nearest", + cmap=color_map_anno, + ) + if show_yticks: + anno_axis.set_yticklabels(["", anno, ""], fontsize=ytick_fontsize) + anno_axis.tick_params(axis="both", which="both", length=0) + else: + anno_axis.set_yticks([]) + anno_axis.set_frame_on(False) + anno_axis.set_xticks([]) + anno_axis.grid(visible=False) + if title is not None: + ax.set_title(title, fontsize=title_fontsize) + if show is None and not ax_was_none: + show = False + else: + show = settings.autoshow if show is None else show + _utils.savefig_or_show("paga_path", show=show, save=save) + if return_data: + df = pd.DataFrame(data=x.T, columns=keys) + df["groups"] = moving_average(groups) # groups is without moving average, yet + if "dpt_pseudotime" in anno_dict: + df["distance"] = anno_dict["dpt_pseudotime"].T + if not ax_was_none or show: + return df if return_data else None + return (ax, df) if return_data else ax + + +def paga_adjacency( + adata: AnnData, + *, + adjacency: str = "connectivities", + adjacency_tree: str = "connectivities_tree", + as_heatmap: bool = True, + color_map: str | Colormap | None = None, + show: bool | None = None, + save: bool | str | None = None, +) -> None: + """Plot connectivity of paga groups.""" + connectivity = adata.uns[adjacency].toarray() + connectivity_select = adata.uns[adjacency_tree] + if as_heatmap: + matrix(connectivity, color_map=color_map, show=False) + for i in range(connectivity_select.shape[0]): + neighbors = connectivity_select[i].nonzero()[1] + plt.scatter([i for j in neighbors], neighbors, color="black", s=1) + # as a stripplot + else: + plt.figure() + for i, cs in enumerate(connectivity): + x = [i for j, d in enumerate(cs) if i != j] + y = [c for j, c in enumerate(cs) if i != j] + plt.scatter(x, y, color="gray", s=1) + neighbors = connectivity_select[i].nonzero()[1] + plt.scatter([i for j in neighbors], cs[neighbors], color="black", s=1) + _utils.savefig_or_show("paga_connectivity", show=show, save=save) diff --git a/.venv/lib/python3.11/site-packages/scanpy/plotting/_tools/scatterplots.py b/.venv/lib/python3.11/site-packages/scanpy/plotting/_tools/scatterplots.py new file mode 100644 index 0000000000000000000000000000000000000000..ee94704b56f3e9f437f93307c120f298160815f8 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/plotting/_tools/scatterplots.py @@ -0,0 +1,1397 @@ +from __future__ import annotations + +import inspect +from collections.abc import Mapping, Sequence # noqa: TC003 +from copy import copy +from functools import partial +from itertools import combinations, product +from numbers import Integral +from typing import ( + TYPE_CHECKING, + Any, # noqa: TC003 + Literal, # noqa: TC003 +) + +import numpy as np +import pandas as pd +from anndata import AnnData # noqa: TC002 +from cycler import Cycler # noqa: TC002 +from matplotlib import colormaps, colors, patheffects, rcParams +from matplotlib import pyplot as plt +from matplotlib.axes import Axes # noqa: TC002 +from matplotlib.colors import ( + Colormap, # noqa: TC002 + Normalize, +) +from matplotlib.figure import Figure # noqa: TC002 +from numpy.typing import NDArray # noqa: TC002 +from packaging.version import Version + +from ... import logging as logg +from ..._compat import deprecated, pkg_version +from ..._settings import settings +from ..._utils import ( + Empty, # noqa: TC001 + _doc_params, + _empty, + sanitize_anndata, +) +from ...get import _check_mask +from ...tools._draw_graph import _Layout # noqa: TC001 +from .. import _utils +from .._docs import ( + doc_adata_color_etc, + doc_edges_arrows, + doc_scatter_embedding, + doc_scatter_spatial, + doc_show_save_ax, +) +from .._utils import ( + ColorLike, # noqa: TC001 + VBound, # noqa: TC001 + _FontSize, # noqa: TC001 + _FontWeight, # noqa: TC001 + _LegendLoc, # noqa: TC001 + check_colornorm, + check_projection, + circles, +) + +if TYPE_CHECKING: + from collections.abc import Collection + + +@_doc_params( + adata_color_etc=doc_adata_color_etc, + edges_arrows=doc_edges_arrows, + scatter_bulk=doc_scatter_embedding, + show_save_ax=doc_show_save_ax, +) +def embedding( # noqa: PLR0912, PLR0913, PLR0915 + adata: AnnData, + basis: str, + *, + color: str | Sequence[str] | None = None, + mask_obs: NDArray[np.bool_] | str | None = None, + gene_symbols: str | None = None, + use_raw: bool | None = None, + sort_order: bool = True, + edges: bool = False, + edges_width: float = 0.1, + edges_color: str | Sequence[float] | Sequence[str] = "grey", + neighbors_key: str | None = None, + arrows: bool = False, + arrows_kwds: Mapping[str, Any] | None = None, + groups: str | Sequence[str] | None = None, + components: str | Sequence[str] | None = None, + dimensions: tuple[int, int] | Sequence[tuple[int, int]] | None = None, + layer: str | None = None, + projection: Literal["2d", "3d"] = "2d", + scale_factor: float | None = None, + color_map: Colormap | str | None = None, + cmap: Colormap | str | None = None, + palette: str | Sequence[str] | Cycler | None = None, + na_color: ColorLike = "lightgray", + na_in_legend: bool = True, + size: float | Sequence[float] | None = None, + frameon: bool | None = None, + legend_fontsize: float | _FontSize | None = None, + legend_fontweight: int | _FontWeight = "bold", + legend_loc: _LegendLoc | None = "right margin", + legend_fontoutline: int | None = None, + colorbar_loc: str | None = "right", + vmax: VBound | Sequence[VBound] | None = None, + vmin: VBound | Sequence[VBound] | None = None, + vcenter: VBound | Sequence[VBound] | None = None, + norm: Normalize | Sequence[Normalize] | None = None, + add_outline: bool | None = False, + outline_width: tuple[float, float] = (0.3, 0.05), + outline_color: tuple[str, str] = ("black", "white"), + ncols: int = 4, + hspace: float = 0.25, + wspace: float | None = None, + title: str | Sequence[str] | None = None, + show: bool | None = None, + save: bool | str | None = None, + ax: Axes | None = None, + return_fig: bool | None = None, + marker: str | Sequence[str] = ".", + **kwargs, +) -> Figure | Axes | list[Axes] | None: + """Scatter plot for user specified embedding basis (e.g. umap, pca, etc). + + Parameters + ---------- + basis + Name of the `obsm` basis to use. + {adata_color_etc} + {edges_arrows} + {scatter_bulk} + {show_save_ax} + + Returns + ------- + If `show==False` a :class:`~matplotlib.axes.Axes` or a list of it. + + """ + ##################### + # Argument handling # + ##################### + + check_projection(projection) + sanitize_anndata(adata) + + basis_values = _get_basis(adata, basis) + dimensions = _components_to_dimensions( + components, dimensions, projection=projection, total_dims=basis_values.shape[1] + ) + args_3d = dict(projection="3d") if projection == "3d" else {} + + # Checking the mask format and if used together with groups + if groups is not None and mask_obs is not None: + msg = "Groups and mask arguments are incompatible." + raise ValueError(msg) + mask_obs = _check_mask(adata, mask_obs, "obs") + + # Figure out if we're using raw + if use_raw is None: + # check if adata.raw is set + use_raw = layer is None and adata.raw is not None + if use_raw and layer is not None: + msg = ( + "Cannot use both a layer and the raw representation. " + f"Was passed: {use_raw=!r}, {layer=!r}." + ) + raise ValueError(msg) + if use_raw and adata.raw is None: + msg = ( + "`use_raw` is set to True but AnnData object does not have raw. " + "Please check." + ) + raise ValueError(msg) + + if isinstance(groups, str): + groups = [groups] + + # Color map + if color_map is not None: + if cmap is not None: + msg = "Cannot specify both `color_map` and `cmap`." + raise ValueError(msg) + else: + cmap = color_map + cmap = copy(colormaps.get_cmap(cmap)) + cmap.set_bad(na_color) + # Prevents warnings during legend creation + na_color = colors.to_hex(na_color, keep_alpha=True) + + # by default turn off edge color. Otherwise, for + # very small sizes the edge will not reduce its size + # (https://github.com/scverse/scanpy/issues/293) + kwargs.setdefault("edgecolor", "none") + + # Vectorized arguments + + # turn color into a python list + color = [color] if isinstance(color, str) or color is None else list(color) + + # turn marker into a python list + marker = [marker] if isinstance(marker, str) else list(marker) + + if title is not None: + # turn title into a python list if not None + title = [title] if isinstance(title, str) else list(title) + + # turn vmax and vmin into a sequence + if isinstance(vmax, str) or not isinstance(vmax, Sequence): + vmax = [vmax] + if isinstance(vmin, str) or not isinstance(vmin, Sequence): + vmin = [vmin] + if isinstance(vcenter, str) or not isinstance(vcenter, Sequence): + vcenter = [vcenter] + if isinstance(norm, Normalize) or not isinstance(norm, Sequence): + norm = [norm] + + # Size + if "s" in kwargs and size is None: + size = kwargs.pop("s") + if size is not None: + # check if size is any type of sequence, and if so + # set as ndarray + if ( + size is not None + and isinstance(size, Sequence | pd.Series | np.ndarray) + and len(size) == adata.shape[0] + ): + size = np.array(size, dtype=float) + else: + size = 120000 / adata.shape[0] + + ########## + # Layout # + ########## + # Most of the code is for the case when multiple plots are required + + if wspace is None: + # try to set a wspace that is not too large or too small given the + # current figure size + wspace = 0.75 / rcParams["figure.figsize"][0] + 0.02 + + if components is not None: + color, dimensions = list(zip(*product(color, dimensions), strict=True)) + + color, dimensions, marker = _broadcast_args(color, dimensions, marker) + + # 'color' is a list of names that want to be plotted. + # Eg. ['Gene1', 'louvain', 'Gene2']. + # component_list is a list of components [[0,1], [1,2]] + if ( + not isinstance(color, str) and isinstance(color, Sequence) and len(color) > 1 + ) or len(dimensions) > 1: + if ax is not None: + msg = ( + "Cannot specify `ax` when plotting multiple panels " + "(each for a given value of 'color')." + ) + raise ValueError(msg) + + # each plot needs to be its own panel + fig, grid = _panel_grid(hspace, wspace, ncols, len(color)) + else: + grid = None + if ax is None: + fig = plt.figure() + ax = fig.add_subplot(111, **args_3d) + + ############ + # Plotting # + ############ + axs = [] + + # use itertools.product to make a plot for each color and for each component + # For example if color=[gene1, gene2] and components=['1,2, '2,3']. + # The plots are: [ + # color=gene1, components=[1,2], color=gene1, components=[2,3], + # color=gene2, components = [1, 2], color=gene2, components=[2,3], + # ] + for count, (value_to_plot, dims) in enumerate(zip(color, dimensions, strict=True)): + kwargs_scatter = kwargs.copy() # is potentially mutated for each plot + color_source_vector = _get_color_source_vector( + adata, + value_to_plot, + layer=layer, + mask_obs=mask_obs, + use_raw=use_raw, + gene_symbols=gene_symbols, + groups=groups, + ) + color_vector, color_type = _color_vector( + adata, + value_to_plot, + values=color_source_vector, + palette=palette, + na_color=na_color, + ) + + # Order points + order = slice(None) + if sort_order and value_to_plot is not None and color_type == "cont": + # Higher values plotted on top, null values on bottom + order = np.argsort(-color_vector, kind="stable")[::-1] + elif sort_order and color_type == "cat": + # Null points go on bottom + order = np.argsort(~pd.isnull(color_source_vector), kind="stable") + # Set orders + if isinstance(size, np.ndarray): + size = np.array(size)[order] + color_source_vector = color_source_vector[order] + color_vector = color_vector[order] + coords = basis_values[:, dims][order, :] + + # if plotting multiple panels, get the ax from the grid spec + # else use the ax value (either user given or created previously) + if grid: + ax = plt.subplot(grid[count], **args_3d) + axs.append(ax) + if not (settings._frameon if frameon is None else frameon): + ax.axis("off") + if title is None: + if value_to_plot is not None: + ax.set_title(value_to_plot) + else: + ax.set_title("") + else: + try: + ax.set_title(title[count]) + except IndexError: + logg.warning( + "The title list is shorter than the number of panels. " + "Using 'color' value instead for some plots." + ) + ax.set_title(value_to_plot) + + if color_type == "cont": + vmin_float, vmax_float, vcenter_float, norm_obj = _get_vboundnorm( + vmin, vmax, vcenter, norm=norm, index=count, colors=color_vector + ) + kwargs_scatter["norm"] = check_colornorm( + vmin_float, + vmax_float, + vcenter_float, + norm_obj, + ) + kwargs_scatter["cmap"] = cmap + + # make the scatter plot + if projection == "3d": + cax = ax.scatter( + coords[:, 0], + coords[:, 1], + coords[:, 2], + c=color_vector, + rasterized=settings._vector_friendly, + marker=marker[count], + **kwargs_scatter, + ) + else: + scatter = ( + partial(ax.scatter, s=size, plotnonfinite=True) + if scale_factor is None + else partial( + circles, s=size, ax=ax, scale_factor=scale_factor + ) # size in circles is radius + ) + + if add_outline: + # the default outline is a black edge followed by a + # thin white edged added around connected clusters. + # To add an outline + # three overlapping scatter plots are drawn: + # First black dots with slightly larger size, + # then, white dots a bit smaller, but still larger + # than the final dots. Then the final dots are drawn + # with some transparency. + + bg_width, gap_width = outline_width + point = np.sqrt(size) + gap_size = (point + (point * gap_width) * 2) ** 2 + bg_size = (np.sqrt(gap_size) + (point * bg_width) * 2) ** 2 + # the default black and white colors can be changes using + # the contour_config parameter + bg_color, gap_color = outline_color + + # remove edge from kwargs if present + # because edge needs to be set to None + kwargs_scatter["edgecolor"] = "none" + # For points, if user did not set alpha, set alpha to 0.7 + kwargs_scatter.setdefault("alpha", 0.7) + + # remove alpha and color mapping for outline + kwargs_outline = { + k: v + for k, v in kwargs.items() + if k not in {"alpha", "cmap", "norm"} + } + + for s, c in [(bg_size, bg_color), (gap_size, gap_color)]: + ax.scatter( + coords[:, 0], + coords[:, 1], + s=s, + c=c, + rasterized=settings._vector_friendly, + marker=marker[count], + **kwargs_outline, + ) + + cax = scatter( + coords[:, 0], + coords[:, 1], + c=color_vector, + rasterized=settings._vector_friendly, + marker=marker[count], + **kwargs_scatter, + ) + + # remove y and x ticks + ax.set_yticks([]) + ax.set_xticks([]) + if projection == "3d": + ax.set_zticks([]) + + # set default axis_labels + name = _basis2name(basis) + axis_labels = [name + str(d + 1) for d in dims] + + ax.set_xlabel(axis_labels[0]) + ax.set_ylabel(axis_labels[1]) + if projection == "3d": + # shift the label closer to the axis + ax.set_zlabel(axis_labels[2], labelpad=-7) + ax.autoscale_view() + + if edges: + _utils.plot_edges( + ax, adata, basis, edges_width, edges_color, neighbors_key=neighbors_key + ) + if arrows: + _utils.plot_arrows(ax, adata, basis, arrows_kwds) + + if value_to_plot is None: + # if only dots were plotted without an associated value + # there is not need to plot a legend or a colorbar + continue + + if legend_fontoutline is not None: + path_effect = [ + patheffects.withStroke(linewidth=legend_fontoutline, foreground="w") + ] + else: + path_effect = None + + # Adding legends + if color_type == "cat": + _add_categorical_legend( + ax, + color_source_vector, + palette=_get_palette(adata, value_to_plot), + scatter_array=coords, + legend_loc=legend_loc, + legend_fontweight=legend_fontweight, + legend_fontsize=legend_fontsize, + legend_fontoutline=path_effect, + na_color=na_color, + na_in_legend=na_in_legend, + multi_panel=bool(grid), + ) + elif colorbar_loc is not None: + plt.colorbar( + cax, ax=ax, pad=0.01, fraction=0.08, aspect=30, location=colorbar_loc + ) + + if return_fig is True: + return fig + axs = axs if grid else ax + _utils.savefig_or_show(basis, show=show, save=save) + show = settings.autoshow if show is None else show + if show: + return None + return axs + + +def _panel_grid(hspace, wspace, ncols, num_panels): + from matplotlib import gridspec + + n_panels_x = min(ncols, num_panels) + n_panels_y = np.ceil(num_panels / n_panels_x).astype(int) + # each panel will have the size of rcParams['figure.figsize'] + fig = plt.figure( + figsize=( + n_panels_x * rcParams["figure.figsize"][0] * (1 + wspace), + n_panels_y * rcParams["figure.figsize"][1], + ), + ) + left = 0.2 / n_panels_x + bottom = 0.13 / n_panels_y + gs = gridspec.GridSpec( + nrows=n_panels_y, + ncols=n_panels_x, + left=left, + right=1 - (n_panels_x - 1) * left - 0.01 / n_panels_x, + bottom=bottom, + top=1 - (n_panels_y - 1) * bottom - 0.1 / n_panels_y, + hspace=hspace, + wspace=wspace, + ) + return fig, gs + + +def _get_vboundnorm( + vmin: Sequence[VBound], + vmax: Sequence[VBound], + vcenter: Sequence[VBound], + *, + norm: Sequence[Normalize], + index: int, + colors: Sequence[float], +) -> tuple[float | None, float | None]: + """Evaluate the value of `vmin`, `vmax` and `vcenter`. + + Each could be a str in which case is interpreted as a percentile and should + be specified in the form `pN` where `N` is the percentile. + Eg. for a percentile of 85 the format would be `p85`. + Floats are accepted as `p99.9`. + + Alternatively, `vmin`/`vmax` could be a function that is applied to + the list of color values (`colors`). E.g. + + >>> def my_vmax(colors): + ... return np.percentile(colors, p=80) + + Parameters + ---------- + index + This index of the plot + colors + Values for the plot + + Returns + ------- + (vmin, vmax, vcenter, norm) containing None or float values for + vmin, vmax, vcenter and matplotlib.colors.Normalize or None for norm. + + """ + out = [] + for v_name, v in [("vmin", vmin), ("vmax", vmax), ("vcenter", vcenter)]: + if len(v) == 1: + # this case usually happens when the user sets eg vmax=0.9, which + # is internally converted into list of len=1, but is expected that this + # value applies to all plots. + v_value = v[0] + else: + try: + v_value = v[index] + except IndexError: + logg.error( + f"The parameter {v_name} is not valid. If setting multiple {v_name} values," + f"check that the length of the {v_name} list is equal to the number " + "of plots. " + ) + v_value = None + + if v_value is not None: + if isinstance(v_value, str) and v_value.startswith("p"): + try: + float(v_value[1:]) + except ValueError: + logg.error( + f"The parameter {v_name}={v_value} for plot number {index + 1} is not valid. " + f"Please check the correct format for percentiles." + ) + # interpret value of vmin/vmax as quantile with the following syntax 'p99.9' + v_value = np.nanpercentile(colors, q=float(v_value[1:])) + elif callable(v_value): + # interpret vmin/vmax as function + v_value = v_value(colors) + if not isinstance(v_value, float): + logg.error( + f"The return of the function given for {v_name} is not valid. " + "Please check that the function returns a number." + ) + v_value = None + else: + try: + float(v_value) + except ValueError: + logg.error( + f"The given {v_name}={v_value} for plot number {index + 1} is not valid. " + f"Please check that the value given is a valid number, a string " + f"starting with 'p' for percentiles or a valid function." + ) + v_value = None + out.append(v_value) + out.append(norm[0] if len(norm) == 1 else norm[index]) + return tuple(out) + + +def _wraps_plot_scatter(wrapper): + """Update the wrapper function to use the correct signature.""" + params = inspect.signature(embedding, eval_str=True).parameters.copy() + wrapper_sig = inspect.signature(wrapper, eval_str=True) + wrapper_params = wrapper_sig.parameters.copy() + + params.pop("basis") + params.pop("kwargs") + wrapper_params.pop("adata") + + params.update(wrapper_params) + annotations = { + k: v.annotation + for k, v in params.items() + if v.annotation != inspect.Parameter.empty + } + if wrapper_sig.return_annotation is not inspect.Signature.empty: + annotations["return"] = wrapper_sig.return_annotation + + wrapper.__signature__ = inspect.Signature( + list(params.values()), return_annotation=wrapper_sig.return_annotation + ) + wrapper.__annotations__ = annotations + + return wrapper + + +# API + + +@_wraps_plot_scatter +@_doc_params( + adata_color_etc=doc_adata_color_etc, + edges_arrows=doc_edges_arrows, + scatter_bulk=doc_scatter_embedding, + show_save_ax=doc_show_save_ax, +) +def umap(adata: AnnData, **kwargs) -> Figure | Axes | list[Axes] | None: + """Scatter plot in UMAP basis. + + Parameters + ---------- + {adata_color_etc} + {edges_arrows} + {scatter_bulk} + {show_save_ax} + + Returns + ------- + If `show==False` a :class:`~matplotlib.axes.Axes` or a list of it. + + Examples + -------- + + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc68k_reduced() + sc.pl.umap(adata) + + Colour points by discrete variable (Louvain clusters). + + .. plot:: + :context: close-figs + + sc.pl.umap(adata, color="louvain") + + Colour points by gene expression. + + .. plot:: + :context: close-figs + + sc.pl.umap(adata, color="HES4") + + Plot muliple umaps for different gene expressions. + + .. plot:: + :context: close-figs + + sc.pl.umap(adata, color=["HES4", "TNFRSF4"]) + + .. currentmodule:: scanpy + + See Also + -------- + tl.umap + + """ + return embedding(adata, "umap", **kwargs) + + +@_wraps_plot_scatter +@_doc_params( + adata_color_etc=doc_adata_color_etc, + edges_arrows=doc_edges_arrows, + scatter_bulk=doc_scatter_embedding, + show_save_ax=doc_show_save_ax, +) +def tsne(adata: AnnData, **kwargs) -> Figure | Axes | list[Axes] | None: + """Scatter plot in tSNE basis. + + Parameters + ---------- + {adata_color_etc} + {edges_arrows} + {scatter_bulk} + {show_save_ax} + + Returns + ------- + If `show==False` a :class:`~matplotlib.axes.Axes` or a list of it. + + Examples + -------- + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc68k_reduced() + sc.tl.tsne(adata) + sc.pl.tsne(adata, color='bulk_labels') + + .. currentmodule:: scanpy + + See Also + -------- + tl.tsne + + """ + return embedding(adata, "tsne", **kwargs) + + +@_wraps_plot_scatter +@_doc_params( + adata_color_etc=doc_adata_color_etc, + scatter_bulk=doc_scatter_embedding, + show_save_ax=doc_show_save_ax, +) +def diffmap(adata: AnnData, **kwargs) -> Figure | Axes | list[Axes] | None: + """Scatter plot in Diffusion Map basis. + + Parameters + ---------- + {adata_color_etc} + {scatter_bulk} + {show_save_ax} + + Returns + ------- + If `show==False` a :class:`~matplotlib.axes.Axes` or a list of it. + + Examples + -------- + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc68k_reduced() + sc.tl.diffmap(adata) + sc.pl.diffmap(adata, color='bulk_labels') + + .. currentmodule:: scanpy + + See Also + -------- + tl.diffmap + + """ + return embedding(adata, "diffmap", **kwargs) + + +@_wraps_plot_scatter +@_doc_params( + adata_color_etc=doc_adata_color_etc, + edges_arrows=doc_edges_arrows, + scatter_bulk=doc_scatter_embedding, + show_save_ax=doc_show_save_ax, +) +def draw_graph( + adata: AnnData, *, layout: _Layout | None = None, **kwargs +) -> Figure | Axes | list[Axes] | None: + """Scatter plot in graph-drawing basis. + + Parameters + ---------- + {adata_color_etc} + layout + One of the :func:`~scanpy.tl.draw_graph` layouts. + By default, the last computed layout is used. + {edges_arrows} + {scatter_bulk} + {show_save_ax} + + Returns + ------- + If `show==False` a :class:`~matplotlib.axes.Axes` or a list of it. + + Examples + -------- + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc68k_reduced() + sc.tl.draw_graph(adata) + sc.pl.draw_graph(adata, color=['phase', 'bulk_labels']) + + .. currentmodule:: scanpy + + See Also + -------- + tl.draw_graph + + """ + if layout is None: + layout = str(adata.uns["draw_graph"]["params"]["layout"]) + basis = f"draw_graph_{layout}" + if f"X_{basis}" not in adata.obsm: + msg = f"Did not find {basis} in adata.obs. Did you compute layout {layout}?" + raise ValueError(msg) + + return embedding(adata, basis, **kwargs) + + +@_wraps_plot_scatter +@_doc_params( + adata_color_etc=doc_adata_color_etc, + scatter_bulk=doc_scatter_embedding, + show_save_ax=doc_show_save_ax, +) +def pca( + adata: AnnData, + *, + annotate_var_explained: bool = False, + show: bool | None = None, + return_fig: bool | None = None, + save: bool | str | None = None, + **kwargs, +) -> Figure | Axes | list[Axes] | None: + """Scatter plot in PCA coordinates. + + Use the parameter `annotate_var_explained` to annotate the explained variance. + + Parameters + ---------- + {adata_color_etc} + annotate_var_explained + {scatter_bulk} + {show_save_ax} + + Returns + ------- + If `show==False` a :class:`~matplotlib.axes.Axes` or a list of it. + + Examples + -------- + + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc3k_processed() + sc.pl.pca(adata) + + Colour points by discrete variable (Louvain clusters). + + .. plot:: + :context: close-figs + + sc.pl.pca(adata, color="louvain") + + Colour points by gene expression. + + .. plot:: + :context: close-figs + + sc.pl.pca(adata, color="CST3") + + .. currentmodule:: scanpy + + See Also + -------- + pp.pca + + """ + if not annotate_var_explained: + return embedding( + adata, "pca", show=show, return_fig=return_fig, save=save, **kwargs + ) + if "pca" not in adata.obsm and "X_pca" not in adata.obsm: + msg = ( + f"Could not find entry in `obsm` for 'pca'.\n" + f"Available keys are: {list(adata.obsm.keys())}." + ) + raise KeyError(msg) + + label_dict = { + f"PC{i + 1}": f"PC{i + 1} ({round(v * 100, 2)}%)" + for i, v in enumerate(adata.uns["pca"]["variance_ratio"]) + } + + if return_fig is True: + # edit axis labels in returned figure + fig = embedding(adata, "pca", return_fig=return_fig, **kwargs) + for ax in fig.axes: + if xlabel := label_dict.get(ax.xaxis.get_label().get_text()): + ax.set_xlabel(xlabel) + if ylabel := label_dict.get(ax.yaxis.get_label().get_text()): + ax.set_ylabel(ylabel) + return fig + + # get the axs, edit the labels and apply show and save from user + axs = embedding(adata, "pca", show=False, save=False, **kwargs) + if isinstance(axs, list): + for ax in axs: + ax.set_xlabel(label_dict[ax.xaxis.get_label().get_text()]) + ax.set_ylabel(label_dict[ax.yaxis.get_label().get_text()]) + else: + axs.set_xlabel(label_dict[axs.xaxis.get_label().get_text()]) + axs.set_ylabel(label_dict[axs.yaxis.get_label().get_text()]) + _utils.savefig_or_show("pca", show=show, save=save) + show = settings.autoshow if show is None else show + if show: + return None + return axs + + +@deprecated("Use `squidpy.pl.spatial_scatter` instead.") +@_wraps_plot_scatter +@_doc_params( + adata_color_etc=doc_adata_color_etc, + scatter_spatial=doc_scatter_spatial, + scatter_bulk=doc_scatter_embedding, + show_save_ax=doc_show_save_ax, +) +def spatial( # noqa: PLR0913 + adata: AnnData, + *, + basis: str = "spatial", + img: np.ndarray | None = None, + img_key: str | None | Empty = _empty, + library_id: str | None | Empty = _empty, + crop_coord: tuple[int, int, int, int] | None = None, + alpha_img: float = 1.0, + bw: bool | None = False, + size: float = 1.0, + scale_factor: float | None = None, + spot_size: float | None = None, + na_color: ColorLike | None = None, + show: bool | None = None, + return_fig: bool | None = None, + save: bool | str | None = None, + **kwargs, +) -> Figure | Axes | list[Axes] | None: + """Scatter plot in spatial coordinates. + + .. deprecated:: 1.11.0 + Use :func:`squidpy.pl.spatial_scatter` instead. + + This function allows overlaying data on top of images. + Use the parameter `img_key` to see the image in the background + And the parameter `library_id` to select the image. + By default, `'hires'` and `'lowres'` are attempted. + + Use `crop_coord`, `alpha_img`, and `bw` to control how it is displayed. + Use `size` to scale the size of the Visium spots plotted on top. + + As this function is designed to for imaging data, there are two key assumptions + about how coordinates are handled: + + 1. The origin (e.g `(0, 0)`) is at the top left – as is common convention + with image data. + + 2. Coordinates are in the pixel space of the source image, so an equal + aspect ratio is assumed. + + If your anndata object has a `"spatial"` entry in `.uns`, the `img_key` + and `library_id` parameters to find values for `img`, `scale_factor`, + and `spot_size` arguments. Alternatively, these values be passed directly. + + Parameters + ---------- + {adata_color_etc} + {scatter_spatial} + {scatter_bulk} + {show_save_ax} + + Returns + ------- + If `show==False` a :class:`~matplotlib.axes.Axes` or a list of it. + + Examples + -------- + This function behaves very similarly to other embedding plots like + :func:`~scanpy.pl.umap` + + >>> import scanpy as sc + >>> adata = sc.datasets.visium_sge("Targeted_Visium_Human_Glioblastoma_Pan_Cancer") + >>> sc.pp.calculate_qc_metrics(adata, inplace=True) + >>> sc.pl.spatial(adata, color="log1p_n_genes_by_counts") + + See Also + -------- + :func:`scanpy.datasets.visium_sge` + Example visium data. + + """ + # get default image params if available + library_id, spatial_data = _check_spatial_data(adata.uns, library_id) + img, img_key = _check_img(spatial_data, img, img_key, bw=bw) + spot_size = _check_spot_size(spatial_data, spot_size) + scale_factor = _check_scale_factor( + spatial_data, img_key=img_key, scale_factor=scale_factor + ) + crop_coord = _check_crop_coord(crop_coord, scale_factor) + na_color = _check_na_color(na_color, img=img) + + cmap_img = "gray" if bw else None + circle_radius = size * scale_factor * spot_size * 0.5 + + axs = embedding( + adata, + basis=basis, + scale_factor=scale_factor, + size=circle_radius, + na_color=na_color, + show=False, + save=False, + **kwargs, + ) + if not isinstance(axs, list): + axs = [axs] + for ax in axs: + cur_coords = np.concatenate([ax.get_xlim(), ax.get_ylim()]) + if img is not None: + ax.imshow(img, cmap=cmap_img, alpha=alpha_img) + else: + ax.set_aspect("equal") + ax.invert_yaxis() + if crop_coord is not None: + ax.set_xlim(crop_coord[0], crop_coord[1]) + ax.set_ylim(crop_coord[3], crop_coord[2]) + else: + ax.set_xlim(cur_coords[0], cur_coords[1]) + ax.set_ylim(cur_coords[3], cur_coords[2]) + _utils.savefig_or_show("show", show=show, save=save) + if return_fig: + return axs[0].figure + show = settings.autoshow if show is None else show + if show: + return None + return axs + + +# Helpers +def _components_to_dimensions( + components: str | Collection[str] | None, + dimensions: Collection[int] | Collection[Collection[int]] | None, + *, + projection: Literal["2d", "3d"] = "2d", + total_dims: int, +) -> list[Collection[int]]: + """Normalize components/ dimensions args for embedding plots.""" + # TODO: Deprecate components kwarg + ndims = {"2d": 2, "3d": 3}[projection] + if components is None and dimensions is None: + dimensions = [tuple(i for i in range(ndims))] + elif components is not None and dimensions is not None: + msg = "Cannot provide both dimensions and components" + raise ValueError(msg) + + # TODO: Consider deprecating this + # If components is not None, parse them and set dimensions + if components == "all": + dimensions = list(combinations(range(total_dims), ndims)) + elif components is not None: + if isinstance(components, str): + components = [components] + # Components use 1 based indexing + dimensions = [[int(dim) - 1 for dim in c.split(",")] for c in components] + + if all(isinstance(el, Integral) for el in dimensions): + dimensions = [dimensions] + # if all(isinstance(el, Collection) for el in dimensions): + for dims in dimensions: + if len(dims) != ndims or not all(isinstance(d, Integral) for d in dims): + raise ValueError() + + return dimensions + + +def _add_categorical_legend( # noqa: PLR0913 + ax: Axes, + color_source_vector, + *, + palette: dict, + legend_loc: _LegendLoc | None, + legend_fontweight, + legend_fontsize, + legend_fontoutline, + multi_panel, + na_color, + na_in_legend: bool, + scatter_array=None, +): + """Add a legend to the passed Axes.""" + if na_in_legend and pd.isnull(color_source_vector).any(): + if "NA" in color_source_vector: + msg = "No fallback for null labels has been defined if NA already in categories." + raise NotImplementedError(msg) + color_source_vector = color_source_vector.add_categories("NA").fillna("NA") + palette = palette.copy() + palette["NA"] = na_color + if color_source_vector.dtype == bool: + cats = pd.Categorical(color_source_vector.astype(str)).categories + else: + cats = color_source_vector.categories + + if multi_panel is True: + # Shrink current axis by 10% to fit legend and match + # size of plots that are not categorical + box = ax.get_position() + ax.set_position([box.x0, box.y0, box.width * 0.91, box.height]) + + if legend_loc == "on data": + # identify centroids to put labels + + all_pos = ( + pd.DataFrame(scatter_array, columns=["x", "y"]) + .groupby(color_source_vector, observed=True) + .median() + # Have to sort_index since if observed=True and categorical is unordered + # the order of values in .index is undefined. Related issue: + # https://github.com/pandas-dev/pandas/issues/25167 + .sort_index() + ) + + for label, x_pos, y_pos in all_pos.itertuples(): + ax.text( + x_pos, + y_pos, + label, + weight=legend_fontweight, + verticalalignment="center", + horizontalalignment="center", + fontsize=legend_fontsize, + path_effects=legend_fontoutline, + ) + elif legend_loc not in {None, "none"}: + for label in cats: + ax.scatter([], [], c=palette[label], label=label) + if legend_loc == "right margin": + ax.legend( + frameon=False, + loc="center left", + bbox_to_anchor=(1, 0.5), + ncol=(1 if len(cats) <= 14 else 2 if len(cats) <= 30 else 3), + fontsize=legend_fontsize, + ) + else: + ax.legend(loc=legend_loc, fontsize=legend_fontsize) + + +def _get_basis(adata: AnnData, basis: str) -> np.ndarray: + """Get array for basis from anndata. Just tries to add 'X_'.""" + if basis in adata.obsm: + return adata.obsm[basis] + elif f"X_{basis}" in adata.obsm: + return adata.obsm[f"X_{basis}"] + else: + msg = f"Could not find {basis!r} or 'X_{basis}' in .obsm" + raise KeyError(msg) + + +def _get_color_source_vector( + adata: AnnData, + value_to_plot: str, + *, + mask_obs: NDArray[np.bool_] | None = None, + use_raw: bool = False, + gene_symbols: str | None = None, + layer: str | None = None, + groups: Sequence[str] | None = None, +) -> np.ndarray | pd.api.extensions.ExtensionArray: + """Get array from adata that colors will be based on.""" + if value_to_plot is None: + # Points will be plotted with `na_color`. Ideally this would work + # with the "bad color" in a color map but that throws a warning. Instead + # _color_vector handles this. + # https://github.com/matplotlib/matplotlib/issues/18294 + return np.broadcast_to(np.nan, adata.n_obs) + if ( + gene_symbols is not None + and value_to_plot not in adata.obs.columns + and value_to_plot not in adata.var_names + ): + # We should probably just make an index for this, and share it over runs + # TODO: Throw helpful error if this doesn't work + value_to_plot = adata.var.index[adata.var[gene_symbols] == value_to_plot][0] + if use_raw and value_to_plot not in adata.obs.columns: + values = adata.raw.obs_vector(value_to_plot) + else: + values = adata.obs_vector(value_to_plot, layer=layer) + if mask_obs is not None: + values = values.copy() + values[~mask_obs] = np.nan + if groups and isinstance(values, pd.Categorical): + values = values.remove_categories(values.categories.difference(groups)) + return values + + +def _get_palette(adata, values_key: str, palette=None): + color_key = f"{values_key}_colors" + if adata.obs[values_key].dtype == bool: + values = pd.Categorical(adata.obs[values_key].astype(str)) + else: + values = pd.Categorical(adata.obs[values_key]) + if palette: + _utils._set_colors_for_categorical_obs(adata, values_key, palette) + elif color_key not in adata.uns or len(adata.uns[color_key]) < len( + values.categories + ): + # set a default palette in case that no colors or too few colors are found + _utils._set_default_colors_for_categorical_obs(adata, values_key) + else: + _utils._validate_palette(adata, values_key) + return dict( + zip( + values.categories, + adata.uns[color_key][: len(values.categories)], + strict=True, + ) + ) + + +def _color_vector( + adata: AnnData, + values_key: str | None, + *, + values: np.ndarray | pd.api.extensions.ExtensionArray, + palette: str | Sequence[str] | Cycler | None, + na_color: ColorLike = "lightgray", +) -> tuple[np.ndarray | pd.api.extensions.ExtensionArray, Literal["cat", "na", "cont"]]: + """Map array of values to array of hex (plus alpha) codes. + + For categorical data, the return value is list of colors taken + from the category palette or from the given `palette` value. + + For continuous values, the input array is returned (may change in future). + """ + ### + # when plotting, the color of the dots is determined for each plot + # the data is either categorical or continuous and the data could be in + # 'obs' or in 'var' + to_hex = partial(colors.to_hex, keep_alpha=True) + if values_key is None: + return np.broadcast_to(to_hex(na_color), adata.n_obs), "na" + if values.dtype == bool: + values = pd.Categorical(values.astype(str)) + elif not isinstance(values, pd.Categorical): + return values, "cont" + + color_map = { + k: to_hex(v) + for k, v in _get_palette(adata, values_key, palette=palette).items() + } + # If color_map does not have unique values, this can be slow as the + # result is not categorical + if pkg_version("pandas") < Version("2.1.0"): + color_vector = pd.Categorical(values.map(color_map)) + else: + color_vector = pd.Categorical(values.map(color_map, na_action="ignore")) + # Set color to 'missing color' for all missing values + if color_vector.isna().any(): + color_vector = color_vector.add_categories([to_hex(na_color)]) + color_vector = color_vector.fillna(to_hex(na_color)) + return color_vector, "cat" + + +def _basis2name(basis): + """Convert the 'basis' into the proper name.""" + component_name = ( + "DC" + if basis == "diffmap" + else "tSNE" + if basis == "tsne" + else "UMAP" + if basis == "umap" + else "PC" + if basis == "pca" + else basis.replace("draw_graph_", "").upper() + if "draw_graph" in basis + else basis + ) + return component_name + + +def _check_spot_size(spatial_data: Mapping | None, spot_size: float | None) -> float: + """Resolve spot_size value. + + This is a required argument for spatial plots. + """ + if spatial_data is None and spot_size is None: + msg = ( + "When .uns['spatial'][library_id] does not exist, spot_size must be " + "provided directly." + ) + raise ValueError(msg) + elif spot_size is None: + return spatial_data["scalefactors"]["spot_diameter_fullres"] + else: + return spot_size + + +def _check_scale_factor( + spatial_data: Mapping | None, + img_key: str | None, + scale_factor: float | None, +) -> float: + """Resolve scale_factor, defaults to 1.""" + if scale_factor is not None: + return scale_factor + elif spatial_data is not None and img_key is not None: + return spatial_data["scalefactors"][f"tissue_{img_key}_scalef"] + else: + return 1.0 + + +def _check_spatial_data( + uns: Mapping, library_id: str | None | Empty +) -> tuple[str | None, Mapping | None]: + """Given a mapping, try and extract a library id/ mapping with spatial data. + + Assumes this is `.uns` from how we parse visium data. + """ + spatial_mapping = uns.get("spatial", {}) + if library_id is _empty: + if len(spatial_mapping) > 1: + msg = ( + "Found multiple possible libraries in `.uns['spatial']. Please specify." + f" Options are:\n\t{list(spatial_mapping.keys())}" + ) + raise ValueError(msg) + elif len(spatial_mapping) == 1: + library_id = next(iter(spatial_mapping.keys())) + else: + library_id = None + spatial_data = spatial_mapping[library_id] if library_id is not None else None + return library_id, spatial_data + + +def _check_img( + spatial_data: Mapping | None, + img: np.ndarray | None, + img_key: None | str | Empty, + *, + bw: bool = False, +) -> tuple[np.ndarray | None, str | None]: + """Resolve image for spatial plots.""" + if img is None and spatial_data is not None and img_key is _empty: + img_key = next( + (k for k in ["hires", "lowres"] if k in spatial_data["images"]), + ) # Throws StopIteration Error if keys not present + if img is None and spatial_data is not None and img_key is not None: + img = spatial_data["images"][img_key] + if bw: + img = np.dot(img[..., :3], [0.2989, 0.5870, 0.1140]) + return img, img_key + + +def _check_crop_coord( + crop_coord: tuple | None, + scale_factor: float, +) -> tuple[float, float, float, float]: + """Handle cropping with image or basis.""" + if crop_coord is None: + return None + if len(crop_coord) != 4: + msg = "Invalid crop_coord of length {len(crop_coord)}(!=4)" + raise ValueError(msg) + crop_coord = tuple(c * scale_factor for c in crop_coord) + return crop_coord + + +def _check_na_color( + na_color: ColorLike | None, *, img: np.ndarray | None = None +) -> ColorLike: + if na_color is None: + na_color = (0.0, 0.0, 0.0, 0.0) if img is not None else "lightgray" + return na_color + + +def _broadcast_args(*args): + """Broadcasts arguments to a common length.""" + lens = [len(arg) for arg in args] + longest = max(lens) + if not (set(lens) == {1, longest} or set(lens) == {longest}): + msg = f"Could not broadcast together arguments with shapes: {lens}." + raise ValueError(msg) + return [[arg[0] for _ in range(longest)] if len(arg) == 1 else arg for arg in args] diff --git a/.venv/lib/python3.11/site-packages/scanpy/plotting/_utils.py b/.venv/lib/python3.11/site-packages/scanpy/plotting/_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f09568c2a043590c37c51f2d288979b11ebf66ee --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/plotting/_utils.py @@ -0,0 +1,1226 @@ +from __future__ import annotations + +import warnings +from collections.abc import Callable, Mapping, Sequence +from typing import TYPE_CHECKING, Literal, TypedDict, overload + +import numpy as np +from cycler import Cycler, cycler +from matplotlib import axes, colormaps, gridspec, rcParams, ticker +from matplotlib import pyplot as plt +from matplotlib.axes import Axes +from matplotlib.collections import PatchCollection +from matplotlib.colors import is_color_like +from matplotlib.figure import SubplotParams +from matplotlib.patches import Circle + +from .. import logging as logg +from .._compat import old_positionals +from .._settings import settings +from .._utils import NeighborsView, _empty +from . import palettes + +if TYPE_CHECKING: + from collections.abc import Collection + + from anndata import AnnData + from matplotlib.colors import Colormap + from matplotlib.figure import Figure + from matplotlib.typing import MarkerType + from numpy.typing import ArrayLike + from PIL.Image import Image + + from .._utils import Empty + + # TODO: more + DensityNorm = Literal["area", "count", "width"] + +# These are needed by _wraps_plot_scatter +VBound = str | float | Callable[[Sequence[float]], float] +_FontWeight = Literal["light", "normal", "medium", "semibold", "bold", "heavy", "black"] +_FontSize = Literal[ + "xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large" +] +_LegendLoc = Literal[ + "none", + "right margin", + "on data", + "on data export", + "best", + "upper right", + "upper left", + "lower left", + "lower right", + "right", + "center left", + "center right", + "lower center", + "upper center", + "center", +] +ColorLike = str | tuple[float, ...] + + +class _AxesSubplot(Axes, axes.SubplotBase): + """Intersection between Axes and SubplotBase: Has methods of both.""" + + +# ------------------------------------------------------------------------------- +# Simple plotting functions +# ------------------------------------------------------------------------------- + + +@old_positionals( + "xlabel", + "ylabel", + "xticks", + "yticks", + "title", + "colorbar_shrink", + "color_map", + "show", + "save", + "ax", +) +def matrix( # noqa: PLR0913 + matrix: ArrayLike | Image, + *, + xlabel: str | None = None, + ylabel: str | None = None, + xticks: Collection[str] | None = None, + yticks: Collection[str] | None = None, + title: str | None = None, + colorbar_shrink: float = 0.5, + color_map: str | Colormap | None = None, + show: bool | None = None, + save: bool | str | None = None, + ax: Axes | None = None, +) -> None: + """Plot a matrix.""" + if ax is None: + ax = plt.gca() + img = ax.imshow(matrix, cmap=color_map) + if xlabel is not None: + ax.set_xlabel(xlabel) + if ylabel is not None: + ax.set_ylabel(ylabel) + if title is not None: + ax.set_title(title) + if xticks is not None: + ax.set_xticks(range(len(xticks)), xticks, rotation="vertical") + if yticks is not None: + ax.set_yticks(range(len(yticks)), yticks) + plt.colorbar( + img, shrink=colorbar_shrink, ax=ax + ) # need a figure instance for colorbar + savefig_or_show("matrix", show=show, save=save) + + +def timeseries(X, **kwargs): # noqa: N803 + """Plot X. See timeseries_subplot.""" + plt.figure( + figsize=tuple(2 * s for s in rcParams["figure.figsize"]), + subplotpars=SubplotParams(left=0.12, right=0.98, bottom=0.13), + ) + timeseries_subplot(X, **kwargs) + + +def timeseries_subplot( # noqa: PLR0912, PLR0913 + X: np.ndarray, # noqa: N803 + *, + time=None, + color=None, + var_names=(), + highlights_x=(), + xlabel="", + ylabel="gene expression", + yticks=None, + xlim=None, + legend=True, + palette: Sequence[str] | Cycler | None = None, + color_map="viridis", + ax: Axes | None = None, + marker: str | Sequence[str] = ".", +): + """Plot X. + + Parameters + ---------- + X + Call this with: + X with one column, color categorical. + X with one column, color continuous. + X with n columns, color is of length n. + + """ + if color is not None: + use_color_map = isinstance(color[0], float | np.floating) + palette = default_palette(palette) + x_range = np.arange(X.shape[0]) if time is None else time + if X.ndim == 1: + X = X[:, None] # noqa: N806 + if X.shape[1] > 1: + colors = palette[: X.shape[1]].by_key()["color"] + subsets = [(x_range, X[:, i]) for i in range(X.shape[1])] + elif use_color_map: + colors = [color] + subsets = [(x_range, X[:, 0])] + else: + levels, _ = np.unique(color, return_inverse=True) + colors = np.array(palette[: len(levels)].by_key()["color"]) + subsets = [(x_range[color == level], X[color == level, :]) for level in levels] + + if isinstance(marker, str): + marker = [marker] + if len(marker) != len(subsets) and len(marker) == 1: + marker = [marker[0] for _ in range(len(subsets))] + + if ax is None: + ax = plt.subplot() + for i, (x, y) in enumerate(subsets): + ax.scatter( + x, + y, + marker=marker[i], + edgecolor="face", + s=rcParams["lines.markersize"], + c=colors[i], + label=var_names[i] if len(var_names) > 0 else "", + cmap=color_map, + rasterized=settings._vector_friendly, + ) + ylim = ax.get_ylim() + for h in highlights_x: + ax.plot([h, h], [ylim[0], ylim[1]], "--", color="black") + ax.set_ylim(ylim) + if xlim is not None: + ax.set_xlim(xlim) + ax.set_xlabel(xlabel) + ax.set_ylabel(ylabel) + if yticks is not None: + ax.set_yticks(yticks) + if len(var_names) > 0 and legend: + ax.legend(frameon=False) + + +def timeseries_as_heatmap( + X: np.ndarray, # noqa: N803 + *, + var_names: Collection[str] = (), + highlights_x=(), + color_map=None, +): + """Plot timeseries as heatmap. + + Parameters + ---------- + X + Data array. + var_names + Array of strings naming variables stored in columns of X. + + """ + if len(var_names) == 0: + var_names = np.arange(X.shape[1]) + if var_names.ndim == 2: + var_names = var_names[:, 0] + + X = X.T # noqa: N806 + + _, ax = plt.subplots(figsize=(1.5 * 4, 2 * 4)) + img = ax.imshow( + np.array(X, dtype=np.float64), + aspect="auto", + interpolation="nearest", + cmap=color_map, + ) + plt.colorbar(img, shrink=0.5) + plt.yticks(range(X.shape[0]), var_names) + for h in highlights_x: + plt.plot([h, h], [0, X.shape[0]], "--", color="black") + plt.xlim([0, X.shape[1] - 1]) + plt.ylim([0, X.shape[0] - 1]) + + +# ------------------------------------------------------------------------------- +# Colors in addition to matplotlib's colors +# ------------------------------------------------------------------------------- + + +additional_colors = { + "gold2": "#eec900", + "firebrick3": "#cd2626", + "khaki2": "#eee685", + "slategray3": "#9fb6cd", + "palegreen3": "#7ccd7c", + "tomato2": "#ee5c42", + "grey80": "#cccccc", + "grey90": "#e5e5e5", + "wheat4": "#8b7e66", + "grey65": "#a6a6a6", + "grey10": "#1a1a1a", + "grey20": "#333333", + "grey50": "#7f7f7f", + "grey30": "#4d4d4d", + "grey40": "#666666", + "antiquewhite2": "#eedfcc", + "grey77": "#c4c4c4", + "snow4": "#8b8989", + "chartreuse3": "#66cd00", + "yellow4": "#8b8b00", + "darkolivegreen2": "#bcee68", + "olivedrab3": "#9acd32", + "azure3": "#c1cdcd", + "violetred": "#d02090", + "mediumpurple3": "#8968cd", + "purple4": "#551a8b", + "seagreen4": "#2e8b57", + "lightblue3": "#9ac0cd", + "orchid3": "#b452cd", + "indianred 3": "#cd5555", + "grey60": "#999999", + "mediumorchid1": "#e066ff", + "plum3": "#cd96cd", + "palevioletred3": "#cd6889", +} + +# ------------------------------------------------------------------------------- +# Helper functions +# ------------------------------------------------------------------------------- + + +def savefig(writekey, dpi=None, ext=None): + """Save current figure to file. + + The `filename` is generated as follows: + + filename = settings.figdir / f"{writekey}{settings.plot_suffix}.{settings.file_format_figs}" + """ + if dpi is None: + # we need this as in notebooks, the internal figures are also influenced by 'savefig.dpi' this... + if ( + not isinstance(rcParams["savefig.dpi"], str) + and rcParams["savefig.dpi"] < 150 + ): + if settings._low_resolution_warning: + logg.warning( + "You are using a low resolution (dpi<150) for saving figures.\n" + "Consider running `set_figure_params(dpi_save=...)`, which will " + "adjust `matplotlib.rcParams['savefig.dpi']`" + ) + settings._low_resolution_warning = False + else: + dpi = rcParams["savefig.dpi"] + settings.figdir.mkdir(parents=True, exist_ok=True) + if ext is None: + ext = settings.file_format_figs + filename = settings.figdir / f"{writekey}{settings.plot_suffix}.{ext}" + # output the following msg at warning level; it's really important for the user + logg.warning(f"saving figure to file {filename}") + plt.savefig(filename, dpi=dpi, bbox_inches="tight") + + +def savefig_or_show( + writekey: str, + *, + show: bool | None = None, + dpi: int | None = None, + ext: str | None = None, + save: bool | str | None = None, +): + if isinstance(save, str): + # check whether `save` contains a figure extension + if ext is None: + for try_ext in [".svg", ".pdf", ".png"]: + if save.endswith(try_ext): + ext = try_ext[1:] + save = save.replace(try_ext, "") + break + # append it + writekey += save + save = True + save = settings.autosave if save is None else save + show = settings.autoshow if show is None else show + if save: + savefig(writekey, dpi=dpi, ext=ext) + if show: + plt.show() + if save: + plt.close() # clear figure + + +def default_palette( + palette: str | Sequence[str] | Cycler | None = None, +) -> str | Cycler: + if palette is None: + return rcParams["axes.prop_cycle"] + elif not isinstance(palette, str | Cycler): + return cycler(color=palette) + else: + return palette + + +def _validate_palette(adata: AnnData, key: str) -> None: + """Validate and update the list of colors in `adata.uns[f'{key}_colors']`. + + Not only valid matplotlib colors are checked but also if the color name + is a valid R color name, in which case it will be translated to a valid name + """ + color_key = f"{key}_colors" + raw_palette = adata.uns[color_key] + try: + # check if the color is a valid R color and translate it + # to a valid hex color value + palette = [ + color if is_color_like(color) else additional_colors[color] + for color in raw_palette + ] + except KeyError as e: + logg.warning( + f"The following color value found in adata.uns['{key}_colors'] " + f"is not valid: {e.args[0]!r}. Default colors will be used instead." + ) + _set_default_colors_for_categorical_obs(adata, key) + palette = None + # Don’t modify if nothing changed + if palette is None or np.array_equal(palette, adata.uns[color_key]): + return + adata.uns[color_key] = palette + + +def _set_colors_for_categorical_obs( + adata, value_to_plot, palette: str | Sequence[str] | Cycler +): + """Set `adata.uns[f'{value_to_plot}_colors']` according to the given palette. + + Parameters + ---------- + adata + annData object + value_to_plot + name of a valid categorical observation + palette + Palette should be either a valid :func:`~matplotlib.pyplot.colormaps` string, + a sequence of colors (in a format that can be understood by matplotlib, + eg. RGB, RGBS, hex, or a cycler object with key='color' + + Returns + ------- + None + + """ + from matplotlib.colors import to_hex + + if adata.obs[value_to_plot].dtype == bool: + categories = ( + adata.obs[value_to_plot].astype(str).astype("category").cat.categories + ) + else: + categories = adata.obs[value_to_plot].cat.categories + # check is palette is a valid matplotlib colormap + if isinstance(palette, str) and palette in colormaps: + # this creates a palette from a colormap. E.g. 'Accent, Dark2, tab20' + cmap = colormaps.get_cmap(palette) + colors_list = [to_hex(x) for x in cmap(np.linspace(0, 1, len(categories)))] + elif isinstance(palette, Mapping): + colors_list = [to_hex(palette[k], keep_alpha=True) for k in categories] + else: + # check if palette is a list and convert it to a cycler, thus + # it doesnt matter if the list is shorter than the categories length: + if isinstance(palette, Sequence): + if len(palette) < len(categories): + logg.warning( + "Length of palette colors is smaller than the number of " + f"categories (palette length: {len(palette)}, " + f"categories length: {len(categories)}. " + "Some categories will have the same color." + ) + try: # check that colors are valid + _color_list = [ + color if is_color_like(color) else additional_colors[color] + for color in palette + ] + except KeyError as e: + msg = ( + "The following color value of the given palette " + f"is not valid: {e.args[0]!r}" + ) + raise ValueError(msg) from None + + palette = cycler(color=_color_list) + if not isinstance(palette, Cycler): + msg = ( + "Please check that the value of 'palette' is a valid " + "matplotlib colormap string (eg. Set2), a list of color names " + "or a cycler with a 'color' key." + ) + raise ValueError(msg) + if "color" not in palette.keys: + msg = "Please set the palette key 'color'." + raise ValueError(msg) + + cc = palette() + colors_list = [to_hex(next(cc)["color"]) for x in range(len(categories))] + + adata.uns[f"{value_to_plot}_colors"] = colors_list + + +def _set_default_colors_for_categorical_obs(adata, value_to_plot): + """Set `adata.uns[f'{value_to_plot}_colors']` using default color palettes. + + Parameters + ---------- + adata + AnnData object + value_to_plot + Name of a valid categorical observation + + Returns + ------- + None + + """ + if adata.obs[value_to_plot].dtype == bool: + categories = ( + adata.obs[value_to_plot].astype(str).astype("category").cat.categories + ) + else: + categories = adata.obs[value_to_plot].cat.categories + + length = len(categories) + + # check if default matplotlib palette has enough colors + if len(rcParams["axes.prop_cycle"].by_key()["color"]) >= length: + cc = rcParams["axes.prop_cycle"]() + palette = [next(cc)["color"] for _ in range(length)] + + elif length <= 20: + palette = palettes.default_20 + elif length <= 28: + palette = palettes.default_28 + elif length <= len(palettes.default_102): # 103 colors + palette = palettes.default_102 + else: + palette = ["grey" for _ in range(length)] + logg.info( + f"the obs value {value_to_plot!r} has more than 103 categories. Uniform " + "'grey' color will be used for all categories." + ) + + _set_colors_for_categorical_obs(adata, value_to_plot, palette[:length]) + + +def add_colors_for_categorical_sample_annotation( + adata, key, *, palette=None, force_update_colors=False +): + color_key = f"{key}_colors" + colors_needed = len(adata.obs[key].cat.categories) + if palette and force_update_colors: + _set_colors_for_categorical_obs(adata, key, palette) + elif color_key in adata.uns and len(adata.uns[color_key]) <= colors_needed: + _validate_palette(adata, key) + else: + _set_default_colors_for_categorical_obs(adata, key) + + +def plot_edges(axs, adata, basis, edges_width, edges_color, *, neighbors_key=None): + import networkx as nx + + if not isinstance(axs, Sequence): + axs = [axs] + + if neighbors_key is None: + neighbors_key = "neighbors" + if neighbors_key not in adata.uns: + msg = "`edges=True` requires `pp.neighbors` to be run before." + raise ValueError(msg) + neighbors = NeighborsView(adata, neighbors_key) + g = nx.Graph(neighbors["connectivities"]) + basis_key = _get_basis(adata, basis) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + for ax in axs: + edge_collection = nx.draw_networkx_edges( + g, + adata.obsm[basis_key], + ax=ax, + width=edges_width, + edge_color=edges_color, + ) + edge_collection.set_zorder(-2) + edge_collection.set_rasterized(settings._vector_friendly) + + +def plot_arrows(axs, adata, basis, arrows_kwds=None): + if not isinstance(axs, Sequence): + axs = [axs] + v_prefix = next( + (p for p in ["velocity", "Delta"] if f"{p}_{basis}" in adata.obsm), None + ) + if v_prefix is None: + msg = ( + "`arrows=True` requires " + f"`'velocity_{basis}'` from scvelo or " + f"`'Delta_{basis}'` from velocyto." + ) + raise ValueError(msg) + if v_prefix == "velocity": + logg.warning( + "The module `scvelo` has improved plotting facilities. " + "Prefer using `scv.pl.velocity_embedding` to `arrows=True`." + ) + + basis_key = _get_basis(adata, basis) + x = adata.obsm[basis_key] + v = adata.obsm[f"{v_prefix}_{basis}"] + for ax in axs: + quiver_kwds = arrows_kwds if arrows_kwds is not None else {} + ax.quiver( + x[:, 0], + x[:, 1], + v[:, 0], + v[:, 1], + **quiver_kwds, + rasterized=settings._vector_friendly, + ) + + +def scatter_group( + ax: Axes, + key: str, + cat_code: int, + adata: AnnData, + y: np.ndarray, + *, + projection: Literal["2d", "3d"] = "2d", + size: int = 3, + alpha: float | None = None, + marker: MarkerType = ".", +): + """Scatter of group using representation of data Y.""" + mask_obs = adata.obs[key].cat.categories[cat_code] == adata.obs[key].values + color = adata.uns[f"{key}_colors"][cat_code] + if not isinstance(color[0], str): + from matplotlib.colors import rgb2hex + + color = rgb2hex(adata.uns[f"{key}_colors"][cat_code]) + if not is_color_like(color): + msg = f"{color!r} is not a valid matplotlib color." + raise ValueError(msg) + data = [y[mask_obs, 0], y[mask_obs, 1]] + if projection == "3d": + data.append(y[mask_obs, 2]) + ax.scatter( + *data, + marker=marker, + alpha=alpha, + c=color, + edgecolors="none", + s=size, + label=adata.obs[key].cat.categories[cat_code], + rasterized=settings._vector_friendly, + ) + return mask_obs + + +def setup_axes( # noqa: PLR0912 + ax: Axes | Sequence[Axes] | None = None, + *, + panels="blue", + colorbars=(False,), + right_margin=None, + left_margin=None, + projection: Literal["2d", "3d"] = "2d", + show_ticks=False, +): + """Grid of axes for plotting, legends and colorbars.""" + check_projection(projection) + if left_margin is not None: + msg = "We currently don’t support `left_margin`." + raise NotImplementedError(msg) + if np.any(colorbars) and right_margin is None: + right_margin = 1 - rcParams["figure.subplot.right"] + 0.21 # 0.25 + elif right_margin is None: + right_margin = 1 - rcParams["figure.subplot.right"] + 0.06 # 0.10 + # make a list of right margins for each panel + if not isinstance(right_margin, list): + right_margin_list = [right_margin for i in range(len(panels))] + else: + right_margin_list = right_margin + + # make a figure with len(panels) panels in a row side by side + top_offset = 1 - rcParams["figure.subplot.top"] + bottom_offset = 0.15 if show_ticks else 0.08 + left_offset = 1 if show_ticks else 0.3 # in units of base_height + base_height = rcParams["figure.figsize"][1] + height = base_height + base_width = rcParams["figure.figsize"][0] + if show_ticks: + base_width *= 1.1 + + draw_region_width = ( + base_width - left_offset - top_offset - 0.5 + ) # this is kept constant throughout + + right_margin_factor = sum([1 + right_margin for right_margin in right_margin_list]) + width_without_offsets = ( + right_margin_factor * draw_region_width + ) # this is the total width that keeps draw_region_width + + right_offset = (len(panels) - 1) * left_offset + figure_width = width_without_offsets + left_offset + right_offset + draw_region_width_frac = draw_region_width / figure_width + left_offset_frac = left_offset / figure_width + right_offset_frac = ( # noqa: F841 # TODO Does this need fixing? + 1 - (len(panels) - 1) * left_offset_frac + ) + + if ax is None: + plt.figure( + figsize=(figure_width, height), + subplotpars=SubplotParams(left=0, right=1, bottom=bottom_offset), + ) + left_positions = [left_offset_frac, left_offset_frac + draw_region_width_frac] + for i in range(1, len(panels)): + right_margin = right_margin_list[i - 1] + left_positions.append( + left_positions[-1] + right_margin * draw_region_width_frac + ) + left_positions.append(left_positions[-1] + draw_region_width_frac) + panel_pos = [[bottom_offset], [1 - top_offset], left_positions] + + axs = [] + if ax is None: + for icolor, _color in enumerate(panels): + left = panel_pos[2][2 * icolor] + bottom = panel_pos[0][0] + width = draw_region_width / figure_width + height = panel_pos[1][0] - bottom + if projection == "2d": + ax = plt.axes([left, bottom, width, height]) + elif projection == "3d": + ax = plt.axes([left, bottom, width, height], projection="3d") + axs.append(ax) + else: + axs = ax if isinstance(ax, Sequence) else [ax] + + return axs, panel_pos, draw_region_width, figure_width + + +def scatter_base( # noqa: PLR0912, PLR0913, PLR0915 + y: np.ndarray, + /, + *, + colors: str | Sequence[ColorLike | np.ndarray] = "blue", + sort_order=True, + alpha=None, + highlights=(), + right_margin=None, + left_margin=None, + projection: Literal["2d", "3d"] = "2d", + title=None, + component_name="DC", + component_indexnames=(1, 2, 3), + axis_labels=None, + colorbars=(False,), + sizes=(1,), + markers=".", + color_map="viridis", + show_ticks=True, + ax=None, +) -> Axes | list[Axes]: + """Plot scatter plot of data. + + Parameters + ---------- + y + Data array. + projection + + Returns + ------- + Depending on whether supplying a single array or a list of arrays, + return a single axis or a list of axes. + + """ + if isinstance(highlights, Mapping): + highlights_indices = sorted(highlights) + highlights_labels = [highlights[i] for i in highlights_indices] + else: + highlights_indices = map(int, highlights) + highlights_labels = map(str, highlights) + # if we have a single array, transform it into a list with a single array + if isinstance(colors, str): + colors = [colors] + if isinstance(markers, str): + markers = [markers] + if len(sizes) != len(colors) and len(sizes) == 1: + sizes = [sizes[0] for _ in range(len(colors))] + if len(markers) != len(colors) and len(markers) == 1: + markers = [markers[0] for _ in range(len(colors))] + axs, panel_pos, draw_region_width, _figure_width = setup_axes( + ax, + panels=colors, + colorbars=colorbars, + projection=projection, + right_margin=right_margin, + left_margin=left_margin, + show_ticks=show_ticks, + ) + for icolor, color_spec in enumerate(colors): + ax = axs[icolor] + marker = markers[icolor] + bottom = panel_pos[0][0] + height = panel_pos[1][0] - bottom + y_sort = y + if not is_color_like(color_spec) and sort_order: + sort = np.argsort(color_spec) + color = color_spec[sort] + y_sort = y[sort] + else: + color = color_spec + if projection == "2d": + data = y_sort[:, 0], y_sort[:, 1] + elif projection == "3d": + data = y_sort[:, 0], y_sort[:, 1], y_sort[:, 2] + else: + msg = f"Unknown projection {projection!r} not in '2d', '3d'" + raise ValueError(msg) + if not isinstance(color, str) or color != "white": + sct = ax.scatter( + *data, + marker=marker, + c=color, + alpha=alpha, + edgecolors="none", # 'face', + s=sizes[icolor], + cmap=color_map, + rasterized=settings._vector_friendly, + ) + if colorbars[icolor]: + width = 0.006 * draw_region_width / len(colors) + left = ( + panel_pos[2][2 * icolor + 1] + + (1.2 if projection == "3d" else 0.2) * width + ) + rectangle = [left, bottom, width, height] + fig = plt.gcf() + ax_cb = fig.add_axes(rectangle) + _ = plt.colorbar( + sct, format=ticker.FuncFormatter(ticks_formatter), cax=ax_cb + ) + # set the title + if title is not None: + ax.set_title(title[icolor]) + # output highlighted data points + for ihighlight, highlight_text in zip( + highlights_indices, highlights_labels, strict=True + ): + data = [y[ihighlight, 0]], [y[ihighlight, 1]] + if "3d" in projection: + data = [y[ihighlight, 0]], [y[ihighlight, 1]], [y[ihighlight, 2]] + ax.scatter( + *data, + c="black", + facecolors="black", + edgecolors="black", + marker="x", + s=10, + zorder=20, + ) + # the following is a Python 2 compatibility hack + ax.text( + *([d[0] for d in data] + [highlight_text]), + zorder=20, + fontsize=10, + color="black", + ) + if not show_ticks: + ax.set_xticks([]) + ax.set_yticks([]) + if "3d" in projection: + ax.set_zticks([]) + # set default axis_labels + if axis_labels is None: + axis_labels = [ + [component_name + str(i) for i in component_indexnames] + for _ in range(len(axs)) + ] + else: + axis_labels = [axis_labels for _ in range(len(axs))] + for iax, ax in enumerate(axs): + ax.set_xlabel(axis_labels[iax][0]) + ax.set_ylabel(axis_labels[iax][1]) + if "3d" in projection: + # shift the label closer to the axis + ax.set_zlabel(axis_labels[iax][2], labelpad=-7) + for ax in axs: + # scale limits to match data + ax.autoscale_view() + return axs + + +def ticks_formatter(x, pos): + # pretty scientific notation + if False: + a, b = f"{x:.2e}".split("e") + b = int(b) + return rf"${a} \times 10^{{{b}}}$" + else: + return f"{x:.3f}".rstrip("0").rstrip(".") + + +class _Level(TypedDict): + total: int + current: int + + +def hierarchy_pos( + g, /, root: int, levels_: Mapping[int, int] | None = None, width=1.0, height=1.0 +) -> dict[int, tuple[float, float]]: + """Tree layout for networkx graph. + + See https://stackoverflow.com/questions/29586520/can-one-get-hierarchical-graphs-from-networkx-with-python-3 + answer by burubum. + + If there is a cycle that is reachable from root, then this will see + infinite recursion. + + Parameters + ---------- + G: the graph + root: the root node + levels: a dictionary + key: level number (starting from 0) + value: number of nodes in this level + width: horizontal space allocated for drawing + height: vertical space allocated for drawing + + """ + + def make_levels( + levels: dict[int, _Level], + node: int = root, + current_level: int = 0, + parent: int | None = None, + ) -> dict[int, _Level]: + """Compute the number of nodes for each level.""" + if current_level not in levels: + levels[current_level] = _Level(total=0, current=0) + levels[current_level]["total"] += 1 + neighbors: list[int] = list(g.neighbors(node)) + if parent is not None: + neighbors.remove(parent) + for neighbor in neighbors: + levels = make_levels(levels, neighbor, current_level + 1, node) + return levels + + if levels_ is None: + levels = make_levels({}) + else: + levels = {k: _Level(total=0, current=0) for k, v in levels_.items()} + + def make_pos( + pos: dict[int, tuple[float, float]], + node: int = root, + current_level: int = 0, + parent: int | None = None, + vert_loc: float = 0.0, + ): + dx = 1 / levels[current_level]["total"] + left = dx / 2 + pos[node] = ((left + dx * levels[current_level]["current"]) * width, vert_loc) + levels[current_level]["current"] += 1 + neighbors: list[int] = list(g.neighbors(node)) + if parent is not None: + neighbors.remove(parent) + for neighbor in neighbors: + pos = make_pos(pos, neighbor, current_level + 1, node, vert_loc - vert_gap) + return pos + + vert_gap = height / (max(levels.keys()) + 1) + return make_pos({}) + + +def zoom(ax, xy="x", factor=1): + """Zoom into axis.""" + limits = ax.get_xlim() if xy == "x" else ax.get_ylim() + new_limits = 0.5 * (limits[0] + limits[1]) + 1.0 / factor * np.array(( + -0.5, + 0.5, + )) * (limits[1] - limits[0]) + if xy == "x": + ax.set_xlim(new_limits) + else: + ax.set_ylim(new_limits) + + +def get_ax_size(ax: Axes, fig: Figure): + """Get axis size. + + Parameters + ---------- + ax + Axis object from matplotlib. + fig + Figure. + + """ + bbox = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted()) + width, height = bbox.width, bbox.height + width *= fig.dpi + height *= fig.dpi + + +def axis_to_data(ax: Axes, width: float): + """For a width in axis coordinates, return the corresponding in data coordinates. + + Parameters + ---------- + ax + Axis object from matplotlib. + width + Width in xaxis coordinates. + + """ + xlim = ax.get_xlim() + widthx = width * (xlim[1] - xlim[0]) + ylim = ax.get_ylim() + widthy = width * (ylim[1] - ylim[0]) + return 0.5 * (widthx + widthy) + + +def axis_to_data_points(ax: Axes, points_axis: np.ndarray): + """Map points in axis coordinates to data coordinates. + + Uses matplotlib.transform. + + Parameters + ---------- + ax + Axis object from matplotlib. + points_axis + Points in axis coordinates. + + """ + axis_to_data = ax.transAxes + ax.transData.inverted() + return axis_to_data.transform(points_axis) + + +def data_to_axis_points(ax: Axes, points_data: np.ndarray): + """Map points in data coordinates to axis coordinates. + + Uses matplotlib.transform. + + Parameters + ---------- + ax + Axis object from matplotlib. + points_data + Points in data coordinates. + + """ + data_to_axis = axis_to_data.inverted() + return data_to_axis(points_data) + + +def check_projection(projection): + """Validate projection argument.""" + if projection not in {"2d", "3d"}: + msg = f"Projection must be '2d' or '3d', was '{projection}'." + raise ValueError(msg) + + +def circles( + x, y, *, s, ax, marker=None, c="b", vmin=None, vmax=None, scale_factor=1.0, **kwargs +): + """Make a scatter plot of circles. + + Similar to pl.scatter, but the size of circles are in data scale. + + Taken from here: + + Parameters + ---------- + x, y : scalar or array_like, shape (n, ) + Input data + s : scalar or array_like, shape (n, ) + Radius of circles. + c : color or sequence of color, optional, default : 'b' + `c` can be a single color format string, or a sequence of color + specifications of length `N`, or a sequence of `N` numbers to be + mapped to colors using the `cmap` and `norm` specified via kwargs. + Note that `c` should not be a single numeric RGB or RGBA sequence + because that is indistinguishable from an array of values + to be colormapped. (If you insist, use `color` instead.) + `c` can be a 2-D array in which the rows are RGB or RGBA, however. + vmin, vmax : scalar, optional, default: None + `vmin` and `vmax` are used in conjunction with `norm` to normalize + luminance data. If either are `None`, the min and max of the + color array is used. + kwargs : `~matplotlib.collections.Collection` properties + Eg. alpha, edgecolor(ec), facecolor(fc), linewidth(lw), linestyle(ls), + norm, cmap, transform, etc. + + Returns + ------- + paths : `~matplotlib.collections.PathCollection` + + Examples + -------- + a = np.arange(11) + circles(a, a, s=a*0.2, c=a, alpha=0.5, ec='none') + pl.colorbar() + License + -------- + This code is under [The BSD 3-Clause License] + (https://opensource.org/license/bsd-3-clause/) + + """ + # You can set `facecolor` with an array for each patch, + # while you can only set `facecolors` with a value for all. + if scale_factor != 1.0: + x = x * scale_factor + y = y * scale_factor + zipped = np.broadcast(x, y, s) + patches = [Circle((x_, y_), s_) for x_, y_, s_ in zipped] + collection = PatchCollection(patches, **kwargs) + if isinstance(c, np.ndarray) and np.issubdtype(c.dtype, np.number): + collection.set_array(np.ma.masked_invalid(c)) + collection.set_clim(vmin, vmax) + else: + collection.set_facecolor(c) + + ax.add_collection(collection) + + return collection + + +def make_grid_spec( + ax_or_figsize: tuple[int, int] | _AxesSubplot, + *, + nrows: int, + ncols: int, + wspace: float | None = None, + hspace: float | None = None, + width_ratios: Sequence[float] | None = None, + height_ratios: Sequence[float] | None = None, +) -> tuple[Figure, gridspec.GridSpecBase]: + kw = dict( + wspace=wspace, + hspace=hspace, + width_ratios=width_ratios, + height_ratios=height_ratios, + ) + if isinstance(ax_or_figsize, tuple): + fig = plt.figure(figsize=ax_or_figsize) + return fig, gridspec.GridSpec(nrows, ncols, **kw) + else: + ax = ax_or_figsize + ax.axis("off") + ax.set_frame_on(False) + ax.set_xticks([]) + ax.set_yticks([]) + return ax.figure, ax.get_subplotspec().subgridspec(nrows, ncols, **kw) + + +def fix_kwds(kwds_dict, **kwargs): + """Merge the parameters into a single consolidated dictionary. + + Given a dictionary of plot parameters (`kwds_dict`) and a dict of `kwds`, + this function prevents argument duplication errors. + + If `kwds_dict` an kwargs have the same key, only the value in `kwds_dict` is kept. + + Parameters + ---------- + kwds_dict + kwds dictionary + kwargs + + Returns + ------- + `kwds_dict` merged with `kwargs` + + Examples + -------- + >>> def _example(**kwds): + ... return fix_kwds(kwds, key1="value1", key2="value2") + >>> _example(key1="value10", key3="value3") + {'key1': 'value10', 'key2': 'value2', 'key3': 'value3'} + + """ + kwargs.update(kwds_dict) + + return kwargs + + +def _get_basis(adata: AnnData, basis: str): + if basis in adata.obsm: + basis_key = basis + + elif f"X_{basis}" in adata.obsm: + basis_key = f"X_{basis}" + + return basis_key + + +def check_colornorm(vmin=None, vmax=None, vcenter=None, norm=None): + from matplotlib.colors import Normalize + + try: + from matplotlib.colors import TwoSlopeNorm as DivNorm + except ImportError: + # matplotlib<3.2 + from matplotlib.colors import DivergingNorm as DivNorm + + if norm is not None: + if (vmin is not None) or (vmax is not None) or (vcenter is not None): + msg = "Passing both norm and vmin/vmax/vcenter is not allowed." + raise ValueError(msg) + elif vcenter is not None: + norm = DivNorm(vmin=vmin, vmax=vmax, vcenter=vcenter) + else: + norm = Normalize(vmin=vmin, vmax=vmax) + + return norm + + +@overload +def _deprecated_scale( + density_norm: DensityNorm, + scale: DensityNorm | Empty, + *, + default: DensityNorm, +) -> DensityNorm: ... + + +@overload +def _deprecated_scale( + density_norm: DensityNorm | Empty, + scale: DensityNorm | Empty, + *, + default: DensityNorm | Empty = _empty, +) -> DensityNorm | Empty: ... + + +def _deprecated_scale( + density_norm: DensityNorm | Empty, + scale: DensityNorm | Empty, + *, + default: DensityNorm | Empty = _empty, +) -> DensityNorm | Empty: + if scale is _empty: + return density_norm + if density_norm != default: + msg = "can’t specify both `scale` and `density_norm`" + raise ValueError(msg) + msg = "`scale` is deprecated, use `density_norm` instead" + warnings.warn(msg, FutureWarning, stacklevel=3) + return scale + + +def _dk(dendrogram: bool | str | None) -> str | None: # noqa: FBT001 + """Convert the `dendrogram` parameter to a `dendrogram_key` parameter.""" + return None if isinstance(dendrogram, bool) else dendrogram diff --git a/.venv/lib/python3.11/site-packages/scanpy/plotting/palettes.py b/.venv/lib/python3.11/site-packages/scanpy/plotting/palettes.py new file mode 100644 index 0000000000000000000000000000000000000000..2c4cd0b1751f5da5679ca4fc16bc3cf1a3a5b62c --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/plotting/palettes.py @@ -0,0 +1,221 @@ +"""Color palettes in addition to matplotlib's palettes.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from matplotlib import cm, colors + +if TYPE_CHECKING: + from collections.abc import Mapping, Sequence + +# Colorblindness adjusted vega_10 +# See https://github.com/scverse/scanpy/issues/387 +vega_10 = list(map(colors.to_hex, cm.tab10.colors)) +vega_10_scanpy = vega_10.copy() +vega_10_scanpy[2] = "#279e68" # green +vega_10_scanpy[4] = "#aa40fc" # purple +vega_10_scanpy[8] = "#b5bd61" # kakhi + +# default matplotlib 2.0 palette +# see 'category20' on https://github.com/vega/vega/wiki/Scales#scale-range-literals +vega_20 = list(map(colors.to_hex, cm.tab20.colors)) + +# reorderd, some removed, some added +vega_20_scanpy = [ + # dark without grey: + *vega_20[0:14:2], + *vega_20[16::2], + # light without grey: + *vega_20[1:15:2], + *vega_20[17::2], + # manual additions: + "#ad494a", + "#8c6d31", +] +vega_20_scanpy[2] = vega_10_scanpy[2] +vega_20_scanpy[4] = vega_10_scanpy[4] +vega_20_scanpy[7] = vega_10_scanpy[8] # kakhi shifted by missing grey +# TODO: also replace pale colors if necessary + +default_20 = vega_20_scanpy + +# https://graphicdesign.stackexchange.com/questions/3682/where-can-i-find-a-large-palette-set-of-contrasting-colors-for-coloring-many-d +# update 1 +# orig reference https://research.wu.ac.at/en/publications/escaping-rgbland-selecting-colors-for-statistical-graphics-26 +zeileis_28 = [ + "#023fa5", + "#7d87b9", + "#bec1d4", + "#d6bcc0", + "#bb7784", + "#8e063b", + "#4a6fe3", + "#8595e1", + "#b5bbe3", + "#e6afb9", + "#e07b91", + "#d33f6a", + "#11c638", + "#8dd593", + "#c6dec7", + "#ead3c6", + "#f0b98d", + "#ef9708", + "#0fcfc0", + "#9cded6", + "#d5eae7", + "#f3e1eb", + "#f6c4e1", + "#f79cd4", + # these last ones were added: + "#7f7f7f", + "#c7c7c7", + "#1CE6FF", + "#336600", +] + +default_28 = zeileis_28 + +# from https://godsnotwheregodsnot.blogspot.com/2012/09/color-distribution-methodology.html +godsnot_102 = [ + # "#000000", # remove the black, as often, we have black colored annotation + "#FFFF00", + "#1CE6FF", + "#FF34FF", + "#FF4A46", + "#008941", + "#006FA6", + "#A30059", + "#FFDBE5", + "#7A4900", + "#0000A6", + "#63FFAC", + "#B79762", + "#004D43", + "#8FB0FF", + "#997D87", + "#5A0007", + "#809693", + "#6A3A4C", + "#1B4400", + "#4FC601", + "#3B5DFF", + "#4A3B53", + "#FF2F80", + "#61615A", + "#BA0900", + "#6B7900", + "#00C2A0", + "#FFAA92", + "#FF90C9", + "#B903AA", + "#D16100", + "#DDEFFF", + "#000035", + "#7B4F4B", + "#A1C299", + "#300018", + "#0AA6D8", + "#013349", + "#00846F", + "#372101", + "#FFB500", + "#C2FFED", + "#A079BF", + "#CC0744", + "#C0B9B2", + "#C2FF99", + "#001E09", + "#00489C", + "#6F0062", + "#0CBD66", + "#EEC3FF", + "#456D75", + "#B77B68", + "#7A87A1", + "#788D66", + "#885578", + "#FAD09F", + "#FF8A9A", + "#D157A0", + "#BEC459", + "#456648", + "#0086ED", + "#886F4C", + "#34362D", + "#B4A8BD", + "#00A6AA", + "#452C2C", + "#636375", + "#A3C8C9", + "#FF913F", + "#938A81", + "#575329", + "#00FECF", + "#B05B6F", + "#8CD0FF", + "#3B9700", + "#04F757", + "#C8A1A1", + "#1E6E00", + "#7900D7", + "#A77500", + "#6367A9", + "#A05837", + "#6B002C", + "#772600", + "#D790FF", + "#9B9700", + "#549E79", + "#FFF69F", + "#201625", + "#72418F", + "#BC23FF", + "#99ADC0", + "#3A2465", + "#922329", + "#5B4534", + "#FDE8DC", + "#404E55", + "#0089A3", + "#CB7E98", + "#A4E804", + "#324E72", +] + +default_102 = godsnot_102 + + +def _plot_color_cycle(clists: Mapping[str, Sequence[str]]): + import matplotlib.pyplot as plt + import numpy as np + from matplotlib.colors import BoundaryNorm, ListedColormap + + fig, axes = plt.subplots(nrows=len(clists)) # type: plt.Figure, plt.Axes + fig.subplots_adjust(top=0.95, bottom=0.01, left=0.3, right=0.99) + axes[0].set_title("Color Maps/Cycles", fontsize=14) + + for ax, (name, clist) in zip(axes, clists.items(), strict=True): + n = len(clist) + ax.imshow( + np.arange(n)[None, :].repeat(2, 0), + aspect="auto", + cmap=ListedColormap(clist), + norm=BoundaryNorm(np.arange(n + 1) - 0.5, n), + ) + pos = list(ax.get_position().bounds) + x_text = pos[0] - 0.01 + y_text = pos[1] + pos[3] / 2.0 + fig.text(x_text, y_text, name, va="center", ha="right", fontsize=10) + + # Turn off all ticks & spines + for ax in axes: + ax.set_axis_off() + fig.show() + + +if __name__ == "__main__": + _plot_color_cycle({ + name: colors for name, colors in globals().items() if isinstance(colors, list) + }) diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d80412860d6527e1754803ce528cbbe65be6d814 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/__init__.py @@ -0,0 +1,50 @@ +"""Preprocessing functions.""" + +from __future__ import annotations + +from ..neighbors import neighbors +from ._combat import combat +from ._deprecated.highly_variable_genes import filter_genes_dispersion +from ._deprecated.sampling import subsample +from ._highly_variable_genes import highly_variable_genes +from ._normalization import normalize_total +from ._pca import pca +from ._qc import calculate_qc_metrics +from ._recipes import recipe_seurat, recipe_weinreb17, recipe_zheng17 +from ._scale import scale +from ._scrublet import scrublet, scrublet_simulate_doublets +from ._simple import ( + downsample_counts, + filter_cells, + filter_genes, + log1p, + normalize_per_cell, + regress_out, + sample, + sqrt, +) + +__all__ = [ + "calculate_qc_metrics", + "combat", + "downsample_counts", + "filter_cells", + "filter_genes", + "filter_genes_dispersion", + "highly_variable_genes", + "log1p", + "neighbors", + "normalize_per_cell", + "normalize_total", + "pca", + "recipe_seurat", + "recipe_weinreb17", + "recipe_zheng17", + "regress_out", + "sample", + "scale", + "scrublet", + "scrublet_simulate_doublets", + "sqrt", + "subsample", +] diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_combat.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_combat.py new file mode 100644 index 0000000000000000000000000000000000000000..02e8323c91cbe971485f64ee746ed724246d80ad --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_combat.py @@ -0,0 +1,362 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd +from numpy import linalg as la + +from .. import logging as logg +from .._compat import CSBase, old_positionals +from .._utils import sanitize_anndata + +if TYPE_CHECKING: + from collections.abc import Collection + + from anndata import AnnData + + +def _design_matrix( + model: pd.DataFrame, batch_key: str, batch_levels: Collection[str] +) -> pd.DataFrame: + """Compute a simple design matrix. + + Parameters + ---------- + model + Contains the batch annotation + batch_key + Name of the batch column + batch_levels + Levels of the batch annotation + + Returns + ------- + The design matrix for the regression problem + + """ + import patsy + + design = patsy.dmatrix( + f"~ 0 + C(Q('{batch_key}'), levels=batch_levels)", + model, + return_type="dataframe", + ) + model = model.drop([batch_key], axis=1) + numerical_covariates = model.select_dtypes("number").columns.values + + logg.info(f"Found {design.shape[1]} batches\n") + other_cols = [c for c in model.columns.values if c not in numerical_covariates] + + if other_cols: + col_repr = " + ".join(f"Q('{x}')" for x in other_cols) + factor_matrix = patsy.dmatrix( + f"~ 0 + {col_repr}", model[other_cols], return_type="dataframe" + ) + + design = pd.concat((design, factor_matrix), axis=1) + logg.info(f"Found {len(other_cols)} categorical variables:") + logg.info(f"\t{', '.join(other_cols)}\n") + + if numerical_covariates is not None: + logg.info(f"Found {len(numerical_covariates)} numerical variables:") + logg.info(f"\t{', '.join(numerical_covariates)}\n") + + for n_c in numerical_covariates: + design[n_c] = model[n_c] + + return design + + +def _standardize_data( + model: pd.DataFrame, data: pd.DataFrame, batch_key: str +) -> tuple[pd.DataFrame, pd.DataFrame, np.ndarray, np.ndarray]: + """Standardize the data per gene. + + The aim here is to make mean and variance be comparable across batches. + + Parameters + ---------- + model + Contains the batch annotation + data + Contains the Data + batch_key + Name of the batch column in the model matrix + + Returns + ------- + s_data + Standardized Data + design + Batch assignment as one-hot encodings + var_pooled + Pooled variance per gene + stand_mean + Gene-wise mean + + """ + # compute the design matrix + batch_items = model.groupby(batch_key, observed=True).groups.items() + batch_levels, batch_info = zip(*batch_items, strict=True) + n_batch = len(batch_info) + n_batches = np.array([len(v) for v in batch_info]) + n_array = float(sum(n_batches)) + + design = _design_matrix(model, batch_key, batch_levels) + + # compute pooled variance estimator + b_hat = np.dot(np.dot(la.inv(np.dot(design.T, design)), design.T), data.T) + grand_mean = np.dot((n_batches / n_array).T, b_hat[:n_batch, :]) + var_pooled = (data - np.dot(design, b_hat).T) ** 2 + var_pooled = np.dot(var_pooled, np.ones((int(n_array), 1)) / int(n_array)) + + # Compute the means + if np.sum(var_pooled == 0) > 0: + print(f"Found {np.sum(var_pooled == 0)} genes with zero variance.") + stand_mean = np.dot( + grand_mean.T.reshape((len(grand_mean), 1)), np.ones((1, int(n_array))) + ) + tmp = np.array(design.copy()) + tmp[:, :n_batch] = 0 + stand_mean += np.dot(tmp, b_hat).T + + # need to be a bit careful with the zero variance genes + # just set the zero variance genes to zero in the standardized data + s_data = np.where( + var_pooled == 0, + 0, + ((data - stand_mean) / np.dot(np.sqrt(var_pooled), np.ones((1, int(n_array))))), + ) + s_data = pd.DataFrame(s_data, index=data.index, columns=data.columns) + + return s_data, design, var_pooled, stand_mean + + +@old_positionals("covariates", "inplace") +def combat( # noqa: PLR0915 + adata: AnnData, + key: str = "batch", + *, + covariates: Collection[str] | None = None, + inplace: bool = True, +) -> np.ndarray | None: + """ComBat function for batch effect correction :cite:p:`Johnson2006,Leek2012,Pedersen2012`. + + Corrects for batch effects by fitting linear models, gains statistical power + via an EB framework where information is borrowed across genes. + This uses the implementation `combat.py`_ :cite:p:`Pedersen2012`. + + .. _combat.py: https://github.com/brentp/combat.py + + Parameters + ---------- + adata + Annotated data matrix + key + Key to a categorical annotation from :attr:`~anndata.AnnData.obs` + that will be used for batch effect removal. + covariates + Additional covariates besides the batch variable such as adjustment + variables or biological condition. This parameter refers to the design + matrix `X` in Equation 2.1 in :cite:t:`Johnson2006` and to the `mod` argument in + the original combat function in the sva R package. + Note that not including covariates may introduce bias or lead to the + removal of biological signal in unbalanced designs. + inplace + Whether to replace adata.X or to return the corrected data + + Returns + ------- + Returns :class:`numpy.ndarray` if `inplace=False`, else returns `None` and sets the following field in the `adata` object: + + `adata.X` : :class:`numpy.ndarray` (dtype `float`) + Corrected data matrix. + + """ + # check the input + if key not in adata.obs: + msg = f"Could not find the key {key!r} in adata.obs" + raise ValueError(msg) + + if covariates is not None: + cov_exist = np.isin(covariates, adata.obs.columns) + if np.any(~cov_exist): + missing_cov = np.array(covariates)[~cov_exist].tolist() + msg = f"Could not find the covariate(s) {missing_cov!r} in adata.obs" + raise ValueError(msg) + + if key in covariates: + msg = "Batch key and covariates cannot overlap" + raise ValueError(msg) + + if len(covariates) != len(set(covariates)): + msg = "Covariates must be unique" + raise ValueError(msg) + + # only works on dense matrices so far + x = adata.X.toarray().T if isinstance(adata.X, CSBase) else adata.X.T + data = pd.DataFrame(data=x, index=adata.var_names, columns=adata.obs_names) + + sanitize_anndata(adata) + + # construct a pandas series of the batch annotation + model = adata.obs[[key, *(covariates if covariates else [])]] + batch_info = model.groupby(key, observed=True).indices.values() + n_batch = len(batch_info) + n_batches = np.array([len(v) for v in batch_info]) + n_array = float(sum(n_batches)) + + # standardize across genes using a pooled variance estimator + logg.info("Standardizing Data across genes.\n") + s_data, design, var_pooled, stand_mean = _standardize_data(model, data, key) + + # fitting the parameters on the standardized data + logg.info("Fitting L/S model and finding priors\n") + batch_design = design[design.columns[:n_batch]] + # first estimate of the additive batch effect + gamma_hat = ( + la.inv(batch_design.T @ batch_design) @ batch_design.T @ s_data.T + ).values + # first estimate for the multiplicative batch effect + delta_hat = [s_data.iloc[:, batch_idxs].var(axis=1) for batch_idxs in batch_info] + + # empirically fix the prior hyperparameters + gamma_bar = gamma_hat.mean(axis=1) + t2 = gamma_hat.var(axis=1) + # a_prior and b_prior are the priors on lambda and theta from Johnson and Li (2006) + a_prior = list(map(_aprior, delta_hat)) + b_prior = list(map(_bprior, delta_hat)) + + logg.info("Finding parametric adjustments\n") + # gamma star and delta star will be our empirical bayes (EB) estimators + # for the additive and multiplicative batch effect per batch and cell + gamma_star, delta_star = [], [] + for i, batch_idxs in enumerate(batch_info): + # temp stores our estimates for the batch effect parameters. + # temp[0] is the additive batch effect + # temp[1] is the multiplicative batch effect + gamma, delta = _it_sol( + s_data.iloc[:, batch_idxs].values, + gamma_hat[i], + delta_hat[i].values, + g_bar=gamma_bar[i], + t2=t2[i], + a=a_prior[i], + b=b_prior[i], + ) + + gamma_star.append(gamma) + delta_star.append(delta) + + logg.info("Adjusting data\n") + bayesdata = s_data + gamma_star = np.array(gamma_star) + delta_star = np.array(delta_star) + + # we now apply the parametric adjustment to the standardized data from above + # loop over all batches in the data + for j, batch_idxs in enumerate(batch_info): + # we basically subtract the additive batch effect, rescale by the ratio + # of multiplicative batch effect to pooled variance and add the overall gene + # wise mean + dsq = np.sqrt(delta_star[j, :]) + dsq = dsq.reshape((len(dsq), 1)) + denom = np.dot(dsq, np.ones((1, n_batches[j]))) + numer = np.array( + bayesdata.iloc[:, batch_idxs] + - np.dot(batch_design.iloc[batch_idxs], gamma_star).T + ) + bayesdata.iloc[:, batch_idxs] = numer / denom + + vpsq = np.sqrt(var_pooled).reshape((len(var_pooled), 1)) + bayesdata = bayesdata * np.dot(vpsq, np.ones((1, int(n_array)))) + stand_mean + + # put back into the adata object or return + if inplace: + adata.X = bayesdata.values.transpose() + else: + return bayesdata.values.transpose() + + +def _it_sol( + s_data: np.ndarray, + g_hat: np.ndarray, + d_hat: np.ndarray, + *, + g_bar: float, + t2: float, + a: float, + b: float, + conv: float = 0.0001, +) -> tuple[np.ndarray, np.ndarray]: + """Iteratively compute the conditional posterior means for gamma and delta. + + gamma is an estimator for the additive batch effect, deltat is an estimator + for the multiplicative batch effect. We use an EB framework to estimate these + two. Analytical expressions exist for both parameters, which however depend on each other. + We therefore iteratively evalutate these two expressions until convergence is reached. + + Parameters + ---------- + s_data + Contains the standardized Data + g_hat + Initial guess for gamma + d_hat + Initial guess for delta + g_bar, t2, a, b + Hyperparameters + conv: float, optional (default: `0.0001`) + convergence criterium + + Returns + ------- + gamma + estimated value for gamma + delta + estimated value for delta + + """ # noqa: D401 + n = (1 - np.isnan(s_data)).sum(axis=1) + g_old = g_hat.copy() + d_old = d_hat.copy() + + change = 1 + count = 0 + + # They need to be initialized for numba to properly infer types + g_new = g_old + d_new = d_old + # we place a normally distributed prior on gamma and and inverse gamma prior on delta + # in the loop, gamma and delta are updated together. they depend on each other. we iterate until convergence. + while change > conv: + g_new = (t2 * n * g_hat + d_old * g_bar) / (t2 * n + d_old) + sum2 = s_data - g_new.reshape((g_new.shape[0], 1)) @ np.ones(( + 1, + s_data.shape[1], + )) + sum2 = sum2**2 + sum2 = sum2.sum(axis=1) + d_new = (0.5 * sum2 + b) / (n / 2.0 + a - 1.0) + + change = max( + (abs(g_new - g_old) / g_old).max(), (abs(d_new - d_old) / d_old).max() + ) + g_old = g_new # .copy() + d_old = d_new # .copy() + count = count + 1 + + return g_new, d_new + + +def _aprior(delta_hat): + m = delta_hat.mean() + s2 = delta_hat.var() + return (2 * s2 + m**2) / s2 + + +def _bprior(delta_hat): + m = delta_hat.mean() + s2 = delta_hat.var() + return (m * s2 + m**3) / s2 diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_deprecated/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_deprecated/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..97c68451c5c2858011ad4f8cac5d438575e7644f --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_deprecated/__init__.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import numpy as np +from scipy import sparse + +from ..._compat import CSBase, old_positionals +from ..._utils import dematrix + + +@old_positionals("max_fraction", "mult_with_mean") +def normalize_per_cell_weinreb16_deprecated( + x: np.ndarray | CSBase, + *, + max_fraction: float = 1, + mult_with_mean: bool = False, +) -> np.ndarray: + """Normalize each cell :cite:p:`Weinreb2017`. + + This is a deprecated version. See `normalize_per_cell` instead. + + Normalize each cell by UMI count, so that every cell has the same total + count. + + Parameters + ---------- + X + Expression matrix. Rows correspond to cells and columns to genes. + max_fraction + Only use genes that make up more than max_fraction of the total + reads in every cell. + mult_with_mean + Multiply the result with the mean of total counts. + + Returns + ------- + Normalized version of the original expression matrix. + + """ + if max_fraction < 0 or max_fraction > 1: + msg = "Choose max_fraction between 0 and 1." + raise ValueError(msg) + + counts_per_cell = dematrix(x.sum(1)).ravel() + gene_subset = dematrix( + np.all(x <= counts_per_cell[:, None] * max_fraction, axis=0) + ).ravel() + tc_include = dematrix(x[:, gene_subset].sum(1)).ravel() + + x_norm = ( + x.multiply(sparse.csr_matrix(1 / tc_include[:, None])) # noqa: TID251 + if isinstance(x, CSBase) + else x / tc_include[:, None] + ) + if mult_with_mean: + x_norm *= np.mean(counts_per_cell) + + return x_norm + + +def zscore_deprecated(x: np.ndarray, /) -> np.ndarray: + """Z-score standardize each variable/gene in X :cite:p:`Weinreb2017`. + + Use `scale` instead. + + Parameters + ---------- + X + Data matrix. Rows correspond to cells and columns to genes. + + Returns + ------- + Z-score standardized version of the data matrix. + + """ + means = np.tile(np.mean(x, axis=0)[None, :], (x.shape[0], 1)) + stds = np.tile(np.std(x, axis=0)[None, :], (x.shape[0], 1)) + return (x - means) / (stds + 0.0001) diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_deprecated/highly_variable_genes.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_deprecated/highly_variable_genes.py new file mode 100644 index 0000000000000000000000000000000000000000..931ce82539b7ec6f176b173a1043c960405feba6 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_deprecated/highly_variable_genes.py @@ -0,0 +1,272 @@ +from __future__ import annotations + +import warnings +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd +from anndata import AnnData + +from ... import logging as logg +from ..._compat import CSBase, deprecated, old_positionals +from .._distributed import materialize_as_ndarray +from .._utils import _get_mean_var + +if TYPE_CHECKING: + from typing import Literal + + +@deprecated("Use sc.pp.highly_variable_genes instead") +@old_positionals( + "flavor", + "min_disp", + "max_disp", + "min_mean", + "max_mean", + "n_bins", + "n_top_genes", + "log", + "subset", + "copy", +) +def filter_genes_dispersion( # noqa: PLR0912, PLR0913, PLR0915 + data: AnnData | CSBase | np.ndarray, + *, + flavor: Literal["seurat", "cell_ranger"] = "seurat", + min_disp: float | None = None, + max_disp: float | None = None, + min_mean: float | None = None, + max_mean: float | None = None, + n_bins: int = 20, + n_top_genes: int | None = None, + log: bool = True, + subset: bool = True, + copy: bool = False, +) -> AnnData | np.recarray | None: + """Extract highly variable genes :cite:p:`Satija2015,Zheng2017`. + + .. deprecated:: 1.3.6 + + Use :func:`~scanpy.pp.highly_variable_genes` instead. + The new function is equivalent to the present function, except that + + * the new function always expects logarithmized data + * `subset=False` in the new function, it suffices to + merely annotate the genes, tools like `pp.pca` will + detect the annotation + * you can now call: `sc.pl.highly_variable_genes(adata)` + * `copy` is replaced by `inplace` + + If trying out parameters, pass the data matrix instead of AnnData. + + Depending on `flavor`, this reproduces the R-implementations of Seurat + :cite:p:`Satija2015` and Cell Ranger :cite:p:`Zheng2017`. + + The normalized dispersion is obtained by scaling with the mean and standard + deviation of the dispersions for genes falling into a given bin for mean + expression of genes. This means that for each bin of mean expression, highly + variable genes are selected. + + Use `flavor='cell_ranger'` with care and in the same way as in + :func:`~scanpy.pp.recipe_zheng17`. + + Parameters + ---------- + data + The (annotated) data matrix of shape `n_obs` × `n_vars`. Rows correspond + to cells and columns to genes. + flavor + Choose the flavor for computing normalized dispersion. If choosing + 'seurat', this expects non-logarithmized data – the logarithm of mean + and dispersion is taken internally when `log` is at its default value + `True`. For 'cell_ranger', this is usually called for logarithmized data + – in this case you should set `log` to `False`. In their default + workflows, Seurat passes the cutoffs whereas Cell Ranger passes + `n_top_genes`. + min_mean + max_mean + min_disp + max_disp + If `n_top_genes` unequals `None`, these cutoffs for the means and the + normalized dispersions are ignored. + n_bins + Number of bins for binning the mean gene expression. Normalization is + done with respect to each bin. If just a single gene falls into a bin, + the normalized dispersion is artificially set to 1. You'll be informed + about this if you set `settings.verbosity = 4`. + n_top_genes + Number of highly-variable genes to keep. + log + Use the logarithm of the mean to variance ratio. + subset + Keep highly-variable genes only (if True) else write a bool array for h + ighly-variable genes while keeping all genes + copy + If an :class:`~anndata.AnnData` is passed, determines whether a copy + is returned. + + Returns + ------- + If an AnnData `adata` is passed, returns or updates `adata` depending on + `copy`. It filters the `adata` and adds the annotations + + **means** : adata.var + Means per gene. Logarithmized when `log` is `True`. + **dispersions** : adata.var + Dispersions per gene. Logarithmized when `log` is `True`. + **dispersions_norm** : adata.var + Normalized dispersions per gene. Logarithmized when `log` is `True`. + + If a data matrix `X` is passed, the annotation is returned as `np.recarray` + with the same information stored in fields: `gene_subset`, `means`, `dispersions`, `dispersion_norm`. + + """ + if n_top_genes is not None and not all( + x is None for x in [min_disp, max_disp, min_mean, max_mean] + ): + msg = "If you pass `n_top_genes`, all cutoffs are ignored." + warnings.warn(msg, UserWarning, stacklevel=2) + if min_disp is None: + min_disp = 0.5 + if min_mean is None: + min_mean = 0.0125 + if max_mean is None: + max_mean = 3 + if isinstance(data, AnnData): + adata = data.copy() if copy else data + result = filter_genes_dispersion( + adata.X, + log=log, + min_disp=min_disp, + max_disp=max_disp, + min_mean=min_mean, + max_mean=max_mean, + n_top_genes=n_top_genes, + flavor=flavor, + ) + adata.var["means"] = result["means"] + adata.var["dispersions"] = result["dispersions"] + adata.var["dispersions_norm"] = result["dispersions_norm"] + if subset: + adata._inplace_subset_var(result["gene_subset"]) + else: + adata.var["highly_variable"] = result["gene_subset"] + return adata if copy else None + start = logg.info("extracting highly variable genes") + x = data # no copy necessary, X remains unchanged in the following + mean, var = materialize_as_ndarray(_get_mean_var(x)) + # now actually compute the dispersion + mean[mean == 0] = 1e-12 # set entries equal to zero to small value + dispersion = var / mean + if log: # logarithmized mean as in Seurat + dispersion[dispersion == 0] = np.nan + dispersion = np.log(dispersion) + mean = np.log1p(mean) + # all of the following quantities are "per-gene" here + df = pd.DataFrame() + df["mean"] = mean + df["dispersion"] = dispersion + if flavor == "seurat": + df["mean_bin"] = pd.cut(df["mean"], bins=n_bins) + disp_grouped = df.groupby("mean_bin", observed=True)["dispersion"] + disp_mean_bin = disp_grouped.mean() + disp_std_bin = disp_grouped.std(ddof=1) + # retrieve those genes that have nan std, these are the ones where + # only a single gene fell in the bin and implicitly set them to have + # a normalized disperion of 1 + one_gene_per_bin = disp_std_bin.isnull() + gen_indices = np.where(one_gene_per_bin[df["mean_bin"].values])[0].tolist() + if len(gen_indices) > 0: + logg.debug( + f"Gene indices {gen_indices} fell into a single bin: their " + "normalized dispersion was set to 1.\n " + "Decreasing `n_bins` will likely avoid this effect." + ) + # Circumvent pandas 0.23 bug. Both sides of the assignment have dtype==float32, + # but there’s still a dtype error without “.value”. + disp_std_bin[one_gene_per_bin] = disp_mean_bin[one_gene_per_bin.values].values + disp_mean_bin[one_gene_per_bin] = 0 + # actually do the normalization + df["dispersion_norm"] = ( + # use values here as index differs + df["dispersion"].values - disp_mean_bin[df["mean_bin"].values].values + ) / disp_std_bin[df["mean_bin"].values].values + elif flavor == "cell_ranger": + from statsmodels import robust + + df["mean_bin"] = pd.cut( + df["mean"], + np.r_[-np.inf, np.percentile(df["mean"], np.arange(10, 105, 5)), np.inf], + ) + disp_grouped = df.groupby("mean_bin", observed=True)["dispersion"] + disp_median_bin = disp_grouped.median() + # the next line raises the warning: "Mean of empty slice" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + disp_mad_bin = disp_grouped.apply(robust.mad) + df["dispersion_norm"] = ( + np.abs( + df["dispersion"].values - disp_median_bin[df["mean_bin"].values].values + ) + / disp_mad_bin[df["mean_bin"].values].values + ) + else: + msg = '`flavor` needs to be "seurat" or "cell_ranger"' + raise ValueError(msg) + dispersion_norm = df["dispersion_norm"].values.astype("float32") + if n_top_genes is not None: + dispersion_norm = dispersion_norm[~np.isnan(dispersion_norm)] + dispersion_norm[ + ::-1 + ].sort() # interestingly, np.argpartition is slightly slower + disp_cut_off = dispersion_norm[n_top_genes - 1] + gene_subset = df["dispersion_norm"].values >= disp_cut_off + logg.debug( + f"the {n_top_genes} top genes correspond to a " + f"normalized dispersion cutoff of {disp_cut_off}" + ) + else: + max_disp = np.inf if max_disp is None else max_disp + dispersion_norm[np.isnan(dispersion_norm)] = 0 # similar to Seurat + gene_subset = np.logical_and.reduce(( + mean > min_mean, + mean < max_mean, + dispersion_norm > min_disp, + dispersion_norm < max_disp, + )) + logg.info(" finished", time=start) + return np.rec.fromarrays( + ( + gene_subset, + df["mean"].values, + df["dispersion"].values, + df["dispersion_norm"].values.astype("float32", copy=False), + ), + dtype=[ + ("gene_subset", bool), + ("means", "float32"), + ("dispersions", "float32"), + ("dispersions_norm", "float32"), + ], + ) + + +def filter_genes_cv_deprecated(x, /, e_cutoff, cv_filter): + """Filter genes by coefficient of variance and mean.""" + return _filter_genes(x, e_cutoff, cv_filter, np.std) + + +def filter_genes_fano_deprecated(x, /, e_cutoff, v_cutoff): + """Filter genes by fano factor and mean.""" + return _filter_genes(x, e_cutoff, v_cutoff, np.var) + + +def _filter_genes(x, /, e_cutoff, v_cutoff, meth): + """See `filter_genes_dispersion` :cite:p:`Weinreb2017`.""" + if isinstance(x, CSBase): + msg = "Not defined for sparse input. See `filter_genes_dispersion`." + raise ValueError(msg) + mean_filter = np.mean(x, axis=0) > e_cutoff + var_filter = meth(x, axis=0) / (np.mean(x, axis=0) + 0.0001) > v_cutoff + gene_subset = np.nonzero(np.all([mean_filter, var_filter], axis=0))[0] + return gene_subset diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_deprecated/sampling.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_deprecated/sampling.py new file mode 100644 index 0000000000000000000000000000000000000000..2280f3c9a02ed9481a496bccaacd2c5e32055d41 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_deprecated/sampling.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ..._compat import old_positionals +from ..._utils.random import legacy_numpy_gen +from .._simple import sample + +if TYPE_CHECKING: + import numpy as np + from anndata import AnnData + from numpy.typing import NDArray + + from ..._compat import CSBase + from ..._utils.random import _LegacyRandom + + +@old_positionals("n_obs", "random_state", "copy") +def subsample( + data: AnnData | np.ndarray | CSBase, + fraction: float | None = None, + *, + n_obs: int | None = None, + random_state: _LegacyRandom = 0, + copy: bool = False, +) -> AnnData | tuple[np.ndarray | CSBase, NDArray[np.int64]] | None: + """Subsample to a fraction of the number of observations. + + .. deprecated:: 1.11.0 + + Use :func:`~scanpy.pp.sample` instead. + + Parameters + ---------- + data + The (annotated) data matrix of shape `n_obs` × `n_vars`. + Rows correspond to cells and columns to genes. + fraction + Subsample to this `fraction` of the number of observations. + n_obs + Subsample to this number of observations. + random_state + Random seed to change subsampling. + copy + If an :class:`~anndata.AnnData` is passed, + determines whether a copy is returned. + + Returns + ------- + Returns `X[obs_indices], obs_indices` if data is array-like, otherwise + subsamples the passed :class:`~anndata.AnnData` (`copy == False`) or + returns a subsampled copy of it (`copy == True`). + + """ + rng = legacy_numpy_gen(random_state) + return sample( + data=data, fraction=fraction, n=n_obs, rng=rng, copy=copy, replace=False, axis=0 + ) diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_distributed.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_distributed.py new file mode 100644 index 0000000000000000000000000000000000000000..c143428212b574f4e066ae6de95cca891e071dda --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_distributed.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, overload + +import numpy as np + +from .._compat import DaskArray + +if TYPE_CHECKING: + from numpy.typing import ArrayLike + + from .._compat import ZappyArray + + +@overload +def materialize_as_ndarray(a: ArrayLike) -> np.ndarray: ... + + +@overload +def materialize_as_ndarray(a: tuple[ArrayLike]) -> tuple[np.ndarray]: ... + + +@overload +def materialize_as_ndarray( + a: tuple[ArrayLike, ArrayLike], +) -> tuple[np.ndarray, np.ndarray]: ... + + +@overload +def materialize_as_ndarray( + a: tuple[ArrayLike, ArrayLike, ArrayLike], +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: ... + + +def materialize_as_ndarray( + a: DaskArray | ArrayLike | tuple[ArrayLike | ZappyArray | DaskArray, ...], +) -> tuple[np.ndarray] | np.ndarray: + """Compute distributed arrays and convert them to numpy ndarrays.""" + if isinstance(a, DaskArray): + return a.compute() + if not isinstance(a, tuple): + return np.asarray(a) + + if not any(isinstance(arr, DaskArray) for arr in a): + return tuple(np.asarray(arr) for arr in a) + + import dask.array as da + + return da.compute(*a, sync=True) diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_docs.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_docs.py new file mode 100644 index 0000000000000000000000000000000000000000..aa1378bd6721d8d34020333f26da564955d090c9 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_docs.py @@ -0,0 +1,85 @@ +"""Shared docstrings for preprocessing function parameters.""" + +from __future__ import annotations + +doc_adata_basic = """\ +adata + Annotated data matrix.\ +""" + +doc_expr_reps = """\ +layer + If provided, use `adata.layers[layer]` for expression values instead + of `adata.X`. +use_raw + If True, use `adata.raw.X` for expression values instead of `adata.X`.\ +""" + +doc_mask_var_hvg = """\ +mask_var + To run only on a certain set of genes given by a boolean array + or a string referring to an array in :attr:`~anndata.AnnData.var`. + By default, uses `.var['highly_variable']` if available, else everything. +use_highly_variable + Whether to use highly variable genes only, stored in + `.var['highly_variable']`. + By default uses them if they have been determined beforehand. + + .. deprecated:: 1.10.0 + Use `mask_var` instead +""" + +doc_obs_qc_args = """\ +qc_vars + Keys for boolean columns of `.var` which identify variables you could + want to control for (e.g. "ERCC" or "mito"). +percent_top + List of ranks (where genes are ranked by expression) at which the cumulative + proportion of expression will be reported as a percentage. This can be used to + assess library complexity. Ranks are considered 1-indexed, and if empty or None + don't calculate. + + E.g. `percent_top=[50]` finds cumulative proportion to the 50th most expressed gene. +""" + +doc_qc_metric_naming = """\ +expr_type + Name of kind of values in X. +var_type + The kind of thing the variables are.\ +""" + +doc_obs_qc_returns = """\ +Observation level metrics include: + +`total_{var_type}_by_{expr_type}` + E.g. "total_genes_by_counts". Number of genes with positive counts in a cell. +`total_{expr_type}` + E.g. "total_counts". Total number of counts for a cell. +`pct_{expr_type}_in_top_{n}_{var_type}` – for `n` in `percent_top` + E.g. "pct_counts_in_top_50_genes". Cumulative percentage of counts + for 50 most expressed genes in a cell. +`total_{expr_type}_{qc_var}` – for `qc_var` in `qc_vars` + E.g. "total_counts_mito". Total number of counts for variables in + `qc_vars`. +`pct_{expr_type}_{qc_var}` – for `qc_var` in `qc_vars` + E.g. "pct_counts_mito". Proportion of total counts for a cell which + are mitochondrial.\ +""" + +doc_var_qc_returns = """\ +Variable level metrics include: + +`total_{expr_type}` + E.g. "total_counts". Sum of counts for a gene. +`n_genes_by_{expr_type}` + E.g. "n_genes_by_counts". The number of genes with at least 1 count in a cell. Calculated for all cells. +`mean_{expr_type}` + E.g. "mean_counts". Mean expression over all cells. +`n_cells_by_{expr_type}` + E.g. "n_cells_by_counts". Number of cells this expression is + measured in. +`pct_dropout_by_{expr_type}` + E.g. "pct_dropout_by_counts". Percentage of cells this feature does + not appear in.\ +""" diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_highly_variable_genes.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_highly_variable_genes.py new file mode 100644 index 0000000000000000000000000000000000000000..ef3f2279a8cdf8017b565e7df7d76fad77289410 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_highly_variable_genes.py @@ -0,0 +1,763 @@ +from __future__ import annotations + +import warnings +from dataclasses import dataclass +from inspect import signature +from typing import TYPE_CHECKING, TypedDict, cast + +import numba +import numpy as np +import pandas as pd +from anndata import AnnData +from scipy import sparse + +from .. import logging as logg +from .._compat import CSBase, CSRBase, DaskArray, old_positionals +from .._settings import Verbosity, settings +from .._utils import check_nonnegative_integers, sanitize_anndata +from ..get import _get_obs_rep +from ._distributed import materialize_as_ndarray +from ._simple import filter_genes +from ._utils import _get_mean_var + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Concatenate, Literal, ParamSpec, TypeVar, Unpack + + from numpy.typing import NDArray + + from .._types import HVGFlavor + + P = ParamSpec("P") + R = TypeVar("R") + + +def _highly_variable_genes_seurat_v3( # noqa: PLR0912, PLR0915 + adata: AnnData, + *, + flavor: Literal["seurat_v3", "seurat_v3_paper"] = "seurat_v3", + layer: str | None = None, + n_top_genes: int = 2000, + batch_key: str | None = None, + check_values: bool = True, + span: float = 0.3, + subset: bool = False, + inplace: bool = True, +) -> pd.DataFrame | None: + """See `highly_variable_genes`. + + For further implementation details see https://www.overleaf.com/read/ckptrbgzzzpg + + Returns + ------- + Depending on `inplace` returns calculated metrics (:class:`~pd.DataFrame`) or + updates `.var` with the following fields: + + highly_variable : :class:`bool` + boolean indicator of highly-variable genes. + **means** + means per gene. + **variances** + variance per gene. + **variances_norm** + normalized variance per gene, averaged in the case of multiple batches. + highly_variable_rank : :class:`float` + Rank of the gene according to normalized variance, median rank in the case of multiple batches. + highly_variable_nbatches : :class:`int` + If batch_key is given, this denotes in how many batches genes are detected as HVG. + + """ + try: + from skmisc.loess import loess + except ImportError as e: + msg = "Please install skmisc package via `pip install --user scikit-misc" + raise ImportError(msg) from e + df = pd.DataFrame(index=adata.var_names) + data = _get_obs_rep(adata, layer=layer) + + if check_values and not check_nonnegative_integers(data): + warnings.warn( + f"`{flavor=!r}` expects raw count data, but non-integers were found.", + UserWarning, + stacklevel=3, + ) + + df["means"], df["variances"] = _get_mean_var(data) + + if batch_key is None: + batch_info = pd.Categorical(np.zeros(adata.shape[0], dtype=int)) + else: + batch_info = adata.obs[batch_key].to_numpy() + + norm_gene_vars = [] + for b in np.unique(batch_info): + data_batch = data[batch_info == b] + + mean, var = _get_mean_var(data_batch) + not_const = var > 0 + estimat_var = np.zeros(data.shape[1], dtype=np.float64) + + y = np.log10(var[not_const]) + x = np.log10(mean[not_const]) + model = loess(x, y, span=span, degree=2) + model.fit() + estimat_var[not_const] = model.outputs.fitted_values + reg_std = np.sqrt(10**estimat_var) + + # clip large values as in Seurat + n_obs = data_batch.shape[0] + vmax = np.sqrt(n_obs) + clip_val = reg_std * vmax + mean + if isinstance(data_batch, CSBase): + if isinstance(data_batch, CSRBase): + batch_counts = data_batch + else: + batch_counts = sparse.csr_matrix(data_batch) # noqa: TID251 + + squared_batch_counts_sum, batch_counts_sum = _sum_and_sum_squares_clipped( + batch_counts.indices, + batch_counts.data, + n_cols=batch_counts.shape[1], + clip_val=clip_val, + nnz=batch_counts.nnz, + ) + else: + batch_counts = data_batch.astype(np.float64).copy() + clip_val_broad = np.broadcast_to(clip_val, batch_counts.shape) + np.putmask( + batch_counts, + batch_counts > clip_val_broad, + clip_val_broad, + ) + + squared_batch_counts_sum = np.square(batch_counts).sum(axis=0) + batch_counts_sum = batch_counts.sum(axis=0) + + norm_gene_var = (1 / ((n_obs - 1) * np.square(reg_std))) * ( + (n_obs * np.square(mean)) + + squared_batch_counts_sum + - 2 * batch_counts_sum * mean + ) + norm_gene_vars.append(norm_gene_var.reshape(1, -1)) + + norm_gene_vars = np.concatenate(norm_gene_vars, axis=0) + # argsort twice gives ranks, small rank means most variable + ranked_norm_gene_vars = np.argsort(np.argsort(-norm_gene_vars, axis=1), axis=1) + + # this is done in SelectIntegrationFeatures() in Seurat v3 + ranked_norm_gene_vars = ranked_norm_gene_vars.astype(np.float32) + num_batches_high_var = np.sum( + (ranked_norm_gene_vars < n_top_genes).astype(int), axis=0 + ) + ranked_norm_gene_vars[ranked_norm_gene_vars >= n_top_genes] = np.nan + ma_ranked = np.ma.masked_invalid(ranked_norm_gene_vars) + median_ranked = np.ma.median(ma_ranked, axis=0).filled(np.nan) + + df["gene_name"] = df.index + df["highly_variable_nbatches"] = num_batches_high_var + df["highly_variable_rank"] = median_ranked + df["variances_norm"] = np.mean(norm_gene_vars, axis=0) + if flavor == "seurat_v3": + sort_cols = ["highly_variable_rank", "highly_variable_nbatches"] + sort_ascending = [True, False] + elif flavor == "seurat_v3_paper": + sort_cols = ["highly_variable_nbatches", "highly_variable_rank"] + sort_ascending = [False, True] + else: + msg = f"Did not recognize flavor {flavor}" + raise ValueError(msg) + sorted_index = ( + df[sort_cols] + .sort_values(sort_cols, ascending=sort_ascending, na_position="last") + .index + ) + df["highly_variable"] = False + df.loc[sorted_index[: int(n_top_genes)], "highly_variable"] = True + + if inplace: + adata.uns["hvg"] = {"flavor": flavor} + logg.hint( + "added\n" + " 'highly_variable', boolean vector (adata.var)\n" + " 'highly_variable_rank', float vector (adata.var)\n" + " 'means', float vector (adata.var)\n" + " 'variances', float vector (adata.var)\n" + " 'variances_norm', float vector (adata.var)" + ) + adata.var["highly_variable"] = df["highly_variable"].to_numpy() + adata.var["highly_variable_rank"] = df["highly_variable_rank"].to_numpy() + adata.var["means"] = df["means"].to_numpy() + adata.var["variances"] = df["variances"].to_numpy() + adata.var["variances_norm"] = ( + df["variances_norm"].to_numpy().astype("float64", copy=False) + ) + if batch_key is not None: + adata.var["highly_variable_nbatches"] = df[ + "highly_variable_nbatches" + ].to_numpy() + if subset: + adata._inplace_subset_var(df["highly_variable"].to_numpy()) + else: + if batch_key is None: + df = df.drop(["highly_variable_nbatches"], axis=1) + if subset: + df = df.iloc[df["highly_variable"].to_numpy(), :] + + return df + + +# parallel=False needed for accuracy +@numba.njit(cache=True, parallel=False) # noqa: TID251 +def _sum_and_sum_squares_clipped( + indices: NDArray[np.integer], + data: NDArray[np.floating], + *, + n_cols: int, + clip_val: NDArray[np.float64], + nnz: int, +) -> tuple[NDArray[np.float64], NDArray[np.float64]]: + squared_batch_counts_sum = np.zeros(n_cols, dtype=np.float64) + batch_counts_sum = np.zeros(n_cols, dtype=np.float64) + for i in numba.prange(nnz): + idx = indices[i] + element = min(np.float64(data[i]), clip_val[idx]) + squared_batch_counts_sum[idx] += element**2 + batch_counts_sum[idx] += element + + return squared_batch_counts_sum, batch_counts_sum + + +@dataclass +class _Cutoffs: + min_disp: float + max_disp: float + min_mean: float + max_mean: float + + @classmethod + def validate( + cls, + *, + n_top_genes: int | None, + min_disp: float, + max_disp: float, + min_mean: float, + max_mean: float, + ) -> _Cutoffs | int: + if n_top_genes is None: + return cls(min_disp, max_disp, min_mean, max_mean) + + cutoffs = {"min_disp", "max_disp", "min_mean", "max_mean"} + defaults = { + p.name: p.default + for p in signature(highly_variable_genes).parameters.values() + if p.name in cutoffs + } + if {k: v for k, v in locals().items() if k in cutoffs} != defaults: + msg = "If you pass `n_top_genes`, all cutoffs are ignored." + # 3: caller -> 2: `highly_variable_genes` -> 1: here + warnings.warn(msg, UserWarning, stacklevel=3) + return n_top_genes + + def in_bounds( + self, + mean: NDArray[np.floating] | DaskArray, + dispersion_norm: NDArray[np.floating] | DaskArray, + ) -> NDArray[np.bool_] | DaskArray: + return ( + (mean > self.min_mean) + & (mean < self.max_mean) + & (dispersion_norm > self.min_disp) + & (dispersion_norm < self.max_disp) + ) + + +class HvgArgs(TypedDict): + cutoff: _Cutoffs | int + n_bins: int + flavor: Literal["seurat", "cell_ranger"] + + +def _highly_variable_genes_single_batch( + adata: AnnData, + *, + layer: str | None = None, + filter_unexpressed_genes: bool = False, + **kwargs: Unpack[HvgArgs], +) -> pd.DataFrame: + """See `highly_variable_genes`. + + Returns + ------- + A DataFrame that contains the columns + `highly_variable`, `means`, `dispersions`, and `dispersions_norm`. + + """ + cutoff = kwargs["cutoff"] + flavor = kwargs["flavor"] + n_bins = kwargs["n_bins"] + + x = _get_obs_rep(adata, layer=layer) + + # Filter to genes that are expressed + if filter_unexpressed_genes: + with settings.verbosity.override(Verbosity.error): + # TODO use groupby or so instead of materialize_as_ndarray + filt, _ = materialize_as_ndarray( + filter_genes(x, min_cells=1, inplace=False) + ) + else: + filt = np.ones(x.shape[1], dtype=bool) + + n_removed = np.sum(~filt) + if n_removed: + x = x[:, filt].copy() + + if hasattr(x, "_view_args"): # AnnData array view + # For compatibility with anndata<0.9 + x = x.copy() # Doesn't actually copy memory, just removes View class wrapper + + if flavor == "seurat": + x = x.copy() + if (base := adata.uns.get("log1p", {}).get("base")) is not None: + x *= np.log(base) + # use out if possible. only possible since we copy the data matrix + if isinstance(x, np.ndarray): + np.expm1(x, out=x) + else: + x = np.expm1(x) + + mean, var = materialize_as_ndarray(_get_mean_var(x)) + # now actually compute the dispersion + mean[mean == 0] = 1e-12 # set entries equal to zero to small value + dispersion = var / mean + if flavor == "seurat": # logarithmized mean as in Seurat + dispersion[dispersion == 0] = np.nan + dispersion = np.log(dispersion) + mean = np.log1p(mean) + + # all of the following quantities are "per-gene" here + df = pd.DataFrame( + dict(zip(["means", "dispersions"], (mean, dispersion), strict=True)) + ) + df["mean_bin"] = _get_mean_bins(df["means"], flavor, n_bins) + disp_stats = _get_disp_stats(df, flavor) + + # actually do the normalization + df["dispersions_norm"] = (df["dispersions"] - disp_stats["avg"]) / disp_stats["dev"] + df["highly_variable"] = _subset_genes( + adata[:, filt], + mean=mean, + dispersion_norm=df["dispersions_norm"].to_numpy(), + cutoff=cutoff, + ) + + df.index = adata[:, filt].var_names + + if n_removed > 0: + # df.reset_index(drop=False, inplace=True, names=["gene"]) + # Add 0 values for genes that were filtered out + missing_hvg = pd.DataFrame( + np.zeros((n_removed, len(df.columns))), + columns=df.columns, + ) + missing_hvg["highly_variable"] = missing_hvg["highly_variable"].astype(bool) + missing_hvg.index = adata.var_names[~filt] + df = pd.concat([df, missing_hvg]).loc[adata.var_names] + + return df + + +def _get_mean_bins( + means: pd.Series, flavor: Literal["seurat", "cell_ranger"], n_bins: int +) -> pd.Series: + if flavor == "seurat": + bins = n_bins + elif flavor == "cell_ranger": + bins = np.r_[-np.inf, np.percentile(means, np.arange(10, 105, 5)), np.inf] + else: + msg = '`flavor` needs to be "seurat" or "cell_ranger"' + raise ValueError(msg) + + return pd.cut(means, bins=bins) + + +def _get_disp_stats( + df: pd.DataFrame, flavor: Literal["seurat", "cell_ranger"] +) -> pd.DataFrame: + disp_grouped = df.groupby("mean_bin", observed=True)["dispersions"] + if flavor == "seurat": + disp_bin_stats = disp_grouped.agg(avg="mean", dev="std") + _postprocess_dispersions_seurat(disp_bin_stats, df["mean_bin"]) + elif flavor == "cell_ranger": + disp_bin_stats = disp_grouped.agg(avg="median", dev=_mad) + else: + msg = '`flavor` needs to be "seurat" or "cell_ranger"' + raise ValueError(msg) + return disp_bin_stats.loc[df["mean_bin"]].set_index(df.index) + + +def _postprocess_dispersions_seurat( + disp_bin_stats: pd.DataFrame, mean_bin: pd.Series +) -> None: + # retrieve those genes that have nan std, these are the ones where + # only a single gene fell in the bin and implicitly set them to have + # a normalized disperion of 1 + one_gene_per_bin = disp_bin_stats["dev"].isnull() + gen_indices = np.flatnonzero(one_gene_per_bin.loc[mean_bin]) + if len(gen_indices) == 0: + return + logg.debug( + f"Gene indices {gen_indices} fell into a single bin: their " + "normalized dispersion was set to 1.\n " + "Decreasing `n_bins` will likely avoid this effect." + ) + disp_bin_stats.loc[one_gene_per_bin, "dev"] = disp_bin_stats.loc[ + one_gene_per_bin, "avg" + ] + disp_bin_stats.loc[one_gene_per_bin, "avg"] = 0 + + +def _mad(a): + from statsmodels.robust import mad + + with warnings.catch_warnings(): + # MAD calculation raises the warning: "Mean of empty slice" + warnings.simplefilter("ignore", category=RuntimeWarning) + return mad(a) + + +def _subset_genes( + adata: AnnData, + *, + mean: NDArray[np.float64] | DaskArray, + dispersion_norm: NDArray[np.float64] | DaskArray, + cutoff: _Cutoffs | int, +) -> NDArray[np.bool_] | DaskArray: + """Get boolean mask of genes with normalized dispersion in bounds.""" + if isinstance(cutoff, _Cutoffs): + dispersion_norm = np.nan_to_num(dispersion_norm) # similar to Seurat + return cutoff.in_bounds(mean, dispersion_norm) + n_top_genes = cutoff + del cutoff + + if n_top_genes > adata.n_vars: + logg.info("`n_top_genes` > `adata.n_var`, returning all genes.") + n_top_genes = adata.n_vars + disp_cut_off = _nth_highest(dispersion_norm, n_top_genes) + logg.debug( + f"the {n_top_genes} top genes correspond to a " + f"normalized dispersion cutoff of {disp_cut_off}" + ) + return np.nan_to_num(dispersion_norm, nan=-np.inf) >= disp_cut_off + + +def _nth_highest(x: NDArray[np.float64] | DaskArray, n: int) -> float | DaskArray: + x = x[~np.isnan(x)] + if n > x.size: + msg = "`n_top_genes` > number of normalized dispersions, returning all genes with normalized dispersions." + # 5: caller -> 4: `highly_variable_genes` -> 3: `_…_single_batch` -> 2: `_subset_genes` -> 1: here + warnings.warn(msg, UserWarning, stacklevel=5) + n = x.size + if isinstance(x, DaskArray): + return x.topk(n)[-1] + # interestingly, np.argpartition is slightly slower + x[::-1].sort() + return x[n - 1] + + +def _per_batch_func( + func: Callable[Concatenate[AnnData, P], R], + adata: AnnData, + batch_mask: pd.Series[bool], + *args: P.args, + **kwargs: P.kwargs, +) -> R: + return func(adata[batch_mask].copy(), *args, **kwargs) + + +def _highly_variable_genes_batched( + adata: AnnData, batch_key: str, *, layer: str | None, **kwargs: Unpack[HvgArgs] +) -> pd.DataFrame: + cutoff = kwargs["cutoff"] + sanitize_anndata(adata) + batches = adata.obs[batch_key].cat.categories + x = _get_obs_rep(adata, layer=layer) + + func = _per_batch_func + if is_dask := isinstance(x, DaskArray): + from dask import delayed + + func = delayed(_per_batch_func) + + dfs = ( + func( + _highly_variable_genes_single_batch, + adata=adata, + batch_mask=adata.obs[batch_key] == batch, + layer=layer, + filter_unexpressed_genes=True, + **kwargs, + ) + for batch in batches + ) + + if is_dask: + from dask import compute + + dfs = (compute(df)[0] for df in dfs) + + df = pd.concat(dfs, axis=0) + + df["highly_variable"] = df["highly_variable"].astype(int) + df = df.groupby(df.index, observed=True).agg( + dict( + means="mean", + dispersions="mean", + dispersions_norm="mean", + highly_variable="sum", + ) + ) + df["highly_variable_nbatches"] = df["highly_variable"] + df["highly_variable_intersection"] = df["highly_variable_nbatches"] == len(batches) + + if isinstance(cutoff, int): + # sort genes by how often they selected as hvg within each batch and + # break ties with normalized dispersion across batches + + df_orig_ind = adata.var.index.copy() + df.sort_values( + ["highly_variable_nbatches", "dispersions_norm"], + ascending=False, + na_position="last", + inplace=True, + ) + df["highly_variable"] = np.arange(df.shape[0]) < cutoff + df = df.loc[df_orig_ind] + else: + df["dispersions_norm"] = df["dispersions_norm"].fillna(0) # similar to Seurat + df["highly_variable"] = cutoff.in_bounds(df["means"], df["dispersions_norm"]) + + return df + + +@old_positionals( + "layer", + "n_top_genes", + "min_disp", + "max_disp", + "min_mean", + "max_mean", + "span", + "n_bins", + "flavor", + "subset", + "inplace", + "batch_key", + "check_values", +) +def highly_variable_genes( # noqa: PLR0913 + adata: AnnData, + *, + layer: str | None = None, + n_top_genes: int | None = None, + min_disp: float = 0.5, + max_disp: float = np.inf, + min_mean: float = 0.0125, + max_mean: float = 3, + span: float = 0.3, + n_bins: int = 20, + flavor: HVGFlavor = "seurat", + subset: bool = False, + inplace: bool = True, + batch_key: str | None = None, + check_values: bool = True, +) -> pd.DataFrame | None: + """Annotate highly variable genes :cite:p:`Satija2015,Zheng2017,Stuart2019`. + + Expects logarithmized data, except when `flavor='seurat_v3'`/`'seurat_v3_paper'`, in which count + data is expected. + + Depending on `flavor`, this reproduces the R-implementations of Seurat + :cite:p:`Satija2015`, Cell Ranger :cite:p:`Zheng2017`, and Seurat v3 :cite:p:`Stuart2019`. + + `'seurat_v3'`/`'seurat_v3_paper'` requires `scikit-misc` package. If you plan to use this flavor, consider + installing `scanpy` with this optional dependency: `scanpy[skmisc]`. + + For the dispersion-based methods (`flavor='seurat'` :cite:t:`Satija2015` and + `flavor='cell_ranger'` :cite:t:`Zheng2017`), the normalized dispersion is obtained + by scaling with the mean and standard deviation of the dispersions for genes + falling into a given bin for mean expression of genes. This means that for each + bin of mean expression, highly variable genes are selected. + + For `flavor='seurat_v3'`/`'seurat_v3_paper'` :cite:p:`Stuart2019`, a normalized variance for each gene + is computed. First, the data are standardized (i.e., z-score normalization + per feature) with a regularized standard deviation. Next, the normalized variance + is computed as the variance of each gene after the transformation. Genes are ranked + by the normalized variance. + Only if `batch_key` is not `None`, the two flavors differ: For `flavor='seurat_v3'`, genes are first sorted by the median (across batches) rank, with ties broken by the number of batches a gene is a HVG. + For `flavor='seurat_v3_paper'`, genes are first sorted by the number of batches a gene is a HVG, with ties broken by the median (across batches) rank. + + The following may help when comparing to Seurat's naming: + If `batch_key=None` and `flavor='seurat'`, this mimics Seurat's `FindVariableFeatures(…, method='mean.var.plot')`. + If `batch_key=None` and `flavor='seurat_v3'`/`flavor='seurat_v3_paper'`, this mimics Seurat's `FindVariableFeatures(..., method='vst')`. + If `batch_key` is not `None` and `flavor='seurat_v3_paper'`, this mimics Seurat's `SelectIntegrationFeatures`. + + See also `scanpy.experimental.pp._highly_variable_genes` for additional flavors + (e.g. Pearson residuals). + + Parameters + ---------- + adata + The annotated data matrix of shape `n_obs` × `n_vars`. Rows correspond + to cells and columns to genes. + layer + If provided, use `adata.layers[layer]` for expression values instead of `adata.X`. + n_top_genes + Number of highly-variable genes to keep. Mandatory if `flavor='seurat_v3'`. + min_mean + If `n_top_genes` unequals `None`, this and all other cutoffs for the means and the + normalized dispersions are ignored. Ignored if `flavor='seurat_v3'`. + max_mean + If `n_top_genes` unequals `None`, this and all other cutoffs for the means and the + normalized dispersions are ignored. Ignored if `flavor='seurat_v3'`. + min_disp + If `n_top_genes` unequals `None`, this and all other cutoffs for the means and the + normalized dispersions are ignored. Ignored if `flavor='seurat_v3'`. + max_disp + If `n_top_genes` unequals `None`, this and all other cutoffs for the means and the + normalized dispersions are ignored. Ignored if `flavor='seurat_v3'`. + span + The fraction of the data (cells) used when estimating the variance in the loess + model fit if `flavor='seurat_v3'`. + n_bins + Number of bins for binning the mean gene expression. Normalization is + done with respect to each bin. If just a single gene falls into a bin, + the normalized dispersion is artificially set to 1. You'll be informed + about this if you set `settings.verbosity = 4`. + flavor + Choose the flavor for identifying highly variable genes. For the dispersion + based methods in their default workflows, Seurat passes the cutoffs whereas + Cell Ranger passes `n_top_genes`. + subset + Inplace subset to highly-variable genes if `True` otherwise merely indicate + highly variable genes. + inplace + Whether to place calculated metrics in `.var` or return them. + batch_key + If specified, highly-variable genes are selected within each batch separately and merged. + This simple process avoids the selection of batch-specific genes and acts as a + lightweight batch correction method. For all flavors, except `seurat_v3`, genes are first sorted + by how many batches they are a HVG. For dispersion-based flavors ties are broken + by normalized dispersion. For `flavor = 'seurat_v3_paper'`, ties are broken by the median + (across batches) rank based on within-batch normalized variance. + check_values + Check if counts in selected layer are integers. A Warning is returned if set to True. + Only used if `flavor='seurat_v3'`/`'seurat_v3_paper'`. + + Returns + ------- + Returns a :class:`pandas.DataFrame` with calculated metrics if `inplace=False`, else returns an `AnnData` object where it sets the following field: + + `adata.var['highly_variable']` : :class:`pandas.Series` (dtype `bool`) + boolean indicator of highly-variable genes + `adata.var['means']` : :class:`pandas.Series` (dtype `float`) + means per gene + `adata.var['dispersions']` : :class:`pandas.Series` (dtype `float`) + For dispersion-based flavors, dispersions per gene + `adata.var['dispersions_norm']` : :class:`pandas.Series` (dtype `float`) + For dispersion-based flavors, normalized dispersions per gene + `adata.var['variances']` : :class:`pandas.Series` (dtype `float`) + For `flavor='seurat_v3'`/`'seurat_v3_paper'`, variance per gene + `adata.var['variances_norm']`/`'seurat_v3_paper'` : :class:`pandas.Series` (dtype `float`) + For `flavor='seurat_v3'`/`'seurat_v3_paper'`, normalized variance per gene, averaged in + the case of multiple batches + `adata.var['highly_variable_rank']` : :class:`pandas.Series` (dtype `float`) + For `flavor='seurat_v3'`/`'seurat_v3_paper'`, rank of the gene according to normalized + variance, in case of multiple batches description above + `adata.var['highly_variable_nbatches']` : :class:`pandas.Series` (dtype `int`) + If `batch_key` is given, this denotes in how many batches genes are detected as HVG + `adata.var['highly_variable_intersection']` : :class:`pandas.Series` (dtype `bool`) + If `batch_key` is given, this denotes the genes that are highly variable in all batches + + Notes + ----- + This function replaces :func:`~scanpy.pp.filter_genes_dispersion`. + + """ + start = logg.info("extracting highly variable genes") + + if not isinstance(adata, AnnData): + msg = ( + "`pp.highly_variable_genes` expects an `AnnData` argument, " + "pass `inplace=False` if you want to return a `pd.DataFrame`." + ) + raise ValueError(msg) + + if flavor in {"seurat_v3", "seurat_v3_paper"}: + if n_top_genes is None: + sig = signature(_highly_variable_genes_seurat_v3) + n_top_genes = cast("int", sig.parameters["n_top_genes"].default) + return _highly_variable_genes_seurat_v3( + adata, + flavor=flavor, + layer=layer, + n_top_genes=n_top_genes, + batch_key=batch_key, + check_values=check_values, + span=span, + subset=subset, + inplace=inplace, + ) + + cutoff = _Cutoffs.validate( + n_top_genes=n_top_genes, + min_disp=min_disp, + max_disp=max_disp, + min_mean=min_mean, + max_mean=max_mean, + ) + del min_disp, max_disp, min_mean, max_mean, n_top_genes + + if not batch_key: + df = _highly_variable_genes_single_batch( + adata, + layer=layer, + cutoff=cutoff, + n_bins=n_bins, + flavor=flavor, + filter_unexpressed_genes=False, + ) + else: + df = _highly_variable_genes_batched( + adata, batch_key, layer=layer, cutoff=cutoff, n_bins=n_bins, flavor=flavor + ) + + logg.info(" finished", time=start) + + if not inplace: + if subset: + df = df.loc[df["highly_variable"]] + + return df + + adata.uns["hvg"] = {"flavor": flavor} + logg.hint( + "added\n" + " 'highly_variable', boolean vector (adata.var)\n" + " 'means', float vector (adata.var)\n" + " 'dispersions', float vector (adata.var)\n" + " 'dispersions_norm', float vector (adata.var)" + ) + adata.var["highly_variable"] = df["highly_variable"] + adata.var["means"] = df["means"] + adata.var["dispersions"] = df["dispersions"] + adata.var["dispersions_norm"] = df["dispersions_norm"].astype( + np.float32, copy=False + ) + + if batch_key is not None: + adata.var["highly_variable_nbatches"] = df["highly_variable_nbatches"] + adata.var["highly_variable_intersection"] = df["highly_variable_intersection"] + if subset: + adata._inplace_subset_var(df["highly_variable"]) diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_normalization.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_normalization.py new file mode 100644 index 0000000000000000000000000000000000000000..9baffbb03d94dc752772fea6da8a55265444b279 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_normalization.py @@ -0,0 +1,323 @@ +from __future__ import annotations + +from operator import truediv +from typing import TYPE_CHECKING +from warnings import warn + +import numba +import numpy as np + +from .. import logging as logg +from .._compat import CSBase, CSCBase, CSRBase, DaskArray, njit, old_positionals +from .._utils import axis_mul_or_truediv, axis_sum, dematrix, view_to_actual +from ..get import _get_obs_rep, _set_obs_rep + +try: + import dask + import dask.array as da +except ImportError: + da = None + dask = None + +if TYPE_CHECKING: + from anndata import AnnData + + +def _compute_nnz_median(counts: np.ndarray | DaskArray) -> np.floating: + """Given a 1D array of counts, compute the median of the non-zero counts.""" + if isinstance(counts, DaskArray): + counts = counts.compute() + counts_greater_than_zero = counts[counts > 0] + median = np.median(counts_greater_than_zero) + return median + + +@njit +def _normalize_csr( + indptr, + indices, + data, + *, + rows, + columns, + exclude_highly_expressed: bool = False, + max_fraction: float = 0.05, + n_threads: int = 10, +): + """For sparse CSR matrix, compute the normalization factors.""" + counts_per_cell = np.zeros(rows, dtype=data.dtype) + for i in numba.prange(rows): + count = 0.0 + for j in range(indptr[i], indptr[i + 1]): + count += data[j] + counts_per_cell[i] = count + if exclude_highly_expressed: + counts_per_cols_t = np.zeros((n_threads, columns), dtype=np.int32) + counts_per_cols = np.zeros(columns, dtype=np.int32) + + for i in numba.prange(n_threads): + for r in range(i, rows, n_threads): + for j in range(indptr[r], indptr[r + 1]): + if data[j] > max_fraction * counts_per_cell[r]: + minor_index = indices[j] + counts_per_cols_t[i, minor_index] += 1 + for c in numba.prange(columns): + counts_per_cols[c] = counts_per_cols_t[:, c].sum() + + for i in numba.prange(rows): + count = 0.0 + for j in range(indptr[i], indptr[i + 1]): + if counts_per_cols[indices[j]] == 0: + count += data[j] + counts_per_cell[i] = count + + return counts_per_cell, counts_per_cols + + +def _normalize_total_helper( + x: np.ndarray | CSBase | DaskArray, + *, + exclude_highly_expressed: bool, + max_fraction: float, + target_sum: float | None, +) -> tuple[np.ndarray | CSBase | DaskArray, np.ndarray, np.ndarray | None]: + """Calculate the normalized data, counts per cell, and gene subset. + + Parameters + ---------- + See `normalize_total` for details. + + Returns + ------- + X + The normalized data matrix. + counts_per_cell + The normalization factors used for each cell (counts / target_sum). + gene_subset + If `exclude_highly_expressed=True`, a boolean mask indicating which genes + were not considered highly expressed. Otherwise, `None`. + """ + gene_subset = None + counts_per_cell = None + if isinstance(x, CSRBase): + n_threads = numba.get_num_threads() + counts_per_cell, counts_per_cols = _normalize_csr( + x.indptr, + x.indices, + x.data, + rows=x.shape[0], + columns=x.shape[1], + exclude_highly_expressed=exclude_highly_expressed, + max_fraction=max_fraction, + n_threads=n_threads, + ) + if target_sum is None: + target_sum = np.median(counts_per_cell) + if exclude_highly_expressed: + gene_subset = ~np.where(counts_per_cols)[0] + else: + counts_per_cell = axis_sum(x, axis=1) + if exclude_highly_expressed: + # at least one cell as more than max_fraction of counts per cell + hi_exp = dematrix(x > counts_per_cell[:, None] * max_fraction) + gene_subset = axis_sum(hi_exp, axis=0) == 0 + + counts_per_cell = axis_sum(x[:, gene_subset], axis=1) + if target_sum is None: + target_sum = _compute_nnz_median(counts_per_cell) + + counts_per_cell = counts_per_cell / target_sum + out = x if isinstance(x, np.ndarray | CSBase) else None + x = axis_mul_or_truediv( + x, counts_per_cell, op=truediv, out=out, allow_divide_by_zero=False, axis=0 + ) + return x, counts_per_cell, gene_subset + + +@old_positionals( + "target_sum", + "exclude_highly_expressed", + "max_fraction", + "key_added", + "layer", + "inplace", + "copy", +) +def normalize_total( # noqa: PLR0912 + adata: AnnData, + *, + target_sum: float | None = None, + exclude_highly_expressed: bool = False, + max_fraction: float = 0.05, + key_added: str | None = None, + layer: str | None = None, + inplace: bool = True, + copy: bool = False, +) -> AnnData | dict[str, np.ndarray] | None: + """Normalize counts per cell. + + Normalize each cell by total counts over all genes, + so that every cell has the same total count after normalization. + If choosing `target_sum=1e6`, this is CPM normalization. + + If `exclude_highly_expressed=True`, very highly expressed genes are excluded + from the computation of the normalization factor (size factor) for each + cell. This is meaningful as these can strongly influence the resulting + normalized values for all other genes :cite:p:`Weinreb2017`. + + Similar functions are used, for example, by Seurat :cite:p:`Satija2015`, Cell Ranger + :cite:p:`Zheng2017` or SPRING :cite:p:`Weinreb2017`. + + .. note:: + When used with a :class:`~dask.array.Array` in `adata.X`, this function will have to + call functions that trigger `.compute()` on the :class:`~dask.array.Array` if `exclude_highly_expressed` + is `True`, `layer_norm` is not `None`, or if `key_added` is not `None`. + + Params + ------ + adata + The annotated data matrix of shape `n_obs` × `n_vars`. + Rows correspond to cells and columns to genes. + target_sum + If `None`, after normalization, each observation (cell) has a total + count equal to the median of total counts for observations (cells) + before normalization. + exclude_highly_expressed + Exclude (very) highly expressed genes for the computation of the + normalization factor (size factor) for each cell. A gene is considered + highly expressed, if it has more than `max_fraction` of the total counts + in at least one cell. The not-excluded genes will sum up to + `target_sum`. Providing this argument when `adata.X` is a :class:`~dask.array.Array` + will incur blocking `.compute()` calls on the array. + max_fraction + If `exclude_highly_expressed=True`, consider cells as highly expressed + that have more counts than `max_fraction` of the original total counts + in at least one cell. + key_added + Name of the field in `adata.obs` where the normalization factor is + stored. + layer + Layer to normalize instead of `X`. If `None`, `X` is normalized. + inplace + Whether to update `adata` or return dictionary with normalized copies of + `adata.X` and `adata.layers`. + copy + Whether to modify copied input object. Not compatible with inplace=False. + + Returns + ------- + Returns dictionary with normalized copies of `adata.X` and `adata.layers` + or updates `adata` with normalized version of the original + `adata.X` and `adata.layers`, depending on `inplace`. + + Example + ------- + >>> import sys + >>> from anndata import AnnData + >>> import scanpy as sc + >>> sc.settings.verbosity = "info" + >>> sc.settings.logfile = sys.stdout # for doctests + >>> np.set_printoptions(precision=2) + >>> adata = AnnData( + ... np.array( + ... [ + ... [3, 3, 3, 6, 6], + ... [1, 1, 1, 2, 2], + ... [1, 22, 1, 2, 2], + ... ], + ... dtype="float32", + ... ) + ... ) + >>> adata.X + array([[ 3., 3., 3., 6., 6.], + [ 1., 1., 1., 2., 2.], + [ 1., 22., 1., 2., 2.]], dtype=float32) + >>> X_norm = sc.pp.normalize_total(adata, target_sum=1, inplace=False)["X"] + normalizing counts per cell + finished (0:00:00) + >>> X_norm + array([[0.14, 0.14, 0.14, 0.29, 0.29], + [0.14, 0.14, 0.14, 0.29, 0.29], + [0.04, 0.79, 0.04, 0.07, 0.07]], dtype=float32) + >>> X_norm = sc.pp.normalize_total( + ... adata, + ... target_sum=1, + ... exclude_highly_expressed=True, + ... max_fraction=0.2, + ... inplace=False, + ... )["X"] + normalizing counts per cell + The following highly-expressed genes are not considered during normalization factor computation: + ['1', '3', '4'] + finished (0:00:00) + >>> X_norm + array([[ 0.5, 0.5, 0.5, 1. , 1. ], + [ 0.5, 0.5, 0.5, 1. , 1. ], + [ 0.5, 11. , 0.5, 1. , 1. ]], dtype=float32) + + """ + if copy: + if not inplace: + msg = "`copy=True` cannot be used with `inplace=False`." + raise ValueError(msg) + adata = adata.copy() + + if max_fraction < 0 or max_fraction > 1: + msg = "Choose max_fraction between 0 and 1." + raise ValueError(msg) + + view_to_actual(adata) + + x = _get_obs_rep(adata, layer=layer) + if x is None: + msg = f"Layer {layer!r} not found in adata." + raise ValueError(msg) + if isinstance(x, CSCBase): + x = x.tocsr() + if not inplace: + x = x.copy() + if issubclass(x.dtype.type, int | np.integer): + x = x.astype(np.float32) # TODO: Check if float64 should be used + + start = logg.info("normalizing counts per cell") + + x, counts_per_cell, gene_subset = _normalize_total_helper( + x, + exclude_highly_expressed=exclude_highly_expressed, + max_fraction=max_fraction, + target_sum=target_sum, + ) + + if exclude_highly_expressed: + logg.info( + "The following highly-expressed genes are not considered during normalization factor computation:\n" + f"{adata.var_names[~gene_subset].tolist()}" + ) + + cell_subset = counts_per_cell > 0 + if not isinstance(cell_subset, DaskArray) and not np.all(cell_subset): + warn("Some cells have zero counts", UserWarning, stacklevel=2) + + dat = dict( + X=x, + norm_factor=counts_per_cell, + ) + if inplace: + if key_added is not None: + adata.obs[key_added] = dat["norm_factor"] + _set_obs_rep(adata, dat["X"], layer=layer) + + logg.info( + " finished ({time_passed})", + time=start, + ) + if key_added is not None: + logg.debug( + f"and added {key_added!r}, counts per cell before normalization (adata.obs)" + ) + + if copy: + return adata + elif not inplace: + return dat + return None diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_pca/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_pca/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..645877dd082f37b9b3103bdbdaa533510c9edfe4 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_pca/__init__.py @@ -0,0 +1,544 @@ +from __future__ import annotations + +import warnings +from typing import TYPE_CHECKING, Literal, overload +from warnings import warn + +import numpy as np +from anndata import AnnData +from packaging.version import Version +from sklearn.utils import check_random_state + +from ... import logging as logg +from ..._compat import CSBase, DaskArray, pkg_version +from ..._settings import settings +from ..._utils import _doc_params, _empty, get_literal_vals, is_backed_type +from ...get import _check_mask, _get_obs_rep +from .._docs import doc_mask_var_hvg +from ._compat import _pca_compat_sparse + +if TYPE_CHECKING: + from collections.abc import Container + from collections.abc import Set as AbstractSet + from typing import LiteralString, TypeVar + + import dask_ml.decomposition as dmld + import sklearn.decomposition as skld + from numpy.typing import DTypeLike, NDArray + + from ..._utils import Empty + from ..._utils.random import _LegacyRandom + + MethodDaskML = type[dmld.PCA | dmld.IncrementalPCA | dmld.TruncatedSVD] + MethodSklearn = type[skld.PCA | skld.TruncatedSVD] + + T = TypeVar("T", bound=LiteralString) + M = TypeVar("M", bound=LiteralString) + + +SvdSolvPCADaskML = Literal["auto", "full", "tsqr", "randomized"] +SvdSolvTruncatedSVDDaskML = Literal["tsqr", "randomized"] +SvdSolvDaskML = SvdSolvPCADaskML | SvdSolvTruncatedSVDDaskML + +if pkg_version("scikit-learn") >= Version("1.5") or TYPE_CHECKING: + SvdSolvPCASparseSklearn = Literal["arpack", "covariance_eigh"] +else: + SvdSolvPCASparseSklearn = Literal["arpack"] +SvdSolvPCADenseSklearn = Literal["auto", "full", "randomized"] | SvdSolvPCASparseSklearn +SvdSolvTruncatedSVDSklearn = Literal["arpack", "randomized"] +SvdSolvSkearn = ( + SvdSolvPCADenseSklearn | SvdSolvPCASparseSklearn | SvdSolvTruncatedSVDSklearn +) + +SvdSolvPCACustom = Literal["covariance_eigh"] + +SvdSolver = SvdSolvDaskML | SvdSolvSkearn | SvdSolvPCACustom + + +@_doc_params( + mask_var_hvg=doc_mask_var_hvg, +) +def pca( # noqa: PLR0912, PLR0913, PLR0915 + data: AnnData | np.ndarray | CSBase, + n_comps: int | None = None, + *, + layer: str | None = None, + zero_center: bool = True, + svd_solver: SvdSolver | None = None, + chunked: bool = False, + chunk_size: int | None = None, + random_state: _LegacyRandom = 0, + return_info: bool = False, + mask_var: NDArray[np.bool_] | str | None | Empty = _empty, + use_highly_variable: bool | None = None, + dtype: DTypeLike = "float32", + key_added: str | None = None, + copy: bool = False, +) -> AnnData | np.ndarray | CSBase | None: + r"""Principal component analysis :cite:p:`Pedregosa2011`. + + Computes PCA coordinates, loadings and variance decomposition. + Uses the following implementations (and defaults for `svd_solver`): + + .. list-table:: + :header-rows: 1 + :stub-columns: 1 + + - - + - :class:`~numpy.ndarray`, :class:`~scipy.sparse.spmatrix`, or :class:`~scipy.sparse.sparray` + - :class:`dask.array.Array` + - - `chunked=False`, `zero_center=True` + - sklearn :class:`~sklearn.decomposition.PCA` (`'arpack'`) + - - *dense*: dask-ml :class:`~dask_ml.decomposition.PCA`\ [#high-mem]_ (`'auto'`) + - *sparse* or `svd_solver='covariance_eigh'`: custom implementation (`'covariance_eigh'`) + - - `chunked=False`, `zero_center=False` + - sklearn :class:`~sklearn.decomposition.TruncatedSVD` (`'randomized'`) + - dask-ml :class:`~dask_ml.decomposition.TruncatedSVD`\ [#dense-only]_ (`'tsqr'`) + - - `chunked=True` (`zero_center` ignored) + - sklearn :class:`~sklearn.decomposition.IncrementalPCA` (`'auto'`) + - dask-ml :class:`~dask_ml.decomposition.IncrementalPCA`\ [#densifies]_ (`'auto'`) + + .. [#high-mem] Consider `svd_solver='covariance_eigh'` to reduce memory usage (see :issue:`dask/dask-ml#985`). + .. [#dense-only] This implementation can not handle sparse chunks, try manually densifying them. + .. [#densifies] This implementation densifies sparse chunks and therefore has increased memory usage. + + Parameters + ---------- + data + The (annotated) data matrix of shape `n_obs` × `n_vars`. + Rows correspond to cells and columns to genes. + n_comps + Number of principal components to compute. Defaults to 50, + or 1 - minimum dimension size of selected representation. + layer + If provided, which element of layers to use for PCA. + zero_center + If `True`, compute (or approximate) PCA from covariance matrix. + If `False`, performa a truncated SVD instead of PCA. + + Our default PCA algorithms (see `svd_solver`) support implicit zero-centering, + and therefore efficiently operating on sparse data. + svd_solver + SVD solver to use. + See table above to see which solver class is used based on `chunked` and `zero_center`, + as well as the default solver for each class when `svd_solver=None`. + + Efficient computation of the principal components of a sparse matrix + currently only works with the `'arpack`' or `'covariance_eigh`' solver. + + `None` + Choose automatically based on solver class (see table above). + `'arpack'` + ARPACK wrapper in SciPy (:func:`~scipy.sparse.linalg.svds`). + Not available for *dask* arrays. + `'covariance_eigh'` + Classic eigendecomposition of the covariance matrix, suited for tall-and-skinny matrices. + With dask, array must be CSR or dense and chunked as `(N, adata.shape[1])`. + `'randomized'` + Randomized algorithm from :cite:t:`Halko2009`. + For *dask* arrays, this will use :func:`~dask.array.linalg.svd_compressed`. + `'auto'` + Choose automatically depending on the size of the problem: + Will use `'full'` for small shapes and `'randomized'` for large shapes. + `'tsqr'` + “tall-and-skinny QR” algorithm from :cite:t:`Benson2013`. + Only available for dense *dask* arrays. + + .. versionchanged:: 1.9.3 + Default value changed from `'arpack'` to None. + .. versionchanged:: 1.4.5 + Default value changed from `'auto'` to `'arpack'`. + chunked + If `True`, perform an incremental PCA on segments of `chunk_size`. + Automatically zero centers and ignores settings of `zero_center`, `random_seed` and `svd_solver`. + If `False`, perform a full PCA/truncated SVD (see `svd_solver` and `zero_center`). + See table above for which solver class is used. + chunk_size + Number of observations to include in each chunk. + Required if `chunked=True` was passed. + random_state + Change to use different initial states for the optimization. + return_info + Only relevant when not passing an :class:`~anndata.AnnData`: + see “Returns”. + {mask_var_hvg} + layer + Layer of `adata` to use as expression values. + dtype + Numpy data type string to which to convert the result. + key_added + If not specified, the embedding is stored as + :attr:`~anndata.AnnData.obsm`\ `['X_pca']`, the loadings as + :attr:`~anndata.AnnData.varm`\ `['PCs']`, and the the parameters in + :attr:`~anndata.AnnData.uns`\ `['pca']`. + If specified, the embedding is stored as + :attr:`~anndata.AnnData.obsm`\ ``[key_added]``, the loadings as + :attr:`~anndata.AnnData.varm`\ ``[key_added]``, and the the parameters in + :attr:`~anndata.AnnData.uns`\ ``[key_added]``. + copy + If an :class:`~anndata.AnnData` is passed, determines whether a copy + is returned. Is ignored otherwise. + + Returns + ------- + If `data` is array-like and `return_info=False` was passed, + this function returns the PCA representation of `data` as an + array of the same type as the input array. + + Otherwise, it returns `None` if `copy=False`, else an updated `AnnData` object. + Sets the following fields: + + `.obsm['X_pca' | key_added]` : :class:`~scipy.sparse.csr_matrix` | :class:`~scipy.sparse.csc_matrix` | :class:`~numpy.ndarray` (shape `(adata.n_obs, n_comps)`) + PCA representation of data. + `.varm['PCs' | key_added]` : :class:`~numpy.ndarray` (shape `(adata.n_vars, n_comps)`) + The principal components containing the loadings. + `.uns['pca' | key_added]['variance_ratio']` : :class:`~numpy.ndarray` (shape `(n_comps,)`) + Ratio of explained variance. + `.uns['pca' | key_added]['variance']` : :class:`~numpy.ndarray` (shape `(n_comps,)`) + Explained variance, equivalent to the eigenvalues of the + covariance matrix. + + """ + logg_start = logg.info("computing PCA") + if layer is not None and chunked: + # Current chunking implementation relies on pca being called on X + msg = "Cannot use `layer` and `chunked` at the same time." + raise NotImplementedError(msg) + + # chunked calculation is not randomized, anyways + if svd_solver in {"auto", "randomized"} and not chunked: + logg.info( + "Note that scikit-learn's randomized PCA might not be exactly " + "reproducible across different computational platforms. For exact " + "reproducibility, choose `svd_solver='arpack'`." + ) + if return_anndata := isinstance(data, AnnData): + if layer is None and not chunked and is_backed_type(data.X): + msg = f"PCA is not implemented for matrices of type {type(data.X)} with chunked as False" + raise NotImplementedError(msg) + adata = data.copy() if copy else data + elif pkg_version("anndata") < Version("0.8.0rc1"): + adata = AnnData(data, dtype=data.dtype) + else: + adata = AnnData(data) + + # Unify new mask argument and deprecated use_highly_varible argument + mask_var_param, mask_var = _handle_mask_var( + adata, mask_var, use_highly_variable=use_highly_variable + ) + del use_highly_variable + adata_comp = adata[:, mask_var] if mask_var is not None else adata + + if n_comps is None: + min_dim = min(adata_comp.n_vars, adata_comp.n_obs) + n_comps = min_dim - 1 if min_dim <= settings.N_PCS else settings.N_PCS + + logg.info(f" with {n_comps=}") + + x = _get_obs_rep(adata_comp, layer=layer) + if is_backed_type(x) and layer is not None: + msg = f"PCA is not implemented for matrices of type {type(x)} from layers" + raise NotImplementedError(msg) + # See: https://github.com/scverse/scanpy/pull/2816#issuecomment-1932650529 + if ( + pkg_version("anndata") < Version("0.9") + and mask_var is not None + and isinstance(x, np.ndarray) + ): + warnings.warn( + "When using a mask parameter with anndata<0.9 on a dense array, the PCA" + "can have slightly different results due the array being column major " + "instead of row major.", + UserWarning, + stacklevel=2, + ) + + # check_random_state returns a numpy RandomState when passed an int but + # dask needs an int for random state + if not isinstance(x, DaskArray): + random_state = check_random_state(random_state) + elif not isinstance(random_state, int): + msg = f"random_state needs to be an int, not a {type(random_state).__name__} when passing a dask array" + raise TypeError(msg) + + if chunked: + if ( + not zero_center + or random_state + or (svd_solver is not None and svd_solver != "arpack") + ): + logg.debug("Ignoring zero_center, random_state, svd_solver") + + incremental_pca_kwargs = dict() + if isinstance(x, DaskArray): + from dask.array import zeros + from dask_ml.decomposition import IncrementalPCA + + incremental_pca_kwargs["svd_solver"] = _handle_dask_ml_args( + svd_solver, IncrementalPCA + ) + else: + from numpy import zeros + from sklearn.decomposition import IncrementalPCA + + x_pca = zeros((x.shape[0], n_comps), x.dtype) + + pca_ = IncrementalPCA(n_components=n_comps, **incremental_pca_kwargs) + + for chunk, _, _ in adata_comp.chunked_X(chunk_size): + chunk_dense = chunk.toarray() if isinstance(chunk, CSBase) else chunk + pca_.partial_fit(chunk_dense) + + for chunk, start, end in adata_comp.chunked_X(chunk_size): + chunk_dense = chunk.toarray() if isinstance(chunk, CSBase) else chunk + x_pca[start:end] = pca_.transform(chunk_dense) + elif zero_center: + if isinstance(x, CSBase) and ( + pkg_version("scikit-learn") < Version("1.4") or svd_solver == "lobpcg" + ): + if svd_solver not in ( + {"lobpcg"} | get_literal_vals(SvdSolvPCASparseSklearn) + ): + if svd_solver is not None: + msg = ( + f"Ignoring {svd_solver=} and using 'arpack', " + "sparse PCA with sklearn < 1.4 only supports 'lobpcg' and 'arpack'." + ) + warnings.warn(msg, UserWarning, stacklevel=2) + svd_solver = "arpack" + elif svd_solver == "lobpcg": + msg = ( + f"{svd_solver=} for sparse relies on legacy code and will not be supported in the future. " + "Also the lobpcg solver has been observed to be inaccurate. Please use 'arpack' instead." + ) + warnings.warn(msg, FutureWarning, stacklevel=2) + x_pca, pca_ = _pca_compat_sparse( + x, n_comps, solver=svd_solver, random_state=random_state + ) + else: + if not isinstance(x, DaskArray): + from sklearn.decomposition import PCA + + svd_solver = _handle_sklearn_args( + svd_solver, PCA, sparse=isinstance(x, CSBase) + ) + pca_ = PCA( + n_components=n_comps, + svd_solver=svd_solver, + random_state=random_state, + ) + elif isinstance(x._meta, CSBase) or svd_solver == "covariance_eigh": + from ._dask import PCAEighDask + + if random_state != 0: + msg = f"Ignoring {random_state=} when using a sparse dask array" + warnings.warn(msg, UserWarning, stacklevel=2) + if svd_solver not in {None, "covariance_eigh"}: + msg = f"Ignoring {svd_solver=} when using a sparse dask array" + warnings.warn(msg, UserWarning, stacklevel=2) + pca_ = PCAEighDask(n_components=n_comps) + else: + from dask_ml.decomposition import PCA + + svd_solver = _handle_dask_ml_args(svd_solver, PCA) + pca_ = PCA( + n_components=n_comps, + svd_solver=svd_solver, + random_state=random_state, + ) + x_pca = pca_.fit_transform(x) + else: + if isinstance(x, DaskArray): + if isinstance(x._meta, CSBase): + msg = ( + "`zero_center=False` is not supported for sparse Dask arrays (yet). " + "See ." + ) + raise TypeError(msg) + from dask_ml.decomposition import TruncatedSVD + + svd_solver = _handle_dask_ml_args(svd_solver, TruncatedSVD) + else: + from sklearn.decomposition import TruncatedSVD + + svd_solver = _handle_sklearn_args(svd_solver, TruncatedSVD) + + logg.debug( + " without zero-centering: \n" + " the explained variance does not correspond to the exact statistical definition\n" + " the first component, e.g., might be heavily influenced by different means\n" + " the following components often resemble the exact PCA very closely" + ) + pca_ = TruncatedSVD( + n_components=n_comps, random_state=random_state, algorithm=svd_solver + ) + x_pca = pca_.fit_transform(x) + + if x_pca.dtype.descr != np.dtype(dtype).descr: + x_pca = x_pca.astype(dtype) + + if return_anndata: + key_obsm, key_varm, key_uns = ( + ("X_pca", "PCs", "pca") if key_added is None else [key_added] * 3 + ) + adata.obsm[key_obsm] = x_pca + + if mask_var is not None: + adata.varm[key_varm] = np.zeros(shape=(adata.n_vars, n_comps)) + adata.varm[key_varm][mask_var] = pca_.components_.T + else: + adata.varm[key_varm] = pca_.components_.T + + params = dict( + zero_center=zero_center, + use_highly_variable=mask_var_param == "highly_variable", + mask_var=mask_var_param, + ) + if layer is not None: + params["layer"] = layer + adata.uns[key_uns] = dict( + params=params, + variance=pca_.explained_variance_, + variance_ratio=pca_.explained_variance_ratio_, + ) + + logg.info(" finished", time=logg_start) + logg.debug( + "and added\n" + f" {key_obsm!r}, the PCA coordinates (adata.obs)\n" + f" {key_varm!r}, the loadings (adata.varm)\n" + f" 'pca_variance', the variance / eigenvalues (adata.uns[{key_uns!r}])\n" + f" 'pca_variance_ratio', the variance ratio (adata.uns[{key_uns!r}])" + ) + return adata if copy else None + else: + logg.info(" finished", time=logg_start) + if return_info: + return ( + x_pca, + pca_.components_, + pca_.explained_variance_ratio_, + pca_.explained_variance_, + ) + else: + return x_pca + + +def _handle_mask_var( + adata: AnnData, + mask_var: NDArray[np.bool_] | str | Empty | None, + *, + use_highly_variable: bool | None, +) -> tuple[np.ndarray | str | None, np.ndarray | None]: + """Unify new mask argument and deprecated use_highly_varible argument. + + Returns both the normalized mask parameter and the validated mask array. + """ + # First, verify and possibly warn + if use_highly_variable is not None: + hint = ( + 'Use_highly_variable=True can be called through mask_var="highly_variable". ' + "Use_highly_variable=False can be called through mask_var=None" + ) + msg = f"Argument `use_highly_variable` is deprecated, consider using the mask argument. {hint}" + warn(msg, FutureWarning, stacklevel=2) + if mask_var is not _empty: + msg = f"These arguments are incompatible. {hint}" + raise ValueError(msg) + + # Handle default case and explicit use_highly_variable=True + if use_highly_variable or ( + use_highly_variable is None + and mask_var is _empty + and "highly_variable" in adata.var.columns + ): + mask_var = "highly_variable" + + # Without highly variable genes, we don’t use a mask by default + if mask_var is _empty or mask_var is None: + return None, None + return mask_var, _check_mask(adata, mask_var, "var") + + +@overload +def _handle_dask_ml_args( + svd_solver: str | None, method: type[dmld.PCA | dmld.IncrementalPCA] +) -> SvdSolvPCADaskML: ... +@overload +def _handle_dask_ml_args( + svd_solver: str | None, method: type[dmld.TruncatedSVD] +) -> SvdSolvTruncatedSVDDaskML: ... +def _handle_dask_ml_args(svd_solver: str | None, method: MethodDaskML) -> SvdSolvDaskML: + import dask_ml.decomposition as dmld + + args: AbstractSet[SvdSolvDaskML] + default: SvdSolvDaskML + match method: + case dmld.PCA | dmld.IncrementalPCA: + args = get_literal_vals(SvdSolvPCADaskML) + default = "auto" + case dmld.TruncatedSVD: + args = get_literal_vals(SvdSolvTruncatedSVDDaskML) + default = "tsqr" + case _: + msg = f"Unknown {method=} in _handle_dask_ml_args" + raise ValueError(msg) + return _handle_x_args(svd_solver, method, args, default) + + +@overload +def _handle_sklearn_args( + svd_solver: str | None, method: type[skld.TruncatedSVD], *, sparse: None = None +) -> SvdSolvTruncatedSVDSklearn: ... +@overload +def _handle_sklearn_args( + svd_solver: str | None, method: type[skld.PCA], *, sparse: Literal[False] +) -> SvdSolvPCADenseSklearn: ... +@overload +def _handle_sklearn_args( + svd_solver: str | None, method: type[skld.PCA], *, sparse: Literal[True] +) -> SvdSolvPCASparseSklearn: ... +def _handle_sklearn_args( + svd_solver: str | None, method: MethodSklearn, *, sparse: bool | None = None +) -> SvdSolvSkearn: + import sklearn.decomposition as skld + + args: AbstractSet[SvdSolvSkearn] + default: SvdSolvSkearn + suffix = "" + match (method, sparse): + case (skld.TruncatedSVD, None): + args = get_literal_vals(SvdSolvTruncatedSVDSklearn) + default = "randomized" + case (skld.PCA, False): + args = get_literal_vals(SvdSolvPCADenseSklearn) + default = "arpack" + case (skld.PCA, True): + args = get_literal_vals(SvdSolvPCASparseSklearn) + default = "arpack" + suffix = " (with sparse input)" + case _: + msg = f"Unknown {method=} ({sparse=}) in _handle_sklearn_args" + raise ValueError(msg) + + return _handle_x_args(svd_solver, method, args, default, suffix=suffix) + + +def _handle_x_args( + svd_solver: str | None, + method: type, + args: Container[T], + default: T, + *, + suffix: str = "", +) -> T: + if svd_solver in args: + return svd_solver + if svd_solver is not None: + msg = ( + f"Ignoring {svd_solver=} and using {default}, " + f"{method.__module__}.{method.__qualname__}{suffix} only supports {args}." + ) + # (4: caller of `pca` -> 3: `pca` -> 2: `_handle_{sklearn,dask_ml}_args` -> 1: here) + warnings.warn(msg, UserWarning, stacklevel=4) + return default diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_pca/_compat.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_pca/_compat.py new file mode 100644 index 0000000000000000000000000000000000000000..af4a994fb47c261bdf8755fccebc7f6e099849b4 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_pca/_compat.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +from packaging.version import Version +from scipy.sparse.linalg import LinearOperator, svds +from sklearn.utils import check_array, check_random_state +from sklearn.utils.extmath import svd_flip + +from ..._compat import pkg_version +from .._utils import _get_mean_var + +if TYPE_CHECKING: + from typing import Literal + + from numpy.typing import NDArray + from sklearn.decomposition import PCA + + from ..._compat import CSBase + from ..._utils.random import _LegacyRandom + + +def _pca_compat_sparse( + x: CSBase, + n_pcs: int, + *, + solver: Literal["arpack", "lobpcg"], + mu: NDArray[np.floating] | None = None, + random_state: _LegacyRandom = None, +) -> tuple[NDArray[np.floating], PCA]: + """Sparse PCA for scikit-learn <1.4.""" + random_state = check_random_state(random_state) + np.random.set_state(random_state.get_state()) + random_init = np.random.rand(np.min(x.shape)) + x = check_array(x, accept_sparse=["csr", "csc"]) + + if mu is None: + mu = np.asarray(x.mean(0)).flatten()[None, :] + ones = np.ones(x.shape[0])[None, :].dot + + def mat_op(v: NDArray[np.floating]): + return (x @ v) - (mu @ v) + + def rmat_op(v: NDArray[np.floating]): + return (x.T.conj() @ v) - (mu.T @ ones(v)) + + linop = LinearOperator( + dtype=x.dtype, + shape=x.shape, + matvec=mat_op, + matmat=mat_op, + rmatvec=rmat_op, + rmatmat=rmat_op, + ) + + u, s, v = svds(linop, solver=solver, k=n_pcs, v0=random_init) + # u_based_decision was changed in https://github.com/scikit-learn/scikit-learn/pull/27491 + u, v = svd_flip( + u, v, u_based_decision=pkg_version("scikit-learn") < Version("1.5.0rc1") + ) + idx = np.argsort(-s) + v = v[idx, :] + + x_pca = (u * s)[:, idx] + ev = s[idx] ** 2 / (x.shape[0] - 1) + + total_var = _get_mean_var(x)[1].sum() + ev_ratio = ev / total_var + + from sklearn.decomposition import PCA + + pca = PCA(n_components=n_pcs, svd_solver=solver, random_state=random_state) + pca.explained_variance_ = ev + pca.explained_variance_ratio_ = ev_ratio + pca.components_ = v + return x_pca, pca diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_pca/_dask.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_pca/_dask.py new file mode 100644 index 0000000000000000000000000000000000000000..71106adec32668164926de6396a3098afb5b1329 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_pca/_dask.py @@ -0,0 +1,215 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, cast, overload + +import numpy as np +import scipy.linalg + +from scanpy._utils._doctests import doctest_needs + +from ..._compat import CSBase +from .._utils import _get_mean_var + +if TYPE_CHECKING: + from typing import Literal + + from numpy.typing import DTypeLike, NDArray + + from ..._compat import DaskArray + + +@dataclass +class PCAEighDask: + n_components: int | None = None + + @doctest_needs("dask") + def fit(self, x: DaskArray) -> PCAEighDaskFit: + """Fit the model on `x`. + + This method transforms `self` into a `PCAEighDaskFit` object and returns it. + + Examples + -------- + >>> import dask.array as da + >>> import scipy.sparse as sp + >>> x = ( + ... da.array(sp.random(100, 200, density=0.3, dtype="int64").toarray()) + ... .rechunk((10, -1)) + ... .map_blocks(sp.csr_matrix) + ... ) + >>> x + dask.array + >>> pca_fit = PCAEighDask().fit(x) + >>> assert isinstance(pca_fit, PCAEighDaskFit) + >>> pca_fit.transform(x) + dask.array + + """ + if isinstance(x._meta, CSBase) and x._meta.format != "csr": + msg = ( + "Only sparse dask arrays with CSR-meta format are supported. " + f"Got {x._meta.format} as meta." + ) + raise ValueError(msg) + if x.chunksize[1] != x.shape[1]: + msg = ( + "Only dask arrays with chunking along the first axis are supported. " + f"Got chunksize {x.chunksize} with shape {x.shape}. " + "Rechunking should be simple and cost nothing from AnnData's on-disk format when the on-disk layout has this chunking." + ) + raise ValueError(msg) + self.__class__ = PCAEighDaskFit + self = cast("PCAEighDaskFit", self) # noqa: PLW0642 + + self.n_components_ = ( + min(x.shape) if self.n_components is None else self.n_components + ) + self.n_samples_ = x.shape[0] + self.n_features_in_ = x.shape[1] if x.ndim > 1 else 1 + self.dtype_ = x.dtype + covariance, self.mean_ = _cov_sparse_dask(x) + self.explained_variance_, self.components_ = scipy.linalg.eigh( + covariance, lower=False + ) + + # Arrange eigenvectors and eigenvalues in descending order + self.explained_variance_ = self.explained_variance_[::-1] + self.components_ = np.flip(self.components_, axis=1) + self.components_ = self.components_.T[: self.n_components_, :] + + self.explained_variance_ratio_ = self.explained_variance_ / np.sum( + self.explained_variance_ + ) + if self.n_components_ < min(self.n_samples_, self.n_features_in_): + self.noise_variance_ = self.explained_variance_[self.n_components_ :].mean() + else: + self.noise_variance_ = np.array([0.0]) + self.explained_variance_ = self.explained_variance_[: self.n_components_] + + self.explained_variance_ratio_ = self.explained_variance_ratio_[ + : self.n_components_ + ] + return self + + def fit_transform(self, x: DaskArray, y: DaskArray | None = None) -> DaskArray: + if y is None: + y = x + return self.fit(x).transform(y) + + +@dataclass +class PCAEighDaskFit(PCAEighDask): + n_components_: int = field(init=False) + n_samples_: int = field(init=False) + n_features_in_: int = field(init=False) + dtype_: np.dtype = field(init=False) + mean_: NDArray[np.floating] = field(init=False) + components_: NDArray[np.floating] = field(init=False) + explained_variance_: NDArray[np.floating] = field(init=False) + explained_variance_ratio_: NDArray[np.floating] = field(init=False) + noise_variance_: NDArray[np.floating] = field(init=False) + + def transform(self, x: DaskArray) -> DaskArray: + if TYPE_CHECKING: + # The type checker does not understand imports from dask.array + import dask.array.core as da + else: + import dask.array as da + + def transform_block( + x_part: CSBase | NDArray, + mean_: NDArray[np.floating], + components_: NDArray[np.floating], + ): + pre_mean = mean_ @ components_.T + mean_impact = np.ones((x_part.shape[0], 1)) @ pre_mean.reshape(1, -1) + return (x_part @ components_.T) - mean_impact + + return da.map_blocks( + transform_block, + x, + mean_=self.mean_, + components_=self.components_, + chunks=(x.chunks[0], self.n_components_), + meta=np.array([], dtype=np.float64), + ) + + +@overload +def _cov_sparse_dask( + x: DaskArray, *, return_gram: Literal[False] = False, dtype: DTypeLike | None = None +) -> tuple[NDArray[np.floating], NDArray[np.floating]]: ... +@overload +def _cov_sparse_dask( + x: DaskArray, *, return_gram: Literal[True], dtype: DTypeLike | None = None +) -> tuple[NDArray[np.floating], NDArray[np.floating], NDArray[np.floating]]: ... +def _cov_sparse_dask( + x: DaskArray, *, return_gram: bool = False, dtype: DTypeLike | None = None +) -> ( + tuple[NDArray[np.floating], NDArray[np.floating], NDArray[np.floating]] + | tuple[NDArray[np.floating], NDArray[np.floating]] +): + r"""Compute the covariance matrix and row/col means of matrix `x`. + + Parameters + ---------- + x + A sparse matrix + return_gram + If `True`, the gram matrix will be returned and a copy will be created + to store the results of the covariance, + while if `False`, the local gram matrix result will be overwritten. + (only used for unit testing at the moment) + dtype + The data type of the result (excluding the means) + + Returns + ------- + :math:`\cov(X, X)` + The covariance matrix of `x` in the form :math:`\cov(X, X) = \E(XX) - \E(X)\E(X)`. + :math:`\gram(X, X)` + When return_gram is `True`, the gram matrix of `x` in the form :math:`\frac{1}{n} X.T \dot X`. + :math:`\mean(X)` + The row means of `x`. + + """ + if TYPE_CHECKING: + import dask.array.core as da + import dask.base as dask + else: + import dask + import dask.array as da + + if dtype is None: + dtype = np.float64 if np.issubdtype(x.dtype, np.integer) else x.dtype + else: + dtype = np.dtype(dtype) + + def gram_block(x_part: CSBase | NDArray): + gram_matrix = x_part.T @ x_part + if isinstance(gram_matrix, CSBase): + gram_matrix = gram_matrix.toarray() + return gram_matrix[None, ...] # need new axis for summing + + gram_matrix_dask: DaskArray = da.map_blocks( + gram_block, + x, + new_axis=(1,), + chunks=((1,) * x.blocks.size, (x.shape[1],), (x.shape[1],)), + meta=np.array([], dtype=dtype), + dtype=dtype, + ).sum(axis=0) + mean_x_dask, _ = _get_mean_var(x) + gram_matrix, mean_x = cast( + "tuple[NDArray, NDArray[np.float64]]", + dask.compute(gram_matrix_dask, mean_x_dask), + ) + gram_matrix /= x.shape[0] + + cov_result = gram_matrix.copy() if return_gram else gram_matrix + cov_result -= mean_x[:, None] @ mean_x[None, :] + + if return_gram: + return cov_result, gram_matrix, mean_x + return cov_result, mean_x diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_qc.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_qc.py new file mode 100644 index 0000000000000000000000000000000000000000..19bb7e37ff06e33d3284757a26431f8c1103e8a2 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_qc.py @@ -0,0 +1,478 @@ +from __future__ import annotations + +from functools import singledispatch, wraps +from typing import TYPE_CHECKING +from warnings import warn + +import numba +import numpy as np +import pandas as pd +from scipy import sparse + +from scanpy.preprocessing._distributed import materialize_as_ndarray +from scanpy.preprocessing._utils import _get_mean_var + +from .._compat import CSBase, CSRBase, DaskArray, _register_union, njit +from .._utils import _doc_params, axis_nnz, axis_sum +from ._docs import ( + doc_adata_basic, + doc_expr_reps, + doc_obs_qc_args, + doc_obs_qc_returns, + doc_qc_metric_naming, + doc_var_qc_returns, +) + +if TYPE_CHECKING: + from collections.abc import Collection + + from anndata import AnnData + + +def _choose_mtx_rep(adata, *, use_raw: bool = False, layer: str | None = None): + is_layer = layer is not None + if use_raw and is_layer: + msg = ( + "Cannot use expression from both layer and raw. You provided:" + f"{use_raw=!r} and {layer=!r}" + ) + raise ValueError(msg) + if is_layer: + return adata.layers[layer] + elif use_raw: + return adata.raw.X + else: + return adata.X + + +@_doc_params( + doc_adata_basic=doc_adata_basic, + doc_expr_reps=doc_expr_reps, + doc_obs_qc_args=doc_obs_qc_args, + doc_qc_metric_naming=doc_qc_metric_naming, + doc_obs_qc_returns=doc_obs_qc_returns, +) +def describe_obs( # noqa: PLR0913 + adata: AnnData, + *, + expr_type: str = "counts", + var_type: str = "genes", + qc_vars: Collection[str] = (), + percent_top: Collection[int] | None = (50, 100, 200, 500), + layer: str | None = None, + use_raw: bool = False, + log1p: bool | None = True, + inplace: bool = False, + x=None, + parallel=None, +) -> pd.DataFrame | None: + """Describe observations of anndata. + + Calculates a number of qc metrics for observations in AnnData object. See + section `Returns` for a description of those metrics. + + Note that this method can take a while to compile on the first call. That + result is then cached to disk to be used later. + + Params + ------ + {doc_adata_basic} + {doc_qc_metric_naming} + {doc_obs_qc_args} + {doc_expr_reps} + log1p + Add `log1p` transformed metrics. + inplace + Whether to place calculated metrics in `adata.obs`. + X + Matrix to calculate values on. Meant for internal usage. + + Returns + ------- + QC metrics for observations in adata. If inplace, values are placed into + the AnnData's `.obs` dataframe. + + {doc_obs_qc_returns} + + """ + if parallel is not None: + warn( + "Argument `parallel` is deprecated, and currently has no effect.", + FutureWarning, + stacklevel=2, + ) + # Handle whether X is passed + if x is None: + x = _choose_mtx_rep(adata, use_raw=use_raw, layer=layer) + if isinstance(x, sparse.coo_matrix): + x = sparse.csr_matrix(x) # COO not subscriptable # noqa: TID251 + if isinstance(x, CSBase): + x.eliminate_zeros() + obs_metrics = pd.DataFrame(index=adata.obs_names) + obs_metrics[f"n_{var_type}_by_{expr_type}"] = materialize_as_ndarray( + axis_nnz(x, axis=1) + ) + if log1p: + obs_metrics[f"log1p_n_{var_type}_by_{expr_type}"] = np.log1p( + obs_metrics[f"n_{var_type}_by_{expr_type}"] + ) + obs_metrics[f"total_{expr_type}"] = np.ravel(axis_sum(x, axis=1)) + if log1p: + obs_metrics[f"log1p_total_{expr_type}"] = np.log1p( + obs_metrics[f"total_{expr_type}"] + ) + if percent_top: + percent_top = sorted(percent_top) + proportions = top_segment_proportions(x, percent_top) + for i, n in enumerate(percent_top): + obs_metrics[f"pct_{expr_type}_in_top_{n}_{var_type}"] = ( + proportions[:, i] * 100 + ) + for qc_var in qc_vars: + obs_metrics[f"total_{expr_type}_{qc_var}"] = np.ravel( + axis_sum(x[:, adata.var[qc_var].values], axis=1) + ) + if log1p: + obs_metrics[f"log1p_total_{expr_type}_{qc_var}"] = np.log1p( + obs_metrics[f"total_{expr_type}_{qc_var}"] + ) + obs_metrics[f"pct_{expr_type}_{qc_var}"] = ( + obs_metrics[f"total_{expr_type}_{qc_var}"] + / obs_metrics[f"total_{expr_type}"] + * 100 + ) + if inplace: + adata.obs[obs_metrics.columns] = obs_metrics + else: + return obs_metrics + return None + + +@_doc_params( + doc_adata_basic=doc_adata_basic, + doc_expr_reps=doc_expr_reps, + doc_qc_metric_naming=doc_qc_metric_naming, + doc_var_qc_returns=doc_var_qc_returns, +) +def describe_var( + adata: AnnData, + *, + expr_type: str = "counts", + var_type: str = "genes", + layer: str | None = None, + use_raw: bool = False, + inplace: bool = False, + log1p: bool = True, + x: CSBase | sparse.coo_matrix | np.ndarray | None = None, +) -> pd.DataFrame | None: + """Describe variables of anndata. + + Calculates a number of qc metrics for variables in AnnData object. See + section `Returns` for a description of those metrics. + + Params + ------ + {doc_adata_basic} + {doc_qc_metric_naming} + {doc_expr_reps} + inplace + Whether to place calculated metrics in `adata.var`. + X + Matrix to calculate values on. Meant for internal usage. + + Returns + ------- + QC metrics for variables in adata. If inplace, values are placed into the + AnnData's `.var` dataframe. + + {doc_var_qc_returns} + + """ + # Handle whether X is passed + if x is None: + x = _choose_mtx_rep(adata, use_raw=use_raw, layer=layer) + if isinstance(x, sparse.coo_matrix): + x = sparse.csr_matrix(x) # COO not subscriptable # noqa: TID251 + if isinstance(x, CSBase): + x.eliminate_zeros() + var_metrics = pd.DataFrame(index=adata.var_names) + var_metrics[f"n_cells_by_{expr_type}"], var_metrics[f"mean_{expr_type}"] = ( + materialize_as_ndarray((axis_nnz(x, axis=0), _get_mean_var(x, axis=0)[0])) + ) + if log1p: + var_metrics[f"log1p_mean_{expr_type}"] = np.log1p( + var_metrics[f"mean_{expr_type}"] + ) + var_metrics[f"pct_dropout_by_{expr_type}"] = ( + 1 - var_metrics[f"n_cells_by_{expr_type}"] / x.shape[0] + ) * 100 + var_metrics[f"total_{expr_type}"] = np.ravel(axis_sum(x, axis=0)) + if log1p: + var_metrics[f"log1p_total_{expr_type}"] = np.log1p( + var_metrics[f"total_{expr_type}"] + ) + if inplace: + adata.var[var_metrics.columns] = var_metrics + return None + return var_metrics + + +@_doc_params( + doc_adata_basic=doc_adata_basic, + doc_expr_reps=doc_expr_reps, + doc_obs_qc_args=doc_obs_qc_args, + doc_qc_metric_naming=doc_qc_metric_naming, + doc_obs_qc_returns=doc_obs_qc_returns, + doc_var_qc_returns=doc_var_qc_returns, +) +def calculate_qc_metrics( + adata: AnnData, + *, + expr_type: str = "counts", + var_type: str = "genes", + qc_vars: Collection[str] | str = (), + percent_top: Collection[int] | None = (50, 100, 200, 500), + layer: str | None = None, + use_raw: bool = False, + inplace: bool = False, + log1p: bool = True, + parallel: bool | None = None, +) -> tuple[pd.DataFrame, pd.DataFrame] | None: + """Calculate quality control metrics. + + Calculates a number of qc metrics for an AnnData object, see section + `Returns` for specifics. Largely based on `calculateQCMetrics` from scater + :cite:p:`McCarthy2017`. Currently is most efficient on a sparse CSR or dense matrix. + + Note that this method can take a while to compile on the first call. That + result is then cached to disk to be used later. + + Parameters + ---------- + {doc_adata_basic} + {doc_qc_metric_naming} + {doc_obs_qc_args} + {doc_expr_reps} + inplace + Whether to place calculated metrics in `adata`'s `.obs` and `.var`. + log1p + Set to `False` to skip computing `log1p` transformed annotations. + + Returns + ------- + Depending on `inplace` returns calculated metrics + (as :class:`~pandas.DataFrame`) or updates `adata`'s `obs` and `var`. + + {doc_obs_qc_returns} + + {doc_var_qc_returns} + + Example + ------- + Calculate qc metrics for visualization. + + .. plot:: + :context: close-figs + + import scanpy as sc + import seaborn as sns + + pbmc = sc.datasets.pbmc3k() + pbmc.var["mito"] = pbmc.var_names.str.startswith("MT-") + sc.pp.calculate_qc_metrics(pbmc, qc_vars=["mito"], inplace=True) + sns.jointplot( + data=pbmc.obs, + x="log1p_total_counts", + y="log1p_n_genes_by_counts", + kind="hex", + ) + + .. plot:: + :context: close-figs + + sns.histplot(pbmc.obs["pct_counts_mito"]) + + """ + if parallel is not None: + warn( + "Argument `parallel` is deprecated, and currently has no effect.", + FutureWarning, + stacklevel=2, + ) + # Pass X so I only have to do it once + x = _choose_mtx_rep(adata, use_raw=use_raw, layer=layer) + if isinstance(x, sparse.coo_matrix): + x = sparse.csr_matrix(x) # COO not subscriptable # noqa: TID251 + if isinstance(x, CSBase): + x.eliminate_zeros() + + # Convert qc_vars to list if str + if isinstance(qc_vars, str): + qc_vars = [qc_vars] + + obs_metrics = describe_obs( + adata, + expr_type=expr_type, + var_type=var_type, + qc_vars=qc_vars, + percent_top=percent_top, + inplace=inplace, + x=x, + log1p=log1p, + ) + var_metrics = describe_var( + adata, + expr_type=expr_type, + var_type=var_type, + inplace=inplace, + x=x, + log1p=log1p, + ) + + if not inplace: + return obs_metrics, var_metrics + + +def top_proportions(mtx: np.ndarray | CSBase | sparse.coo_matrix, n: int): + """Calculate cumulative proportions of top expressed genes. + + Parameters + ---------- + mtx + Matrix, where each row is a sample, each column a feature. + n + Rank to calculate proportions up to. Value is treated as 1-indexed, + `n=50` will calculate cumulative proportions up to the 50th most + expressed gene. + + """ + if isinstance(mtx, CSBase | sparse.coo_matrix): + if not isinstance(mtx, CSRBase): + mtx = sparse.csr_matrix(mtx) # noqa: TID251 + # Allowing numba to do more + return top_proportions_sparse_csr(mtx.data, mtx.indptr, np.array(n)) + else: + return top_proportions_dense(mtx, n) + + +def top_proportions_dense(mtx, n): + sums = mtx.sum(axis=1) + partitioned = np.apply_along_axis(np.argpartition, 1, -mtx, n - 1) + partitioned = partitioned[:, :n] + values = np.zeros_like(partitioned, dtype=np.float64) + for i in range(partitioned.shape[0]): + vec = mtx[i, partitioned[i, :]] # Not a view + vec[::-1].sort() # Sorting on a reversed view (e.g. a descending sort) + vec = np.cumsum(vec) / sums[i] + values[i, :] = vec + return values + + +def top_proportions_sparse_csr(data, indptr, n): + values = np.zeros((indptr.size - 1, n), dtype=np.float64) + for i in numba.prange(indptr.size - 1): + start, end = indptr[i], indptr[i + 1] + vec = np.zeros(n, dtype=np.float64) + if end - start <= n: + vec[: end - start] = data[start:end] + total = vec.sum() + else: + vec[:] = -(np.partition(-data[start:end], n - 1)[:n]) + total = (data[start:end]).sum() # Is this not just vec.sum()? + vec[::-1].sort() + values[i, :] = vec.cumsum() / total + return values + + +def check_ns(func): + @wraps(func) + def check_ns_inner( + mtx: np.ndarray | CSBase | sparse.coo_matrix | DaskArray, ns: Collection[int] + ): + if not (max(ns) <= mtx.shape[1] and min(ns) > 0): + msg = "Positions outside range of features." + raise IndexError(msg) + return func(mtx, ns) + + return check_ns_inner + + +@singledispatch +@check_ns +def top_segment_proportions(mtx: np.ndarray, ns: Collection[int]) -> np.ndarray: + """Calculate total percentage of counts in top ns genes. + + Parameters + ---------- + mtx + Matrix, where each row is a sample, each column a feature. + ns + Positions to calculate cumulative proportion at. Values are considered + 1-indexed, e.g. `ns=[50]` will calculate cumulative proportion up to + the 50th most expressed gene. + + """ + # Currently ns is considered to be 1 indexed + ns = np.sort(ns) + sums = mtx.sum(axis=1) + partitioned = np.apply_along_axis(np.partition, 1, mtx, mtx.shape[1] - ns)[:, ::-1][ + :, : ns[-1] + ] + values = np.zeros((mtx.shape[0], len(ns))) + acc = np.zeros(mtx.shape[0]) + prev = 0 + for j, n in enumerate(ns): + acc += partitioned[:, prev:n].sum(axis=1) + values[:, j] = acc + prev = n + return values / sums[:, None] + + +@top_segment_proportions.register(DaskArray) +@check_ns +def _(mtx: DaskArray, ns: Collection[int]) -> DaskArray: + if not isinstance(mtx._meta, CSRBase | np.ndarray): + msg = f"DaskArray must have csr matrix or ndarray meta, got {mtx._meta}." + raise ValueError(msg) + return mtx.map_blocks( + lambda x: top_segment_proportions(x, ns), meta=np.array([]) + ).compute() + + +@_register_union(top_segment_proportions, CSBase) +@top_segment_proportions.register(sparse.coo_matrix) +@check_ns +def _(mtx: CSBase | sparse.coo_matrix, ns: Collection[int]) -> DaskArray: + if not isinstance(mtx, CSRBase): + mtx = sparse.csr_matrix(mtx) # noqa: TID251 + return top_segment_proportions_sparse_csr(mtx.data, mtx.indptr, np.array(ns)) + + +@njit +def top_segment_proportions_sparse_csr(data, indptr, ns): + # work around https://github.com/numba/numba/issues/5056 + indptr = indptr.astype(np.int64) + ns = ns.astype(np.int64) + ns = np.sort(ns) + maxidx = ns[-1] + sums = np.zeros((indptr.size - 1), dtype=data.dtype) + values = np.zeros((indptr.size - 1, len(ns)), dtype=np.float64) + # Just to keep it simple, as a dense matrix + partitioned = np.zeros((indptr.size - 1, maxidx), dtype=data.dtype) + for i in numba.prange(indptr.size - 1): + start, end = indptr[i], indptr[i + 1] + sums[i] = np.sum(data[start:end]) + if end - start <= maxidx: + partitioned[i, : end - start] = data[start:end] + elif (end - start) > maxidx: + partitioned[i, :] = -(np.partition(-data[start:end], maxidx))[:maxidx] + partitioned[i, :] = np.partition(partitioned[i, :], maxidx - ns) + partitioned = partitioned[:, ::-1][:, : ns[-1]] + acc = np.zeros((indptr.size - 1), dtype=data.dtype) + prev = 0 + # can’t use enumerate due to https://github.com/numba/numba/issues/2625 + for j in range(ns.size): + acc += partitioned[:, prev : ns[j]].sum(axis=1) + values[:, j] = acc + prev = ns[j] + return values / sums.reshape((indptr.size - 1, 1)) diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_recipes.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_recipes.py new file mode 100644 index 0000000000000000000000000000000000000000..d223036873f5ab6b1e31587087eb6ae3ab2bfcc8 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_recipes.py @@ -0,0 +1,200 @@ +"""Preprocessing recipes from the literature.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .. import logging as logg +from .. import preprocessing as pp +from .._compat import CSBase, old_positionals +from ._deprecated.highly_variable_genes import ( + filter_genes_cv_deprecated, + filter_genes_dispersion, +) +from ._normalization import normalize_total + +if TYPE_CHECKING: + from anndata import AnnData + + from .._utils.random import _LegacyRandom + + +@old_positionals( + "log", + "mean_threshold", + "cv_threshold", + "n_pcs", + "svd_solver", + "random_state", + "copy", +) +def recipe_weinreb17( + adata: AnnData, + *, + log: bool = True, + mean_threshold: float = 0.01, + cv_threshold: int = 2, + n_pcs: int = 50, + svd_solver="randomized", + random_state: _LegacyRandom = 0, + copy: bool = False, +) -> AnnData | None: + """Normalize and filter as of :cite:p:`Weinreb2017`. + + Expects non-logarithmized data. + If using logarithmized data, pass `log=False`. + + Parameters + ---------- + adata + Annotated data matrix. + log + Logarithmize data? + copy + Return a copy if true. + + """ + from ._deprecated import normalize_per_cell_weinreb16_deprecated, zscore_deprecated + + if isinstance(adata.X, CSBase): + msg = "`recipe_weinreb16 does not support sparse matrices." + raise ValueError(msg) + if copy: + adata = adata.copy() + if log: + pp.log1p(adata) + adata.X = normalize_per_cell_weinreb16_deprecated( + adata.X, max_fraction=0.05, mult_with_mean=True + ) + gene_subset = filter_genes_cv_deprecated(adata.X, mean_threshold, cv_threshold) + adata._inplace_subset_var(gene_subset) # this modifies the object itself + x_pca = pp.pca( + zscore_deprecated(adata.X), + n_comps=n_pcs, + svd_solver=svd_solver, + random_state=random_state, + ) + # update adata + adata.obsm["X_pca"] = x_pca + return adata if copy else None + + +@old_positionals("log", "plot", "copy") +def recipe_seurat( + adata: AnnData, *, log: bool = True, plot: bool = False, copy: bool = False +) -> AnnData | None: + """Normalize and filter as of Seurat :cite:p:`Satija2015`. + + This uses a particular preprocessing. + + Expects non-logarithmized data. + If using logarithmized data, pass `log=False`. + + Parameters + ---------- + adata + Annotated data matrix. + log + Logarithmize data? + plot + Show a plot of the gene dispersion vs. mean relation. + copy + Return a copy if true. + + """ + if copy: + adata = adata.copy() + pp.filter_cells(adata, min_genes=200) + pp.filter_genes(adata, min_cells=3) + normalize_total(adata, target_sum=1e4) + filter_result = filter_genes_dispersion( + adata.X, min_mean=0.0125, max_mean=3, min_disp=0.5, log=not log + ) + if plot: + from ..plotting import ( + _preprocessing as ppp, + ) + + ppp.filter_genes_dispersion(filter_result, log=not log) + adata._inplace_subset_var(filter_result.gene_subset) # filter genes + if log: + pp.log1p(adata) + pp.scale(adata, max_value=10) + return adata if copy else None + + +@old_positionals("n_top_genes", "log", "plot", "copy") +def recipe_zheng17( + adata: AnnData, + *, + n_top_genes: int = 1000, + log: bool = True, + plot: bool = False, + copy: bool = False, +) -> AnnData | None: + """Normalize and filter as of :cite:t:`Zheng2017`. + + Reproduces the preprocessing of :cite:t:`Zheng2017` – the Cell Ranger R Kit of 10x + Genomics. + + Expects non-logarithmized data. + If using logarithmized data, pass `log=False`. + + The recipe runs the following steps + + .. code:: python + + sc.pp.filter_genes(adata, min_counts=1) # only consider genes with more than 1 count + sc.pp.normalize_per_cell( # normalize with total UMI count per cell + adata, key_n_counts='n_counts_all' + ) + filter_result = sc.pp.filter_genes_dispersion( # select highly-variable genes + adata.X, flavor='cell_ranger', n_top_genes=n_top_genes, log=False + ) + adata = adata[:, filter_result.gene_subset] # subset the genes + sc.pp.normalize_per_cell(adata) # renormalize after filtering + if log: sc.pp.log1p(adata) # log transform: adata.X = log(adata.X + 1) + sc.pp.scale(adata) # scale to unit variance and shift to zero mean + + + Parameters + ---------- + adata + Annotated data matrix. + n_top_genes + Number of genes to keep. + log + Take logarithm. + plot + Show a plot of the gene dispersion vs. mean relation. + copy + Return a copy of `adata` instead of updating it. + + Returns + ------- + Returns or updates `adata` depending on `copy`. + + """ + start = logg.info("running recipe zheng17") + if copy: + adata = adata.copy() + # only consider genes with more than 1 count + pp.filter_genes(adata, min_counts=1) + # normalize with total UMI count per cell + normalize_total(adata, key_added="n_counts_all") + filter_result = filter_genes_dispersion( + adata.X, flavor="cell_ranger", n_top_genes=n_top_genes, log=False + ) + if plot: # should not import at the top of the file + from ..plotting import _preprocessing as ppp + + ppp.filter_genes_dispersion(filter_result, log=True) + # actually filter the genes, the following is the inplace version of + # adata = adata[:, filter_result.gene_subset] + adata._inplace_subset_var(filter_result.gene_subset) # filter genes + normalize_total(adata) # renormalize after filtering + if log: + pp.log1p(adata) # log transform: X = log(X + 1) + pp.scale(adata) + logg.info(" finished", time=start) + return adata if copy else None diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_scale.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_scale.py new file mode 100644 index 0000000000000000000000000000000000000000..2e048130c1b3efe2e87f9357447e8f3285836cb5 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_scale.py @@ -0,0 +1,321 @@ +from __future__ import annotations + +import warnings +from functools import singledispatch +from operator import truediv +from typing import TYPE_CHECKING + +import numba +import numpy as np +from anndata import AnnData + +from .. import logging as logg +from .._compat import CSBase, CSCBase, CSRBase, DaskArray, njit, old_positionals +from .._utils import ( + _check_array_function_arguments, + axis_mul_or_truediv, + dematrix, + raise_not_implemented_error_if_backed_type, + renamed_arg, + view_to_actual, +) +from ..get import _check_mask, _get_obs_rep, _set_obs_rep +from ._utils import _get_mean_var + +# install dask if available +try: + import dask.array as da +except ImportError: + da = None + +if TYPE_CHECKING: + from typing import TypeVar + + from numpy.typing import ArrayLike, NDArray + + _A = TypeVar("_A", bound=CSBase | np.ndarray | DaskArray) + + +@singledispatch +def clip(x: ArrayLike | _A, *, max_value: float, zero_center: bool = True) -> _A: + return clip_array(x, max_value=max_value, zero_center=zero_center) + + +@clip.register(CSCBase) +@clip.register(CSRBase) +def _(x: CSBase, *, max_value: float, zero_center: bool = True) -> CSBase: + x.data = clip(x.data, max_value=max_value, zero_center=zero_center) + return x + + +@clip.register(DaskArray) +def _(x: DaskArray, *, max_value: float, zero_center: bool = True) -> DaskArray: + return x.map_blocks( + clip, max_value=max_value, zero_center=zero_center, dtype=x.dtype, meta=x._meta + ) + + +@njit +def clip_array( + x: NDArray[np.floating], /, *, max_value: float, zero_center: bool +) -> NDArray[np.floating]: + a_min, a_max = -max_value, max_value + if x.ndim > 1: + for r, c in numba.pndindex(x.shape): + if x[r, c] > a_max: + x[r, c] = a_max + elif x[r, c] < a_min and zero_center: + x[r, c] = a_min + else: + for i in numba.prange(x.size): + if x[i] > a_max: + x[i] = a_max + elif x[i] < a_min and zero_center: + x[i] = a_min + return x + + +@renamed_arg("X", "data", pos_0=True) +@old_positionals("zero_center", "max_value", "copy", "layer", "obsm") +@singledispatch +def scale( + data: AnnData | _A, + *, + zero_center: bool = True, + max_value: float | None = None, + copy: bool = False, + layer: str | None = None, + obsm: str | None = None, + mask_obs: NDArray[np.bool_] | str | None = None, +) -> AnnData | _A | None: + """Scale data to unit variance and zero mean. + + .. note:: + Variables (genes) that do not display any variation (are constant across + all observations) are retained and (for zero_center==True) set to 0 + during this operation. In the future, they might be set to NaNs. + + Parameters + ---------- + data + The (annotated) data matrix of shape `n_obs` × `n_vars`. + Rows correspond to cells and columns to genes. + zero_center + If `False`, omit zero-centering variables, which allows to handle sparse + input efficiently. + max_value + Clip (truncate) to this value after scaling. If `None`, do not clip. + copy + Whether this function should be performed inplace. If an AnnData object + is passed, this also determines if a copy is returned. + layer + If provided, which element of layers to scale. + obsm + If provided, which element of obsm to scale. + mask_obs + Restrict both the derivation of scaling parameters and the scaling itself + to a certain set of observations. The mask is specified as a boolean array + or a string referring to an array in :attr:`~anndata.AnnData.obs`. + This will transform data from csc to csr format if `issparse(data)`. + + Returns + ------- + Returns `None` if `copy=False`, else returns an updated `AnnData` object. Sets the following fields: + + `adata.X` | `adata.layers[layer]` : :class:`numpy.ndarray` | :class:`scipy.sparse.csr_matrix` (dtype `float`) + Scaled count data matrix. + `adata.var['mean']` : :class:`pandas.Series` (dtype `float`) + Means per gene before scaling. + `adata.var['std']` : :class:`pandas.Series` (dtype `float`) + Standard deviations per gene before scaling. + `adata.var['var']` : :class:`pandas.Series` (dtype `float`) + Variances per gene before scaling. + + """ + _check_array_function_arguments(layer=layer, obsm=obsm) + if layer is not None: + msg = f"`layer` argument inappropriate for value of type {type(data)}" + raise ValueError(msg) + if obsm is not None: + msg = f"`obsm` argument inappropriate for value of type {type(data)}" + raise ValueError(msg) + return scale_array( + data, zero_center=zero_center, max_value=max_value, copy=copy, mask_obs=mask_obs + ) + + +@scale.register(np.ndarray) +@scale.register(DaskArray) +@scale.register(CSRBase) +@scale.register(CSCBase) +def scale_array( + x: _A, + *, + zero_center: bool = True, + max_value: float | None = None, + copy: bool = False, + return_mean_std: bool = False, + mask_obs: NDArray[np.bool_] | None = None, +) -> ( + _A + | tuple[ + _A, + NDArray[np.float64] | DaskArray, + NDArray[np.float64], + ] +): + if copy: + x = x.copy() + + if not zero_center and max_value is not None: + logg.info( # Be careful of what? This should be more specific + "... be careful when using `max_value` without `zero_center`." + ) + + if np.issubdtype(x.dtype, np.integer): + logg.info( + "... as scaling leads to float results, integer " + "input is cast to float, returning copy." + ) + x = x.astype(np.float64) + + mask_obs = ( + # For CSR matrices, default to a set mask to take the `scale_array_masked` path. + # This is faster than the maskless `axis_mul_or_truediv` path. + np.ones(x.shape[0], dtype=np.bool_) + if isinstance(x, CSRBase) and mask_obs is None and not zero_center + else _check_mask(x, mask_obs, "obs") + ) + if mask_obs is not None: + return scale_array_masked( + x, + mask_obs, + zero_center=zero_center, + max_value=max_value, + return_mean_std=return_mean_std, + ) + + mean, var = _get_mean_var(x) + std = np.sqrt(var) + std[std == 0] = 1 + if zero_center: + if isinstance(x, CSBase) or ( + isinstance(x, DaskArray) and isinstance(x._meta, CSBase) + ): + msg = "zero-centering a sparse array/matrix densifies it." + warnings.warn(msg, UserWarning, stacklevel=2) + x -= mean + x = dematrix(x) + + x = axis_mul_or_truediv( + x, + std, + op=truediv, + out=x if isinstance(x, np.ndarray | CSBase) else None, + axis=1, + ) + + # do the clipping + if max_value is not None: + x = clip(x, max_value=max_value, zero_center=zero_center) + if return_mean_std: + return x, mean, std + else: + return x + + +def scale_array_masked( + x: _A, + mask_obs: NDArray[np.bool_], + *, + zero_center: bool = True, + max_value: float | None = None, + return_mean_std: bool = False, +) -> ( + _A + | tuple[ + _A, + NDArray[np.float64] | DaskArray, + NDArray[np.float64], + ] +): + if isinstance(x, CSBase) and not zero_center: + if isinstance(x, CSCBase): + x = x.tocsr() + mean, var = _get_mean_var(x[mask_obs, :]) + std = np.sqrt(var) + std[std == 0] = 1 + + scale_and_clip_csr( + x.indptr, + x.indices, + x.data, + std=std, + mask_obs=mask_obs, + max_value=max_value, + ) + else: + x[mask_obs, :], mean, std = scale_array( + x[mask_obs, :], + zero_center=zero_center, + max_value=max_value, + return_mean_std=True, + ) + + if return_mean_std: + return x, mean, std + else: + return x + + +@njit +def scale_and_clip_csr( + indptr: NDArray[np.integer], + indices: NDArray[np.integer], + data: NDArray[np.floating], + *, + std: NDArray[np.floating], + mask_obs: NDArray[np.bool_], + max_value: float | None, +) -> None: + for i in numba.prange(len(indptr) - 1): + if mask_obs[i]: + for j in range(indptr[i], indptr[i + 1]): + if max_value is not None: + data[j] = min(max_value, data[j] / std[indices[j]]) + else: + data[j] /= std[indices[j]] + + +@scale.register(AnnData) +def scale_anndata( + adata: AnnData, + *, + zero_center: bool = True, + max_value: float | None = None, + copy: bool = False, + layer: str | None = None, + obsm: str | None = None, + mask_obs: NDArray[np.bool_] | str | None = None, +) -> AnnData | None: + adata = adata.copy() if copy else adata + str_mean_std = ("mean", "std") + if mask_obs is not None: + if isinstance(mask_obs, str): + str_mean_std = (f"mean of {mask_obs}", f"std of {mask_obs}") + else: + str_mean_std = ("mean with mask", "std with mask") + mask_obs = _check_mask(adata, mask_obs, "obs") + view_to_actual(adata) + x = _get_obs_rep(adata, layer=layer, obsm=obsm) + raise_not_implemented_error_if_backed_type(x, "scale") + x, adata.var[str_mean_std[0]], adata.var[str_mean_std[1]] = scale( + x, + zero_center=zero_center, + max_value=max_value, + copy=False, # because a copy has already been made, if it were to be made + return_mean_std=True, + mask_obs=mask_obs, + ) + _set_obs_rep(adata, x, layer=layer, obsm=obsm) + return adata if copy else None diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_scrublet/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_scrublet/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5c6d39fad41880d374ba7a80ca318a8df7038287 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_scrublet/__init__.py @@ -0,0 +1,565 @@ +from __future__ import annotations + +from importlib.util import find_spec +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd +from anndata import AnnData +from scipy import sparse + +from ... import logging as logg +from ... import preprocessing as pp +from ..._compat import old_positionals +from ...get import _get_obs_rep +from . import pipeline +from .core import Scrublet + +if TYPE_CHECKING: + from ..._utils.random import _LegacyRandom + from ...neighbors import _Metric, _MetricFn + + +@old_positionals( + "batch_key", + "sim_doublet_ratio", + "expected_doublet_rate", + "stdev_doublet_rate", + "synthetic_doublet_umi_subsampling", + "knn_dist_metric", + "normalize_variance", + "log_transform", + "mean_center", + "n_prin_comps", + "use_approx_neighbors", + "get_doublet_neighbor_parents", + "n_neighbors", + "threshold", + "verbose", + "copy", + "random_state", +) +def scrublet( # noqa: PLR0913 + adata: AnnData, + adata_sim: AnnData | None = None, + *, + batch_key: str | None = None, + sim_doublet_ratio: float = 2.0, + expected_doublet_rate: float = 0.05, + stdev_doublet_rate: float = 0.02, + synthetic_doublet_umi_subsampling: float = 1.0, + knn_dist_metric: _Metric | _MetricFn = "euclidean", + normalize_variance: bool = True, + log_transform: bool = False, + mean_center: bool = True, + n_prin_comps: int = 30, + use_approx_neighbors: bool | None = None, + get_doublet_neighbor_parents: bool = False, + n_neighbors: int | None = None, + threshold: float | None = None, + verbose: bool = True, + copy: bool = False, + random_state: _LegacyRandom = 0, +) -> AnnData | None: + """Predict doublets using Scrublet :cite:p:`Wolock2019`. + + Predict cell doublets using a nearest-neighbor classifier of observed + transcriptomes and simulated doublets. Works best if the input is a raw + (unnormalized) counts matrix from a single sample or a collection of + similar samples from the same experiment. + This function is a wrapper around functions that pre-process using Scanpy + and directly call functions of Scrublet(). You may also undertake your own + preprocessing, simulate doublets with + :func:`~scanpy.pp.scrublet_simulate_doublets`, and run the core scrublet + function :func:`~scanpy.pp.scrublet` with ``adata_sim`` set. + + Parameters + ---------- + adata + The annotated data matrix of shape ``n_obs`` × ``n_vars``. Rows + correspond to cells and columns to genes. Expected to be un-normalised + where adata_sim is not supplied, in which case doublets will be + simulated and pre-processing applied to both objects. If adata_sim is + supplied, this should be the observed transcriptomes processed + consistently (filtering, transform, normalisaton, hvg) with adata_sim. + adata_sim + (Advanced use case) Optional annData object generated by + :func:`~scanpy.pp.scrublet_simulate_doublets`, with same number of vars + as adata. This should have been built from adata_obs after + filtering genes and cells and selcting highly-variable genes. + batch_key + Optional :attr:`~anndata.AnnData.obs` column name discriminating between batches. + sim_doublet_ratio + Number of doublets to simulate relative to the number of observed + transcriptomes. + expected_doublet_rate + Where adata_sim not suplied, the estimated doublet rate for the + experiment. + stdev_doublet_rate + Where adata_sim not suplied, uncertainty in the expected doublet rate. + synthetic_doublet_umi_subsampling + Where adata_sim not suplied, rate for sampling UMIs when creating + synthetic doublets. If 1.0, each doublet is created by simply adding + the UMI counts from two randomly sampled observed transcriptomes. For + values less than 1, the UMI counts are added and then randomly sampled + at the specified rate. + knn_dist_metric + Distance metric used when finding nearest neighbors. For list of + valid values, see the documentation for annoy (if `use_approx_neighbors` + is True) or sklearn.neighbors.NearestNeighbors (if `use_approx_neighbors` + is False). + normalize_variance + If True, normalize the data such that each gene has a variance of 1. + :class:`sklearn.decomposition.TruncatedSVD` will be used for dimensionality + reduction, unless `mean_center` is True. + log_transform + Whether to use :func:`~scanpy.pp.log1p` to log-transform the data + prior to PCA. + mean_center + If True, center the data such that each gene has a mean of 0. + :class:`sklearn.decomposition.PCA` will be used for dimensionality + reduction. + n_prin_comps + Number of principal components used to embed the transcriptomes prior + to k-nearest-neighbor graph construction. + use_approx_neighbors + Use approximate nearest neighbor method (annoy) for the KNN + classifier. + get_doublet_neighbor_parents + If True, return (in .uns) the parent transcriptomes that generated the + doublet neighbors of each observed transcriptome. This information can + be used to infer the cell states that generated a given doublet state. + n_neighbors + Number of neighbors used to construct the KNN graph of observed + transcriptomes and simulated doublets. If ``None``, this is + automatically set to ``np.round(0.5 * np.sqrt(n_obs))``. + threshold + Doublet score threshold for calling a transcriptome a doublet. If + `None`, this is set automatically by looking for the minimum between + the two modes of the `doublet_scores_sim_` histogram. It is best + practice to check the threshold visually using the + `doublet_scores_sim_` histogram and/or based on co-localization of + predicted doublets in a 2-D embedding. + verbose + If :data:`True`, log progress updates. + copy + If :data:`True`, return a copy of the input ``adata`` with Scrublet results + added. Otherwise, Scrublet results are added in place. + random_state + Initial state for doublet simulation and nearest neighbors. + + Returns + ------- + if ``copy=True`` it returns or else adds fields to ``adata``. Those fields: + + ``.obs['doublet_score']`` + Doublet scores for each observed transcriptome + + ``.obs['predicted_doublet']`` + Boolean indicating predicted doublet status + + ``.uns['scrublet']['doublet_scores_sim']`` + Doublet scores for each simulated doublet transcriptome + + ``.uns['scrublet']['doublet_parents']`` + Pairs of ``.obs_names`` used to generate each simulated doublet + transcriptome + + ``.uns['scrublet']['parameters']`` + Dictionary of Scrublet parameters + + See Also + -------- + :func:`~scanpy.pp.scrublet_simulate_doublets`: Run Scrublet's doublet + simulation separately for advanced usage. + :func:`~scanpy.pl.scrublet_score_distribution`: Plot histogram of doublet + scores for observed transcriptomes and simulated doublets. + + """ + if threshold is None and not find_spec("skimage"): # pragma: no cover + # Scrublet.call_doublets requires `skimage` with `threshold=None` but PCA + # is called early, which is wasteful if there is not `skimage` + msg = "threshold is None and thus scrublet requires skimage, but skimage is not installed." + raise ValueError(msg) + + if copy: + adata = adata.copy() + + start = logg.info("Running Scrublet") + + adata_obs = adata.copy() + + def _run_scrublet(ad_obs: AnnData, ad_sim: AnnData | None = None): + # With no adata_sim we assume the regular use case, starting with raw + # counts and simulating doublets + + if ad_sim is None: + pp.filter_genes(ad_obs, min_cells=3) + pp.filter_cells(ad_obs, min_genes=3) + + # Doublet simulation will be based on the un-normalised counts, but on the + # selection of genes following normalisation and variability filtering. So + # we need to save the raw and subset at the same time. + + ad_obs.layers["raw"] = ad_obs.X.copy() + pp.normalize_total(ad_obs) + + # HVG process needs log'd data. + ad_obs.layers["log1p"] = ad_obs.X.copy() + pp.log1p(ad_obs, layer="log1p") + pp.highly_variable_genes(ad_obs, layer="log1p") + del ad_obs.layers["log1p"] + ad_obs = ad_obs[:, ad_obs.var["highly_variable"]].copy() + + # Simulate the doublets based on the raw expressions from the normalised + # and filtered object. + + ad_sim = scrublet_simulate_doublets( + ad_obs, + layer="raw", + sim_doublet_ratio=sim_doublet_ratio, + synthetic_doublet_umi_subsampling=synthetic_doublet_umi_subsampling, + random_seed=random_state, + ) + del ad_obs.layers["raw"] + if log_transform: + pp.log1p(ad_obs) + pp.log1p(ad_sim) + + # Now normalise simulated and observed in the same way + + pp.normalize_total(ad_obs, target_sum=1e6) + pp.normalize_total(ad_sim, target_sum=1e6) + + ad_obs = _scrublet_call_doublets( + adata_obs=ad_obs, + adata_sim=ad_sim, + n_neighbors=n_neighbors, + expected_doublet_rate=expected_doublet_rate, + stdev_doublet_rate=stdev_doublet_rate, + mean_center=mean_center, + normalize_variance=normalize_variance, + n_prin_comps=n_prin_comps, + use_approx_neighbors=use_approx_neighbors, + knn_dist_metric=knn_dist_metric, + get_doublet_neighbor_parents=get_doublet_neighbor_parents, + threshold=threshold, + random_state=random_state, + verbose=verbose, + ) + + return {"obs": ad_obs.obs, "uns": ad_obs.uns["scrublet"]} + + if batch_key is not None: + if batch_key not in adata.obs.columns: + msg = ( + "`batch_key` must be a column of .obs in the input AnnData object," + f"but {batch_key!r} is not in {adata.obs.keys()!r}." + ) + raise ValueError(msg) + + # Run Scrublet independently on batches and return just the + # scrublet-relevant parts of the objects to add to the input object + + batches = np.unique(adata.obs[batch_key]) + scrubbed = [ + _run_scrublet( + adata_obs[adata_obs.obs[batch_key] == batch].copy(), + adata_sim, + ) + for batch in batches + ] + scrubbed_obs = pd.concat([scrub["obs"] for scrub in scrubbed]) + + # Now reset the obs to get the scrublet scores + + adata.obs = scrubbed_obs.loc[adata.obs_names.values] + + # Save the .uns from each batch separately + + adata.uns["scrublet"] = {} + adata.uns["scrublet"]["batches"] = dict( + zip(batches, [scrub["uns"] for scrub in scrubbed], strict=True) + ) + + # Record that we've done batched analysis, so e.g. the plotting + # function knows what to do. + + adata.uns["scrublet"]["batched_by"] = batch_key + + else: + scrubbed = _run_scrublet(adata_obs, adata_sim) + + # Copy outcomes to input object from our processed version + + adata.obs["doublet_score"] = scrubbed["obs"]["doublet_score"] + adata.obs["predicted_doublet"] = scrubbed["obs"]["predicted_doublet"] + adata.uns["scrublet"] = scrubbed["uns"] + + logg.info(" Scrublet finished", time=start) + + return adata if copy else None + + +def _scrublet_call_doublets( # noqa: PLR0913 + adata_obs: AnnData, + adata_sim: AnnData, + *, + n_neighbors: int | None = None, + expected_doublet_rate: float = 0.05, + stdev_doublet_rate: float = 0.02, + mean_center: bool = True, + normalize_variance: bool = True, + n_prin_comps: int = 30, + use_approx_neighbors: bool | None = None, + knn_dist_metric: _Metric | _MetricFn = "euclidean", + get_doublet_neighbor_parents: bool = False, + threshold: float | None = None, + random_state: _LegacyRandom = 0, + verbose: bool = True, +) -> AnnData: + """Core function for predicting doublets using Scrublet :cite:p:`Wolock2019`. + + Predict cell doublets using a nearest-neighbor classifier of observed + transcriptomes and simulated doublets. + + Parameters + ---------- + adata_obs + The annotated data matrix of shape ``n_obs`` × ``n_vars``. Rows + correspond to cells and columns to genes. Should be normalised with + :func:`~scanpy.pp.normalize_total` and filtered to include only highly + variable genes. + adata_sim + Anndata object generated by + :func:`~scanpy.pp.scrublet_simulate_doublets`, with same number of vars + as adata_obs. This should have been built from adata_obs after + filtering genes and cells and selcting highly-variable genes. + n_neighbors + Number of neighbors used to construct the KNN graph of observed + transcriptomes and simulated doublets. If ``None``, this is + automatically set to ``np.round(0.5 * np.sqrt(n_obs))``. + expected_doublet_rate + The estimated doublet rate for the experiment. + stdev_doublet_rate + Uncertainty in the expected doublet rate. + mean_center + If True, center the data such that each gene has a mean of 0. + `sklearn.decomposition.PCA` will be used for dimensionality + reduction. + normalize_variance + If True, normalize the data such that each gene has a variance of 1. + `sklearn.decomposition.TruncatedSVD` will be used for dimensionality + reduction, unless `mean_center` is True. + n_prin_comps + Number of principal components used to embed the transcriptomes prior + to k-nearest-neighbor graph construction. + use_approx_neighbors + Use approximate nearest neighbor method (annoy) for the KNN + classifier. + knn_dist_metric + Distance metric used when finding nearest neighbors. For list of + valid values, see the documentation for annoy (if `use_approx_neighbors` + is True) or sklearn.neighbors.NearestNeighbors (if `use_approx_neighbors` + is False). + get_doublet_neighbor_parents + If True, return the parent transcriptomes that generated the + doublet neighbors of each observed transcriptome. This information can + be used to infer the cell states that generated a given + doublet state. + threshold + Doublet score threshold for calling a transcriptome a doublet. If + `None`, this is set automatically by looking for the minimum between + the two modes of the `doublet_scores_sim_` histogram. It is best + practice to check the threshold visually using the + `doublet_scores_sim_` histogram and/or based on co-localization of + predicted doublets in a 2-D embedding. + random_state + Initial state for doublet simulation and nearest neighbors. + verbose + If :data:`True`, log progress updates. + + Returns + ------- + if ``copy=True`` it returns or else adds fields to ``adata``: + + ``.obs['doublet_score']`` + Doublet scores for each observed transcriptome + + ``.obs['predicted_doublets']`` + Boolean indicating predicted doublet status + + ``.uns['scrublet']['doublet_scores_sim']`` + Doublet scores for each simulated doublet transcriptome + + ``.uns['scrublet']['doublet_parents']`` + Pairs of ``.obs_names`` used to generate each simulated doublet transcriptome + + ``.uns['scrublet']['parameters']`` + Dictionary of Scrublet parameters + + """ + # Estimate n_neighbors if not provided, and create scrublet object. + + if n_neighbors is None: + n_neighbors = round(0.5 * np.sqrt(adata_obs.shape[0])) + + # Note: Scrublet() will sparse adata_obs.X if it's not already, but this + # matrix won't get used if we pre-set the normalised slots. + + scrub = Scrublet( + adata_obs.X, + n_neighbors=n_neighbors, + expected_doublet_rate=expected_doublet_rate, + stdev_doublet_rate=stdev_doublet_rate, + random_state=random_state, + ) + + # Ensure normalised matrix sparseness as Scrublet does + # https://github.com/swolock/scrublet/blob/67f8ecbad14e8e1aa9c89b43dac6638cebe38640/src/scrublet/scrublet.py#L100 + + scrub._counts_obs_norm = sparse.csc_matrix(adata_obs.X) # noqa: TID251 + scrub._counts_sim_norm = sparse.csc_matrix(adata_sim.X) # noqa: TID251 + + scrub.doublet_parents_ = adata_sim.obsm["doublet_parents"] + + # Call scrublet-specific preprocessing where specified + + if mean_center and normalize_variance: + pipeline.zscore(scrub) + elif mean_center: + pipeline.mean_center(scrub) + elif normalize_variance: + pipeline.normalize_variance(scrub) + + # Do PCA. Scrublet fits to the observed matrix and decomposes both observed + # and simulated based on that fit, so we'll just let it do its thing rather + # than trying to use Scanpy's PCA wrapper of the same functions. + + if mean_center: + logg.info("Embedding transcriptomes using PCA...") + pipeline.pca(scrub, n_prin_comps=n_prin_comps, random_state=scrub._random_state) + else: + logg.info("Embedding transcriptomes using Truncated SVD...") + pipeline.truncated_svd( + scrub, n_prin_comps=n_prin_comps, random_state=scrub._random_state + ) + + # Score the doublets + + scrub.calculate_doublet_scores( + use_approx_neighbors=use_approx_neighbors, + distance_metric=knn_dist_metric, + get_doublet_neighbor_parents=get_doublet_neighbor_parents, + ) + + # Actually call doublets + + scrub.call_doublets(threshold=threshold, verbose=verbose) + + # Store results in AnnData for return + + adata_obs.obs["doublet_score"] = scrub.doublet_scores_obs_ + + # Store doublet Scrublet metadata + + adata_obs.uns["scrublet"] = { + "doublet_scores_sim": scrub.doublet_scores_sim_, + "doublet_parents": adata_sim.obsm["doublet_parents"], + "parameters": { + "expected_doublet_rate": expected_doublet_rate, + "sim_doublet_ratio": ( + adata_sim.uns.get("scrublet", {}) + .get("parameters", {}) + .get("sim_doublet_ratio", None) + ), + "n_neighbors": n_neighbors, + "random_state": random_state, + }, + } + + # If threshold hasn't been located successfully then we couldn't make any + # predictions. The user will get a warning from Scrublet, but we need to + # set the boolean so that any downstream filtering on + # predicted_doublet=False doesn't incorrectly filter cells. The user can + # still use this object to generate the plot and derive a threshold + # manually. + + if hasattr(scrub, "threshold_"): + adata_obs.uns["scrublet"]["threshold"] = scrub.threshold_ + adata_obs.obs["predicted_doublet"] = scrub.predicted_doublets_ + else: + adata_obs.obs["predicted_doublet"] = False + + if get_doublet_neighbor_parents: + adata_obs.uns["scrublet"]["doublet_neighbor_parents"] = ( + scrub.doublet_neighbor_parents_ + ) + + return adata_obs + + +@old_positionals( + "layer", "sim_doublet_ratio", "synthetic_doublet_umi_subsampling", "random_seed" +) +def scrublet_simulate_doublets( + adata: AnnData, + *, + layer: str | None = None, + sim_doublet_ratio: float = 2.0, + synthetic_doublet_umi_subsampling: float = 1.0, + random_seed: _LegacyRandom = 0, +) -> AnnData: + """Simulate doublets by adding the counts of random observed transcriptome pairs. + + Parameters + ---------- + adata + The annotated data matrix of shape ``n_obs`` × ``n_vars``. Rows + correspond to cells and columns to genes. Genes should have been + filtered for expression and variability, and the object should contain + raw expression of the same dimensions. + layer + Layer of adata where raw values are stored, or 'X' if values are in .X. + sim_doublet_ratio + Number of doublets to simulate relative to the number of observed + transcriptomes. If `None`, self.sim_doublet_ratio is used. + synthetic_doublet_umi_subsampling + Rate for sampling UMIs when creating synthetic doublets. If 1.0, + each doublet is created by simply adding the UMIs from two randomly + sampled observed transcriptomes. For values less than 1, the + UMI counts are added and then randomly sampled at the specified + rate. + + Returns + ------- + adata : anndata.AnnData with simulated doublets in .X + Adds fields to ``adata``: + + ``.obsm['scrublet']['doublet_parents']`` + Pairs of ``.obs_names`` used to generate each simulated doublet transcriptome + + ``.uns['scrublet']['parameters']`` + Dictionary of Scrublet parameters + + See Also + -------- + :func:`~scanpy.pp.scrublet`: Main way of running Scrublet, runs + preprocessing, doublet simulation (this function) and calling. + :func:`~scanpy.pl.scrublet_score_distribution`: Plot histogram of doublet + scores for observed transcriptomes and simulated doublets. + + """ + x = _get_obs_rep(adata, layer=layer) + scrub = Scrublet(x, random_state=random_seed) + + scrub.simulate_doublets( + sim_doublet_ratio=sim_doublet_ratio, + synthetic_doublet_umi_subsampling=synthetic_doublet_umi_subsampling, + ) + + adata_sim = AnnData(scrub._counts_sim) + adata_sim.obs["n_counts"] = scrub._total_counts_sim + adata_sim.obsm["doublet_parents"] = scrub.doublet_parents_ + adata_sim.uns["scrublet"] = {"parameters": {"sim_doublet_ratio": sim_doublet_ratio}} + return adata_sim diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_scrublet/core.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_scrublet/core.py new file mode 100644 index 0000000000000000000000000000000000000000..a5cb80fd435ff53cb6637a0e23b97b3afca09043 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_scrublet/core.py @@ -0,0 +1,473 @@ +from __future__ import annotations + +from dataclasses import InitVar, dataclass, field +from typing import TYPE_CHECKING, cast + +import numpy as np +import pandas as pd +from anndata import AnnData, concat +from scipy import sparse +from sklearn.utils import check_random_state + +from ... import logging as logg +from ...neighbors import ( + Neighbors, + _get_indices_distances_from_sparse_matrix, +) +from .._utils import sample_comb +from .sparse_utils import subsample_counts + +if TYPE_CHECKING: + from numpy.random import RandomState + from numpy.typing import NDArray + + from ..._compat import CSBase, CSCBase + from ..._utils.random import _LegacyRandom + from ...neighbors import _Metric, _MetricFn + +__all__ = ["Scrublet"] + + +@dataclass(kw_only=True) +class Scrublet: + """Initialize Scrublet object with counts matrix and doublet prediction parameters. + + Parameters + ---------- + counts_obs + Matrix with shape (n_cells, n_genes) containing raw (unnormalized) + UMI-based transcript counts. + Converted into a :class:`scipy.sparse.csc_matrix`. + + total_counts_obs + Array with shape (n_cells,) of total UMI counts per cell. + If `None`, this is calculated as the row sums of `counts_obs`. + + sim_doublet_ratio + Number of doublets to simulate relative to the number of observed + transcriptomes. + + n_neighbors + Number of neighbors used to construct the KNN graph of observed + transcriptomes and simulated doublets. + If `None`, this is set to round(0.5 * sqrt(n_cells)) + + expected_doublet_rate + The estimated doublet rate for the experiment. + + stdev_doublet_rate + Uncertainty in the expected doublet rate. + + random_state + Random state for doublet simulation, approximate + nearest neighbor search, and PCA/TruncatedSVD. + + """ + + # init fields + + counts_obs: InitVar[CSBase | NDArray[np.integer]] = field(kw_only=False) + total_counts_obs: InitVar[NDArray[np.integer] | None] = None + sim_doublet_ratio: float = 2.0 + n_neighbors: InitVar[int | None] = None + expected_doublet_rate: float = 0.1 + stdev_doublet_rate: float = 0.02 + random_state: InitVar[_LegacyRandom] = 0 + + # private fields + + _n_neighbors: int = field(init=False, repr=False) + _random_state: RandomState = field(init=False, repr=False) + + _counts_obs: CSCBase = field(init=False, repr=False) + _total_counts_obs: NDArray[np.integer] = field(init=False, repr=False) + _counts_obs_norm: CSBase = field(init=False, repr=False) + + _counts_sim: CSBase = field(init=False, repr=False) + _total_counts_sim: NDArray[np.integer] = field(init=False, repr=False) + _counts_sim_norm: CSBase | None = field(default=None, init=False, repr=False) + + # Fields set by methods + + predicted_doublets_: NDArray[np.bool_] | None = field(init=False) + """(shape: n_cells) + Boolean mask of predicted doublets in the observed transcriptomes. + """ + + doublet_scores_obs_: NDArray[np.float64] = field(init=False) + """(shape: n_cells) + Doublet scores for observed transcriptomes. + """ + + doublet_scores_sim_: NDArray[np.float64] = field(init=False) + """(shape: n_doublets) + Doublet scores for simulated doublets. + """ + + doublet_errors_obs_: NDArray[np.float64] = field(init=False) + """(shape: n_cells) + Standard error in the doublet scores for observed transcriptomes. + """ + + doublet_errors_sim_: NDArray[np.float64] = field(init=False) + """(shape: n_doublets) + Standard error in the doublet scores for simulated doublets. + """ + + threshold_: float = field(init=False) + """Doublet score threshold for calling a transcriptome a doublet.""" + + z_scores_: NDArray[np.float64] = field(init=False) + """(shape: n_cells) + Z-score conveying confidence in doublet calls. + Z = `(doublet_score_obs_ - threhsold_) / doublet_errors_obs_` + """ + + detected_doublet_rate_: float = field(init=False) + """Fraction of observed transcriptomes that have been called doublets.""" + + detectable_doublet_fraction_: float = field(init=False) + """Estimated fraction of doublets that are detectable, i.e., + fraction of simulated doublets with doublet scores above `threshold_` + """ + + overall_doublet_rate_: float = field(init=False) + """Estimated overall doublet rate, + `detected_doublet_rate_ / detectable_doublet_fraction_`. + Should agree (roughly) with `expected_doublet_rate`. + """ + + manifold_obs_: NDArray[np.float64] = field(init=False) + """(shape: n_cells × n_features) + The single-cell "manifold" coordinates (e.g., PCA coordinates) + for observed transcriptomes. Nearest neighbors are found using + the union of `manifold_obs_` and `manifold_sim_` (see below). + """ + + manifold_sim_: NDArray[np.float64] = field(init=False) + """shape (n_doublets × n_features) + The single-cell "manifold" coordinates (e.g., PCA coordinates) + for simulated doublets. Nearest neighbors are found using + the union of `manifold_obs_` (see above) and `manifold_sim_`. + """ + + doublet_parents_: NDArray[np.intp] = field(init=False) + """(shape: n_doublets × 2) + Indices of the observed transcriptomes used to generate the + simulated doublets. + """ + + doublet_neighbor_parents_: list[NDArray[np.intp]] = field(init=False) + """(length: n_cells) + A list of arrays of the indices of the doublet neighbors of + each observed transcriptome (the ith entry is an array of + the doublet neighbors of transcriptome i). + """ + + def __post_init__( + self, + counts_obs: CSBase | NDArray[np.integer], + total_counts_obs: NDArray[np.integer] | None, + n_neighbors: int | None, + random_state: _LegacyRandom, + ) -> None: + self._counts_obs = sparse.csc_matrix(counts_obs) # noqa: TID251 + self._total_counts_obs = ( + np.asarray(self._counts_obs.sum(1)).squeeze() + if total_counts_obs is None + else total_counts_obs + ) + self._n_neighbors = ( + round(0.5 * np.sqrt(self._counts_obs.shape[0])) + if n_neighbors is None + else n_neighbors + ) + self._random_state = check_random_state(random_state) + + def simulate_doublets( + self, + *, + sim_doublet_ratio: float | None = None, + synthetic_doublet_umi_subsampling: float = 1.0, + ) -> None: + """Simulate doublets by adding the counts of random observed transcriptome pairs. + + Parameters + ---------- + sim_doublet_ratio + Number of doublets to simulate relative to the number of observed + transcriptomes. If `None`, self.sim_doublet_ratio is used. + + synthetic_doublet_umi_subsampling + Rate for sampling UMIs when creating synthetic doublets. + If 1.0, each doublet is created by simply adding the UMIs from two randomly + sampled observed transcriptomes. + For values less than 1, the UMI counts are added and then randomly sampled + at the specified rate. + + Sets + ---- + doublet_parents_ + + """ + if sim_doublet_ratio is None: + sim_doublet_ratio = self.sim_doublet_ratio + else: + self.sim_doublet_ratio = sim_doublet_ratio + + n_obs = self._counts_obs.shape[0] + n_sim = int(n_obs * sim_doublet_ratio) + + pair_ix = sample_comb((n_obs, n_obs), n_sim, random_state=self._random_state) + + e1 = cast("CSCBase", self._counts_obs[pair_ix[:, 0], :]) + e2 = cast("CSCBase", self._counts_obs[pair_ix[:, 1], :]) + tots1 = self._total_counts_obs[pair_ix[:, 0]] + tots2 = self._total_counts_obs[pair_ix[:, 1]] + if synthetic_doublet_umi_subsampling < 1: + self._counts_sim, self._total_counts_sim = subsample_counts( + e1 + e2, + rate=synthetic_doublet_umi_subsampling, + original_totals=tots1 + tots2, + random_seed=self._random_state, + ) + else: + self._counts_sim = e1 + e2 + self._total_counts_sim = tots1 + tots2 + self.doublet_parents_ = pair_ix + + def set_manifold( + self, manifold_obs: NDArray[np.float64], manifold_sim: NDArray[np.float64] + ) -> None: + """Set the manifold coordinates used in k-nearest-neighbor graph construction. + + Parameters + ---------- + manifold_obs + (shape: n_cells × n_features) + The single-cell "manifold" coordinates (e.g., PCA coordinates) + for observed transcriptomes. Nearest neighbors are found using + the union of `manifold_obs` and `manifold_sim` (see below). + + manifold_sim + (shape: n_doublets × n_features) + The single-cell "manifold" coordinates (e.g., PCA coordinates) + for simulated doublets. Nearest neighbors are found using + the union of `manifold_obs` (see above) and `manifold_sim`. + + Sets + ---- + manifold_obs_, manifold_sim_, + + """ + self.manifold_obs_ = manifold_obs + self.manifold_sim_ = manifold_sim + + def calculate_doublet_scores( + self, + *, + use_approx_neighbors: bool | None = None, + distance_metric: _Metric | _MetricFn = "euclidean", + get_doublet_neighbor_parents: bool = False, + ) -> NDArray[np.float64]: + """Calculate doublet scores for observed transcriptomes and simulated doublets. + + Requires that manifold_obs_ and manifold_sim_ have already been set. + + Parameters + ---------- + use_approx_neighbors + Use approximate nearest neighbor method (annoy) for the KNN + classifier. + + distance_metric + Distance metric used when finding nearest neighbors. For list of + valid values, see the documentation for annoy (if `use_approx_neighbors` + is True) or sklearn.neighbors.NearestNeighbors (if `use_approx_neighbors` + is False). + + get_doublet_neighbor_parents + If True, return the parent transcriptomes that generated the + doublet neighbors of each observed transcriptome. This information can + be used to infer the cell states that generated a given + doublet state. + + Sets + ---- + doublet_scores_obs_, doublet_scores_sim_, + doublet_errors_obs_, doublet_errors_sim_, + doublet_neighbor_parents_ + + """ + self._nearest_neighbor_classifier( + k=self._n_neighbors, + exp_doub_rate=self.expected_doublet_rate, + stdev_doub_rate=self.stdev_doublet_rate, + use_approx_neighbors=use_approx_neighbors, + distance_metric=distance_metric, + get_neighbor_parents=get_doublet_neighbor_parents, + ) + return self.doublet_scores_obs_ + + def _nearest_neighbor_classifier( + self, + k: int = 40, + *, + use_approx_neighbors: bool | None = None, + distance_metric: _Metric | _MetricFn = "euclidean", + exp_doub_rate: float = 0.1, + stdev_doub_rate: float = 0.03, + get_neighbor_parents: bool = False, + ) -> None: + adatas = [ + AnnData( + (arr := getattr(self, f"manifold_{n}_")), + obs=dict( + obs_names=pd.RangeIndex(arr.shape[0]).astype("string") + n, + doub_labels=n, + ), + ) + for n in ["obs", "sim"] + ] + manifold = concat(adatas) + + n_obs: int = (manifold.obs["doub_labels"] == "obs").sum() + n_sim: int = (manifold.obs["doub_labels"] == "sim").sum() + + # Adjust k (number of nearest neighbors) based on the ratio of simulated to observed cells + k_adj = round(k * (1 + n_sim / float(n_obs))) + + # Find k_adj nearest neighbors + knn = Neighbors(manifold) + transformer = None + if use_approx_neighbors is not None: + transformer = "pynndescent" if use_approx_neighbors else "sklearn" + knn.compute_neighbors( + k_adj, + metric=distance_metric, + knn=True, + transformer=transformer, + method=None, + random_state=self._random_state, + ) + neighbors, _ = _get_indices_distances_from_sparse_matrix(knn.distances, k_adj) + if use_approx_neighbors: + neighbors = neighbors[:, 1:] + # Calculate doublet score based on ratio of simulated cell neighbors vs. observed cell neighbors + doub_neigh_mask: NDArray[np.bool_] = ( + manifold.obs["doub_labels"].to_numpy()[neighbors] == "sim" + ) + n_sim_neigh: NDArray[np.int64] = doub_neigh_mask.sum(axis=1) + + rho = exp_doub_rate + r = n_sim / float(n_obs) + nd = n_sim_neigh.astype(np.float64) + n = float(k_adj) + + # Bayesian + q = (nd + 1) / (n + 2) + ld = q * rho / r / (1 - rho - q * (1 - rho - rho / r)) + + se_q = np.sqrt(q * (1 - q) / (n + 3)) + se_rho = stdev_doub_rate + + se_ld = ( + q + * rho + / r + / (1 - rho - q * (1 - rho - rho / r)) ** 2 + * np.sqrt((se_q / q * (1 - rho)) ** 2 + (se_rho / rho * (1 - q)) ** 2) + ) + + self.doublet_scores_obs_ = ld[manifold.obs["doub_labels"] == "obs"] + self.doublet_scores_sim_ = ld[manifold.obs["doub_labels"] == "sim"] + self.doublet_errors_obs_ = se_ld[manifold.obs["doub_labels"] == "obs"] + self.doublet_errors_sim_ = se_ld[manifold.obs["doub_labels"] == "sim"] + + # get parents of doublet neighbors, if requested + neighbor_parents = None + if get_neighbor_parents: + parent_cells = self.doublet_parents_ + neighbors = neighbors - n_obs + neighbor_parents = [] + for c in range(n_obs): + this_doub_neigh = neighbors[c, :][neighbors[c, :] > -1] + if len(this_doub_neigh) > 0: + this_doub_neigh_parents = np.unique( + parent_cells[this_doub_neigh, :].flatten() + ) + neighbor_parents.append(this_doub_neigh_parents) + else: + neighbor_parents.append(np.array([], dtype=np.intp)) + self.doublet_neighbor_parents_ = neighbor_parents + + def call_doublets( + self, *, threshold: float | None = None, verbose: bool = True + ) -> NDArray[np.bool_] | None: + """Call trancriptomes as doublets or singlets. + + Parameters + ---------- + threshold + Doublet score threshold for calling a transcriptome + a doublet. If `None`, this is set automatically by looking + for the minimum between the two modes of the `doublet_scores_sim_` + histogram. It is best practice to check the threshold visually + using the `doublet_scores_sim_` histogram and/or based on + co-localization of predicted doublets in a 2-D embedding. + + verbose + If True, log summary statistics. + + Sets + ---- + predicted_doublets_, z_scores_, threshold_, + detected_doublet_rate_, detectable_doublet_fraction, + overall_doublet_rate_ + + """ + if threshold is None: + # automatic threshold detection + # http://scikit-image.org/docs/dev/api/skimage.filters.html + from skimage.filters import threshold_minimum + + try: + threshold = cast("float", threshold_minimum(self.doublet_scores_sim_)) + if verbose: + logg.info( + f"Automatically set threshold at doublet score = {threshold:.2f}" + ) + except Exception: # noqa: BLE001 + self.predicted_doublets_ = None + if verbose: + logg.warning( + "Failed to automatically identify doublet score threshold. " + "Run `call_doublets` with user-specified threshold." + ) + return self.predicted_doublets_ + + ld_obs = self.doublet_scores_obs_ + ld_sim = self.doublet_scores_sim_ + se_obs = self.doublet_errors_obs_ + z = (ld_obs - threshold) / se_obs + self.predicted_doublets_ = ld_obs > threshold + self.z_scores_ = z + self.threshold_ = threshold + self.detected_doublet_rate_ = (ld_obs > threshold).sum() / float(len(ld_obs)) + self.detectable_doublet_fraction_ = (ld_sim > threshold).sum() / float( + len(ld_sim) + ) + self.overall_doublet_rate_ = ( + self.detected_doublet_rate_ / self.detectable_doublet_fraction_ + ) + + if verbose: + logg.info( + f"Detected doublet rate = {100 * self.detected_doublet_rate_:.1f}%\n" + f"Estimated detectable doublet fraction = {100 * self.detectable_doublet_fraction_:.1f}%\n" + "Overall doublet rate:\n" + f"\tExpected = {100 * self.expected_doublet_rate:.1f}%\n" + f"\tEstimated = {100 * self.overall_doublet_rate_:.1f}%" + ) + + return self.predicted_doublets_ diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_scrublet/pipeline.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_scrublet/pipeline.py new file mode 100644 index 0000000000000000000000000000000000000000..091b942d5bf9b719bde10fff5053df62c16efe8c --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_scrublet/pipeline.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +from scipy import sparse + +from scanpy.preprocessing._utils import _get_mean_var + +from .sparse_utils import sparse_multiply, sparse_zscore + +if TYPE_CHECKING: + from typing import Literal + + from ..._utils.random import _LegacyRandom + from .core import Scrublet + + +def mean_center(self: Scrublet) -> None: + gene_means = self._counts_obs_norm.mean(0) + self._counts_obs_norm = sparse.csc_matrix(self._counts_obs_norm - gene_means) # noqa: TID251 + if self._counts_sim_norm is not None: + self._counts_sim_norm = sparse.csc_matrix(self._counts_sim_norm - gene_means) # noqa: TID251 + + +def normalize_variance(self: Scrublet) -> None: + _, gene_vars = _get_mean_var(self._counts_obs_norm, axis=0) + gene_stdevs = np.sqrt(gene_vars) + self._counts_obs_norm = sparse_multiply(self._counts_obs_norm.T, 1 / gene_stdevs).T + if self._counts_sim_norm is not None: + self._counts_sim_norm = sparse_multiply( + self._counts_sim_norm.T, 1 / gene_stdevs + ).T + + +def zscore(self: Scrublet) -> None: + gene_means, gene_vars = _get_mean_var(self._counts_obs_norm, axis=0) + gene_stdevs = np.sqrt(gene_vars) + self._counts_obs_norm = sparse_zscore( + self._counts_obs_norm, gene_mean=gene_means, gene_stdev=gene_stdevs + ) + if self._counts_sim_norm is not None: + self._counts_sim_norm = sparse_zscore( + self._counts_sim_norm, gene_mean=gene_means, gene_stdev=gene_stdevs + ) + + +def truncated_svd( + self: Scrublet, + n_prin_comps: int = 30, + *, + random_state: _LegacyRandom = 0, + algorithm: Literal["arpack", "randomized"] = "arpack", +) -> None: + if self._counts_sim_norm is None: + msg = "_counts_sim_norm is not set" + raise RuntimeError(msg) + from sklearn.decomposition import TruncatedSVD + + svd = TruncatedSVD( + n_components=n_prin_comps, random_state=random_state, algorithm=algorithm + ).fit(self._counts_obs_norm) + self.set_manifold( + svd.transform(self._counts_obs_norm), svd.transform(self._counts_sim_norm) + ) + + +def pca( + self: Scrublet, + n_prin_comps: int = 50, + *, + random_state: _LegacyRandom = 0, + svd_solver: Literal["auto", "full", "arpack", "randomized"] = "arpack", +) -> None: + if self._counts_sim_norm is None: + msg = "_counts_sim_norm is not set" + raise RuntimeError(msg) + from sklearn.decomposition import PCA + + x_obs = self._counts_obs_norm.toarray() + x_sim = self._counts_sim_norm.toarray() + + pca = PCA( + n_components=n_prin_comps, random_state=random_state, svd_solver=svd_solver + ).fit(x_obs) + self.set_manifold(pca.transform(x_obs), pca.transform(x_sim)) diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_scrublet/sparse_utils.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_scrublet/sparse_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..2218c9137d7fedb62853e5bd15655b5022f542f8 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_scrublet/sparse_utils.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +from scipy import sparse +from sklearn.utils import check_random_state + +from scanpy.preprocessing._utils import _get_mean_var + +if TYPE_CHECKING: + from numpy.typing import NDArray + + from ..._compat import CSBase + from ..._utils.random import _LegacyRandom + + +def sparse_multiply( + e: CSBase | NDArray[np.float64], + a: float | NDArray[np.float64], + /, +) -> CSBase: + """Multiply each row of E by a scalar.""" + nrow = e.shape[0] + w = sparse.dia_matrix((a, 0), shape=(nrow, nrow), dtype=a.dtype) + r = w @ e + if isinstance(r, np.ndarray): + return sparse.csc_matrix(r) # noqa: TID251 + return r + + +def sparse_zscore( + e: CSBase, + /, + *, + gene_mean: NDArray[np.float64] | None = None, + gene_stdev: NDArray[np.float64] | None = None, +) -> CSBase: + """z-score normalize each column of E.""" + if gene_mean is None or gene_stdev is None: + _gene_means, gene_stdevs = _get_mean_var(e, axis=0) + gene_stdevs = np.sqrt(gene_stdevs) + return sparse_multiply(np.asarray((e - gene_mean).T), 1 / gene_stdev).T + + +def subsample_counts( + e: CSBase, + /, + *, + rate: float, + original_totals, + random_seed: _LegacyRandom = 0, +) -> tuple[CSBase, NDArray[np.int64]]: + if rate < 1: + random_seed = check_random_state(random_seed) + e.data = random_seed.binomial(np.round(e.data).astype(int), rate) + current_totals = np.asarray(e.sum(1)).squeeze() + unsampled_orig_totals = original_totals - current_totals + unsampled_downsamp_totals = np.random.binomial( + np.round(unsampled_orig_totals).astype(int), rate + ) + final_downsamp_totals = current_totals + unsampled_downsamp_totals + else: + final_downsamp_totals = original_totals + return e, final_downsamp_totals diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_simple.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_simple.py new file mode 100644 index 0000000000000000000000000000000000000000..2ca1018056a1798e0dc26b47d0b4f6a7bf850139 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_simple.py @@ -0,0 +1,1182 @@ +"""Simple Preprocessing Functions. + +Compositions of these functions are found in sc.preprocess.recipes. +""" + +from __future__ import annotations + +import warnings +from functools import singledispatch +from itertools import repeat +from typing import TYPE_CHECKING, TypeVar, overload + +import numba +import numpy as np +from anndata import AnnData +from pandas.api.types import CategoricalDtype +from scipy import sparse +from sklearn.utils import check_array, sparsefuncs + +from .. import logging as logg +from .._compat import ( + CSBase, + CSRBase, + DaskArray, + _register_union, + deprecated, + njit, + old_positionals, +) +from .._settings import settings as sett +from .._utils import ( + _check_array_function_arguments, + _resolve_axis, + axis_sum, + is_backed_type, + raise_not_implemented_error_if_backed_type, + renamed_arg, + sanitize_anndata, + view_to_actual, +) +from ..get import _check_mask, _get_obs_rep, _set_obs_rep +from ._distributed import materialize_as_ndarray +from ._utils import _to_dense + +try: + import dask.array as da +except ImportError: + da = None + +if TYPE_CHECKING: + from collections.abc import Collection, Iterable, Sequence + from numbers import Number + from typing import Literal + + import pandas as pd + from numpy.typing import NDArray + + from .._utils.random import RNGLike, SeedLike, _LegacyRandom + + +A = TypeVar("A", bound=np.ndarray | CSBase | DaskArray) + + +@old_positionals( + "min_counts", "min_genes", "max_counts", "max_genes", "inplace", "copy" +) +def filter_cells( + data: AnnData | CSBase | np.ndarray | DaskArray, + *, + min_counts: int | None = None, + min_genes: int | None = None, + max_counts: int | None = None, + max_genes: int | None = None, + inplace: bool = True, + copy: bool = False, +) -> AnnData | tuple[np.ndarray, np.ndarray] | None: + """Filter cell outliers based on counts and numbers of genes expressed. + + For instance, only keep cells with at least `min_counts` counts or + `min_genes` genes expressed. This is to filter measurement outliers, + i.e. “unreliable” observations. + + Only provide one of the optional parameters `min_counts`, `min_genes`, + `max_counts`, `max_genes` per call. + + Parameters + ---------- + data + The (annotated) data matrix of shape `n_obs` × `n_vars`. + Rows correspond to cells and columns to genes. + min_counts + Minimum number of counts required for a cell to pass filtering. + min_genes + Minimum number of genes expressed required for a cell to pass filtering. + max_counts + Maximum number of counts required for a cell to pass filtering. + max_genes + Maximum number of genes expressed required for a cell to pass filtering. + inplace + Perform computation inplace or return result. + + Returns + ------- + Depending on `inplace`, returns the following arrays or directly subsets + and annotates the data matrix: + + cells_subset + Boolean index mask that does filtering. `True` means that the + cell is kept. `False` means the cell is removed. + number_per_cell + Depending on what was thresholded (`counts` or `genes`), + the array stores `n_counts` or `n_cells` per gene. + + Examples + -------- + >>> import scanpy as sc + >>> adata = sc.datasets.krumsiek11() + UserWarning: Observation names are not unique. To make them unique, call `.obs_names_make_unique`. + utils.warn_names_duplicates("obs") + >>> adata.obs_names_make_unique() + >>> adata.n_obs + 640 + >>> adata.var_names.tolist() # doctest: +NORMALIZE_WHITESPACE + ['Gata2', 'Gata1', 'Fog1', 'EKLF', 'Fli1', 'SCL', + 'Cebpa', 'Pu.1', 'cJun', 'EgrNab', 'Gfi1'] + >>> # add some true zeros + >>> adata.X[adata.X < 0.3] = 0 + >>> # simply compute the number of genes per cell + >>> sc.pp.filter_cells(adata, min_genes=0) + >>> adata.n_obs + 640 + >>> int(adata.obs["n_genes"].min()) + 1 + >>> # filter manually + >>> adata_copy = adata[adata.obs["n_genes"] >= 3] + >>> adata_copy.n_obs + 554 + >>> int(adata_copy.obs["n_genes"].min()) + 3 + >>> # actually do some filtering + >>> sc.pp.filter_cells(adata, min_genes=3) + >>> adata.n_obs + 554 + >>> int(adata.obs["n_genes"].min()) + 3 + + """ + if copy: + logg.warning("`copy` is deprecated, use `inplace` instead.") + n_given_options = sum( + option is not None for option in [min_genes, min_counts, max_genes, max_counts] + ) + if n_given_options != 1: + msg = ( + "Only provide one of the optional parameters `min_counts`, " + "`min_genes`, `max_counts`, `max_genes` per call." + ) + raise ValueError(msg) + if isinstance(data, AnnData): + raise_not_implemented_error_if_backed_type(data.X, "filter_cells") + adata = data.copy() if copy else data + cell_subset, number = materialize_as_ndarray( + filter_cells( + adata.X, + min_counts=min_counts, + min_genes=min_genes, + max_counts=max_counts, + max_genes=max_genes, + ), + ) + if not inplace: + return cell_subset, number + if min_genes is None and max_genes is None: + adata.obs["n_counts"] = number + else: + adata.obs["n_genes"] = number + adata._inplace_subset_obs(cell_subset) + return adata if copy else None + + min_number = min_counts if min_genes is None else min_genes + max_number = max_counts if max_genes is None else max_genes + number_per_cell = axis_sum( + data if min_genes is None and max_genes is None else data > 0, axis=1 + ) + if isinstance(number_per_cell, np.matrix): + number_per_cell = number_per_cell.A1 + if min_number is not None: + cell_subset = number_per_cell >= min_number + if max_number is not None: + cell_subset = number_per_cell <= max_number + + s = axis_sum(~cell_subset) + if s > 0: + msg = f"filtered out {s} cells that have " + if min_genes is not None or min_counts is not None: + msg += "less than " + msg += ( + f"{min_genes} genes expressed" + if min_counts is None + else f"{min_counts} counts" + ) + if max_genes is not None or max_counts is not None: + msg += "more than " + msg += ( + f"{max_genes} genes expressed" + if max_counts is None + else f"{max_counts} counts" + ) + logg.info(msg) + return cell_subset, number_per_cell + + +@old_positionals( + "min_counts", "min_cells", "max_counts", "max_cells", "inplace", "copy" +) +def filter_genes( + data: AnnData | CSBase | np.ndarray | DaskArray, + *, + min_counts: int | None = None, + min_cells: int | None = None, + max_counts: int | None = None, + max_cells: int | None = None, + inplace: bool = True, + copy: bool = False, +) -> AnnData | tuple[np.ndarray, np.ndarray] | None: + """Filter genes based on number of cells or counts. + + Keep genes that have at least `min_counts` counts or are expressed in at + least `min_cells` cells or have at most `max_counts` counts or are expressed + in at most `max_cells` cells. + + Only provide one of the optional parameters `min_counts`, `min_cells`, + `max_counts`, `max_cells` per call. + + Parameters + ---------- + data + An annotated data matrix of shape `n_obs` × `n_vars`. Rows correspond + to cells and columns to genes. + min_counts + Minimum number of counts required for a gene to pass filtering. + min_cells + Minimum number of cells expressed required for a gene to pass filtering. + max_counts + Maximum number of counts required for a gene to pass filtering. + max_cells + Maximum number of cells expressed required for a gene to pass filtering. + inplace + Perform computation inplace or return result. + + Returns + ------- + Depending on `inplace`, returns the following arrays or directly subsets + and annotates the data matrix + + gene_subset + Boolean index mask that does filtering. `True` means that the + gene is kept. `False` means the gene is removed. + number_per_gene + Depending on what was thresholded (`counts` or `cells`), the array stores + `n_counts` or `n_cells` per gene. + + """ + if copy: + logg.warning("`copy` is deprecated, use `inplace` instead.") + n_given_options = sum( + option is not None for option in [min_cells, min_counts, max_cells, max_counts] + ) + if n_given_options != 1: + msg = ( + "Only provide one of the optional parameters `min_counts`, " + "`min_cells`, `max_counts`, `max_cells` per call." + ) + raise ValueError(msg) + + if isinstance(data, AnnData): + raise_not_implemented_error_if_backed_type(data.X, "filter_genes") + adata = data.copy() if copy else data + gene_subset, number = materialize_as_ndarray( + filter_genes( + adata.X, + min_cells=min_cells, + min_counts=min_counts, + max_cells=max_cells, + max_counts=max_counts, + ) + ) + if not inplace: + return gene_subset, number + if min_cells is None and max_cells is None: + adata.var["n_counts"] = number + else: + adata.var["n_cells"] = number + adata._inplace_subset_var(gene_subset) + return adata if copy else None + + min_number = min_counts if min_cells is None else min_cells + max_number = max_counts if max_cells is None else max_cells + number_per_gene = axis_sum( + data if min_cells is None and max_cells is None else data > 0, axis=0 + ) + if isinstance(number_per_gene, np.matrix): + number_per_gene = number_per_gene.A1 + if min_number is not None: + gene_subset = number_per_gene >= min_number + if max_number is not None: + gene_subset = number_per_gene <= max_number + + s = axis_sum(~gene_subset) + if s > 0: + msg = f"filtered out {s} genes that are detected " + if min_cells is not None or min_counts is not None: + msg += "in less than " + msg += ( + f"{min_cells} cells" if min_counts is None else f"{min_counts} counts" + ) + if max_cells is not None or max_counts is not None: + msg += "in more than " + msg += ( + f"{max_cells} cells" if max_counts is None else f"{max_counts} counts" + ) + logg.info(msg) + return gene_subset, number_per_gene + + +@renamed_arg("X", "data", pos_0=True) +@singledispatch +def log1p( + data: AnnData | np.ndarray | CSBase, + *, + base: Number | None = None, + copy: bool = False, + chunked: bool | None = None, + chunk_size: int | None = None, + layer: str | None = None, + obsm: str | None = None, +) -> AnnData | np.ndarray | CSBase | None: + r"""Logarithmize the data matrix. + + Computes :math:`X = \log(X + 1)`, + where :math:`log` denotes the natural logarithm unless a different base is given. + + Parameters + ---------- + data + The (annotated) data matrix of shape `n_obs` × `n_vars`. + Rows correspond to cells and columns to genes. + base + Base of the logarithm. Natural logarithm is used by default. + copy + If an :class:`~anndata.AnnData` is passed, determines whether a copy + is returned. + chunked + Process the data matrix in chunks, which will save memory. + Applies only to :class:`~anndata.AnnData`. + chunk_size + `n_obs` of the chunks to process the data in. + layer + Entry of layers to transform. + obsm + Entry of obsm to transform. + + Returns + ------- + Returns or updates `data`, depending on `copy`. + + """ + _check_array_function_arguments( + chunked=chunked, chunk_size=chunk_size, layer=layer, obsm=obsm + ) + return log1p_array(data, copy=copy, base=base) + + +@_register_union(log1p, CSBase) +def log1p_sparse(x: CSBase, *, base: Number | None = None, copy: bool = False): + x = check_array( + x, accept_sparse=("csr", "csc"), dtype=(np.float64, np.float32), copy=copy + ) + x.data = log1p(x.data, copy=False, base=base) + return x + + +@log1p.register(np.ndarray) +def log1p_array(x: np.ndarray, *, base: Number | None = None, copy: bool = False): + # Can force arrays to be np.ndarrays, but would be useful to not + # X = check_array(X, dtype=(np.float64, np.float32), ensure_2d=False, copy=copy) + if copy: + x = x.astype(float) if not np.issubdtype(x.dtype, np.floating) else x.copy() + elif not (np.issubdtype(x.dtype, np.floating) or np.issubdtype(x.dtype, complex)): + x = x.astype(float) + np.log1p(x, out=x) + if base is not None: + np.divide(x, np.log(base), out=x) + return x + + +@log1p.register(AnnData) +def log1p_anndata( + adata: AnnData, + *, + base: Number | None = None, + copy: bool = False, + chunked: bool = False, + chunk_size: int | None = None, + layer: str | None = None, + obsm: str | None = None, +) -> AnnData | None: + if "log1p" in adata.uns: + logg.warning("adata.X seems to be already log-transformed.") + + adata = adata.copy() if copy else adata + view_to_actual(adata) + + if chunked: + if (layer is not None) or (obsm is not None): + msg = ( + "Currently cannot perform chunked operations on arrays not stored in X." + ) + raise NotImplementedError(msg) + if adata.isbacked and adata.file._filemode != "r+": + msg = "log1p is not implemented for backed AnnData with backed mode not r+" + raise NotImplementedError(msg) + for chunk, start, end in adata.chunked_X(chunk_size): + adata.X[start:end] = log1p(chunk, base=base, copy=False) + else: + x = _get_obs_rep(adata, layer=layer, obsm=obsm) + if is_backed_type(x): + msg = f"log1p is not implemented for matrices of type {type(x)}" + if layer is not None: + msg = f"{msg} from layers" + raise NotImplementedError(msg) + msg = f"{msg} without `chunked=True`" + raise NotImplementedError(msg) + x = log1p(x, copy=False, base=base) + _set_obs_rep(adata, x, layer=layer, obsm=obsm) + + adata.uns["log1p"] = {"base": base} + if copy: + return adata + + +@old_positionals("copy", "chunked", "chunk_size") +def sqrt( + data: AnnData | CSBase | np.ndarray, + *, + copy: bool = False, + chunked: bool = False, + chunk_size: int | None = None, +) -> AnnData | CSBase | np.ndarray | None: + r"""Take square root of the data matrix. + + Computes :math:`X = \sqrt(X)`. + + Parameters + ---------- + data + The (annotated) data matrix of shape `n_obs` × `n_vars`. + Rows correspond to cells and columns to genes. + copy + If an :class:`~anndata.AnnData` object is passed, + determines whether a copy is returned. + chunked + Process the data matrix in chunks, which will save memory. + Applies only to :class:`~anndata.AnnData`. + chunk_size + `n_obs` of the chunks to process the data in. + + Returns + ------- + Returns or updates `data`, depending on `copy`. + + """ + if isinstance(data, AnnData): + adata = data.copy() if copy else data + if chunked: + for chunk, start, end in adata.chunked_X(chunk_size): + adata.X[start:end] = sqrt(chunk) + else: + adata.X = sqrt(data.X) + return adata if copy else None + x = data # proceed with data matrix + return x.sqrt() if isinstance(x, CSBase) else np.sqrt(x) + + +@deprecated("Use sc.pp.normalize_total instead") +@old_positionals( + "counts_per_cell_after", + "counts_per_cell", + "key_n_counts", + "copy", + "layers", + "use_rep", + "min_counts", +) +def normalize_per_cell( + data: AnnData | np.ndarray | CSBase, + *, + counts_per_cell_after: float | None = None, + counts_per_cell: np.ndarray | None = None, + key_n_counts: str = "n_counts", + copy: bool = False, + layers: Literal["all"] | Iterable[str] = (), + use_rep: Literal["after", "X"] | None = None, + min_counts: int = 1, +) -> AnnData | np.ndarray | CSBase | None: + """Normalize total counts per cell. + + .. deprecated:: 1.3.7 + + Use :func:`~scanpy.pp.normalize_total` instead. + The new function is equivalent to the present + function, except that + + * the new function doesn't filter cells based on `min_counts`, + use :func:`~scanpy.pp.filter_cells` if filtering is needed. + * some arguments were renamed + * `copy` is replaced by `inplace` + + Normalize each cell by total counts over all genes, so that every cell has + the same total count after normalization. + + Similar functions are used, for example, by Seurat :cite:p:`Satija2015`, Cell Ranger + :cite:p:`Zheng2017` or SPRING :cite:p:`Weinreb2017`. + + Parameters + ---------- + data + The (annotated) data matrix of shape `n_obs` × `n_vars`. Rows correspond + to cells and columns to genes. + counts_per_cell_after + If `None`, after normalization, each cell has a total count equal + to the median of the *counts_per_cell* before normalization. + counts_per_cell + Precomputed counts per cell. + key_n_counts + Name of the field in `adata.obs` where the total counts per cell are + stored. + copy + If an :class:`~anndata.AnnData` is passed, determines whether a copy + is returned. + min_counts + Cells with counts less than `min_counts` are filtered out during + normalization. + + Returns + ------- + Returns `None` if `copy=False`, else returns an updated `AnnData` object. Sets the following fields: + + `adata.X` : :class:`numpy.ndarray` | :class:`scipy.sparse.csr_matrix` (dtype `float`) + Normalized count data matrix. + + Examples + -------- + >>> import scanpy as sc + >>> adata = AnnData(np.array([[1, 0], [3, 0], [5, 6]], dtype=np.float32)) + >>> print(adata.X.sum(axis=1)) + [ 1. 3. 11.] + >>> sc.pp.normalize_per_cell(adata) + >>> print(adata.obs) + n_counts + 0 1.0 + 1 3.0 + 2 11.0 + >>> print(adata.X.sum(axis=1)) + [3. 3. 3.] + >>> sc.pp.normalize_per_cell( + ... adata, + ... counts_per_cell_after=1, + ... key_n_counts="n_counts2", + ... ) + >>> print(adata.obs) + n_counts n_counts2 + 0 1.0 3.0 + 1 3.0 3.0 + 2 11.0 3.0 + >>> print(adata.X.sum(axis=1)) + [1. 1. 1.] + + """ + if isinstance(data, AnnData): + start = logg.info("normalizing by total count per cell") + adata = data.copy() if copy else data + if counts_per_cell is None: + cell_subset, counts_per_cell = materialize_as_ndarray( + filter_cells(adata.X, min_counts=min_counts) + ) + adata.obs[key_n_counts] = counts_per_cell + adata._inplace_subset_obs(cell_subset) + counts_per_cell = counts_per_cell[cell_subset] + normalize_per_cell( + adata.X, + counts_per_cell_after=counts_per_cell_after, + counts_per_cell=counts_per_cell, + ) + + layers = adata.layers.keys() if layers == "all" else layers + if use_rep == "after": + after = counts_per_cell_after + elif use_rep == "X": + after = np.median(counts_per_cell[cell_subset]) + elif use_rep is None: + after = None + else: + msg = 'use_rep should be "after", "X" or None' + raise ValueError(msg) + for layer in layers: + _subset, counts = filter_cells(adata.layers[layer], min_counts=min_counts) + temp = normalize_per_cell(adata.layers[layer], after, counts, copy=True) + adata.layers[layer] = temp + + logg.info( + " finished ({time_passed}): normalized adata.X and added\n" + f" {key_n_counts!r}, counts per cell before normalization (adata.obs)", + time=start, + ) + return adata if copy else None + # proceed with data matrix + x = data.copy() if copy else data + if counts_per_cell is None: + if not copy: + msg = "Can only be run with copy=True" + raise ValueError(msg) + cell_subset, counts_per_cell = filter_cells(x, min_counts=min_counts) + x = x[cell_subset] + counts_per_cell = counts_per_cell[cell_subset] + if counts_per_cell_after is None: + counts_per_cell_after = np.median(counts_per_cell) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + counts_per_cell += counts_per_cell == 0 + counts_per_cell /= counts_per_cell_after + if not isinstance(x, CSBase): + x /= counts_per_cell[:, np.newaxis] + else: + sparsefuncs.inplace_row_scale(x, 1 / counts_per_cell) + return x if copy else None + + +DT = TypeVar("DT") + + +@njit +def _create_regressor_categorical( + x: np.ndarray, /, number_categories: int, cat_array: np.ndarray +) -> np.ndarray: + # create regressor matrix for categorical variables + # would be best to use X dtype but this matches old behavior + regressors = np.zeros(x.shape, dtype=np.float32) + # iterate over categories + for category in range(number_categories): + # iterate over genes and calculate mean expression + # for each gene per category + mask = category == cat_array + for ix in numba.prange(x.T.shape[0]): + regressors[mask, ix] = x.T[ix, mask].mean() + return regressors + + +@njit +def get_resid( + data: np.ndarray, + regressor: np.ndarray, + coeff: np.ndarray, +) -> np.ndarray: + for i in numba.prange(data.shape[0]): + data[i] -= regressor[i] @ coeff + return data + + +def numpy_regress_out( + data: np.ndarray, + regressor: np.ndarray, +) -> np.ndarray: + """Numba kernel for regress out unwanted sorces of variantion. + + Finding coefficient using Linear regression (Linear Least Squares). + """ + inv_gram_matrix = np.linalg.inv(regressor.T @ regressor) + coeff = inv_gram_matrix @ (regressor.T @ data) + data = get_resid(data, regressor, coeff) + return data + + +@old_positionals("layer", "n_jobs", "copy") +def regress_out( + adata: AnnData, + keys: str | Sequence[str], + *, + layer: str | None = None, + n_jobs: int | None = None, + copy: bool = False, +) -> AnnData | None: + """Regress out (mostly) unwanted sources of variation. + + Uses simple linear regression. This is inspired by Seurat's `regressOut` + function in R :cite:p:`Satija2015`. Note that this function tends to overcorrect + in certain circumstances as described in :issue:`526`. + + Parameters + ---------- + adata + The annotated data matrix. + keys + Keys for observation annotation on which to regress on. + layer + If provided, which element of layers to regress on. + n_jobs + Number of jobs for parallel computation. + `None` means using :attr:`scanpy.settings.n_jobs`. + copy + Determines whether a copy of `adata` is returned. + + Returns + ------- + Returns `None` if `copy=False`, else returns an updated `AnnData` object. Sets the following fields: + + `adata.X` | `adata.layers[layer]` : :class:`numpy.ndarray` | :class:`scipy.sparse.csr_matrix` (dtype `float`) + Corrected count data matrix. + + """ + from joblib import Parallel, delayed + + start = logg.info(f"regressing out {keys}") + adata = adata.copy() if copy else adata + + sanitize_anndata(adata) + + view_to_actual(adata) + + if isinstance(keys, str): + keys = [keys] + + x = _get_obs_rep(adata, layer=layer) + raise_not_implemented_error_if_backed_type(x, "regress_out") + + if isinstance(x, CSBase): + logg.info(" sparse input is densified and may lead to high memory use") + + n_jobs = sett.n_jobs if n_jobs is None else n_jobs + + # regress on a single categorical variable + variable_is_categorical = False + if keys[0] in adata.obs and isinstance(adata.obs[keys[0]].dtype, CategoricalDtype): + if len(keys) > 1: + msg = ( + "If providing categorical variable, " + "only a single one is allowed. For this one " + "we regress on the mean for each category." + ) + raise ValueError(msg) + logg.debug("... regressing on per-gene means within categories") + # set number of categories to the same dtype as the categories + cat_array = adata.obs[keys[0]].cat.codes.to_numpy() + number_categories = cat_array.dtype.type(len(adata.obs[keys[0]].cat.categories)) + + x = _to_dense(x, order="F") if isinstance(x, CSBase) else x + if np.issubdtype(x.dtype, np.integer): + target_dtype = np.float32 if x.dtype.itemsize <= 4 else np.float64 + x = x.astype(target_dtype) + regressors = _create_regressor_categorical(x, number_categories, cat_array) + variable_is_categorical = True + # regress on one or several ordinal variables + else: + # create data frame with selected keys (if given) + regressors = adata.obs[keys] if keys else adata.obs.copy() + + # add column of ones at index 0 (first column) + regressors.insert(0, "ones", 1.0) + regressors = regressors.to_numpy() + + # if the regressors are not categorical and the matrix is not singular + # use the shortcut numpy_regress_out + if not variable_is_categorical and np.linalg.det(regressors.T @ regressors) != 0: + # Because we update `X` in `numpy_regress_out`, it needs to be floating point to match + # the incoming values. + if np.issubdtype(x.dtype, np.integer): + target_dtype = np.float32 if x.dtype.itemsize <= 4 else np.float64 + kwargs = {} + if isinstance(x, np.ndarray): + kwargs["order"] = "C" + x = x.astype(target_dtype, **kwargs) + x = _to_dense(x, order="C") if isinstance(x, CSBase) else x + res = numpy_regress_out(x, regressors) + + # for a categorical variable or if the above checks failed, + # we fall back to the GLM implemetation of regression. + else: + # split the adata.X matrix by columns in chunks of size n_chunk + # (the last chunk could be of smaller size than the others) + len_chunk = int(np.ceil(min(1000, x.shape[1]) / n_jobs)) + n_chunks = int(np.ceil(x.shape[1] / len_chunk)) + x = _to_dense(x, order="F") if isinstance(x, CSBase) else x + chunk_list = np.array_split(x, n_chunks, axis=1) + regressors_chunk = ( + np.array_split(regressors, n_chunks, axis=1) + if variable_is_categorical + else repeat(regressors) + ) + + # each task is passed a data chunk (e.g. `adata.X[:, 0:100]``) and the regressors. + # This data will be passed to each of the jobs. + # TODO: figure out how to test that this doesn't oversubscribe resources + res = Parallel(n_jobs=n_jobs)( + delayed(_regress_out_chunk)( + data_chunk, regres, variable_is_categorical=variable_is_categorical + ) + for data_chunk, regres in zip(chunk_list, regressors_chunk, strict=False) + ) + + # res is a list of vectors (each corresponding to a regressed gene column). + # The transpose is needed to get the matrix in the shape needed + res = np.vstack(res).T + + _set_obs_rep(adata, res, layer=layer) + logg.info(" finished", time=start) + return adata if copy else None + + +def _regress_out_chunk( + data_chunk: NDArray[np.floating], + regressors: pd.DataFrame | NDArray[np.floating], + *, + variable_is_categorical: bool, +) -> NDArray[np.floating]: + import statsmodels.api as sm + import statsmodels.tools.sm_exceptions as sme + + responses_chunk_list = [] + for col_index in range(data_chunk.shape[1]): + # if all values are identical, the statsmodel.api.GLM throws an error; + # but then no regression is necessary anyways... + if not (data_chunk[:, col_index] != data_chunk[0, col_index]).any(): + responses_chunk_list.append(data_chunk[:, col_index]) + continue + + if variable_is_categorical: + regres = np.c_[np.ones(regressors.shape[0]), regressors[:, col_index]] + else: + regres = regressors + + try: + with warnings.catch_warnings(): + warnings.simplefilter("error", sme.PerfectSeparationWarning) + result = sm.GLM( + data_chunk[:, col_index], regres, family=sm.families.Gaussian() + ).fit() + new_column = result.resid_response + except (sme.PerfectSeparationError, sme.PerfectSeparationWarning): + logg.warning("Encountered perfect separation, setting to 0 as in R.") + new_column = np.zeros(data_chunk.shape[0]) + + responses_chunk_list.append(new_column) + + return np.vstack(responses_chunk_list) + + +@overload +def sample( + data: AnnData, + fraction: float | None = None, + *, + n: int | None = None, + rng: RNGLike | SeedLike | None = 0, + copy: Literal[False] = False, + replace: bool = False, + axis: Literal["obs", 0, "var", 1] = "obs", + p: str | NDArray[np.bool_] | NDArray[np.floating] | None = None, +) -> None: ... +@overload +def sample( + data: AnnData, + fraction: float | None = None, + *, + n: int | None = None, + rng: RNGLike | SeedLike | None = None, + copy: Literal[True], + replace: bool = False, + axis: Literal["obs", 0, "var", 1] = "obs", + p: str | NDArray[np.bool_] | NDArray[np.floating] | None = None, +) -> AnnData: ... +@overload +def sample( + data: A, + fraction: float | None = None, + *, + n: int | None = None, + rng: RNGLike | SeedLike | None = None, + copy: bool = False, + replace: bool = False, + axis: Literal["obs", 0, "var", 1] = "obs", + p: str | NDArray[np.bool_] | NDArray[np.floating] | None = None, +) -> tuple[A, NDArray[np.int64]]: ... +def sample( # noqa: PLR0912 + data: AnnData | np.ndarray | CSBase | DaskArray, + fraction: float | None = None, + *, + n: int | None = None, + rng: RNGLike | SeedLike | None = None, + copy: bool = False, + replace: bool = False, + axis: Literal["obs", 0, "var", 1] = "obs", + p: str | NDArray[np.bool_] | NDArray[np.floating] | None = None, +) -> AnnData | None | tuple[np.ndarray | CSBase | DaskArray, NDArray[np.int64]]: + r"""Sample observations or variables with or without replacement. + + Parameters + ---------- + data + The (annotated) data matrix of shape `n_obs` × `n_vars`. + Rows correspond to cells and columns to genes. + fraction + Sample to this `fraction` of the number of observations or variables. + (All of them, even if there are `0`\ s/`False`\ s in `p`.) + This can be larger than 1.0, if `replace=True`. + See `axis` and `replace`. + n + Sample to this number of observations or variables. See `axis`. + rng + Random seed to change subsampling. + copy + If an :class:`~anndata.AnnData` is passed, + determines whether a copy is returned. + replace + If True, samples are drawn with replacement. + axis + Sample `obs`\ ervations (axis 0) or `var`\ iables (axis 1). + p + Drawing probabilities (floats) or mask (bools). + Either an `axis`-sized array, or the name of a column. + If `p` is an array of probabilities, it must sum to 1. + + Returns + ------- + If `isinstance(data, AnnData)` and `copy=False`, + this function returns `None`. Otherwise: + + `data[indices, :]` | `data[:, indices]` (depending on `axis`) + If `data` is array-like or `copy=True`, returns the subset. + `indices` : numpy.ndarray + If `data` is array-like, also returns the indices into the original. + + """ + # parameter validation + if not copy and isinstance(data, AnnData) and data.isbacked: + msg = "Inplace sampling (`copy=False`) is not implemented for backed objects." + raise NotImplementedError(msg) + axis, axis_name = _resolve_axis(axis) + p = _check_mask(data, p, dim=axis_name, allow_probabilities=True) + if p is not None and p.dtype == bool: + p = p.astype(np.float64) / p.sum() + old_n = data.shape[axis] + match (fraction, n): + case (None, None): + msg = "Either `fraction` or `n` must be set." + raise TypeError(msg) + case (None, _): + pass + case (_, None): + if fraction < 0: + msg = f"`{fraction=}` needs to be nonnegative." + raise ValueError(msg) + if not replace and fraction > 1: + msg = f"If `replace=False`, `{fraction=}` needs to be within [0, 1]." + raise ValueError(msg) + n = int(fraction * old_n) + logg.debug(f"... sampled to {n} {axis_name}") + case _: + msg = "Providing both `fraction` and `n` is not allowed." + raise TypeError(msg) + del fraction + + # actually do subsampling + rng = np.random.default_rng(rng) + indices = rng.choice(old_n, size=n, replace=replace, p=p) + + # overload 1: inplace AnnData subset + if not copy and isinstance(data, AnnData): + if axis_name == "obs": + data._inplace_subset_obs(indices) + else: + data._inplace_subset_var(indices) + return None + + subset = data[indices] if axis_name == "obs" else data[:, indices] + + # overload 2: copy AnnData subset + if copy and isinstance(data, AnnData): + assert isinstance(subset, AnnData) + return subset.to_memory() if data.isbacked else subset.copy() + + # overload 3: return array and indices + assert isinstance(subset, np.ndarray | CSBase | DaskArray), type(subset) + if copy: + subset = subset.copy() + return subset, indices + + +@renamed_arg("target_counts", "counts_per_cell") +def downsample_counts( + adata: AnnData, + counts_per_cell: int | Collection[int] | None = None, + total_counts: int | None = None, + *, + random_state: _LegacyRandom = 0, + replace: bool = False, + copy: bool = False, +) -> AnnData | None: + """Downsample counts from count matrix. + + If `counts_per_cell` is specified, each cell will downsampled. + If `total_counts` is specified, expression matrix will be downsampled to + contain at most `total_counts`. + + Parameters + ---------- + adata + Annotated data matrix. + counts_per_cell + Target total counts per cell. If a cell has more than 'counts_per_cell', + it will be downsampled to this number. Resulting counts can be specified + on a per cell basis by passing an array.Should be an integer or integer + ndarray with same length as number of obs. + total_counts + Target total counts. If the count matrix has more than `total_counts` + it will be downsampled to have this number. + random_state + Random seed for subsampling. + replace + Whether to sample the counts with replacement. + copy + Determines whether a copy of `adata` is returned. + + Returns + ------- + Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following fields: + + `adata.X` : :class:`~numpy.ndarray` | :class:`~scipy.sparse.csr_matrix` | :class:`~scipy.sparse.csc_matrix` (dtype `float`) + Downsampled counts matrix. + + """ + raise_not_implemented_error_if_backed_type(adata.X, "downsample_counts") + # This logic is all dispatch + total_counts_call = total_counts is not None + counts_per_cell_call = counts_per_cell is not None + if total_counts_call is counts_per_cell_call: + msg = "Must specify exactly one of `total_counts` or `counts_per_cell`." + raise ValueError(msg) + if copy: + adata = adata.copy() + if total_counts_call: + adata.X = _downsample_total_counts( + adata.X, total_counts, random_state=random_state, replace=replace + ) + elif counts_per_cell_call: + adata.X = _downsample_per_cell( + adata.X, counts_per_cell, random_state=random_state, replace=replace + ) + if copy: + return adata + + +def _downsample_per_cell( + x: CSBase, + /, + counts_per_cell: int, + *, + random_state: _LegacyRandom, + replace: bool, +) -> CSBase: + n_obs = x.shape[0] + if isinstance(counts_per_cell, int): + counts_per_cell = np.full(n_obs, counts_per_cell) + else: + counts_per_cell = np.asarray(counts_per_cell) + # np.random.choice needs int arguments in numba code: + counts_per_cell = counts_per_cell.astype(np.int_, copy=False) + if not isinstance(counts_per_cell, np.ndarray) or len(counts_per_cell) != n_obs: + msg = ( + "If provided, 'counts_per_cell' must be either an integer, or " + "coercible to an `np.ndarray` of length as number of observations" + " by `np.asarray(counts_per_cell)`." + ) + raise ValueError(msg) + if isinstance(x, CSBase): + original_type = type(x) + if not isinstance(x, CSRBase): + x = sparse.csr_matrix(x) # noqa: TID251 + totals = np.ravel(axis_sum(x, axis=1)) # Faster for csr matrix + under_target = np.nonzero(totals > counts_per_cell)[0] + rows = np.split(x.data, x.indptr[1:-1]) + for rowidx in under_target: + row = rows[rowidx] + _downsample_array( + row, + counts_per_cell[rowidx], + random_state=random_state, + replace=replace, + inplace=True, + ) + x.eliminate_zeros() + if not issubclass(original_type, CSRBase): # Put it back + x = original_type(x) + else: + totals = np.ravel(axis_sum(x, axis=1)) + under_target = np.nonzero(totals > counts_per_cell)[0] + for rowidx in under_target: + row = x[rowidx, :] + _downsample_array( + row, + counts_per_cell[rowidx], + random_state=random_state, + replace=replace, + inplace=True, + ) + return x + + +def _downsample_total_counts( + x: CSBase, + /, + total_counts: int, + *, + random_state: _LegacyRandom, + replace: bool, +) -> CSBase: + total_counts = int(total_counts) + total = x.sum() + if total < total_counts: + return x + if isinstance(x, CSBase): + original_type = type(x) + if not isinstance(x, CSRBase): + x = sparse.csr_matrix(x) # noqa: TID251 + _downsample_array( + x.data, + total_counts, + random_state=random_state, + replace=replace, + inplace=True, + ) + x.eliminate_zeros() + if not issubclass(original_type, CSRBase): + x = original_type(x) + else: + v = x.reshape(np.multiply(*x.shape)) + _downsample_array( + v, total_counts, random_state=random_state, replace=replace, inplace=True + ) + return x + + +# TODO: can/should this be parallelized? +@numba.njit(cache=True) # noqa: TID251 +def _downsample_array( + col: np.ndarray, + target: int, + *, + random_state: _LegacyRandom = 0, + replace: bool = True, + inplace: bool = False, +): + """Evenly reduce counts in cell to target amount. + + This is an internal function and has some restrictions: + + * total counts in cell must be less than target + """ + np.random.seed(random_state) + cumcounts = col.cumsum() + if inplace: + col[:] = 0 + else: + col = np.zeros_like(col) + total = np.int_(cumcounts[-1]) + sample = np.random.choice(total, target, replace=replace) + sample.sort() + geneptr = 0 + for count in sample: + while count >= cumcounts[geneptr]: + geneptr += 1 + col[geneptr] += 1 + return col diff --git a/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_utils.py b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..54090bf90eda80adfc5cf49ced61903a22f7ebff --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/preprocessing/_utils.py @@ -0,0 +1,204 @@ +from __future__ import annotations + +from functools import singledispatch +from typing import TYPE_CHECKING + +import numba +import numpy as np +from sklearn.random_projection import sample_without_replacement + +from .._compat import CSBase, CSCBase, CSRBase, SpBase, njit +from .._utils import axis_sum, elem_mul + +if TYPE_CHECKING: + from typing import Literal + + from numpy.typing import DTypeLike, NDArray + + from .._compat import DaskArray + from .._utils import _SupportedArray + from .._utils.random import _LegacyRandom + + +@singledispatch +def axis_mean(x: DaskArray, /, *, axis: Literal[0, 1], dtype: DTypeLike) -> DaskArray: + total = axis_sum(x, axis=axis, dtype=dtype) + return total / x.shape[axis] + + +@axis_mean.register(np.ndarray) +def _(x: np.ndarray, /, *, axis: Literal[0, 1], dtype: DTypeLike) -> np.ndarray: + return x.mean(axis=axis, dtype=dtype) + + +def _get_mean_var( + x: _SupportedArray, /, *, axis: Literal[0, 1] = 0 +) -> tuple[NDArray[np.float64], NDArray[np.float64]]: + if isinstance(x, CSBase): + mean, var = sparse_mean_variance_axis(x, axis=axis) + elif isinstance(x, SpBase): + msg = f"Unsupported type {type(x)}" + raise TypeError(msg) + else: + mean = axis_mean(x, axis=axis, dtype=np.float64) + mean_sq = axis_mean(elem_mul(x, x), axis=axis, dtype=np.float64) + var = mean_sq - mean**2 + # enforce R convention (unbiased estimator) for variance + if x.shape[axis] != 1: + var *= x.shape[axis] / (x.shape[axis] - 1) + return mean, var + + +def sparse_mean_variance_axis( + mtx: CSBase, axis: Literal[0, 1] +) -> tuple[NDArray[np.float64], NDArray[np.float64]]: + """Compute mean and variance along one axis of a sparse matrix. + + This code and internal functions are based on sklearns `sparsefuncs.mean_variance_axis`. + + Modifications: + * allow deciding on the output type, which can increase accuracy when calculating the mean and variance of 32bit floats. + * This doesn't currently implement support for null values, but could. + * Uses numba not cython + """ + assert axis in (0, 1) + if isinstance(mtx, CSRBase): + ax_minor = 1 + shape = mtx.shape + elif isinstance(mtx, CSCBase): + ax_minor = 0 + shape = mtx.shape[::-1] + else: + msg = "This function only works on sparse csr and csc matrices" + raise ValueError(msg) + if axis == ax_minor: + return sparse_mean_var_major_axis( + mtx.data, + mtx.indptr, + major_len=shape[0], + minor_len=shape[1], + n_threads=numba.get_num_threads(), + ) + else: + return sparse_mean_var_minor_axis( + mtx.data, + mtx.indices, + mtx.indptr, + major_len=shape[0], + minor_len=shape[1], + n_threads=numba.get_num_threads(), + ) + + +@njit +def sparse_mean_var_minor_axis( + data, indices, indptr, *, major_len, minor_len, n_threads +): + """Compute mean and variance for a sparse matrix for the minor axis. + + Given arrays for a csr matrix, returns the means and variances for each + column back. + """ + rows = len(indptr) - 1 + sums_minor = np.zeros((n_threads, minor_len)) + squared_sums_minor = np.zeros((n_threads, minor_len)) + means = np.zeros(minor_len) + variances = np.zeros(minor_len) + for i in numba.prange(n_threads): + for r in range(i, rows, n_threads): + for j in range(indptr[r], indptr[r + 1]): + minor_index = indices[j] + if minor_index >= minor_len: + continue + value = data[j] + sums_minor[i, minor_index] += value + squared_sums_minor[i, minor_index] += value * value + for c in numba.prange(minor_len): + sum_minor = sums_minor[:, c].sum() + means[c] = sum_minor / major_len + variances[c] = ( + squared_sums_minor[:, c].sum() / major_len - (sum_minor / major_len) ** 2 + ) + return means, variances + + +@njit +def sparse_mean_var_major_axis(data, indptr, *, major_len, minor_len, n_threads): + """Compute mean and variance for a sparse array for the major axis. + + Given arrays for a csr matrix, returns the means and variances for each + row back. + """ + rows = len(indptr) - 1 + means = np.zeros(major_len) + variances = np.zeros_like(means) + + for i in numba.prange(n_threads): + for r in range(i, rows, n_threads): + sum_major = 0.0 + squared_sum_minor = 0.0 + for j in range(indptr[r], indptr[r + 1]): + value = np.float64(data[j]) + sum_major += value + squared_sum_minor += value * value + means[r] = sum_major + variances[r] = squared_sum_minor + for c in numba.prange(major_len): + mean = means[c] / minor_len + means[c] = mean + variances[c] = variances[c] / minor_len - mean * mean + return means, variances + + +def sample_comb( + dims: tuple[int, ...], + nsamp: int, + *, + random_state: _LegacyRandom = None, + method: Literal[ + "auto", "tracking_selection", "reservoir_sampling", "pool" + ] = "auto", +) -> NDArray[np.int64]: + """Randomly sample indices from a grid, without repeating the same tuple.""" + idx = sample_without_replacement( + np.prod(dims), nsamp, random_state=random_state, method=method + ) + return np.vstack(np.unravel_index(idx, dims)).T + + +def _to_dense(x: CSBase, /, order: Literal["C", "F"] = "C") -> NDArray: + """Numba kernel for np.toarray() function.""" + out = np.zeros(x.shape, dtype=x.dtype, order=order) + if x.format == "csr": + _to_dense_csr_numba(x.indptr, x.indices, x.data, out, x.shape) + elif x.format == "csc": + _to_dense_csc_numba(x.indptr, x.indices, x.data, out, x.shape) + else: + out = x.toarray(order=order) + return out + + +@njit +def _to_dense_csc_numba( + indptr: NDArray, + indices: NDArray, + data: NDArray, + x: NDArray, + shape: tuple[int, int], +) -> None: + for c in numba.prange(x.shape[1]): + for i in range(indptr[c], indptr[c + 1]): + x[indices[i], c] = data[i] + + +@njit +def _to_dense_csr_numba( + indptr: NDArray, + indices: NDArray, + data: NDArray, + x: NDArray, + shape: tuple[int, int], +) -> None: + for r in numba.prange(shape[0]): + for i in range(indptr[r], indptr[r + 1]): + x[r, indices[i]] = data[i] diff --git a/.venv/lib/python3.11/site-packages/scanpy/queries/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/queries/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6e618d378263c447de44c56f586caa8b94cd88f6 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/queries/__init__.py @@ -0,0 +1,17 @@ +"""Biomart queries.""" + +from __future__ import annotations + +from ._queries import ( + biomart_annotations, + enrich, # gprofiler queries + gene_coordinates, + mitochondrial_genes, +) + +__all__ = [ + "biomart_annotations", + "enrich", + "gene_coordinates", + "mitochondrial_genes", +] diff --git a/.venv/lib/python3.11/site-packages/scanpy/queries/_queries.py b/.venv/lib/python3.11/site-packages/scanpy/queries/_queries.py new file mode 100644 index 0000000000000000000000000000000000000000..22e9a63603d1537a95caa153ad47b2d0b425f18f --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/queries/_queries.py @@ -0,0 +1,321 @@ +from __future__ import annotations + +from collections.abc import Iterable +from functools import singledispatch +from types import MappingProxyType +from typing import TYPE_CHECKING + +from anndata import AnnData + +from .._utils import _doc_params +from .._utils._doctests import doctest_needs +from ..get import rank_genes_groups_df + +if TYPE_CHECKING: + from collections.abc import Mapping + from typing import Any + + import pandas as pd + +_doc_org = """\ +org + Organism to query. Must be an organism in ensembl biomart. "hsapiens", + "mmusculus", "drerio", etc.\ +""" + +_doc_host = """\ +host + A valid BioMart host URL. Alternative values include archive urls (like + "grch37.ensembl.org") or regional mirrors (like "useast.ensembl.org").\ +""" + +_doc_use_cache = """\ +use_cache + Whether pybiomart should use a cache for requests. Will create a + `.pybiomart.sqlite` file in current directory if used.\ +""" + + +@_doc_params(doc_org=_doc_org, doc_host=_doc_host, doc_use_cache=_doc_use_cache) +def simple_query( + org: str, + attrs: Iterable[str] | str, + *, + filters: dict[str, Any] | None = None, + host: str = "www.ensembl.org", + use_cache: bool = False, +) -> pd.DataFrame: + """Interface with biomart. + + Params + ------ + {doc_org} + attrs + What you want returned. + filters + What you want to pick out. + {doc_host} + {doc_use_cache} + """ + if isinstance(attrs, str): + attrs = [attrs] + elif isinstance(attrs, Iterable): + attrs = list(attrs) + else: + msg = f"attrs must be of type list or str, was {type(attrs)}." + raise TypeError(msg) + try: + from pybiomart import Server + except ImportError as e: + msg = "This method requires the `pybiomart` module to be installed." + raise ImportError(msg) from e + server = Server(host, use_cache=use_cache) + dataset = server.marts["ENSEMBL_MART_ENSEMBL"].datasets[f"{org}_gene_ensembl"] + res = dataset.query(attributes=attrs, filters=filters, use_attr_names=True) + return res + + +@doctest_needs("pybiomart") +@_doc_params(doc_org=_doc_org, doc_host=_doc_host, doc_use_cache=_doc_use_cache) +def biomart_annotations( + org: str, + attrs: Iterable[str], + *, + host: str = "www.ensembl.org", + use_cache: bool = False, +) -> pd.DataFrame: + """Retrieve gene annotations from ensembl biomart. + + Parameters + ---------- + {doc_org} + attrs + Attributes to query biomart for. + {doc_host} + {doc_use_cache} + + Returns + ------- + Dataframe containing annotations. + + Examples + -------- + Retrieve genes coordinates and chromosomes + + >>> import scanpy as sc + >>> annot = sc.queries.biomart_annotations( + ... "hsapiens", + ... ["ensembl_gene_id", "start_position", "end_position", "chromosome_name"], + ... ).set_index("ensembl_gene_id") + >>> adata.var[annot.columns] = annot + + """ + return simple_query(org=org, attrs=attrs, host=host, use_cache=use_cache) + + +@doctest_needs("pybiomart") +@_doc_params(doc_org=_doc_org, doc_host=_doc_host, doc_use_cache=_doc_use_cache) +def gene_coordinates( + org: str, + gene_name: str, + *, + gene_attr: str = "external_gene_name", + chr_exclude: Iterable[str] = (), + host: str = "www.ensembl.org", + use_cache: bool = False, +) -> pd.DataFrame: + """Retrieve gene coordinates for specific organism through BioMart. + + Parameters + ---------- + {doc_org} + gene_name + The gene symbol (e.g. "hgnc_symbol" for human) for which to retrieve + coordinates. + gene_attr + The biomart attribute the gene symbol should show up for. + chr_exclude + A list of chromosomes to exclude from query. + {doc_host} + {doc_use_cache} + + Returns + ------- + Dataframe containing gene coordinates for the specified gene symbol. + + Examples + -------- + >>> import scanpy as sc + >>> sc.queries.gene_coordinates("hsapiens", "MT-TF") + + """ + res = simple_query( + org=org, + attrs=["chromosome_name", "start_position", "end_position"], + filters={gene_attr: gene_name}, + host=host, + use_cache=use_cache, + ) + return res[~res["chromosome_name"].isin(chr_exclude)] + + +@doctest_needs("pybiomart") +@_doc_params(doc_org=_doc_org, doc_host=_doc_host, doc_use_cache=_doc_use_cache) +def mitochondrial_genes( + org: str, + *, + attrname: str = "external_gene_name", + host: str = "www.ensembl.org", + use_cache: bool = False, + chromosome: str = "MT", +) -> pd.DataFrame: + """Mitochondrial gene symbols for specific organism through BioMart. + + Parameters + ---------- + {doc_org} + attrname + Biomart attribute field to return. Possible values include + "external_gene_name", "ensembl_gene_id", "hgnc_symbol", "mgi_symbol", + and "zfin_id_symbol". + {doc_host} + {doc_use_cache} + chromosome + Mitochrondrial chromosome name used in BioMart for organism. + + Returns + ------- + Dataframe containing identifiers for mitochondrial genes. + + Examples + -------- + >>> import scanpy as sc + >>> mito_gene_names = sc.queries.mitochondrial_genes("hsapiens") + >>> mito_ensembl_ids = sc.queries.mitochondrial_genes( + ... "hsapiens", attrname="ensembl_gene_id" + ... ) + >>> mito_gene_names_fly = sc.queries.mitochondrial_genes( + ... "dmelanogaster", chromosome="mitochondrion_genome" + ... ) + + """ + return simple_query( + org, + attrs=[attrname], + filters={"chromosome_name": [chromosome]}, + host=host, + use_cache=use_cache, + ) + + +@doctest_needs("gprofiler") +@singledispatch +@_doc_params(doc_org=_doc_org) +def enrich( + container: Iterable[str] | Mapping[str, Iterable[str]], + *, + org: str = "hsapiens", + gprofiler_kwargs: Mapping[str, Any] = MappingProxyType({}), +) -> pd.DataFrame: + """Get enrichment for DE results. + + This is a thin convenience wrapper around the very useful gprofiler_. + + This method dispatches on the first argument, leading to the following two + signatures:: + + enrich(container, ...) + enrich(adata: AnnData, group, key: str, ...) + + Where:: + + enrich(adata, group, key, ...) = enrich(adata.uns[key]["names"][group], ...) + + .. _gprofiler: https://pypi.org/project/gprofiler-official/#description + + Parameters + ---------- + container + Contains list of genes you'd like to search. If container is a `dict` all + enrichment queries are made at once. + adata + AnnData object whose group will be looked for. + group + The group whose genes should be used for enrichment. + key + Key in `uns` to find group under. + {doc_org} + gprofiler_kwargs + Keyword arguments to pass to `GProfiler.profile`, see gprofiler_. Some + useful options are `no_evidences=False` which reports gene intersections, + `sources=['GO:BP']` which limits gene sets to only GO biological processes and + `all_results=True` which returns all results including the non-significant ones. + **kwargs + All other keyword arguments are passed to `sc.get.rank_genes_groups_df`. E.g. + pval_cutoff, log2fc_min. + + Returns + ------- + Dataframe of enrichment results. + + Examples + -------- + Using `sc.queries.enrich` on a list of genes: + + >>> import scanpy as sc + >>> sc.queries.enrich(["KLF4", "PAX5", "SOX2", "NANOG"], org="hsapiens") + >>> sc.queries.enrich( + ... {{"set1": ["KLF4", "PAX5"], "set2": ["SOX2", "NANOG"]}}, org="hsapiens" + ... ) + + Using `sc.queries.enrich` on an :class:`anndata.AnnData` object: + + >>> pbmcs = sc.datasets.pbmc68k_reduced() + >>> sc.tl.rank_genes_groups(pbmcs, "bulk_labels") + >>> sc.queries.enrich(pbmcs, "CD34+") + + """ + try: + from gprofiler import GProfiler + except ImportError as e: + msg = "This method requires the `gprofiler-official` module to be installed." + raise ImportError(msg) from e + gprofiler = GProfiler(user_agent="scanpy", return_dataframe=True) + gprofiler_kwargs = dict(gprofiler_kwargs) + for k in ["organism"]: + if gprofiler_kwargs.get(k) is not None: + msg = ( + f"Argument `{k}` should be passed directly through `enrich`, " + "not through `gprofiler_kwargs`" + ) + raise ValueError(msg) + return gprofiler.profile(container, organism=org, **gprofiler_kwargs) + + +@enrich.register(AnnData) +def _enrich_anndata( + adata: AnnData, + group: str, + *, + org: str | None = "hsapiens", + key: str = "rank_genes_groups", + pval_cutoff: float = 0.05, + log2fc_min: float | None = None, + log2fc_max: float | None = None, + gene_symbols: str | None = None, + gprofiler_kwargs: Mapping[str, Any] = MappingProxyType({}), +) -> pd.DataFrame: + de = rank_genes_groups_df( + adata, + group=group, + key=key, + pval_cutoff=pval_cutoff, + log2fc_min=log2fc_min, + log2fc_max=log2fc_max, + gene_symbols=gene_symbols, + ) + if gene_symbols is not None: + gene_list = list(de[gene_symbols].dropna()) + else: + gene_list = list(de["names"].dropna()) + return enrich(gene_list, org=org, gprofiler_kwargs=gprofiler_kwargs) diff --git a/.venv/lib/python3.11/site-packages/scanpy/readwrite.py b/.venv/lib/python3.11/site-packages/scanpy/readwrite.py new file mode 100644 index 0000000000000000000000000000000000000000..445e1b04cc03f39e2ff28f85f6d56e79e3156c0a --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/readwrite.py @@ -0,0 +1,1113 @@ +"""Reading and Writing.""" + +from __future__ import annotations + +import json +from functools import partial +from pathlib import Path, PurePath +from typing import TYPE_CHECKING, overload + +import anndata.utils +import h5py +import numpy as np +import pandas as pd +from anndata import AnnData +from matplotlib.image import imread +from packaging.version import Version + +from . import logging as logg +from ._compat import add_note, deprecated, old_positionals, pkg_version +from ._settings import settings +from ._utils import _empty + +if pkg_version("anndata") >= Version("0.11.0rc2"): + from anndata.io import ( + read_csv, + read_excel, + read_h5ad, + read_hdf, + read_loom, + read_mtx, + read_text, + ) +else: + from anndata import ( + read_csv, + read_excel, + read_h5ad, + read_hdf, + read_loom, + read_mtx, + read_text, + ) + + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import BinaryIO, Literal + + from ._utils import Empty + +# .gz and .bz2 suffixes are also allowed for text formats +text_exts = { + "csv", + "tsv", + "tab", + "data", + "txt", # these four are all equivalent +} +avail_exts = { + "anndata", + "xlsx", + "h5", + "h5ad", + "mtx", + "mtx.gz", + "soft.gz", + "loom", +} | text_exts +"""Available file formats for reading data. """ + + +# -------------------------------------------------------------------------------- +# Reading and Writing data files and AnnData objects +# -------------------------------------------------------------------------------- + + +@old_positionals( + "sheet", + "ext", + "delimiter", + "first_column_names", + "backup_url", + "cache", + "cache_compression", +) +def read( + filename: Path | str, + backed: Literal["r", "r+"] | None = None, + *, + sheet: str | None = None, + ext: str | None = None, + delimiter: str | None = None, + first_column_names: bool = False, + backup_url: str | None = None, + cache: bool = False, + cache_compression: Literal["gzip", "lzf"] | None | Empty = _empty, + **kwargs, +) -> AnnData: + """Read file and return :class:`~anndata.AnnData` object. + + To speed up reading, consider passing ``cache=True``, which creates an hdf5 + cache file. + + Parameters + ---------- + filename + If the filename has no file extension, it is interpreted as a key for + generating a filename via ``sc.settings.writedir / (filename + + sc.settings.file_format_data)``. This is the same behavior as in + ``sc.read(filename, ...)``. + backed + If ``'r'``, load :class:`~anndata.AnnData` in ``backed`` mode instead + of fully loading it into memory (`memory` mode). If you want to modify + backed attributes of the AnnData object, you need to choose ``'r+'``. + sheet + Name of sheet/table in hdf5 or Excel file. + ext + Extension that indicates the file type. If ``None``, uses extension of + filename. + delimiter + Delimiter that separates data within text file. If ``None``, will split at + arbitrary number of white spaces, which is different from enforcing + splitting at any single white space ``' '``. + first_column_names + Assume the first column stores row names. This is only necessary if + these are not strings: strings in the first column are automatically + assumed to be row names. + backup_url + Retrieve the file from an URL if not present on disk. + cache + If `False`, read from source, if `True`, read from fast 'h5ad' cache. + cache_compression + See the h5py :ref:`dataset_compression`. + (Default: `settings.cache_compression`) + kwargs + Parameters passed to :func:`~anndata.io.read_loom`. + + Returns + ------- + An :class:`~anndata.AnnData` object + + """ + filename = Path(filename) # allow passing strings + if is_valid_filename(filename, ext=ext): + return _read( + filename, + backed=backed, + sheet=sheet, + ext=ext, + delimiter=delimiter, + first_column_names=first_column_names, + backup_url=backup_url, + cache=cache, + cache_compression=cache_compression, + **kwargs, + ) + # generate filename and read to dict + filekey = str(filename) + filename = settings.writedir / f"{filekey}.{settings.file_format_data}" + if not filename.exists(): + msg = ( + f"Reading with filekey {filekey!r} failed, " + f"the inferred filename {filename!r} does not exist. " + "If you intended to provide a filename, either use a filename " + f"ending on one of the available extensions {avail_exts} " + "or pass the parameter `ext`." + ) + raise ValueError(msg) + return read_h5ad(filename, backed=backed) + + +@old_positionals("genome", "gex_only", "backup_url") +def read_10x_h5( + filename: Path | str, + *, + genome: str | None = None, + gex_only: bool = True, + backup_url: str | None = None, +) -> AnnData: + r"""Read 10x-Genomics-formatted hdf5 file. + + Parameters + ---------- + filename + Path to a 10x hdf5 file. + genome + Filter expression to genes within this genome. For legacy 10x h5 + files, this must be provided if the data contains more than one genome. + gex_only + Only keep 'Gene Expression' data and ignore other feature types, + e.g. 'Antibody Capture', 'CRISPR Guide Capture', or 'Custom' + backup_url + Retrieve the file from an URL if not present on disk. + + Returns + ------- + Annotated data matrix, where observations/cells are named by their + barcode and variables/genes by gene name. Stores the following information: + + :attr:`~anndata.AnnData.X` + The data matrix is stored + :attr:`~anndata.AnnData.obs_names` + Cell names + :attr:`~anndata.AnnData.var_names` + Gene names for a feature barcode matrix, probe names for a probe bc matrix + :attr:`~anndata.AnnData.var`\ `['gene_ids']` + Gene IDs + :attr:`~anndata.AnnData.var`\ `['feature_types']` + Feature types + :attr:`~anndata.AnnData.obs`\ `[filtered_barcodes]` + filtered barcodes if present in the matrix + :attr:`~anndata.AnnData.var` + Any additional metadata present in /matrix/features is read in. + + """ + path = Path(filename) + start = logg.info(f"reading {path}") + is_present = _check_datafile_present_and_download(path, backup_url=backup_url) + if not is_present: + logg.debug(f"... did not find original file {path}") + with h5py.File(str(path), "r") as f: + v3 = "/matrix" in f + if v3: + adata = _read_10x_h5(path, _read_v3_10x_h5) + if genome: + if genome not in adata.var["genome"].values: + msg = ( + f"Could not find data corresponding to genome {genome!r} in {path}. " + f"Available genomes are: {list(adata.var['genome'].unique())}." + ) + raise ValueError(msg) + adata = adata[:, adata.var["genome"] == genome] + if gex_only: + adata = adata[:, adata.var["feature_types"] == "Gene Expression"] + if adata.is_view: + adata = adata.copy() + else: + adata = _read_10x_h5(path, partial(_read_legacy_10x_h5, genome=genome)) + logg.info("", time=start) + return adata + + +def _read_10x_h5(path: Path, cb: Callable[[h5py.File], AnnData]) -> AnnData: + """Read hdf5 file from Cell Ranger v3 or later versions.""" + with h5py.File(str(path), "r") as f: + try: + return cb(f) + except KeyError as e: + msg = "File is missing one or more required datasets." + raise Exception(msg) from e + + +def _collect_datasets(dsets: dict, group: h5py.Group) -> None: + for k, v in group.items(): + if isinstance(v, h5py.Dataset): + dsets[k] = v[()] + else: + _collect_datasets(dsets, v) + + +def _read_v3_10x_h5(f: h5py.File) -> AnnData: + dsets = {} + _collect_datasets(dsets, f["matrix"]) + + from scipy.sparse import csr_matrix # noqa: TID251 + + n_cols, n_rows = dsets["shape"] # transposed + data = dsets["data"] + if dsets["data"].dtype == np.dtype("int32"): + data = dsets["data"].view("float32") + data[:] = dsets["data"] + matrix = csr_matrix( + (data, dsets["indices"], dsets["indptr"]), + shape=(n_rows, n_cols), + ) + obs_dict = {"obs_names": dsets["barcodes"].astype(str)} + var_dict = {"var_names": dsets["name"].astype(str)} + + if "gene_id" not in dsets: + # Read metadata specific to a feature-barcode matrix + var_dict["gene_ids"] = dsets["id"].astype(str) + else: + # Read metadata specific to a probe-barcode matrix + var_dict.update({ + "gene_ids": dsets["gene_id"].astype(str), + "probe_ids": dsets["id"].astype(str), + }) + var_dict["feature_types"] = dsets["feature_type"].astype(str) + if "filtered_barcodes" in f["matrix"]: + obs_dict["filtered_barcodes"] = dsets["filtered_barcodes"].astype(bool) + + if "features" in f["matrix"]: + var_dict.update( + ( + feature_metadata_name, + dsets[feature_metadata_name].astype( + bool if feature_metadata_item.dtype.kind == "b" else str + ), + ) + for feature_metadata_name, feature_metadata_item in f["matrix"][ + "features" + ].items() + if isinstance(feature_metadata_item, h5py.Dataset) + and feature_metadata_name + not in ["name", "feature_type", "id", "gene_id", "_all_tag_keys"] + ) + else: + msg = "10x h5 has no features group" + raise ValueError(msg) + return AnnData(matrix, obs=obs_dict, var=var_dict) + + +def _read_legacy_10x_h5(f: h5py.File, genome: str | None) -> AnnData: + children = list(f.keys()) + if not genome: + if len(children) > 1: + msg = ( + f"{f.filename} contains more than one genome. " + "For legacy 10x h5 files you must specify the genome " + "if more than one is present. " + f"Available genomes are: {children}" + ) + raise ValueError(msg) + genome = children[0] + elif genome not in children: + msg = ( + f"Could not find genome {genome!r} in {f.filename}. " + f"Available genomes are: {children}" + ) + raise ValueError(msg) + + dsets = {} + _collect_datasets(dsets, f[genome]) + + # AnnData works with csr matrices + # 10x stores the transposed data, so we do the transposition right away + from scipy.sparse import csr_matrix # noqa: TID251 + + n_cols, n_rows = dsets["shape"] + data = dsets["data"] + if dsets["data"].dtype == np.dtype("int32"): + data = dsets["data"].view("float32") + data[:] = dsets["data"] + matrix = csr_matrix( + (data, dsets["indices"], dsets["indptr"]), + shape=(n_rows, n_cols), + ) + # the csc matrix is automatically the transposed csr matrix + # as scanpy expects it, so, no need for a further transpostion + adata = AnnData( + matrix, + obs=dict(obs_names=dsets["barcodes"].astype(str)), + var=dict( + var_names=dsets["gene_names"].astype(str), + gene_ids=dsets["genes"].astype(str), + ), + ) + return adata + + +@deprecated("Use `squidpy.read.visium` instead.") +def read_visium( + path: Path | str, + genome: str | None = None, + *, + count_file: str = "filtered_feature_bc_matrix.h5", + library_id: str | None = None, + load_images: bool | None = True, + source_image_path: Path | str | None = None, +) -> AnnData: + r"""Read 10x-Genomics-formatted visum dataset. + + .. deprecated:: 1.11.0 + Use :func:`squidpy.read.visium` instead. + + In addition to reading regular 10x output, + this looks for the `spatial` folder and loads images, + coordinates and scale factors. + Based on the `Space Ranger output docs`_. + + See :func:`~scanpy.pl.spatial` for a compatible plotting function. + + .. _Space Ranger output docs: + + Parameters + ---------- + path + Path to directory for visium datafiles. + genome + Filter expression to genes within this genome. + count_file + Which file in the passed directory to use as the count file. Typically would be one of: + 'filtered_feature_bc_matrix.h5' or 'raw_feature_bc_matrix.h5'. + library_id + Identifier for the visium library. Can be modified when concatenating multiple adata objects. + source_image_path + Path to the high-resolution tissue image. Path will be included in + `.uns["spatial"][library_id]["metadata"]["source_image_path"]`. + + Returns + ------- + Annotated data matrix, where observations/cells are named by their + barcode and variables/genes by gene name. Stores the following information: + + :attr:`~anndata.AnnData.X` + The data matrix is stored + :attr:`~anndata.AnnData.obs_names` + Cell names + :attr:`~anndata.AnnData.var_names` + Gene names for a feature barcode matrix, probe names for a probe bc matrix + :attr:`~anndata.AnnData.var`\ `['gene_ids']` + Gene IDs + :attr:`~anndata.AnnData.var`\ `['feature_types']` + Feature types + :attr:`~anndata.AnnData.obs`\ `[filtered_barcodes]` + filtered barcodes if present in the matrix + :attr:`~anndata.AnnData.var` + Any additional metadata present in /matrix/features is read in. + :attr:`~anndata.AnnData.uns`\ `['spatial']` + Dict of spaceranger output files with 'library_id' as key + :attr:`~anndata.AnnData.uns`\ `['spatial'][library_id]['images']` + Dict of images (`'hires'` and `'lowres'`) + :attr:`~anndata.AnnData.uns`\ `['spatial'][library_id]['scalefactors']` + Scale factors for the spots + :attr:`~anndata.AnnData.uns`\ `['spatial'][library_id]['metadata']` + Files metadata: 'chemistry_description', 'software_version', 'source_image_path' + :attr:`~anndata.AnnData.obsm`\ `['spatial']` + Spatial spot coordinates, usable as `basis` by :func:`~scanpy.pl.embedding`. + + """ + path = Path(path) + adata = read_10x_h5(path / count_file, genome=genome) + + adata.uns["spatial"] = dict() + + from h5py import File + + with File(path / count_file, mode="r") as f: + attrs = dict(f.attrs) + if library_id is None: + library_id = str(attrs.pop("library_ids")[0], "utf-8") + + adata.uns["spatial"][library_id] = dict() + + if load_images: + tissue_positions_file = ( + path / "spatial/tissue_positions.csv" + if (path / "spatial/tissue_positions.csv").exists() + else path / "spatial/tissue_positions_list.csv" + ) + files = dict( + tissue_positions_file=tissue_positions_file, + scalefactors_json_file=path / "spatial/scalefactors_json.json", + hires_image=path / "spatial/tissue_hires_image.png", + lowres_image=path / "spatial/tissue_lowres_image.png", + ) + + # check if files exists, continue if images are missing + for f in files.values(): + if not f.exists(): + if any(x in str(f) for x in ["hires_image", "lowres_image"]): + logg.warning( + f"You seem to be missing an image file.\nCould not find {f}." + ) + else: + msg = f"Could not find {f}" + raise OSError(msg) + + adata.uns["spatial"][library_id]["images"] = dict() + for res in ["hires", "lowres"]: + try: + adata.uns["spatial"][library_id]["images"][res] = imread( + str(files[f"{res}_image"]) + ) + except Exception as e: # noqa: PERF203 + msg = f"Could not find '{res}_image'" + raise OSError(msg) from e + + # read json scalefactors + adata.uns["spatial"][library_id]["scalefactors"] = json.loads( + files["scalefactors_json_file"].read_bytes() + ) + + adata.uns["spatial"][library_id]["metadata"] = { + k: (str(attrs[k], "utf-8") if isinstance(attrs[k], bytes) else attrs[k]) + for k in ("chemistry_description", "software_version") + if k in attrs + } + + # read coordinates + positions = pd.read_csv( + files["tissue_positions_file"], + header=0 if tissue_positions_file.name == "tissue_positions.csv" else None, + index_col=0, + ) + positions.columns = [ + "in_tissue", + "array_row", + "array_col", + "pxl_col_in_fullres", + "pxl_row_in_fullres", + ] + + adata.obs = adata.obs.join(positions, how="left") + + adata.obsm["spatial"] = adata.obs[ + ["pxl_row_in_fullres", "pxl_col_in_fullres"] + ].to_numpy() + adata.obs.drop( + columns=["pxl_row_in_fullres", "pxl_col_in_fullres"], + inplace=True, + ) + + # put image path in uns + if source_image_path is not None: + # get an absolute path + source_image_path = str(Path(source_image_path).resolve()) + adata.uns["spatial"][library_id]["metadata"]["source_image_path"] = str( + source_image_path + ) + + return adata + + +@old_positionals("var_names", "make_unique", "cache", "cache_compression", "gex_only") +def read_10x_mtx( + path: Path | str, + *, + var_names: Literal["gene_symbols", "gene_ids"] = "gene_symbols", + make_unique: bool = True, + cache: bool = False, + cache_compression: Literal["gzip", "lzf"] | None | Empty = _empty, + gex_only: bool = True, + prefix: str | None = None, +) -> AnnData: + """Read 10x-Genomics-formatted mtx directory. + + Parameters + ---------- + path + Path to directory for `.mtx` and `.tsv` files, + e.g. './filtered_gene_bc_matrices/hg19/'. + var_names + The variables index. + make_unique + Whether to make the variables index unique by appending '-1', + '-2' etc. or not. + cache + If `False`, read from source, if `True`, read from fast 'h5ad' cache. + cache_compression + See the h5py :ref:`dataset_compression`. + (Default: `settings.cache_compression`) + gex_only + Only keep 'Gene Expression' data and ignore other feature types, + e.g. 'Antibody Capture', 'CRISPR Guide Capture', or 'Custom' + prefix + Any prefix before `matrix.mtx`, `genes.tsv` and `barcodes.tsv`. For instance, + if the files are named `patientA_matrix.mtx`, `patientA_genes.tsv` and + `patientA_barcodes.tsv` the prefix is `patientA_`. + (Default: no prefix) + + Returns + ------- + An :class:`~anndata.AnnData` object + + """ + path = Path(path) + prefix = "" if prefix is None else prefix + is_legacy = (path / f"{prefix}genes.tsv").is_file() + adata = _read_10x_mtx( + path, + var_names=var_names, + make_unique=make_unique, + cache=cache, + cache_compression=cache_compression, + prefix=prefix, + is_legacy=is_legacy, + ) + if is_legacy or not gex_only: + return adata + gex_rows = adata.var["feature_types"] == "Gene Expression" + return adata[:, gex_rows].copy() + + +def _read_10x_mtx( + path: Path, + *, + var_names: Literal["gene_symbols", "gene_ids"] = "gene_symbols", + make_unique: bool = True, + cache: bool = False, + cache_compression: Literal["gzip", "lzf"] | None | Empty = _empty, + prefix: str = "", + is_legacy: bool, +) -> AnnData: + """Read mex from output from Cell Ranger v2- or v3+.""" + suffix = "" if is_legacy else ".gz" + adata = read( + path / f"{prefix}matrix.mtx{suffix}", + cache=cache, + cache_compression=cache_compression, + ).T # transpose the data + genes = pd.read_csv( + path / f"{prefix}{'genes' if is_legacy else 'features'}.tsv{suffix}", + header=None, + sep="\t", + ) + if var_names == "gene_symbols": + var_names_idx = pd.Index(genes[1].values) + if make_unique: + var_names_idx = anndata.utils.make_index_unique(var_names_idx) + adata.var_names = var_names_idx + adata.var["gene_ids"] = genes[0].values + elif var_names == "gene_ids": + adata.var_names = genes[0].values + adata.var["gene_symbols"] = genes[1].values + else: + msg = "`var_names` needs to be 'gene_symbols' or 'gene_ids'" + raise ValueError(msg) + if not is_legacy: + adata.var["feature_types"] = genes[2].values + barcodes = pd.read_csv(path / f"{prefix}barcodes.tsv{suffix}", header=None) + adata.obs_names = barcodes[0].values + return adata + + +@old_positionals("ext", "compression", "compression_opts") +def write( + filename: Path | str, + adata: AnnData, + *, + ext: Literal["h5", "csv", "txt", "npz"] | None = None, + compression: Literal["gzip", "lzf"] | None = "gzip", + compression_opts: int | None = None, +): + """Write :class:`~anndata.AnnData` objects to file. + + Parameters + ---------- + filename + If the filename has no file extension, it is interpreted as a key for + generating a filename via `sc.settings.writedir / (filename + + sc.settings.file_format_data)`. This is the same behavior as in + :func:`~scanpy.read`. + adata + Annotated data matrix. + ext + File extension from wich to infer file format. If `None`, defaults to + `sc.settings.file_format_data`. + compression + See https://docs.h5py.org/en/latest/high/dataset.html. + compression_opts + See https://docs.h5py.org/en/latest/high/dataset.html. + + """ + filename = Path(filename) # allow passing strings + if is_valid_filename(filename): + ext_ = is_valid_filename(filename, return_ext=True) + if ext is None: + ext = ext_ + elif ext != ext_: + msg = ( + "It suffices to provide the file type by " + "providing a proper extension to the filename." + 'One of "txt", "csv", "h5" or "npz".' + ) + raise ValueError(msg) + else: + key = filename + ext = settings.file_format_data if ext is None else ext + filename = _get_filename_from_key(key, ext) + if ext == "csv": + adata.write_csvs(filename) + else: + adata.write( + filename, compression=compression, compression_opts=compression_opts + ) + + +# ------------------------------------------------------------------------------- +# Reading and writing parameter files +# ------------------------------------------------------------------------------- + + +@old_positionals("as_header") +def read_params( + filename: Path | str, *, as_header: bool = False +) -> dict[str, int | float | bool | str | None]: + """Read parameter dictionary from text file. + + Assumes that parameters are specified in the format:: + + par1 = value1 + par2 = value2 + + Comments that start with '#' are allowed. + + Parameters + ---------- + filename + Filename of data file. + asheader + Read the dictionary from the header (comment section) of a file. + + Returns + ------- + Dictionary that stores parameters. + + """ + filename = Path(filename) # allow passing str objects + from collections import OrderedDict + + params = OrderedDict([]) + for line_raw in filename.open(): + if "=" in line_raw and (not as_header or line_raw.startswith("#")): + line = line_raw[1:] if line_raw.startswith("#") else line_raw + key, val = line.split("=") + key = key.strip() + val = val.strip() + params[key] = convert_string(val) + return params + + +def write_params(path: Path | str, *args, **maps): + """Write parameters to file, so that it's readable by read_params. + + Uses INI file format. + """ + path = Path(path) + if not path.parent.is_dir(): + path.parent.mkdir(parents=True) + if len(args) == 1: + maps[None] = args[0] + with path.open("w") as f: + for header, map in maps.items(): + if header is not None: + f.write(f"[{header}]\n") + for key, val in map.items(): + f.write(f"{key} = {val}\n") + + +# ------------------------------------------------------------------------------- +# Reading and Writing data files +# ------------------------------------------------------------------------------- + + +def _read( # noqa: PLR0912, PLR0915 + filename: Path, + *, + backed=None, + sheet=None, + ext=None, + delimiter=None, + first_column_names=None, + backup_url=None, + cache=False, + cache_compression=None, + suppress_cache_warning=False, + **kwargs, +): + if ext is not None and ext not in avail_exts: + msg = f"Please provide one of the available extensions.\n{avail_exts}" + raise ValueError(msg) + else: + ext = is_valid_filename(filename, return_ext=True, ext=ext) + is_present = _check_datafile_present_and_download(filename, backup_url=backup_url) + if not is_present: + logg.debug(f"... did not find original file {filename}") + # read hdf5 files + if ext in {"h5", "h5ad"}: + if sheet is None: + return read_h5ad(filename, backed=backed) + else: + logg.debug(f"reading sheet {sheet} from file {filename}") + return read_hdf(filename, sheet) + # read other file types + path_cache: Path = settings.cachedir / _slugify(filename).replace( + f".{ext}", ".h5ad" + ) + if path_cache.suffix in {".gz", ".bz2"}: + path_cache = path_cache.with_suffix("") + if cache and path_cache.is_file(): + logg.info(f"... reading from cache file {path_cache}") + return read_h5ad(path_cache) + + if not is_present: + msg = f"Did not find file {filename}." + raise FileNotFoundError(msg) + logg.debug(f"reading {filename}") + if not cache and not suppress_cache_warning: + logg.hint( + "This might be very slow. Consider passing `cache=True`, " + "which enables much faster reading from a cache file." + ) + # do the actual reading + if ext in {"xlsx", "xls"}: + if sheet is None: + msg = "Provide `sheet` parameter when reading '.xlsx' files." + raise ValueError(msg) + else: + adata = read_excel(filename, sheet) + elif ext in {"mtx", "mtx.gz"}: + adata = read_mtx(filename) + elif ext == "csv": + if delimiter is None: + delimiter = "," + adata = read_csv( + filename, first_column_names=first_column_names, delimiter=delimiter + ) + elif ext in {"txt", "tab", "data", "tsv"}: + if ext == "data": + logg.hint( + "... assuming '.data' means tab or white-space separated text file" + ) + logg.hint("change this by passing `ext` to sc.read") + adata = read_text(filename, delimiter, first_column_names=first_column_names) + elif ext == "soft.gz": + adata = _read_softgz(filename) + elif ext == "loom": + adata = read_loom(filename=filename, **kwargs) + else: + msg = f"Unknown extension {ext}." + raise ValueError(msg) + if cache: + logg.info( + f"... writing an {settings.file_format_data} " + "cache file to speedup reading next time" + ) + if cache_compression is _empty: + cache_compression = settings.cache_compression + if not path_cache.parent.is_dir(): + path_cache.parent.mkdir(parents=True) + # write for faster reading when calling the next time + adata.write(path_cache, compression=cache_compression) + return adata + + +def _slugify(path: str | PurePath) -> str: + """Make a path into a filename.""" + if not isinstance(path, PurePath): + path = PurePath(path) + parts = list(path.parts) + if parts[0] == "/": + parts.pop(0) + elif len(parts[0]) == 3 and parts[0][1:] == ":\\": + parts[0] = parts[0][0] # C:\ → C + filename = "-".join(parts) + assert "/" not in filename, filename + assert not filename[1:].startswith(":"), filename + return filename + + +def _read_softgz(filename: str | bytes | Path | BinaryIO) -> AnnData: + """Read a SOFT format data file. + + The SOFT format is documented here + https://www.ncbi.nlm.nih.gov/geo/info/soft.html. + + Notes + ----- + The function is based on a script by Kerby Shedden. + https://dept.stat.lsa.umich.edu/~kshedden/Python-Workshop/gene_expression_comparison.html + + """ + import gzip + + with gzip.open(filename, mode="rt") as file: + # The header part of the file contains information about the + # samples. Read that information first. + samples_info = {} + for line in file: + if line.startswith("!dataset_table_begin"): + break + elif line.startswith("!subset_description"): + subset_description = line.split("=")[1].strip() + elif line.startswith("!subset_sample_id"): + subset_ids = line.split("=")[1].split(",") + subset_ids = [x.strip() for x in subset_ids] + for k in subset_ids: + samples_info[k] = subset_description + # Next line is the column headers (sample id's) + sample_names = file.readline().strip().split("\t") + # The column indices that contain gene expression data + indices = [i for i, x in enumerate(sample_names) if x.startswith("GSM")] + # Restrict the column headers to those that we keep + sample_names = [sample_names[i] for i in indices] + # Get a list of sample labels + groups = [samples_info[k] for k in sample_names] + # Read the gene expression data as a list of lists, also get the gene + # identifiers + gene_names, x = [], [] + for line in file: + # This is what signals the end of the gene expression data + # section in the file + if line.startswith("!dataset_table_end"): + break + v = line.split("\t") + # Extract the values that correspond to gene expression measures + # and convert the strings to numbers + x.append([float(v[i]) for i in indices]) + gene_names.append(v[1]) + # Convert the Python list of lists to a Numpy array and transpose to match + # the Scanpy convention of storing samples in rows and variables in colums. + x = np.array(x).T + obs = pd.DataFrame({"groups": groups}, index=sample_names) + var = pd.DataFrame(index=gene_names) + return AnnData(X=x, obs=obs, var=var) + + +# ------------------------------------------------------------------------------- +# Type conversion +# ------------------------------------------------------------------------------- + + +def is_float(string: str) -> float: + """Check whether string is float. + + See Also + -------- + https://stackoverflow.com/questions/736043/checking-if-a-string-can-be-converted-to-float-in-python + + """ + try: + float(string) + return True + except ValueError: + return False + + +def is_int(string: str) -> bool: + """Check whether string is integer.""" + try: + int(string) + return True + except ValueError: + return False + + +def convert_bool(string: str) -> tuple[bool, bool]: + """Check whether string is boolean.""" + if string == "True": + return True, True + elif string == "False": + return True, False + else: + return False, False + + +def convert_string(string: str) -> int | float | bool | str | None: + """Convert string to int, float or bool.""" + if is_int(string): + return int(string) + elif is_float(string): + return float(string) + elif convert_bool(string)[0]: + return convert_bool(string)[1] + elif string == "None": + return None + else: + return string + + +# ------------------------------------------------------------------------------- +# Helper functions for reading and writing +# ------------------------------------------------------------------------------- + + +def get_used_files(): + """Get files used by processes with name scanpy.""" + import psutil + + loop_over_scanpy_processes = ( + proc for proc in psutil.process_iter() if proc.name() == "scanpy" + ) + filenames = [] + for proc in loop_over_scanpy_processes: + try: + flist = proc.open_files() + filenames.extend(nt.path for nt in flist) + # This catches a race condition where a process ends + # before we can examine its files + except psutil.NoSuchProcess: # noqa: PERF203 + pass + return set(filenames) + + +def _get_filename_from_key(key, ext=None) -> Path: + ext = settings.file_format_data if ext is None else ext + return settings.writedir / f"{key}.{ext}" + + +def _download(url: str, path: Path): + from urllib.error import URLError + from urllib.request import Request, urlopen + + from tqdm.auto import tqdm + + blocksize = 1024 * 8 + blocknum = 0 + + try: + req = Request(url, headers={"User-agent": "scanpy-user"}) + + try: + open_url = urlopen(req) + except URLError: + if not url.startswith("https://"): + raise # No need to try using certifi + + msg = "Failed to open the url with default certificates." + try: + from certifi import where + except ImportError as e: + add_note(e, f"{msg} Please install `certifi` and try again.") + raise + else: + logg.warning(f"{msg} Trying to use certifi.") + + from ssl import create_default_context + + open_url = urlopen(req, context=create_default_context(cafile=where())) + + with open_url as resp: + total = resp.info().get("content-length", None) + with ( + tqdm( + unit="B", + unit_scale=True, + miniters=1, + unit_divisor=1024, + total=total if total is None else int(total), + ) as t, + path.open("wb") as f, + ): + block = resp.read(blocksize) + while block: + f.write(block) + blocknum += 1 + t.update(len(block)) + block = resp.read(blocksize) + + except (KeyboardInterrupt, Exception): + # Make sure file doesn’t exist half-downloaded + if path.is_file(): + path.unlink() + raise + + +def _check_datafile_present_and_download(path: Path, backup_url=None): + """Check whether the file is present, otherwise download.""" + path = Path(path) + if path.is_file(): + return True + if backup_url is None: + return False + logg.info( + f"try downloading from url\n{backup_url}\n" + "... this may take a while but only happens once" + ) + if not path.parent.is_dir(): + logg.info(f"creating directory {path.parent}/ for saving data") + path.parent.mkdir(parents=True) + + _download(backup_url, path) + return True + + +@overload +def is_valid_filename( + filename: Path, *, return_ext: Literal[False] = False, ext: str | None = None +) -> bool: ... +@overload +def is_valid_filename( + filename: Path, *, return_ext: Literal[True], ext: str | None = None +) -> str: ... +def is_valid_filename( + filename: Path, *, return_ext: bool = False, ext: str | None = None +) -> str | bool: + """Check whether the argument is a filename.""" + ext_from_file = filename.suffixes + if ext is not None: + if not (joined_file_ext := ".".join(ext_from_file)).endswith(ext): + msg = f"{joined_file_ext} does not end in expected extension {ext}" + raise ValueError(msg) + return ext if return_ext else True + if len(ext_from_file) > 2: + logg.warning( + f"Your filename has more than two extensions: {ext_from_file}.\n" + f"Only considering the two last: {ext_from_file[-2:]}." + ) + ext_from_file = ext_from_file[-2:] + + # cases for gzipped/bzipped text files + if ( + len(ext_from_file) == 2 + and ext_from_file[0][1:] in text_exts + and ext_from_file[1][1:] in ("gz", "bz2") + ): + return ext_from_file[0][1:] if return_ext else True + elif ext_from_file and ext_from_file[-1][1:] in avail_exts: + return ext_from_file[-1][1:] if return_ext else True + elif "".join(ext_from_file) == ".soft.gz": + return "soft.gz" if return_ext else True + elif "".join(ext_from_file) == ".mtx.gz": + return "mtx.gz" if return_ext else True + elif not return_ext: + return False + msg = f"""\ +{filename!r} does not end on a valid extension. +Please, provide one of the available extensions. +{avail_exts} +Text files with .gz and .bz2 extensions are also supported.\ +""" + raise ValueError(msg) diff --git a/.venv/lib/python3.11/site-packages/scanpy/sim_models/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/sim_models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ea6d2ba410743d0d53f34987c465b0effacf3a20 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/sim_models/__init__.py @@ -0,0 +1 @@ +"""Package containing the models for simulating scRNA-seq data.""" diff --git a/.venv/lib/python3.11/site-packages/scanpy/sim_models/krumsiek11.txt b/.venv/lib/python3.11/site-packages/scanpy/sim_models/krumsiek11.txt new file mode 100644 index 0000000000000000000000000000000000000000..9e86952ff553619451aa9deef30944a828b28bac --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/sim_models/krumsiek11.txt @@ -0,0 +1,54 @@ +# See Table 1 in Krumsiek et al. (2011), p. 3 or +# Table 1, in Suppl. Mat. of Moignard et al. (2015), p. 28. +# +# For each "variable = ", there must be a right hand side: +# either an empty string or a python-style logical expression +# involving variable names, "or", "and", "(", ")". +# The order of equations matters! +# +# modelType = hill +# invTimeStep = 0.02 +# +# boolean update rules: +Gata2 = Gata2 and not (Gata1 and Fog1) and not Pu.1 +Gata1 = (Gata1 or Gata2 or Fli1) and not Pu.1 +Fog1 = Gata1 +EKLF = Gata1 and not Fli1 +Fli1 = Gata1 and not EKLF +SCL = Gata1 and not Pu.1 +Cebpa = Cebpa and not (Gata1 and Fog1 and SCL) +Pu.1 = (Cebpa or Pu.1) and not (Gata1 or Gata2) +cJun = Pu.1 and not Gfi1 +EgrNab = (Pu.1 and cJun) and not Gfi1 +Gfi1 = Cebpa and not EgrNab +# coupling list: +Gata2 Gata2 1.0 +Gata2 Gata1 -0.1 +Gata2 Fog1 -1.0 +Gata2 Pu.1 -1.15 +Gata1 Gata2 1.0 +Gata1 Gata1 0.1 +Gata1 Fli1 1.0 +Gata1 Pu.1 -1.21 +Fog1 Gata1 0.1 +EKLF Gata1 0.2 +EKLF Fli1 -1.0 +Fli1 Gata1 0.2 +Fli1 EKLF -1.0 +SCL Gata1 1.0 +SCL Pu.1 -1.0 +Cebpa Gata1 -1.0 +Cebpa Fog1 -1.0 +Cebpa SCL -1.0 +Cebpa Cebpa 10.0 +Pu.1 Gata2 -1.0 +Pu.1 Gata1 -1.0 +Pu.1 Cebpa 10.0 +Pu.1 Pu.1 10.0 +cJun Pu.1 1.0 +cJun Gfi1 -1.0 +EgrNab Pu.1 1.0 +EgrNab cJun 1.0 +EgrNab Gfi1 -1.3 +Gfi1 Cebpa 1.0 +Gfi1 EgrNab -5.0 diff --git a/.venv/lib/python3.11/site-packages/scanpy/sim_models/krumsiek11_params.txt b/.venv/lib/python3.11/site-packages/scanpy/sim_models/krumsiek11_params.txt new file mode 100644 index 0000000000000000000000000000000000000000..29324b98b7528ad338388a2bb7a7bf37f48c75e6 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/sim_models/krumsiek11_params.txt @@ -0,0 +1,8 @@ +model = SCANPY_PATH/sim_models/krumsiek11.txt +tmax = 800 +branching = True +nrRealizations = 4 +noiseObs = 0 +noiseDyn = 0.001 +step = 5 +seed = 0 diff --git a/.venv/lib/python3.11/site-packages/scanpy/sim_models/toggleswitch.txt b/.venv/lib/python3.11/site-packages/scanpy/sim_models/toggleswitch.txt new file mode 100644 index 0000000000000000000000000000000000000000..b7af38921d84ed9494165c8f68456a47db543c55 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/sim_models/toggleswitch.txt @@ -0,0 +1,16 @@ +# For each "variable = ", there must be a right hand side: +# either an empty string or a python-style logical expression +# involving variable names, "or", "and", "(", ")". +# The order of equations matters! +# +# modelType = hill +# invTimeStep = 0.1 +# +# boolean update rules: +0 = 0 and not 1 +1 = 1 and not 0 +# coupling list: +0 0 1.0 +0 1 -1.0 +1 1 1.0 +1 0 -1.0 diff --git a/.venv/lib/python3.11/site-packages/scanpy/sim_models/toggleswitch_params.txt b/.venv/lib/python3.11/site-packages/scanpy/sim_models/toggleswitch_params.txt new file mode 100644 index 0000000000000000000000000000000000000000..03312b214e91050da3cf147dc52c6edd2b9b8217 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/sim_models/toggleswitch_params.txt @@ -0,0 +1,8 @@ +model = SCANPY_PATH/sim_models/toggleswitch.txt +tmax = 100 +branching = True +nrRealizations = 2 +noiseObs = 0.01 +noiseDyn = 0.001 +step = 1 +seed = 0 diff --git a/.venv/lib/python3.11/site-packages/scanpy/tools/__init__.py b/.venv/lib/python3.11/site-packages/scanpy/tools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e426470b8433a11546992372bb07044e5a33d47a --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/tools/__init__.py @@ -0,0 +1,61 @@ +"""Analysis tools.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ._dendrogram import dendrogram +from ._diffmap import diffmap +from ._dpt import dpt +from ._draw_graph import draw_graph +from ._embedding_density import embedding_density +from ._ingest import ( + Ingest, # noqa: F401 + ingest, +) +from ._leiden import leiden +from ._louvain import louvain +from ._marker_gene_overlap import marker_gene_overlap +from ._paga import ( + paga, + paga_compare_paths, # noqa: F401 + paga_degrees, # noqa: F401 + paga_expression_entropies, # noqa: F401 +) +from ._rank_genes_groups import filter_rank_genes_groups, rank_genes_groups +from ._score_genes import score_genes, score_genes_cell_cycle +from ._sim import sim +from ._tsne import tsne +from ._umap import umap + +if TYPE_CHECKING: + from typing import Any + + +def __getattr__(name: str) -> Any: + if name == "pca": + from ..preprocessing import pca + + return pca + raise AttributeError(name) + + +__all__ = [ + "dendrogram", + "diffmap", + "dpt", + "draw_graph", + "embedding_density", + "filter_rank_genes_groups", + "ingest", + "leiden", + "louvain", + "marker_gene_overlap", + "paga", + "rank_genes_groups", + "score_genes", + "score_genes_cell_cycle", + "sim", + "tsne", + "umap", +] diff --git a/.venv/lib/python3.11/site-packages/scanpy/tools/_dendrogram.py b/.venv/lib/python3.11/site-packages/scanpy/tools/_dendrogram.py new file mode 100644 index 0000000000000000000000000000000000000000..8badbfaafa6f93e0e62721ae347f80e8f718b613 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/tools/_dendrogram.py @@ -0,0 +1,194 @@ +"""Computes a dendrogram based on a given categorical observation.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pandas as pd +from pandas.api.types import CategoricalDtype + +from .. import logging as logg +from .._compat import old_positionals +from .._utils import _doc_params, raise_not_implemented_error_if_backed_type +from ..neighbors._doc import doc_n_pcs, doc_use_rep +from ._utils import _choose_representation + +if TYPE_CHECKING: + from collections.abc import Sequence + from typing import Any + + from anndata import AnnData + + +@old_positionals( + "n_pcs", + "use_rep", + "var_names", + "use_raw", + "cor_method", + "linkage_method", + "optimal_ordering", + "key_added", + "inplace", +) +@_doc_params(n_pcs=doc_n_pcs, use_rep=doc_use_rep) +def dendrogram( # noqa: PLR0913 + adata: AnnData, + groupby: str | Sequence[str], + *, + n_pcs: int | None = None, + use_rep: str | None = None, + var_names: Sequence[str] | None = None, + use_raw: bool | None = None, + cor_method: str = "pearson", + linkage_method: str = "complete", + optimal_ordering: bool = False, + key_added: str | None = None, + inplace: bool = True, +) -> dict[str, Any] | None: + """Compute a hierarchical clustering for the given `groupby` categories. + + By default, the PCA representation is used unless `.X` + has less than 50 variables. + + Alternatively, a list of `var_names` (e.g. genes) can be given. + + Average values of either `var_names` or components are used + to compute a correlation matrix. + + The hierarchical clustering can be visualized using + :func:`scanpy.pl.dendrogram` or multiple other visualizations + that can include a dendrogram: :func:`~scanpy.pl.matrixplot`, + :func:`~scanpy.pl.heatmap`, :func:`~scanpy.pl.dotplot`, + and :func:`~scanpy.pl.stacked_violin`. + + .. note:: + The computation of the hierarchical clustering is based on predefined + groups and not per cell. The correlation matrix is computed using by + default pearson but other methods are available. + + Parameters + ---------- + adata + Annotated data matrix + {n_pcs} + {use_rep} + var_names + List of var_names to use for computing the hierarchical clustering. + If `var_names` is given, then `use_rep` and `n_pcs` are ignored. + use_raw + Only when `var_names` is not None. + Use `raw` attribute of `adata` if present. + cor_method + Correlation method to use. + Options are 'pearson', 'kendall', and 'spearman' + linkage_method + Linkage method to use. See :func:`scipy.cluster.hierarchy.linkage` + for more information. + optimal_ordering + Same as the optimal_ordering argument of :func:`scipy.cluster.hierarchy.linkage` + which reorders the linkage matrix so that the distance between successive + leaves is minimal. + key_added + By default, the dendrogram information is added to + `.uns[f'dendrogram_{{groupby}}']`. + Notice that the `groupby` information is added to the dendrogram. + inplace + If `True`, adds dendrogram information to `adata.uns[key_added]`, + else this function returns the information. + + Returns + ------- + Returns `None` if `inplace=True`, else returns a `dict` with dendrogram information. Sets the following field if `inplace=True`: + + `adata.uns[f'dendrogram_{{group_by}}' | key_added]` : :class:`dict` + Dendrogram information. + + Examples + -------- + >>> import scanpy as sc + >>> adata = sc.datasets.pbmc68k_reduced() + >>> sc.tl.dendrogram(adata, groupby="bulk_labels") + >>> sc.pl.dendrogram(adata, groupby="bulk_labels") # doctest: +SKIP + + >>> markers = ["C1QA", "PSAP", "CD79A", "CD79B", "CST3", "LYZ"] + >>> sc.pl.dotplot(adata, markers, groupby="bulk_labels", dendrogram=True) + + """ + raise_not_implemented_error_if_backed_type(adata.X, "dendrogram") + if isinstance(groupby, str): + # if not a list, turn into a list + groupby = [groupby] + for group in groupby: + if group not in adata.obs: + msg = ( + "groupby has to be a valid observation. " + f"Given value: {group}, valid observations: {adata.obs.columns.tolist()}" + ) + raise ValueError(msg) + if not isinstance(adata.obs[group].dtype, CategoricalDtype): + msg = ( + "groupby has to be a categorical observation. " + f"Given value: {group}, Column type: {adata.obs[group].dtype}" + ) + raise ValueError(msg) + + if var_names is None: + rep_df = pd.DataFrame( + _choose_representation(adata, use_rep=use_rep, n_pcs=n_pcs) + ) + categorical = adata.obs[groupby[0]] + if len(groupby) > 1: + for group in groupby[1:]: + # create new category by merging the given groupby categories + categorical = ( + categorical.astype(str) + "_" + adata.obs[group].astype(str) + ).astype("category") + categorical.name = "_".join(groupby) + + rep_df.set_index(categorical, inplace=True) + categories: pd.Index = rep_df.index.categories + else: + gene_names = adata.raw.var_names if use_raw else adata.var_names + from ..plotting._anndata import _prepare_dataframe + + categories, rep_df = _prepare_dataframe( + adata, gene_names, groupby, use_raw=use_raw + ) + + # aggregate values within categories using 'mean' + mean_df = ( + rep_df.groupby(level=0, observed=True) + .mean() + .loc[categories] # Fixed ordering for pandas < 2 + ) + + import scipy.cluster.hierarchy as sch + from scipy.spatial import distance + + corr_matrix = mean_df.T.corr(method=cor_method).clip(-1, 1) + corr_condensed = distance.squareform(1 - corr_matrix) + z_var = sch.linkage( + corr_condensed, method=linkage_method, optimal_ordering=optimal_ordering + ) + dendro_info = sch.dendrogram(z_var, labels=list(categories), no_plot=True) + + dat = dict( + linkage=z_var, + groupby=groupby, + use_rep=use_rep, + cor_method=cor_method, + linkage_method=linkage_method, + categories_ordered=dendro_info["ivl"], + categories_idx_ordered=dendro_info["leaves"], + dendrogram_info=dendro_info, + correlation_matrix=corr_matrix.values, + ) + + if inplace: + if key_added is None: + key_added = f"dendrogram_{'_'.join(groupby)}" + logg.info(f"Storing dendrogram info using `.uns[{key_added!r}]`") + adata.uns[key_added] = dat + else: + return dat diff --git a/.venv/lib/python3.11/site-packages/scanpy/tools/_diffmap.py b/.venv/lib/python3.11/site-packages/scanpy/tools/_diffmap.py new file mode 100644 index 0000000000000000000000000000000000000000..baaa21653be954f7d82fe2dc1fefc457a037d433 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/tools/_diffmap.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .._compat import old_positionals +from ._dpt import _diffmap + +if TYPE_CHECKING: + from anndata import AnnData + + from .._utils.random import _LegacyRandom + + +@old_positionals("neighbors_key", "random_state", "copy") +def diffmap( + adata: AnnData, + n_comps: int = 15, + *, + neighbors_key: str | None = None, + random_state: _LegacyRandom = 0, + copy: bool = False, +) -> AnnData | None: + """Diffusion Maps :cite:p:`Coifman2005,Haghverdi2015,Wolf2018`. + + Diffusion maps :cite:p:`Coifman2005` have been proposed for visualizing single-cell + data by :cite:t:`Haghverdi2015`. This tool uses the adapted Gaussian kernel suggested + by :cite:t:`Haghverdi2016` with the implementation of :cite:t:`Wolf2018`. + + The width ("sigma") of the connectivity kernel is implicitly determined by + the number of neighbors used to compute the single-cell graph in + :func:`~scanpy.pp.neighbors`. To reproduce the original implementation + using a Gaussian kernel, use `method=='gauss'` in + :func:`~scanpy.pp.neighbors`. To use an exponential kernel, use the default + `method=='umap'`. Differences between these options shouldn't usually be + dramatic. + + Parameters + ---------- + adata + Annotated data matrix. + n_comps + The number of dimensions of the representation. + neighbors_key + If not specified, diffmap looks in .uns['neighbors'] for neighbors settings + and .obsp['connectivities'] and .obsp['distances'] for connectivities and + distances, respectively (default storage places for pp.neighbors). + If specified, diffmap looks in .uns[neighbors_key] for neighbors settings and + .obsp[.uns[neighbors_key]['connectivities_key']] and + .obsp[.uns[neighbors_key]['distances_key']] for connectivities and distances, + respectively. + random_state + A numpy random seed + copy + Return a copy instead of writing to adata. + + Returns + ------- + Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following fields: + + `adata.obsm['X_diffmap']` : :class:`numpy.ndarray` (dtype `float`) + Diffusion map representation of data, which is the right eigen basis of + the transition matrix with eigenvectors as columns. + + `adata.uns['diffmap_evals']` : :class:`numpy.ndarray` (dtype `float`) + Array of size (number of eigen vectors). + Eigenvalues of transition matrix. + + Notes + ----- + The 0-th column in `adata.obsm["X_diffmap"]` is the steady-state solution, + which is non-informative in diffusion maps. + Therefore, the first diffusion component is at index 1, + e.g. `adata.obsm["X_diffmap"][:,1]` + + """ + if neighbors_key is None: + neighbors_key = "neighbors" + + if neighbors_key not in adata.uns: + msg = "You need to run `pp.neighbors` first to compute a neighborhood graph." + raise ValueError(msg) + if n_comps <= 2: + msg = "Provide any value greater than 2 for `n_comps`. " + raise ValueError(msg) + adata = adata.copy() if copy else adata + _diffmap( + adata, n_comps=n_comps, neighbors_key=neighbors_key, random_state=random_state + ) + return adata if copy else None diff --git a/.venv/lib/python3.11/site-packages/scanpy/tools/_dpt.py b/.venv/lib/python3.11/site-packages/scanpy/tools/_dpt.py new file mode 100644 index 0000000000000000000000000000000000000000..9d3e26acf4ff2417261bfa854444ec6718ecd21e --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/tools/_dpt.py @@ -0,0 +1,1151 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd +import scipy as sp +from natsort import natsorted + +from .. import logging as logg +from .._compat import old_positionals +from ..neighbors import Neighbors, OnFlySymMatrix + +if TYPE_CHECKING: + from collections.abc import Sequence + + from anndata import AnnData + + +def _diffmap(adata, n_comps=15, neighbors_key=None, random_state=0): + start = logg.info(f"computing Diffusion Maps using {n_comps=}(=n_dcs)") + dpt = DPT(adata, neighbors_key=neighbors_key) + dpt.compute_transitions() + dpt.compute_eigen(n_comps=n_comps, random_state=random_state) + adata.obsm["X_diffmap"] = dpt.eigen_basis + adata.uns["diffmap_evals"] = dpt.eigen_values + logg.info( + " finished", + time=start, + deep=( + "added\n" + " 'X_diffmap', diffmap coordinates (adata.obsm)\n" + " 'diffmap_evals', eigenvalues of transition matrix (adata.uns)" + ), + ) + + +@old_positionals( + "n_branchings", "min_group_size", "allow_kendall_tau_shift", "neighbors_key", "copy" +) +def dpt( + adata: AnnData, + n_dcs: int = 10, + *, + n_branchings: int = 0, + min_group_size: float = 0.01, + allow_kendall_tau_shift: bool = True, + neighbors_key: str | None = None, + copy: bool = False, +) -> AnnData | None: + """Infer progression of cells through geodesic distance along the graph :cite:p:`Haghverdi2016,Wolf2019`. + + Reconstruct the progression of a biological process from snapshot + data. `Diffusion Pseudotime` was introduced by :cite:t:`Haghverdi2016` and + implemented within Scanpy :cite:p:`Wolf2018`. Here, we use a further developed + version, which is able to deal with disconnected graphs :cite:p:`Wolf2019` and can + be run in a `hierarchical` mode by setting the parameter + `n_branchings>1`. We recommend, however, to only use + :func:`~scanpy.tl.dpt` for computing pseudotime (`n_branchings=0`) and + to detect branchings via :func:`~scanpy.tl.paga`. For pseudotime, you need + to annotate your data with a root cell. For instance:: + + adata.uns["iroot"] = np.flatnonzero(adata.obs["cell_types"] == "Stem")[0] + + This requires running :func:`~scanpy.pp.neighbors`, first. In order to + reproduce the original implementation of DPT, use `method=='gauss'`. + Using the default `method=='umap'` only leads to minor quantitative + differences, though. + + .. versionadded:: 1.1 + + :func:`~scanpy.tl.dpt` also requires to run + :func:`~scanpy.tl.diffmap` first. As previously, + :func:`~scanpy.tl.dpt` came with a default parameter of ``n_dcs=10`` but + :func:`~scanpy.tl.diffmap` has a default parameter of ``n_comps=15``, + you need to pass ``n_comps=10`` in :func:`~scanpy.tl.diffmap` in order + to exactly reproduce previous :func:`~scanpy.tl.dpt` results. + + Parameters + ---------- + adata + Annotated data matrix. + n_dcs + The number of diffusion components to use. + n_branchings + Number of branchings to detect. + min_group_size + During recursive splitting of branches ('dpt groups') for `n_branchings` + > 1, do not consider groups that contain less than `min_group_size` data + points. If a float, `min_group_size` refers to a fraction of the total + number of data points. + allow_kendall_tau_shift + If a very small branch is detected upon splitting, shift away from + maximum correlation in Kendall tau criterion of :cite:t:`Haghverdi2016` to + stabilize the splitting. + neighbors_key + If not specified, dpt looks in .uns['neighbors'] for neighbors settings + and .obsp['connectivities'] and .obsp['distances'] for connectivities and + distances, respectively (default storage places for pp.neighbors). + If specified, dpt looks in .uns[neighbors_key] for neighbors settings and + .obsp[.uns[neighbors_key]['connectivities_key']] and + .obsp[.uns[neighbors_key]['distances_key']] for connectivities and distances, + respectively. + copy + Copy instance before computation and return a copy. + Otherwise, perform computation inplace and return `None`. + + Returns + ------- + Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following fields (If `n_branchings==0`, no field `adata.obs['dpt_groups']` will be written): + + `adata.obs['dpt_pseudotime']` : :class:`pandas.Series` (dtype `float`) + Array of dim (number of samples) that stores the pseudotime of each + cell, that is, the DPT distance with respect to the root cell. + `adata.obs['dpt_groups']` : :class:`pandas.Series` (dtype `category`) + Array of dim (number of samples) that stores the subgroup id ('0', + '1', ...) for each cell. The groups typically correspond to + 'progenitor cells', 'undecided cells' or 'branches' of a process. + + Notes + ----- + The tool is similar to the R package `destiny` of :cite:t:`Angerer2015`. + + """ + # standard errors, warnings etc. + adata = adata.copy() if copy else adata + + if neighbors_key is None: + neighbors_key = "neighbors" + if neighbors_key not in adata.uns: + msg = "You need to run `pp.neighbors` and `tl.diffmap` first." + raise ValueError(msg) + if "iroot" not in adata.uns and "xroot" not in adata.var: + logg.warning( + "No root cell found. To compute pseudotime, pass the index or " + "expression vector of a root cell, one of:\n" + " adata.uns['iroot'] = root_cell_index\n" + " adata.var['xroot'] = adata[root_cell_name, :].X" + ) + if "X_diffmap" not in adata.obsm: + logg.warning( + "Trying to run `tl.dpt` without prior call of `tl.diffmap`. " + "Falling back to `tl.diffmap` with default parameters." + ) + _diffmap(adata, neighbors_key=neighbors_key) + # start with the actual computation + dpt = DPT( + adata, + n_dcs=n_dcs, + min_group_size=min_group_size, + n_branchings=n_branchings, + allow_kendall_tau_shift=allow_kendall_tau_shift, + neighbors_key=neighbors_key, + ) + start = logg.info(f"computing Diffusion Pseudotime using {n_dcs=}") + if n_branchings > 1: + logg.info(" this uses a hierarchical implementation") + if dpt.iroot is not None: + dpt._set_pseudotime() # pseudotimes are distances from root point + adata.uns["iroot"] = ( + dpt.iroot + ) # update iroot, might have changed when subsampling, for example + adata.obs["dpt_pseudotime"] = dpt.pseudotime + # detect branchings and partition the data into segments + if n_branchings > 0: + dpt.branchings_segments() + adata.obs["dpt_groups"] = pd.Categorical( + values=dpt.segs_names.astype("U"), + categories=natsorted(np.array(dpt.segs_names_unique).astype("U")), + ) + # the "change points" separate segments in the ordering above + adata.uns["dpt_changepoints"] = dpt.changepoints + # the tip points of segments + adata.uns["dpt_grouptips"] = dpt.segs_tips + # the ordering according to segments and pseudotime + ordering_id = np.zeros(adata.n_obs, dtype=int) + for count, idx in enumerate(dpt.indices): + ordering_id[idx] = count + adata.obs["dpt_order"] = ordering_id + adata.obs["dpt_order_indices"] = dpt.indices + logg.info( + " finished", + time=start, + deep=( + "added\n" + + ( + " 'dpt_pseudotime', the pseudotime (adata.obs)" + if dpt.iroot is not None + else "" + ) + + ( + "\n 'dpt_groups', the branching subgroups of dpt (adata.obs)" + "\n 'dpt_order', cell order (adata.obs)" + if n_branchings > 0 + else "" + ) + ), + ) + return adata if copy else None + + +class DPT(Neighbors): + """Hierarchical Diffusion Pseudotime.""" + + def __init__( + self, + adata: AnnData, + *, + n_dcs: int | None = None, + min_group_size: float = 0.01, + n_branchings: int = 0, + allow_kendall_tau_shift: bool = False, + neighbors_key: str | None = None, + ): + super().__init__(adata, n_dcs=n_dcs, neighbors_key=neighbors_key) + self.flavor = "haghverdi16" + self.n_branchings = n_branchings + self.min_group_size = ( + min_group_size + if min_group_size >= 1 + else int(min_group_size * self._adata.shape[0]) + ) + self.passed_adata = adata # just for debugging purposes + self.choose_largest_segment = False + self.allow_kendall_tau_shift = allow_kendall_tau_shift + + def branchings_segments(self): + """Detect branchings and partition the data into corresponding segments. + + Detect all branchings up to `n_branchings`. + + Writes + ------ + segs : :class:`~numpy.ndarray` + Array of dimension (number of segments) × (number of data + points). Each row stores a mask array that defines a segment. + segs_tips : :class:`~numpy.ndarray` + Array of dimension (number of segments) × 2. Each row stores the + indices of the two tip points of each segment. + segs_names : :class:`~numpy.ndarray` + Array of dimension (number of data points). Stores an integer label + for each segment. + """ + self.detect_branchings() + self.postprocess_segments() + self.set_segs_names() + self.order_pseudotime() + + def detect_branchings(self): + """Detect all branchings up to `n_branchings`. + + Writes Attributes + ----------------- + segs : :class:`~numpy.ndarray` + List of integer index arrays. + segs_tips : :class:`~numpy.ndarray` + List of indices of the tips of segments. + """ + logg.debug( + f" detect {self.n_branchings} " + f"branching{'' if self.n_branchings == 1 else 's'}", + ) + # a segment is a subset of points of the data set (defined by the + # indices of the points in the segment) + # initialize the search for branchings with a single segment, + # that is, get the indices of the whole data set + indices_all = np.arange(self._adata.shape[0], dtype=int) + # let's keep a list of segments, the first segment to add is the + # whole data set + segs = [indices_all] + # a segment can as well be defined by the two points that have maximal + # distance in the segment, the "tips" of the segment + # + # the rest of the points in the segment is then defined by demanding + # them to "be close to the line segment that connects the tips", that + # is, for such a point, the normalized added distance to both tips is + # smaller than one: + # (D[tips[0],i] + D[tips[1],i])/D[tips[0],tips[1] < 1 + # of course, this condition is fulfilled by the full cylindrical + # subspace surrounding that line segment, where the radius of the + # cylinder can be infinite + # + # if D denotes a euclidian distance matrix, a line segment is a linear + # object, and the name "line" is justified. if we take the + # diffusion-based distance matrix Dchosen, which approximates geodesic + # distance, with "line", we mean the shortest path between two points, + # which can be highly non-linear in the original space + # + # let us define the tips of the whole data set + if False: # this is safe, but not compatible with on-the-fly computation + tips_all = np.array( + np.unravel_index( + np.argmax(self.distances_dpt), self.distances_dpt.shape + ) + ) + else: + if self.iroot is not None: + tip_0 = np.argmax(self.distances_dpt[self.iroot]) + else: + tip_0 = np.argmax(self.distances_dpt[0]) + tips_all = np.array([tip_0, np.argmax(self.distances_dpt[tip_0])]) + # we keep a list of the tips of each segment + segs_tips = [tips_all] + segs_connects = [[]] + segs_undecided = [True] + segs_adjacency = [[]] + logg.debug( + " do not consider groups with less than " + f"{self.min_group_size} points for splitting" + ) + for ibranch in range(self.n_branchings): + iseg, tips3 = self.select_segment(segs, segs_tips, segs_undecided) + if iseg == -1: + logg.debug(" partitioning converged") + break + logg.debug( + f" branching {ibranch + 1}: split group {iseg}", + ) # [third start end] + # detect branching and update segs and segs_tips + self.detect_branching( + segs=segs, + segs_tips=segs_tips, + segs_connects=segs_connects, + segs_undecided=segs_undecided, + segs_adjacency=segs_adjacency, + iseg=iseg, + tips3=tips3, + ) + # store as class members + self.segs = segs + self.segs_tips = segs_tips + self.segs_undecided = segs_undecided + # the following is a bit too much, but this allows easy storage + self.segs_adjacency = sp.sparse.lil_matrix((len(segs), len(segs)), dtype=float) + self.segs_connects = sp.sparse.lil_matrix((len(segs), len(segs)), dtype=int) + for i, seg_adjacency in enumerate(segs_adjacency): + self.segs_connects[i, seg_adjacency] = segs_connects[i] + for i in range(len(segs)): + for j in range(len(segs)): + self.segs_adjacency[i, j] = self.distances_dpt[ + self.segs_connects[i, j], self.segs_connects[j, i] + ] + self.segs_adjacency = self.segs_adjacency.tocsr() + self.segs_connects = self.segs_connects.tocsr() + + def check_adjacency(self): + n_edges_per_seg = np.sum(self.segs_adjacency > 0, axis=1).A1 + for n_edges in range(1, np.max(n_edges_per_seg) + 1): + for iseg in range(self.segs_adjacency.shape[0]): + if n_edges_per_seg[iseg] == n_edges: + neighbor_segs = ( # noqa: F841 TODO Evaluate whether to assign the variable or not + self.segs_adjacency[iseg].todense().A1 + ) + closest_points_other_segs = [ + seg[np.argmin(self.distances_dpt[self.segs_tips[iseg][0], seg])] + for seg in self.segs + ] + seg = self.segs[iseg] + closest_points_in_segs = [ + seg[np.argmin(self.distances_dpt[tips[0], seg])] + for tips in self.segs_tips + ] + distance_segs = [ + self.distances_dpt[closest_points_other_segs[ipoint], point] + for ipoint, point in enumerate(closest_points_in_segs) + ] + # exclude the first point, the segment itself + closest_segs = np.argsort(distance_segs)[1 : n_edges + 1] + # update adjacency matrix within the loop! + # self.segs_adjacency[iseg, neighbor_segs > 0] = 0 + # self.segs_adjacency[iseg, closest_segs] = np.array(distance_segs)[closest_segs] + # self.segs_adjacency[neighbor_segs > 0, iseg] = 0 + # self.segs_adjacency[closest_segs, iseg] = np.array(distance_segs)[closest_segs].reshape(len(closest_segs), 1) + # n_edges_per_seg = np.sum(self.segs_adjacency > 0, axis=1).A1 + print(iseg, distance_segs, closest_segs) + # print(self.segs_adjacency) + # self.segs_adjacency.eliminate_zeros() + + def select_segment(self, segs, segs_tips, segs_undecided) -> tuple[int, int]: # noqa: PLR0912 + """Out of a list of line segments, choose segment that has the most distant second data point. + + Assume the distance matrix Ddiff is sorted according to seg_idcs. + Compute all the distances. + + Returns + ------- + iseg + Index identifying the position within the list of line segments. + tips3 + Positions of tips within chosen segment. + + """ + scores_tips = np.zeros((len(segs), 4)) + allindices = np.arange(self._adata.shape[0], dtype=int) + for iseg, seg in enumerate(segs): + # do not consider too small segments + if segs_tips[iseg][0] == -1: + continue + # restrict distance matrix to points in segment + if not isinstance(self.distances_dpt, OnFlySymMatrix): + d_seg = self.distances_dpt[np.ix_(seg, seg)] + else: + d_seg = self.distances_dpt.restrict(seg) + third_maximizer = None + if segs_undecided[iseg]: + # check that none of our tips "connects" with a tip of the + # other segments + for jseg in range(len(segs)): + if jseg != iseg: + # take the inner tip, the "second tip" of the segment + for itip in range(2): + if ( + self.distances_dpt[ + segs_tips[jseg][1], segs_tips[iseg][itip] + ] + < 0.5 + * self.distances_dpt[ + segs_tips[iseg][~itip], segs_tips[iseg][itip] + ] + ): + # logg.debug( + # ' group', iseg, 'with tip', segs_tips[iseg][itip], + # 'connects with', jseg, 'with tip', segs_tips[jseg][1], + # ) + # logg.debug(' do not use the tip for "triangulation"') + third_maximizer = itip + # map the global position to the position within the segment + tips = [np.where(allindices[seg] == tip)[0][0] for tip in segs_tips[iseg]] + # find the third point on the segment that has maximal + # added distance from the two tip points + dseg = d_seg[tips[0]] + d_seg[tips[1]] + if not np.isfinite(dseg).any(): + continue + # add this point to tips, it's a third tip, we store it at the first + # position in an array called tips3 + third_tip = np.argmax(dseg) + if third_maximizer is not None: + # find a fourth point that has maximal distance to all three + dseg += d_seg[third_tip] + fourth_tip = np.argmax(dseg) + if fourth_tip != tips[0] and fourth_tip != third_tip: + tips[1] = fourth_tip + dseg -= d_seg[tips[1]] + else: + dseg -= d_seg[third_tip] + tips3 = np.append(tips, third_tip) + # compute the score as ratio of the added distance to the third tip, + # to what it would be if it were on the straight line between the + # two first tips, given by Dseg[tips[:2]] + # if we did not normalize, there would be a danger of simply + # assigning the highest score to the longest segment + score = dseg[tips3[2]] / d_seg[tips3[0], tips3[1]] + score = ( + len(seg) if self.choose_largest_segment else score + ) # simply the number of points + logg.debug( + f" group {iseg} score {score} n_points {len(seg)}" + f"{' (too small)' if len(seg) < self.min_group_size else ''}" + ) + if len(seg) <= self.min_group_size: + score = 0 + # write result + scores_tips[iseg, 0] = score + scores_tips[iseg, 1:] = tips3 + iseg = np.argmax(scores_tips[:, 0]) + if scores_tips[iseg, 0] == 0: + return -1, None + tips3 = scores_tips[iseg, 1:].astype(int) + return iseg, tips3 + + def postprocess_segments(self): + """Convert the format of the segment class members.""" + # make segs a list of mask arrays, it's easier to store + # as there is a hdf5 equivalent + for iseg, seg in enumerate(self.segs): + mask = np.zeros(self._adata.shape[0], dtype=bool) + mask[seg] = True + self.segs[iseg] = mask + # convert to arrays + self.segs = np.array(self.segs) + self.segs_tips = np.array(self.segs_tips) + + def set_segs_names(self): + """Return a single array that stores integer segment labels.""" + segs_names = np.zeros(self._adata.shape[0], dtype=np.int8) + self.segs_names_unique = [] + for iseg, seg in enumerate(self.segs): + segs_names[seg] = iseg + self.segs_names_unique.append(iseg) + self.segs_names = segs_names + + def order_pseudotime(self): + """Define indices that reflect segment and pseudotime order. + + Writes + ------ + indices : :class:`~numpy.ndarray` + Index array of shape n, which stores an ordering of the data points + with respect to increasing segment index and increasing pseudotime. + changepoints : :class:`~numpy.ndarray` + Index array of shape len(ssegs)-1, which stores the indices of + points where the segment index changes, with respect to the ordering + of indices. + """ + # within segs_tips, order tips according to pseudotime + if self.iroot is not None: + for itips, tips in enumerate(self.segs_tips): + if tips[0] != -1: + indices = np.argsort(self.pseudotime[tips]) + self.segs_tips[itips] = self.segs_tips[itips][indices] + else: + logg.debug(f" group {itips} is very small") + # sort indices according to segments + indices = np.argsort(self.segs_names) + segs_names = self.segs_names[indices] + # find changepoints of segments + changepoints = np.arange(indices.size - 1)[np.diff(segs_names) == 1] + 1 + if self.iroot is not None: + pseudotime = self.pseudotime[indices] + for seg in self.segs: + # only consider one segment, it's already ordered by segment + seg_sorted = seg[indices] + # consider the pseudotime on this segment and sort them + seg_indices = np.argsort(pseudotime[seg_sorted]) + # within the segment, order indices according to increasing pseudotime + indices[seg_sorted] = indices[seg_sorted][seg_indices] + # define class members + self.indices = indices + self.changepoints = changepoints + + def detect_branching( # noqa: PLR0912, PLR0915 + self, + *, + segs: Sequence[np.ndarray], + segs_tips: Sequence[np.ndarray], + segs_connects, + segs_undecided, + segs_adjacency, + iseg: int, + tips3: np.ndarray, + ): + """Detect branching on given segment. + + Updates all list parameters inplace. + + Call function _detect_branching and perform bookkeeping on segs and + segs_tips. + + Parameters + ---------- + segs + Dchosen distance matrix restricted to segment. + segs_tips + Stores all tip points for the segments in segs. + iseg + Position of segment under study in segs. + tips3 + The three tip points. They form a 'triangle' that contains the data. + + """ + seg = segs[iseg] + # restrict distance matrix to points in segment + if not isinstance(self.distances_dpt, OnFlySymMatrix): + d_seg = self.distances_dpt[np.ix_(seg, seg)] + else: + d_seg = self.distances_dpt.restrict(seg) + # given the three tip points and the distance matrix detect the + # branching on the segment, return the list ssegs of segments that + # are defined by splitting this segment + result = self._detect_branching(d_seg, tips3, seg) + ssegs, ssegs_tips, _ssegs_adjacency, ssegs_connects, trunk = result + # map back to global indices + for iseg_new, seg_new in enumerate(ssegs): + ssegs[iseg_new] = seg[seg_new] + ssegs_tips[iseg_new] = seg[ssegs_tips[iseg_new]] + ssegs_connects[iseg_new] = list(seg[ssegs_connects[iseg_new]]) + # remove previous segment + segs.pop(iseg) + segs_tips.pop(iseg) + # insert trunk/undecided_cells at same position + segs.insert(iseg, ssegs[trunk]) + segs_tips.insert(iseg, ssegs_tips[trunk]) + # append other segments + segs += [seg for iseg, seg in enumerate(ssegs) if iseg != trunk] + segs_tips += [ + seg_tips for iseg, seg_tips in enumerate(ssegs_tips) if iseg != trunk + ] + if len(ssegs) == 4: + # insert undecided cells at same position + segs_undecided.pop(iseg) + segs_undecided.insert(iseg, True) + # correct edges in adjacency matrix + n_add = len(ssegs) - 1 + prev_connecting_segments = segs_adjacency[iseg].copy() + if self.flavor == "haghverdi16": + segs_adjacency += [[iseg] for i in range(n_add)] + segs_connects += [ + seg_connects + for iseg, seg_connects in enumerate(ssegs_connects) + if iseg != trunk + ] + # TODO Evaluate whether to assign the variable or not + prev_connecting_points = segs_connects[iseg] # noqa: F841 + for jseg in prev_connecting_segments: + iseg_cnt = 0 + for iseg_new, seg_new in enumerate(ssegs): + if iseg_new != trunk: + pos = segs_adjacency[jseg].index(iseg) + connection_to_iseg = segs_connects[jseg][pos] + if connection_to_iseg in seg_new: + kseg = len(segs) - n_add + iseg_cnt + segs_adjacency[jseg][pos] = kseg + pos_2 = segs_adjacency[iseg].index(jseg) + segs_adjacency[iseg].pop(pos_2) + idx = segs_connects[iseg].pop(pos_2) + segs_adjacency[kseg].append(jseg) + segs_connects[kseg].append(idx) + break + iseg_cnt += 1 + segs_adjacency[iseg] += list( + range(len(segs_adjacency) - n_add, len(segs_adjacency)) + ) + segs_connects[iseg] += ssegs_connects[trunk] + else: + import networkx as nx + + segs_adjacency += [[] for i in range(n_add)] + segs_connects += [[] for i in range(n_add)] + kseg_list = [iseg, *range(len(segs) - n_add, len(segs))] + for jseg in prev_connecting_segments: + pos = segs_adjacency[jseg].index(iseg) + distances = [] + closest_points_in_jseg = [] + closest_points_in_kseg = [] + for kseg in kseg_list: + reference_point_in_k = segs_tips[kseg][0] + closest_points_in_jseg.append( + segs[jseg][ + np.argmin( + self.distances_dpt[reference_point_in_k, segs[jseg]] + ) + ] + ) + # do not use the tip in the large segment j, instead, use the closest point + reference_point_in_j = closest_points_in_jseg[ + -1 + ] # segs_tips[jseg][0] + closest_points_in_kseg.append( + segs[kseg][ + np.argmin( + self.distances_dpt[reference_point_in_j, segs[kseg]] + ) + ] + ) + distances.append( + self.distances_dpt[ + closest_points_in_jseg[-1], closest_points_in_kseg[-1] + ] + ) + # print(jseg, '(', segs_tips[jseg][0], closest_points_in_jseg[-1], ')', + # kseg, '(', segs_tips[kseg][0], closest_points_in_kseg[-1], ') :', distances[-1]) + idx = np.argmin(distances) + kseg_min = kseg_list[idx] + segs_adjacency[jseg][pos] = kseg_min + segs_connects[jseg][pos] = closest_points_in_kseg[idx] + pos_2 = segs_adjacency[iseg].index(jseg) + segs_adjacency[iseg].pop(pos_2) + segs_connects[iseg].pop(pos_2) + segs_adjacency[kseg_min].append(jseg) + segs_connects[kseg_min].append(closest_points_in_jseg[idx]) + # if we split two clusters, we need to check whether the new segments connect to any of the other + # old segments + # if not, we add a link between the new segments, if yes, we add two links to connect them at the + # correct old segments + do_not_attach_kseg = False + for kseg in kseg_list: + distances = [] + closest_points_in_jseg = [] + closest_points_in_kseg = [] + jseg_list = [ + jseg + for jseg in range(len(segs)) + if jseg != kseg and jseg not in prev_connecting_segments + ] + for jseg in jseg_list: + reference_point_in_k = segs_tips[kseg][0] + closest_points_in_jseg.append( + segs[jseg][ + np.argmin( + self.distances_dpt[reference_point_in_k, segs[jseg]] + ) + ] + ) + # do not use the tip in the large segment j, instead, use the closest point + reference_point_in_j = closest_points_in_jseg[ + -1 + ] # segs_tips[jseg][0] + closest_points_in_kseg.append( + segs[kseg][ + np.argmin( + self.distances_dpt[reference_point_in_j, segs[kseg]] + ) + ] + ) + distances.append( + self.distances_dpt[ + closest_points_in_jseg[-1], closest_points_in_kseg[-1] + ] + ) + idx = np.argmin(distances) + jseg_min = jseg_list[idx] + if jseg_min not in kseg_list: + segs_adjacency_sparse = sp.sparse.lil_matrix( + (len(segs), len(segs)), dtype=float + ) + for i, seg_adjacency in enumerate(segs_adjacency): + segs_adjacency_sparse[i, seg_adjacency] = 1 + g = nx.Graph(segs_adjacency_sparse) + paths_all = nx.single_source_dijkstra_path(g, source=kseg) + if jseg_min not in paths_all: + segs_adjacency[jseg_min].append(kseg) + segs_connects[jseg_min].append(closest_points_in_kseg[idx]) + segs_adjacency[kseg].append(jseg_min) + segs_connects[kseg].append(closest_points_in_jseg[idx]) + logg.debug(f" attaching new segment {kseg} at {jseg_min}") + # if we split the cluster, we should not attach kseg + do_not_attach_kseg = True + else: + logg.debug( + f" cannot attach new segment {kseg} at {jseg_min} " + "(would produce cycle)" + ) + if kseg != kseg_list[-1]: + logg.debug(" continue") + continue + else: + logg.debug(" do not add another link") + break + if jseg_min in kseg_list and not do_not_attach_kseg: + segs_adjacency[jseg_min].append(kseg) + segs_connects[jseg_min].append(closest_points_in_kseg[idx]) + segs_adjacency[kseg].append(jseg_min) + segs_connects[kseg].append(closest_points_in_jseg[idx]) + break + segs_undecided += [False for i in range(n_add)] + + def _detect_branching( # noqa: PLR0915 + self, + d_seg: np.ndarray, + tips: np.ndarray, + seg_reference=None, + ) -> tuple[ + list[np.ndarray], + list[np.ndarray], + list[list[int]], + list[list[int]], + int, + ]: + """Detect branching on given segment. + + Call function __detect_branching three times for all three orderings of + tips. Points that do not belong to the same segment in all three + orderings are assigned to a fourth segment. The latter is, by Haghverdi + et al. (2016) referred to as 'undecided cells'. + + Parameters + ---------- + Dseg + Dchosen distance matrix restricted to segment. + tips + The three tip points. They form a 'triangle' that contains the data. + + Returns + ------- + ssegs + List of segments obtained from splitting the single segment defined + via the first two tip cells. + ssegs_tips + List of tips of segments in ssegs. + ssegs_adjacency + ? + ssegs_connects + ? + trunk + ? + + """ + if self.flavor == "haghverdi16": + ssegs = self._detect_branching_single_haghverdi16(d_seg, tips) + elif self.flavor == "wolf17_tri": + ssegs = self._detect_branching_single_wolf17_tri(d_seg, tips) + elif self.flavor in {"wolf17_bi", "wolf17_bi_un"}: + ssegs = self._detect_branching_single_wolf17_bi(d_seg, tips) + else: + msg = '`flavor` needs to be in {"haghverdi16", "wolf17_tri", "wolf17_bi"}.' + raise ValueError(msg) + # make sure that each data point has a unique association with a segment + masks = np.zeros((len(ssegs), d_seg.shape[0]), dtype=bool) + for iseg, seg in enumerate(ssegs): + masks[iseg][seg] = True + nonunique = np.sum(masks, axis=0) > 1 + ssegs = [] + for mask in masks: + mask[nonunique] = False + ssegs.append(np.arange(d_seg.shape[0], dtype=int)[mask]) + # compute new tips within new segments + ssegs_tips = [] + for inewseg, newseg in enumerate(ssegs): + if len(np.flatnonzero(newseg)) <= 1: + logg.warning(f"detected group with only {np.flatnonzero(newseg)} cells") + secondtip = newseg[np.argmax(d_seg[tips[inewseg]][newseg])] + ssegs_tips.append([tips[inewseg], secondtip]) + undecided_cells = np.arange(d_seg.shape[0], dtype=int)[nonunique] + if len(undecided_cells) > 0: + ssegs.append(undecided_cells) + # establish the connecting points with the other segments + ssegs_connects = [[], [], [], []] + for inewseg, newseg_tips in enumerate(ssegs_tips): + reference_point = newseg_tips[0] + # closest cell to the new segment within undecided cells + closest_cell = undecided_cells[ + np.argmin(d_seg[reference_point][undecided_cells]) + ] + ssegs_connects[inewseg].append(closest_cell) + # closest cell to the undecided cells within new segment + closest_cell = ssegs[inewseg][ + np.argmin(d_seg[closest_cell][ssegs[inewseg]]) + ] + ssegs_connects[-1].append(closest_cell) + # also compute tips for the undecided cells + tip_0 = undecided_cells[ + np.argmax(d_seg[undecided_cells[0]][undecided_cells]) + ] + tip_1 = undecided_cells[np.argmax(d_seg[tip_0][undecided_cells])] + ssegs_tips.append([tip_0, tip_1]) + ssegs_adjacency = [[3], [3], [3], [0, 1, 2]] + trunk = 3 + elif len(ssegs) == 3: + reference_point = np.zeros(3, dtype=int) + reference_point[0] = ssegs_tips[0][0] + reference_point[1] = ssegs_tips[1][0] + reference_point[2] = ssegs_tips[2][0] + closest_points = np.zeros((3, 3), dtype=int) + # this is another strategy than for the undecided_cells + # here it's possible to use the more symmetric procedure + # shouldn't make much of a difference + closest_points[0, 1] = ssegs[1][ + np.argmin(d_seg[reference_point[0]][ssegs[1]]) + ] + closest_points[1, 0] = ssegs[0][ + np.argmin(d_seg[reference_point[1]][ssegs[0]]) + ] + closest_points[0, 2] = ssegs[2][ + np.argmin(d_seg[reference_point[0]][ssegs[2]]) + ] + closest_points[2, 0] = ssegs[0][ + np.argmin(d_seg[reference_point[2]][ssegs[0]]) + ] + closest_points[1, 2] = ssegs[2][ + np.argmin(d_seg[reference_point[1]][ssegs[2]]) + ] + closest_points[2, 1] = ssegs[1][ + np.argmin(d_seg[reference_point[2]][ssegs[1]]) + ] + added_dist = np.zeros(3) + added_dist[0] = ( + d_seg[closest_points[1, 0], closest_points[0, 1]] + + d_seg[closest_points[2, 0], closest_points[0, 2]] + ) + added_dist[1] = ( + d_seg[closest_points[0, 1], closest_points[1, 0]] + + d_seg[closest_points[2, 1], closest_points[1, 2]] + ) + added_dist[2] = ( + d_seg[closest_points[1, 2], closest_points[2, 1]] + + d_seg[closest_points[0, 2], closest_points[2, 0]] + ) + trunk = np.argmin(added_dist) + ssegs_adjacency = [ + [trunk] if i != trunk else [j for j in range(3) if j != trunk] + for i in range(3) + ] + ssegs_connects = [ + [closest_points[i, trunk]] + if i != trunk + else [closest_points[trunk, j] for j in range(3) if j != trunk] + for i in range(3) + ] + else: + trunk = 0 + ssegs_adjacency = [[1], [0]] + reference_point_in_0 = ssegs_tips[0][0] + closest_point_in_1 = ssegs[1][ + np.argmin(d_seg[reference_point_in_0][ssegs[1]]) + ] + reference_point_in_1 = closest_point_in_1 # ssegs_tips[1][0] + closest_point_in_0 = ssegs[0][ + np.argmin(d_seg[reference_point_in_1][ssegs[0]]) + ] + ssegs_connects = [[closest_point_in_1], [closest_point_in_0]] + return ssegs, ssegs_tips, ssegs_adjacency, ssegs_connects, trunk + + def _detect_branching_single_haghverdi16(self, d_seg, tips): + """Detect branching on given segment.""" + # permutations of tip cells + ps = [ + [0, 1, 2], # start by computing distances from the first tip + [1, 2, 0], # -"- second tip + [2, 0, 1], # -"- third tip + ] + + # compute branchings using different starting points the first index of + # tips is the starting point for the other two, the order does not + # matter + return [self.__detect_branching_haghverdi16(d_seg, tips[p]) for p in ps] + + def _detect_branching_single_wolf17_tri(self, d_seg, tips): + # all pairwise distances + dist_from_0 = d_seg[tips[0]] + dist_from_1 = d_seg[tips[1]] + dist_from_2 = d_seg[tips[2]] + closer_to_0_than_to_1 = dist_from_0 < dist_from_1 + closer_to_0_than_to_2 = dist_from_0 < dist_from_2 + closer_to_1_than_to_2 = dist_from_1 < dist_from_2 + masks = np.zeros((2, d_seg.shape[0]), dtype=bool) + masks[0] = closer_to_0_than_to_1 + masks[1] = closer_to_0_than_to_2 + segment_0 = np.sum(masks, axis=0) == 2 + masks = np.zeros((2, d_seg.shape[0]), dtype=bool) + masks[0] = ~closer_to_0_than_to_1 + masks[1] = closer_to_1_than_to_2 + segment_1 = np.sum(masks, axis=0) == 2 + masks = np.zeros((2, d_seg.shape[0]), dtype=bool) + masks[0] = ~closer_to_0_than_to_2 + masks[1] = ~closer_to_1_than_to_2 + segment_2 = np.sum(masks, axis=0) == 2 + ssegs = [segment_0, segment_1, segment_2] + return ssegs + + def _detect_branching_single_wolf17_bi(self, d_seg: np.ndarray, tips: np.ndarray): + dist_from_0 = d_seg[tips[0]] + dist_from_1 = d_seg[tips[1]] + closer_to_0_than_to_1 = dist_from_0 < dist_from_1 + ssegs = [closer_to_0_than_to_1, ~closer_to_0_than_to_1] + return ssegs + + def __detect_branching_haghverdi16( + self, d_seg: np.ndarray, tips: np.ndarray + ) -> np.ndarray: + """Detect branching on given segment. + + Compute point that maximizes kendall tau correlation of the sequences of + distances to the second and the third tip, respectively, when 'moving + away' from the first tip: tips[0]. 'Moving away' means moving in the + direction of increasing distance from the first tip. + + Parameters + ---------- + d_seg + Chosen distance matrix restricted to segment. + tips + The three tip points. They form a 'triangle' that contains the data. + + Returns + ------- + Segments obtained from "splitting away the first tip cell". + + """ + # sort distance from first tip point + # then the sequence of distances Dseg[tips[0]][idcs] increases + idcs = np.argsort(d_seg[tips[0]]) + # consider now the sequence of distances from the other + # two tip points, which only increase when being close to `tips[0]` + # where they become correlated + # at the point where this happens, we define a branching point + if True: + imax = self.kendall_tau_split( + d_seg[tips[1]][idcs], + d_seg[tips[2]][idcs], + ) + if False: + # if we were in euclidian space, the following should work + # as well, but here, it doesn't because the scales in Dseg are + # highly different, one would need to write the following equation + # in terms of an ordering, such as exploited by the kendall + # correlation method above + imax = np.argmin( + d_seg[tips[0]][idcs] + d_seg[tips[1]][idcs] + d_seg[tips[2]][idcs] + ) + # init list to store new segments + ssegs = [] # noqa: F841 # TODO Look into this + # first new segment: all points until, but excluding the branching point + # increasing the following slightly from imax is a more conservative choice + # as the criterion based on normalized distances, which follows below, + # is less stable + if imax > 0.95 * len(idcs) and self.allow_kendall_tau_shift: + # if "everything" is correlated (very large value of imax), a more + # conservative choice amounts to reducing this + logg.warning( + "shifting branching point away from maximal kendall-tau " + "correlation (suppress this with `allow_kendall_tau_shift=False`)" + ) + ibranch = int(0.95 * imax) + else: + # otherwise, a more conservative choice is the following + ibranch = imax + 1 + return idcs[:ibranch] + + def kendall_tau_split(self, a: np.ndarray, b: np.ndarray) -> int: + """Return splitting index that maximizes correlation in the sequences. + + Compute difference in Kendall tau for all splitted sequences. + + For each splitting index i, compute the difference of the two + correlation measures kendalltau(a[:i], b[:i]) and + kendalltau(a[i:], b[i:]). + + Returns the splitting index that maximizes + kendalltau(a[:i], b[:i]) - kendalltau(a[i:], b[i:]) + + Parameters + ---------- + a + b + One dimensional sequences. + + Returns + ------- + Splitting index according to above description. + + """ + if a.size != b.size: + msg = "a and b need to have the same size" + raise ValueError(msg) + if a.ndim != b.ndim != 1: + msg = "a and b need to be one-dimensional arrays" + raise ValueError(msg) + import scipy as sp + + min_length = 5 + n = a.size + idx_range = np.arange(min_length, a.size - min_length - 1, dtype=int) + corr_coeff = np.zeros(idx_range.size) + pos_old = sp.stats.kendalltau(a[:min_length], b[:min_length])[0] + neg_old = sp.stats.kendalltau(a[min_length:], b[min_length:])[0] + for ii, i in enumerate(idx_range): + if True: + # compute differences in concordance when adding a[i] and b[i] + # to the first subsequence, and removing these elements from + # the second subsequence + diff_pos, diff_neg = self._kendall_tau_diff(a, b, i) + pos = pos_old + self._kendall_tau_add(i, diff_pos, pos_old) + neg = neg_old + self._kendall_tau_subtract(n - i, diff_neg, neg_old) + pos_old = pos + neg_old = neg + if False: + # computation using sp.stats.kendalltau, takes much longer! + # just for debugging purposes + pos = sp.stats.kendalltau(a[: i + 1], b[: i + 1])[0] + neg = sp.stats.kendalltau(a[i + 1 :], b[i + 1 :])[0] + if False: + # the following is much slower than using sp.stats.kendalltau, + # it is only good for debugging because it allows to compute the + # tau-a version, which does not account for ties, whereas + # sp.stats.kendalltau computes tau-b version, which accounts for + # ties + pos = sp.stats.mstats.kendalltau(a[:i], b[:i], use_ties=False)[0] + neg = sp.stats.mstats.kendalltau(a[i:], b[i:], use_ties=False)[0] + corr_coeff[ii] = pos - neg + iimax = np.argmax(corr_coeff) + imax = min_length + iimax + corr_coeff_max = corr_coeff[iimax] + if corr_coeff_max < 0.3: + logg.debug(" is root itself, never obtain significant correlation") + return imax + + def _kendall_tau_add(self, len_old: int, diff_pos: int, tau_old: float): + """Compute Kendall tau delta. + + The new sequence has length len_old + 1. + + Parameters + ---------- + len_old + The length of the old sequence, used to compute tau_old. + diff_pos + Difference between concordant and non-concordant pairs. + tau_old + Kendall rank correlation of the old sequence. + + """ + return 2.0 / (len_old + 1) * (float(diff_pos) / len_old - tau_old) + + def _kendall_tau_subtract(self, len_old: int, diff_neg: int, tau_old: float): + """Compute Kendall tau delta. + + The new sequence has length len_old - 1. + + Parameters + ---------- + len_old + The length of the old sequence, used to compute tau_old. + diff_neg + Difference between concordant and non-concordant pairs. + tau_old + Kendall rank correlation of the old sequence. + + """ + return 2.0 / (len_old - 2) * (-float(diff_neg) / (len_old - 1) + tau_old) + + def _kendall_tau_diff(self, a: np.ndarray, b: np.ndarray, i) -> tuple[int, int]: + """Compute difference in concordance of pairs in split sequences. + + Consider splitting a and b at index i. + + Parameters + ---------- + a + ? + b + ? + + Returns + ------- + diff_pos + Difference between concordant pairs for both subsequences. + diff_neg + Difference between non-concordant pairs for both subsequences. + + """ + # compute ordering relation of the single points a[i] and b[i] + # with all previous points of the sequences a and b, respectively + a_pos = np.zeros(a[:i].size, dtype=int) + a_pos[a[:i] > a[i]] = 1 + a_pos[a[:i] < a[i]] = -1 + b_pos = np.zeros(b[:i].size, dtype=int) + b_pos[b[:i] > b[i]] = 1 + b_pos[b[:i] < b[i]] = -1 + diff_pos = np.dot(a_pos, b_pos).astype(float) + + # compute ordering relation of the single points a[i] and b[i] + # with all later points of the sequences + a_neg = np.zeros(a[i:].size, dtype=int) + a_neg[a[i:] > a[i]] = 1 + a_neg[a[i:] < a[i]] = -1 + b_neg = np.zeros(b[i:].size, dtype=int) + b_neg[b[i:] > b[i]] = 1 + b_neg[b[i:] < b[i]] = -1 + diff_neg = np.dot(a_neg, b_neg) + + return diff_pos, diff_neg diff --git a/.venv/lib/python3.11/site-packages/scanpy/tools/_draw_graph.py b/.venv/lib/python3.11/site-packages/scanpy/tools/_draw_graph.py new file mode 100644 index 0000000000000000000000000000000000000000..6d9dcbfdccb8289329f149a33a6459165b2de1d2 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/tools/_draw_graph.py @@ -0,0 +1,221 @@ +from __future__ import annotations + +import random +from importlib.util import find_spec +from typing import TYPE_CHECKING, Literal + +import numpy as np + +from .. import _utils +from .. import logging as logg +from .._compat import old_positionals +from .._utils import _choose_graph, get_literal_vals +from ._utils import get_init_pos_from_paga + +if TYPE_CHECKING: + from typing import LiteralString, TypeVar + + from anndata import AnnData + + from .._compat import SpBase + from .._utils.random import _LegacyRandom + + S = TypeVar("S", bound=LiteralString) + + +_Layout = Literal["fr", "drl", "kk", "grid_fr", "lgl", "rt", "rt_circular", "fa"] + + +@old_positionals( + "init_pos", + "root", + "random_state", + "n_jobs", + "adjacency", + "key_added_ext", + "neighbors_key", + "obsp", + "copy", +) +def draw_graph( # noqa: PLR0913 + adata: AnnData, + layout: _Layout = "fa", + *, + init_pos: str | bool | None = None, + root: int | None = None, + random_state: _LegacyRandom = 0, + n_jobs: int | None = None, + adjacency: SpBase | None = None, + key_added_ext: str | None = None, + neighbors_key: str | None = None, + obsp: str | None = None, + copy: bool = False, + **kwds, +) -> AnnData | None: + """Force-directed graph drawing :cite:p:`Islam2011,Jacomy2014,Chippada2018`. + + An alternative to tSNE that often preserves the topology of the data + better. This requires running :func:`~scanpy.pp.neighbors`, first. + + The default layout ('fa', `ForceAtlas2`, :cite:t:`Jacomy2014`) uses the package |fa2-modified|_ + :cite:p:`Chippada2018`, which can be installed via `pip install fa2-modified`. + + `Force-directed graph drawing`_ describes a class of long-established + algorithms for visualizing graphs. + It was suggested for visualizing single-cell data by :cite:t:`Islam2011`. + Many other layouts as implemented in igraph :cite:p:`Csardi2006` are available. + Similar approaches have been used by :cite:t:`Zunder2015` or :cite:t:`Weinreb2017`. + + .. |fa2-modified| replace:: `fa2-modified` + .. _fa2-modified: https://github.com/AminAlam/fa2_modified + .. _Force-directed graph drawing: https://en.wikipedia.org/wiki/Force-directed_graph_drawing + + Parameters + ---------- + adata + Annotated data matrix. + layout + 'fa' (`ForceAtlas2`) or any valid `igraph layout + `__. Of particular interest + are 'fr' (Fruchterman Reingold), 'grid_fr' (Grid Fruchterman Reingold, + faster than 'fr'), 'kk' (Kamadi Kawai', slower than 'fr'), 'lgl' (Large + Graph, very fast), 'drl' (Distributed Recursive Layout, pretty fast) and + 'rt' (Reingold Tilford tree layout). + root + Root for tree layouts. + random_state + For layouts with random initialization like 'fr', change this to use + different intial states for the optimization. If `None`, no seed is set. + adjacency + Sparse adjacency matrix of the graph, defaults to neighbors connectivities. + key_added_ext + By default, append `layout`. + proceed + Continue computation, starting off with 'X_draw_graph_`layout`'. + init_pos + `'paga'`/`True`, `None`/`False`, or any valid 2d-`.obsm` key. + Use precomputed coordinates for initialization. + If `False`/`None` (the default), initialize randomly. + neighbors_key + If not specified, draw_graph looks at .obsp['connectivities'] for connectivities + (default storage place for pp.neighbors). + If specified, draw_graph looks at + .obsp[.uns[neighbors_key]['connectivities_key']] for connectivities. + obsp + Use .obsp[obsp] as adjacency. You can't specify both + `obsp` and `neighbors_key` at the same time. + copy + Return a copy instead of writing to adata. + **kwds + Parameters of chosen igraph layout. See e.g. + :meth:`~igraph.GraphBase.layout_fruchterman_reingold` :cite:p:`Fruchterman1991`. + One of the most important ones is `maxiter`. + + Returns + ------- + Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following fields: + + `adata.obsm['X_draw_graph_[layout | key_added_ext]']` : :class:`numpy.ndarray` (dtype `float`) + Coordinates of graph layout. E.g. for `layout='fa'` (the default), + the field is called `'X_draw_graph_fa'`. `key_added_ext` overwrites `layout`. + `adata.uns['draw_graph']`: :class:`dict` + `draw_graph` parameters. + + """ + start = logg.info(f"drawing single-cell graph using layout {layout!r}") + if layout not in (layouts := get_literal_vals(_Layout)): + msg = f"Provide a valid layout, one of {layouts}." + raise ValueError(msg) + adata = adata.copy() if copy else adata + if adjacency is None: + adjacency = _choose_graph(adata, obsp, neighbors_key) + # init coordinates + if init_pos in adata.obsm: + init_coords = adata.obsm[init_pos] + elif init_pos == "paga" or init_pos: + init_coords = get_init_pos_from_paga( + adata, + adjacency, + random_state=random_state, + neighbors_key=neighbors_key, + obsp=obsp, + ) + else: + np.random.seed(random_state) + init_coords = np.random.random((adjacency.shape[0], 2)) + layout = coerce_fa2_layout(layout) + # actual drawing + if layout == "fa": + positions = np.array(fa2_positions(adjacency, init_coords, **kwds)) + else: + # igraph doesn't use numpy seed + random.seed(random_state) + + g = _utils.get_igraph_from_adjacency(adjacency) + if layout in {"fr", "drl", "kk", "grid_fr"}: + ig_layout = g.layout(layout, seed=init_coords.tolist(), **kwds) + elif "rt" in layout: + if root is not None: + root = [root] + ig_layout = g.layout(layout, root=root, **kwds) + else: + ig_layout = g.layout(layout, **kwds) + positions = np.array(ig_layout.coords) + adata.uns["draw_graph"] = {} + adata.uns["draw_graph"]["params"] = dict(layout=layout, random_state=random_state) + key_added = f"X_draw_graph_{key_added_ext or layout}" + adata.obsm[key_added] = positions + logg.info( + " finished", + time=start, + deep=f"added\n {key_added!r}, graph_drawing coordinates (adata.obsm)", + ) + return adata if copy else None + + +def fa2_positions( + adjacency: SpBase | np.ndarray, init_coords: np.ndarray, **kwds +) -> list[tuple[float, float]]: + from fa2_modified import ForceAtlas2 + + forceatlas2 = ForceAtlas2( + # Behavior alternatives + outboundAttractionDistribution=False, # Dissuade hubs + linLogMode=False, # NOT IMPLEMENTED + adjustSizes=False, # Prevent overlap (NOT IMPLEMENTED) + edgeWeightInfluence=1.0, + # Performance + jitterTolerance=1.0, # Tolerance + barnesHutOptimize=True, + barnesHutTheta=1.2, + multiThreaded=False, # NOT IMPLEMENTED + # Tuning + scalingRatio=2.0, + strongGravityMode=False, + gravity=1.0, + # Log + verbose=False, + ) + if "maxiter" in kwds: + iterations = kwds["maxiter"] + elif "iterations" in kwds: + iterations = kwds["iterations"] + else: + iterations = 500 + return forceatlas2.forceatlas2(adjacency, pos=init_coords, iterations=iterations) + + +def coerce_fa2_layout(layout: S) -> S | Literal["fa", "fr"]: + # see whether fa2 is installed + if layout != "fa": + return layout + + if find_spec("fa2_modified") is None: + logg.warning( + "Package 'fa2-modified' is not installed, falling back to layout 'fr'." + "To use the faster and better ForceAtlas2 layout, " + "install package 'fa2-modified' (`pip install fa2-modified`)." + ) + return "fr" + + return "fa" diff --git a/.venv/lib/python3.11/site-packages/scanpy/tools/_embedding_density.py b/.venv/lib/python3.11/site-packages/scanpy/tools/_embedding_density.py new file mode 100644 index 0000000000000000000000000000000000000000..ed57789bfadd1ac06fb5884469fe9a7a64d1b085 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/tools/_embedding_density.py @@ -0,0 +1,201 @@ +"""Calculate density of cells in embeddings.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np + +from .. import logging as logg +from .._compat import old_positionals +from .._utils import sanitize_anndata + +if TYPE_CHECKING: + from collections.abc import Sequence + + from anndata import AnnData + + +def _calc_density(x: np.ndarray, y: np.ndarray): + """Calculate the density of points in 2 dimensions.""" + from scipy.stats import gaussian_kde + + # Calculate the point density + xy = np.vstack([x, y]) + z = gaussian_kde(xy)(xy) + + min_z = np.min(z) + max_z = np.max(z) + + # Scale between 0 and 1 + scaled_z = (z - min_z) / (max_z - min_z) + + return scaled_z + + +@old_positionals("groupby", "key_added", "components") +def embedding_density( # noqa: PLR0912 + adata: AnnData, + basis: str = "umap", + *, + groupby: str | None = None, + key_added: str | None = None, + components: str | Sequence[str] | None = None, +) -> None: + """Calculate the density of cells in an embedding (per condition). + + Gaussian kernel density estimation is used to calculate the density of + cells in an embedded space. This can be performed per category over a + categorical cell annotation. The cell density can be plotted using the + `pl.embedding_density` function. + + Note that density values are scaled to be between 0 and 1. Thus, the + density value at each cell is only comparable to densities in + the same category. + + Beware that the KDE estimate used (`scipy.stats.gaussian_kde`) becomes + unreliable if you don't have enough cells in a category. + + This function was written by Sophie Tritschler and implemented into + Scanpy by Malte Luecken. + + Parameters + ---------- + adata + The annotated data matrix. + basis + The embedding over which the density will be calculated. This embedded + representation is found in `adata.obsm['X_[basis]']``. + groupby + Key for categorical observation/cell annotation for which densities + are calculated per category. + key_added + Name of the `.obs` covariate that will be added with the density + estimates. + components + The embedding dimensions over which the density should be calculated. + This is limited to two components. + + Returns + ------- + Sets the following fields (`key_added` defaults to `[basis]_density_[groupby]`, where `[basis]` is one of `umap`, `diffmap`, `pca`, `tsne`, or `draw_graph_fa` and `[groupby]` denotes the parameter input): + + `adata.obs[key_added]` : :class:`numpy.ndarray` (dtype `float`) + Embedding density values for each cell. + `adata.uns['[key_added]_params']` : :class:`dict` + A dict with the values for the parameters `covariate` (for the `groupby` parameter) and `components`. + + Examples + -------- + + .. plot:: + :context: close-figs + + import scanpy as sc + adata = sc.datasets.pbmc68k_reduced() + sc.tl.umap(adata) + sc.tl.embedding_density(adata, basis='umap', groupby='phase') + sc.pl.embedding_density( + adata, basis='umap', key='umap_density_phase', group='G1' + ) + + .. plot:: + :context: close-figs + + sc.pl.embedding_density( + adata, basis='umap', key='umap_density_phase', group='S' + ) + + .. currentmodule:: scanpy + + See Also + -------- + pl.embedding_density + + """ + # to ensure that newly created covariates are categorical + # to test for category numbers + sanitize_anndata(adata) + + logg.info(f"computing density on {basis!r}") + + # Test user inputs + basis = basis.lower() + + if basis == "fa": + basis = "draw_graph_fa" + + if f"X_{basis}" not in adata.obsm: + msg = ( + "Cannot find the embedded representation " + f"`adata.obsm['X_{basis}']`. Compute the embedding first." + ) + raise ValueError(msg) + + if components is None: + components = "1,2" + if isinstance(components, str): + components = components.split(",") + components = np.array(components).astype(int) - 1 + + if len(components) != 2: + msg = "Please specify exactly 2 components, or `None`." + raise ValueError(msg) + + if basis == "diffmap": + components += 1 + + if groupby is not None: + if groupby not in adata.obs: + msg = f"Could not find {groupby!r} `.obs` column." + raise ValueError(msg) + + if adata.obs[groupby].dtype.name != "category": + msg = f"{groupby!r} column does not contain categorical data" + raise ValueError(msg) + + # Define new covariate name + if key_added is not None: + density_covariate = key_added + elif groupby is not None: + density_covariate = f"{basis}_density_{groupby}" + else: + density_covariate = f"{basis}_density" + + # Calculate the densities over each category in the groupby column + if groupby is not None: + categories = adata.obs[groupby].cat.categories + + density_values = np.zeros(adata.n_obs) + + for cat in categories: + cat_mask = adata.obs[groupby] == cat + embed_x = adata.obsm[f"X_{basis}"][cat_mask, components[0]] + embed_y = adata.obsm[f"X_{basis}"][cat_mask, components[1]] + + dens_embed = _calc_density(embed_x, embed_y) + density_values[cat_mask] = dens_embed + + adata.obs[density_covariate] = density_values + else: # if groupby is None + # Calculate the density over the whole embedding without subsetting + embed_x = adata.obsm[f"X_{basis}"][:, components[0]] + embed_y = adata.obsm[f"X_{basis}"][:, components[1]] + + adata.obs[density_covariate] = _calc_density(embed_x, embed_y) + + # Reduce diffmap components for labeling + # Note: plot_scatter takes care of correcting diffmap components + # for plotting automatically + if basis != "diffmap": + components += 1 + + adata.uns[f"{density_covariate}_params"] = dict( + covariate=groupby, components=components.tolist() + ) + + logg.hint( + f"added\n" + f" '{density_covariate}', densities (adata.obs)\n" + f" '{density_covariate}_params', parameter (adata.uns)" + ) diff --git a/.venv/lib/python3.11/site-packages/scanpy/tools/_ingest.py b/.venv/lib/python3.11/site-packages/scanpy/tools/_ingest.py new file mode 100644 index 0000000000000000000000000000000000000000..856bc25d4c0769faae158da6dec58844031130ce --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/tools/_ingest.py @@ -0,0 +1,523 @@ +from __future__ import annotations + +from collections.abc import MutableMapping +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd +from packaging.version import Version +from sklearn.utils import check_random_state + +from .. import logging as logg +from .._compat import CSBase, old_positionals, pkg_version +from .._settings import settings +from .._utils import NeighborsView, raise_not_implemented_error_if_backed_type +from .._utils._doctests import doctest_skip +from ..neighbors import FlatTree + +if TYPE_CHECKING: + from collections.abc import Generator, Iterable + + from anndata import AnnData + + from ..neighbors import RPForestDict + +ANNDATA_MIN_VERSION = Version("0.7rc1") + + +@old_positionals( + "obs", + "embedding_method", + "labeling_method", + "neighbors_key", + "neighbors_key", + "inplace", +) +@doctest_skip("illustrative short example but not runnable") +def ingest( + adata: AnnData, + adata_ref: AnnData, + *, + obs: str | Iterable[str] | None = None, + embedding_method: str | Iterable[str] = ("umap", "pca"), + labeling_method: str = "knn", + neighbors_key: str | None = None, + inplace: bool = True, + **kwargs, +): + """Map labels and embeddings from reference data to new data. + + :doc:`/tutorials/basics/integrating-data-using-ingest` + + Integrates embeddings and annotations of an `adata` with a reference dataset + `adata_ref` through projecting on a PCA (or alternate + model) that has been fitted on the reference data. The function uses a knn + classifier for mapping labels and the UMAP package :cite:p:`McInnes2018` for mapping + the embeddings. + + .. note:: + + We refer to this *asymmetric* dataset integration as *ingesting* + annotations from reference data to new data. This is different from + learning a joint representation that integrates both datasets in an + unbiased way, as CCA (e.g. in Seurat) or a conditional VAE (e.g. in + scVI) would do. + + You need to run :func:`~scanpy.pp.neighbors` on `adata_ref` before + passing it. + + Parameters + ---------- + adata + The annotated data matrix of shape `n_obs` × `n_vars`. Rows correspond + to cells and columns to genes. This is the dataset without labels and + embeddings. + adata_ref + The annotated data matrix of shape `n_obs` × `n_vars`. Rows correspond + to cells and columns to genes. + Variables (`n_vars` and `var_names`) of `adata_ref` should be the same + as in `adata`. + This is the dataset with labels and embeddings + which need to be mapped to `adata`. + obs + Labels' keys in `adata_ref.obs` which need to be mapped to `adata.obs` + (inferred for observation of `adata`). + embedding_method + Embeddings in `adata_ref` which need to be mapped to `adata`. + The only supported values are 'umap' and 'pca'. + labeling_method + The method to map labels in `adata_ref.obs` to `adata.obs`. + The only supported value is 'knn'. + neighbors_key + If not specified, ingest looks at adata_ref.uns['neighbors'] + for neighbors settings and adata_ref.obsp['distances'] for + distances (default storage places for pp.neighbors). + If specified, ingest looks at adata_ref.uns[neighbors_key] for + neighbors settings and + adata_ref.obsp[adata_ref.uns[neighbors_key]['distances_key']] for distances. + inplace + Only works if `return_joint=False`. + Add labels and embeddings to the passed `adata` (if `True`) + or return a copy of `adata` with mapped embeddings and labels. + + Returns + ------- + Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following fields: + + `adata.obs[obs]` : :class:`pandas.Series` (dtype ``category``) + Mapped labels. + `adata.obsm['X_umap' | 'X_pca']` : :class:`numpy.ndarray` (dtype ``float``) + Mapped embeddings. `'X_umap'` if `embedding_method` is `'umap'`, `'X_pca'` if `embedding_method` is `'pca'`. + + Example + ------- + Call sequence: + + >>> import scanpy as sc + >>> sc.pp.neighbors(adata_ref) + >>> sc.tl.umap(adata_ref) + >>> sc.tl.ingest(adata, adata_ref, obs="cell_type") + + """ + # anndata version check + anndata_version = pkg_version("anndata") + if anndata_version < ANNDATA_MIN_VERSION: + msg = ( + f"ingest only works correctly with anndata>={ANNDATA_MIN_VERSION} " + f"(you have {anndata_version}) as prior to {ANNDATA_MIN_VERSION}, " + "`AnnData.concatenate` did not concatenate `.obsm`." + ) + raise ValueError(msg) + + start = logg.info("running ingest") + obs = [obs] if isinstance(obs, str) else obs + embedding_method = ( + [embedding_method] if isinstance(embedding_method, str) else embedding_method + ) + labeling_method = ( + [labeling_method] if isinstance(labeling_method, str) else labeling_method + ) + + if len(labeling_method) == 1 and len(obs or []) > 1: + labeling_method = labeling_method * len(obs) + + ing = Ingest(adata_ref, neighbors_key) + ing.fit(adata) + + for method in embedding_method: + ing.map_embedding(method) + + if obs is not None: + ing.neighbors(**kwargs) + for i, col in enumerate(obs): + ing.map_labels(col, labeling_method[i]) + + logg.info(" finished", time=start) + return ing.to_adata(inplace=inplace) + + +def _rp_forest_generate( + rp_forest_dict: RPForestDict, +) -> Generator[FlatTree, None, None]: + props = FlatTree._fields + num_trees = len(rp_forest_dict[props[0]]["start"]) - 1 + + for i in range(num_trees): + tree = [] + for prop in props: + start = rp_forest_dict[prop]["start"][i] + end = rp_forest_dict[prop]["start"][i + 1] + tree.append(rp_forest_dict[prop]["data"][start:end]) + yield FlatTree(*tree) + + tree = [] + for prop in props: + start = rp_forest_dict[prop]["start"][num_trees] + tree.append(rp_forest_dict[prop]["data"][start:]) + yield FlatTree(*tree) + + +class _DimDict(MutableMapping): + def __init__(self, dim, axis=0, vals=None): + self._data = {} + self._dim = dim + self._axis = axis + if vals is not None: + self.update(vals) + + def __setitem__(self, key, value): + if value.shape[self._axis] != self._dim: + msg = ( + f"Value passed for key {key!r} is of incorrect shape. " + f"Value has shape {value.shape[self._axis]} " + f"for dimension {self._axis} while " + f"it should have {self._dim}." + ) + raise ValueError(msg) + self._data[key] = value + + def __getitem__(self, key): + return self._data[key] + + def __delitem__(self, key): + del self._data[key] + + def __iter__(self): + return iter(self._data) + + def __len__(self): + return len(self._data) + + def __repr__(self): + return f"{type(self).__name__}({self._data})" + + +class Ingest: + """Class to map labels and embeddings from existing data to new data. + + You need to run :func:`~scanpy.pp.neighbors` on `adata` before + initializing Ingest with it. + + Parameters + ---------- + adata : :class:`~anndata.AnnData` + The annotated data matrix of shape `n_obs` × `n_vars` + with embeddings and labels. + + """ + + def _init_umap(self, adata): + from umap import UMAP + + self._umap = UMAP( + metric=self._metric, + random_state=adata.uns["umap"]["params"].get("random_state", 0), + ) + + self._umap._initial_alpha = self._umap.learning_rate + self._umap._raw_data = self._rep + self._umap.knn_dists = None + + self._umap._validate_parameters() + + self._umap.embedding_ = adata.obsm["X_umap"] + self._umap._sparse_data = isinstance(self._rep, CSBase) + self._umap._small_data = self._rep.shape[0] < 4096 + self._umap._metric_kwds = self._metric_kwds + + self._umap._n_neighbors = self._n_neighbors + self._umap.n_neighbors = self._n_neighbors + + self._umap._knn_search_index = self._nnd_idx + + self._umap._a = adata.uns["umap"]["params"]["a"] + self._umap._b = adata.uns["umap"]["params"]["b"] + + self._umap._input_hash = None + + def _init_pynndescent(self, distances): + from pynndescent import NNDescent + + first_col = np.arange(distances.shape[0])[:, None] + init_indices = np.hstack((first_col, np.stack(distances.tolil().rows))) + + self._nnd_idx = NNDescent( + data=self._rep, + metric=self._metric, + metric_kwds=self._metric_kwds, + n_neighbors=self._n_neighbors, + init_graph=init_indices, + random_state=self._neigh_random_state, + ) + + # temporary hack for the broken forest storage + from pynndescent.rp_trees import make_forest + + current_random_state = check_random_state(self._nnd_idx.random_state) + self._nnd_idx._rp_forest = make_forest( + self._nnd_idx._raw_data, + self._nnd_idx.n_neighbors, + self._nnd_idx.n_search_trees, + self._nnd_idx.leaf_size, + self._nnd_idx.rng_state, + current_random_state, + self._nnd_idx.n_jobs, + self._nnd_idx._angular_trees, + ) + + def _init_neighbors(self, adata, neighbors_key): + neighbors = NeighborsView(adata, neighbors_key) + + self._n_neighbors = neighbors["params"]["n_neighbors"] + + if "use_rep" in neighbors["params"]: + self._use_rep = neighbors["params"]["use_rep"] + self._rep = adata.X if self._use_rep == "X" else adata.obsm[self._use_rep] + elif "n_pcs" in neighbors["params"]: + self._use_rep = "X_pca" + self._n_pcs = neighbors["params"]["n_pcs"] + self._rep = adata.obsm["X_pca"][:, : self._n_pcs] + elif adata.n_vars > settings.N_PCS and "X_pca" in adata.obsm: + self._use_rep = "X_pca" + self._rep = adata.obsm["X_pca"][:, : settings.N_PCS] + self._n_pcs = self._rep.shape[1] + + self._metric_kwds = neighbors["params"].get("metric_kwds", {}) + self._metric = neighbors["params"]["metric"] + + self._neigh_random_state = neighbors["params"].get("random_state", 0) + self._init_pynndescent(neighbors["distances"]) + + def _init_pca(self, adata): + self._pca_centered = adata.uns["pca"]["params"]["zero_center"] + self._pca_use_hvg = adata.uns["pca"]["params"]["use_highly_variable"] + + mask = "highly_variable" + if self._pca_use_hvg and mask not in adata.var.columns: + msg = f"Did not find `adata.var[{mask!r}']`." + raise ValueError(msg) + + if self._pca_use_hvg: + self._pca_basis = adata.varm["PCs"][adata.var[mask]] + else: + self._pca_basis = adata.varm["PCs"] + + def __init__(self, adata: AnnData, neighbors_key: str | None = None): + # assume rep is X if all initializations fail to identify it + self._rep = adata.X + self._use_rep = "X" + + self._n_pcs = None + + self._adata_ref = adata + self._adata_new: AnnData | None = None + + if "pca" in adata.uns: + self._init_pca(adata) + + if neighbors_key is None: + neighbors_key = "neighbors" + + if neighbors_key in adata.uns: + self._init_neighbors(adata, neighbors_key) + else: + msg = ( + f'There is no neighbors data in `adata.uns["{neighbors_key}"]`.\n' + "Please run pp.neighbors." + ) + raise ValueError(msg) + + if "X_umap" in adata.obsm: + self._init_umap(adata) + + self._obsm = None + self._obs = None + self._labels = None + + self._indices = None + self._distances = None + + def _pca(self, n_pcs=None): + x = self._adata_new.X + x = x.toarray() if isinstance(x, CSBase) else x.copy() + if self._pca_use_hvg: + x = x[:, self._adata_ref.var["highly_variable"]] + if self._pca_centered: + x -= x.mean(axis=0) + x_pca = np.dot(x, self._pca_basis[:, :n_pcs]) + return x_pca + + def _same_rep(self): + adata = self._adata_new + if self._n_pcs is not None: + return self._pca(self._n_pcs) + if self._use_rep == "X": + return adata.X + if self._use_rep in adata.obsm: + return adata.obsm[self._use_rep] + return adata.X + + def fit(self, adata_new): + """Map `adata_new` to the same representation as `adata`. + + This function identifies the representation which was used to + calculate neighbors in 'adata' and maps `adata_new` to + this representation. + Variables (`n_vars` and `var_names`) of `adata_new` should be the same + as in `adata`. + + `adata` refers to the :class:`~anndata.AnnData` object + that is passed during the initialization of an Ingest instance. + """ + raise_not_implemented_error_if_backed_type(adata_new.X, "Ingest.fit") + ref_var_names = self._adata_ref.var_names.str.upper() + new_var_names = adata_new.var_names.str.upper() + + if not ref_var_names.equals(new_var_names): + msg = ( + "Variables in the new adata are different " + "from variables in the reference adata" + ) + raise ValueError(msg) + + self._obs = pd.DataFrame(index=adata_new.obs.index) + self._obsm = _DimDict(adata_new.n_obs, axis=0) + + self._adata_new = adata_new + self._obsm["rep"] = self._same_rep() + + def neighbors(self, k=None, queue_size=5, epsilon=0.1, random_state=0): + """Calculate neighbors of `adata_new` observations in `adata`. + + This function calculates `k` neighbors in `adata` for + each observation of `adata_new`. + """ + from umap.umap_ import INT32_MAX, INT32_MIN + + random_state = check_random_state(random_state) + rng_state = random_state.randint(INT32_MIN, INT32_MAX, 3).astype(np.int64) + + test = self._obsm["rep"] + + if k is None: + k = self._n_neighbors + + self._nnd_idx.search_rng_state = rng_state + self._indices, self._distances = self._nnd_idx.query(test, k, epsilon) + + def _umap_transform(self): + return self._umap.transform(self._obsm["rep"]) + + def map_embedding(self, method): + """Map embeddings of `adata` to `adata_new`. + + This function infers embeddings, specified by `method`, + for `adata_new` from existing embeddings in `adata`. + `method` can be 'umap' or 'pca'. + """ + if method == "umap": + self._obsm["X_umap"] = self._umap_transform() + elif method == "pca": + self._obsm["X_pca"] = self._pca() + else: + msg = "Ingest supports only umap and pca embeddings for now." + raise NotImplementedError(msg) + + def _knn_classify(self, labels): + # ensure it's categorical + cat_array: pd.Series = self._adata_ref.obs[labels].astype("category") + values = [cat_array.iloc[inds].mode()[0] for inds in self._indices] + return pd.Categorical(values=values, categories=cat_array.cat.categories) + + def map_labels(self, labels, method): + """Map labels of `adata` to `adata_new`. + + This function infers `labels` for `adata_new.obs` + from existing labels in `adata.obs`. + `method` can be only 'knn'. + """ + if method == "knn": + self._obs[labels] = self._knn_classify(labels) + else: + msg = "Ingest supports knn labeling for now." + raise NotImplementedError(msg) + + @old_positionals("inplace") + def to_adata(self, *, inplace: bool = False) -> AnnData | None: + """Return `adata_new` with mapped embeddings and labels. + + If `inplace=False` returns a copy of `adata_new` + with mapped embeddings and labels in `obsm` and `obs` correspondingly. + If `inplace=True` returns nothing and updates `adata_new.obsm` + and `adata_new.obs` with mapped embeddings and labels. + """ + adata = self._adata_new if inplace else self._adata_new.copy() + + adata.obsm.update(self._obsm) + + for key in self._obs: + adata.obs[key] = self._obs[key] + + if not inplace: + return adata + + def to_adata_joint( + self, batch_key="batch", batch_categories=None, index_unique="-" + ): + """Return concatenated object. + + This function returns the new :class:`~anndata.AnnData` object + with concatenated existing embeddings and labels of 'adata' + and inferred embeddings and labels for `adata_new`. + """ + adata = self._adata_ref.concatenate( + self._adata_new, + batch_key=batch_key, + batch_categories=batch_categories, + index_unique=index_unique, + ) + + obs_update = self._obs.copy() + obs_update.index = adata[adata.obs[batch_key] == "1"].obs_names + adata.obs.update(obs_update) + + for key in self._obsm: + if key in self._adata_ref.obsm: + adata.obsm[key] = np.vstack(( + self._adata_ref.obsm[key], + self._obsm[key], + )) + + if self._use_rep not in ("X_pca", "X"): + adata.obsm[self._use_rep] = np.vstack(( + self._adata_ref.obsm[self._use_rep], + self._obsm["rep"], + )) + + if "X_umap" in self._obsm: + adata.uns["umap"] = self._adata_ref.uns["umap"] + if "X_pca" in self._obsm: + adata.uns["pca"] = self._adata_ref.uns["pca"] + adata.varm["PCs"] = self._adata_ref.varm["PCs"] + + return adata diff --git a/.venv/lib/python3.11/site-packages/scanpy/tools/_leiden.py b/.venv/lib/python3.11/site-packages/scanpy/tools/_leiden.py new file mode 100644 index 0000000000000000000000000000000000000000..17def3dee4fb448ee3ac4b04a591400338176098 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/tools/_leiden.py @@ -0,0 +1,215 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd +from natsort import natsorted + +from .. import _utils +from .. import logging as logg +from .._utils.random import set_igraph_random_state +from ._utils_clustering import rename_groups, restrict_adjacency + +if TYPE_CHECKING: + from collections.abc import Sequence + from typing import Literal + + from anndata import AnnData + from leidenalg.VertexPartition import MutableVertexPartition + + from .._compat import CSBase + from .._utils.random import _LegacyRandom +else: + try: + from leidenalg.VertexPartition import MutableVertexPartition + except ImportError: + MutableVertexPartition = type("MutableVertexPartition", (), {}) + MutableVertexPartition.__module__ = "leidenalg.VertexPartition" + + +def leiden( # noqa: PLR0912, PLR0913, PLR0915 + adata: AnnData, + resolution: float = 1, + *, + restrict_to: tuple[str, Sequence[str]] | None = None, + random_state: _LegacyRandom = 0, + key_added: str = "leiden", + adjacency: CSBase | None = None, + directed: bool | None = None, + use_weights: bool = True, + n_iterations: int = -1, + partition_type: type[MutableVertexPartition] | None = None, + neighbors_key: str | None = None, + obsp: str | None = None, + copy: bool = False, + flavor: Literal["leidenalg", "igraph"] = "leidenalg", + **clustering_args, +) -> AnnData | None: + """Cluster cells into subgroups :cite:p:`Traag2019`. + + Cluster cells using the Leiden algorithm :cite:p:`Traag2019`, + an improved version of the Louvain algorithm :cite:p:`Blondel2008`. + It was proposed for single-cell analysis by :cite:t:`Levine2015`. + + This requires having run :func:`~scanpy.pp.neighbors` or + :func:`~scanpy.external.pp.bbknn` first. + + Parameters + ---------- + adata + The annotated data matrix. + resolution + A parameter value controlling the coarseness of the clustering. + Higher values lead to more clusters. + Set to `None` if overriding `partition_type` + to one that doesn’t accept a `resolution_parameter`. + random_state + Change the initialization of the optimization. + restrict_to + Restrict the clustering to the categories within the key for sample + annotation, tuple needs to contain `(obs_key, list_of_categories)`. + key_added + `adata.obs` key under which to add the cluster labels. + adjacency + Sparse adjacency matrix of the graph, defaults to neighbors connectivities. + directed + Whether to treat the graph as directed or undirected. + use_weights + If `True`, edge weights from the graph are used in the computation + (placing more emphasis on stronger edges). + n_iterations + How many iterations of the Leiden clustering algorithm to perform. + Positive values above 2 define the total number of iterations to perform, + -1 has the algorithm run until it reaches its optimal clustering. + 2 is faster and the default for underlying packages. + partition_type + Type of partition to use. + Defaults to :class:`~leidenalg.RBConfigurationVertexPartition`. + For the available options, consult the documentation for + :func:`~leidenalg.find_partition`. + neighbors_key + Use neighbors connectivities as adjacency. + If not specified, leiden looks at .obsp['connectivities'] for connectivities + (default storage place for pp.neighbors). + If specified, leiden looks at + .obsp[.uns[neighbors_key]['connectivities_key']] for connectivities. + obsp + Use .obsp[obsp] as adjacency. You can't specify both + `obsp` and `neighbors_key` at the same time. + copy + Whether to copy `adata` or modify it inplace. + flavor + Which package's implementation to use. + **clustering_args + Any further arguments to pass to :func:`~leidenalg.find_partition` (which in turn passes arguments to the `partition_type`) + or :meth:`igraph.Graph.community_leiden` from `igraph`. + + Returns + ------- + Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following fields: + + `adata.obs['leiden' | key_added]` : :class:`pandas.Series` (dtype ``category``) + Array of dim (number of samples) that stores the subgroup id + (``'0'``, ``'1'``, ...) for each cell. + + `adata.uns['leiden' | key_added]['params']` : :class:`dict` + A dict with the values for the parameters `resolution`, `random_state`, + and `n_iterations`. + + """ + if flavor not in {"igraph", "leidenalg"}: + msg = ( + f"flavor must be either 'igraph' or 'leidenalg', but {flavor!r} was passed" + ) + raise ValueError(msg) + _utils.ensure_igraph() + if flavor == "igraph": + if directed: + msg = "Cannot use igraph’s leiden implementation with a directed graph." + raise ValueError(msg) + if partition_type is not None: + msg = "Do not pass in partition_type argument when using igraph." + raise ValueError(msg) + else: + try: + import leidenalg + + msg = 'In the future, the default backend for leiden will be igraph instead of leidenalg.\n\n To achieve the future defaults please pass: flavor="igraph" and n_iterations=2. directed must also be False to work with igraph\'s implementation.' + _utils.warn_once(msg, FutureWarning, stacklevel=3) + except ImportError as e: + msg = "Please install the leiden algorithm: `conda install -c conda-forge leidenalg` or `pip3 install leidenalg`." + raise ImportError(msg) from e + clustering_args = dict(clustering_args) + + start = logg.info("running Leiden clustering") + adata = adata.copy() if copy else adata + # are we clustering a user-provided graph or the default AnnData one? + if adjacency is None: + adjacency = _utils._choose_graph(adata, obsp, neighbors_key) + if restrict_to is not None: + restrict_key, restrict_categories = restrict_to + adjacency, restrict_indices = restrict_adjacency( + adata, + restrict_key, + restrict_categories=restrict_categories, + adjacency=adjacency, + ) + # Prepare find_partition arguments as a dictionary, + # appending to whatever the user provided. It needs to be this way + # as this allows for the accounting of a None resolution + # (in the case of a partition variant that doesn't take it on input) + clustering_args["n_iterations"] = n_iterations + if flavor == "leidenalg": + if resolution is not None: + clustering_args["resolution_parameter"] = resolution + directed = True if directed is None else directed + g = _utils.get_igraph_from_adjacency(adjacency, directed=directed) + if partition_type is None: + partition_type = leidenalg.RBConfigurationVertexPartition + if use_weights: + clustering_args["weights"] = np.array(g.es["weight"]).astype(np.float64) + clustering_args["seed"] = random_state + part = leidenalg.find_partition(g, partition_type, **clustering_args) + else: + g = _utils.get_igraph_from_adjacency(adjacency, directed=False) + if use_weights: + clustering_args["weights"] = "weight" + if resolution is not None: + clustering_args["resolution"] = resolution + clustering_args.setdefault("objective_function", "modularity") + with set_igraph_random_state(random_state): + part = g.community_leiden(**clustering_args) + # store output into adata.obs + groups = np.array(part.membership) + if restrict_to is not None: + if key_added == "leiden": + key_added += "_R" + groups = rename_groups( + adata, + key_added=key_added, + restrict_key=restrict_key, + restrict_categories=restrict_categories, + restrict_indices=restrict_indices, + groups=groups, + ) + adata.obs[key_added] = pd.Categorical( + values=groups.astype("U"), + categories=natsorted(map(str, np.unique(groups))), + ) + # store information on the clustering parameters + adata.uns[key_added] = {} + adata.uns[key_added]["params"] = dict( + resolution=resolution, + random_state=random_state, + n_iterations=n_iterations, + ) + logg.info( + " finished", + time=start, + deep=( + f"found {len(np.unique(groups))} clusters and added\n" + f" {key_added!r}, the cluster labels (adata.obs, categorical)" + ), + ) + return adata if copy else None diff --git a/.venv/lib/python3.11/site-packages/scanpy/tools/_louvain.py b/.venv/lib/python3.11/site-packages/scanpy/tools/_louvain.py new file mode 100644 index 0000000000000000000000000000000000000000..76c18647ef0c7c3bc06a898d2e87cbc375970dfd --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/tools/_louvain.py @@ -0,0 +1,265 @@ +from __future__ import annotations + +import warnings +from types import MappingProxyType +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd +from natsort import natsorted + +from .. import _utils +from .. import logging as logg +from .._compat import old_positionals +from .._utils import _choose_graph, dematrix +from ._utils_clustering import rename_groups, restrict_adjacency + +if TYPE_CHECKING: + from collections.abc import Mapping, Sequence + from typing import Any, Literal + + from anndata import AnnData + + from .._compat import CSBase + from .._utils.random import _LegacyRandom + +try: + from louvain.VertexPartition import MutableVertexPartition +except ImportError: + + class MutableVertexPartition: + pass + + MutableVertexPartition.__module__ = "louvain.VertexPartition" + + +@old_positionals( + "random_state", + "restrict_to", + "key_added", + "adjacency", + "flavor", + "directed", + "use_weights", + "partition_type", + "partition_kwargs", + "neighbors_key", + "obsp", + "copy", +) +def louvain( # noqa: PLR0912, PLR0913, PLR0915 + adata: AnnData, + resolution: float | None = None, + *, + random_state: _LegacyRandom = 0, + restrict_to: tuple[str, Sequence[str]] | None = None, + key_added: str = "louvain", + adjacency: CSBase | None = None, + flavor: Literal["vtraag", "igraph", "rapids"] = "vtraag", + directed: bool = True, + use_weights: bool = False, + partition_type: type[MutableVertexPartition] | None = None, + partition_kwargs: Mapping[str, Any] = MappingProxyType({}), + neighbors_key: str | None = None, + obsp: str | None = None, + copy: bool = False, +) -> AnnData | None: + """Cluster cells into subgroups :cite:p:`Blondel2008,Levine2015,Traag2017`. + + Cluster cells using the Louvain algorithm :cite:p:`Blondel2008` in the implementation + of :cite:t:`Traag2017`. The Louvain algorithm was proposed for single-cell + analysis by :cite:t:`Levine2015`. + + This requires having run :func:`~scanpy.pp.neighbors` or + :func:`~scanpy.external.pp.bbknn` first, + or explicitly passing a ``adjacency`` matrix. + + Parameters + ---------- + adata + The annotated data matrix. + resolution + For the default flavor (``'vtraag'``) or for ```RAPIDS```, you can provide a + resolution (higher resolution means finding more and smaller clusters), + which defaults to 1.0. + See “Time as a resolution parameter” in :cite:t:`Lambiotte2014`. + random_state + Change the initialization of the optimization. + restrict_to + Restrict the clustering to the categories within the key for sample + annotation, tuple needs to contain ``(obs_key, list_of_categories)``. + key_added + Key under which to add the cluster labels. (default: ``'louvain'``) + adjacency + Sparse adjacency matrix of the graph, defaults to neighbors connectivities. + flavor + Choose between to packages for computing the clustering. + + ``'vtraag'`` + Much more powerful than ``'igraph'``, and the default. + ``'igraph'`` + Built in ``igraph`` method. + ``'rapids'`` + GPU accelerated implementation. + + .. deprecated:: 1.10.0 + Use :func:`rapids_singlecell.tl.louvain` instead. + directed + Interpret the ``adjacency`` matrix as directed graph? + use_weights + Use weights from knn graph. + partition_type + Type of partition to use. + Only a valid argument if ``flavor`` is ``'vtraag'``. + partition_kwargs + Key word arguments to pass to partitioning, + if ``vtraag`` method is being used. + neighbors_key + Use neighbors connectivities as adjacency. + If not specified, louvain looks .obsp['connectivities'] for connectivities + (default storage place for pp.neighbors). + If specified, louvain looks + .obsp[.uns[neighbors_key]['connectivities_key']] for connectivities. + obsp + Use .obsp[obsp] as adjacency. You can't specify both + `obsp` and `neighbors_key` at the same time. + copy + Copy adata or modify it inplace. + + Returns + ------- + Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following fields: + + `adata.obs['louvain' | key_added]` : :class:`pandas.Series` (dtype ``category``) + Array of dim (number of samples) that stores the subgroup id + (``'0'``, ``'1'``, ...) for each cell. + + `adata.uns['louvain' | key_added]['params']` : :class:`dict` + A dict with the values for the parameters `resolution`, `random_state`, + and `n_iterations`. + + """ + partition_kwargs = dict(partition_kwargs) + start = logg.info("running Louvain clustering") + if (flavor != "vtraag") and (partition_type is not None): + msg = '`partition_type` is only a valid argument when `flavour` is "vtraag"' + raise ValueError(msg) + adata = adata.copy() if copy else adata + if adjacency is None: + adjacency = _choose_graph(adata, obsp, neighbors_key) + if restrict_to is not None: + restrict_key, restrict_categories = restrict_to + adjacency, restrict_indices = restrict_adjacency( + adata, + restrict_key, + restrict_categories=restrict_categories, + adjacency=adjacency, + ) + if flavor in {"vtraag", "igraph"}: + if flavor == "igraph" and resolution is not None: + logg.warning('`resolution` parameter has no effect for flavor "igraph"') + if directed and flavor == "igraph": + directed = False + if not directed: + logg.debug(" using the undirected graph") + g = _utils.get_igraph_from_adjacency(adjacency, directed=directed) + weights = np.array(g.es["weight"]).astype(np.float64) if use_weights else None + if flavor == "vtraag": + import louvain + + if partition_type is None: + partition_type = louvain.RBConfigurationVertexPartition + if resolution is not None: + partition_kwargs["resolution_parameter"] = resolution + if use_weights: + partition_kwargs["weights"] = weights + logg.info(' using the "louvain" package of Traag (2017)') + part = louvain.find_partition( + g, + partition_type, + seed=random_state, + **partition_kwargs, + ) + # adata.uns['louvain_quality'] = part.quality() + else: + part = g.community_multilevel(weights=weights) + groups = np.array(part.membership) + elif flavor == "rapids": + msg = ( + "`flavor='rapids'` is deprecated. " + "Use `rapids_singlecell.tl.louvain` instead." + ) + warnings.warn(msg, FutureWarning, stacklevel=2) + # nvLouvain only works with undirected graphs, + # and `adjacency` must have a directed edge in both directions + import cudf + import cugraph + + offsets = cudf.Series(adjacency.indptr) + indices = cudf.Series(adjacency.indices) + if use_weights: + sources, targets = adjacency.nonzero() + weights = dematrix(adjacency[sources, targets]).ravel() + weights = cudf.Series(weights) + else: + weights = None + g = cugraph.Graph() + + if hasattr(g, "add_adj_list"): + g.add_adj_list(offsets, indices, weights) + else: + g.from_cudf_adjlist(offsets, indices, weights) + + logg.info(' using the "louvain" package of rapids') + if resolution is not None: + louvain_parts, _ = cugraph.louvain(g, resolution=resolution) + else: + louvain_parts, _ = cugraph.louvain(g) + groups = ( + louvain_parts.to_pandas() + .sort_values("vertex")[["partition"]] + .to_numpy() + .ravel() + ) + elif flavor == "taynaud": + # this is deprecated + import community + import networkx as nx + + g = nx.Graph(adjacency) + partition = community.best_partition(g) + groups = np.zeros(len(partition), dtype=int) + for k, v in partition.items(): + groups[k] = v + else: + msg = '`flavor` needs to be "vtraag" or "igraph" or "taynaud".' + raise ValueError(msg) + if restrict_to is not None: + if key_added == "louvain": + key_added += "_R" + groups = rename_groups( + adata, + key_added=key_added, + restrict_key=restrict_key, + restrict_categories=restrict_categories, + restrict_indices=restrict_indices, + groups=groups, + ) + adata.obs[key_added] = pd.Categorical( + values=groups.astype("U"), + categories=natsorted(map(str, np.unique(groups))), + ) + adata.uns[key_added] = {} + adata.uns[key_added]["params"] = dict( + resolution=resolution, + random_state=random_state, + ) + logg.info( + " finished", + time=start, + deep=( + f"found {len(np.unique(groups))} clusters and added\n" + f" {key_added!r}, the cluster labels (adata.obs, categorical)" + ), + ) + return adata if copy else None diff --git a/.venv/lib/python3.11/site-packages/scanpy/tools/_marker_gene_overlap.py b/.venv/lib/python3.11/site-packages/scanpy/tools/_marker_gene_overlap.py new file mode 100644 index 0000000000000000000000000000000000000000..44581ec78b9d586524d6863633e67e87357a613a --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/tools/_marker_gene_overlap.py @@ -0,0 +1,294 @@ +"""Calculate overlaps of rank_genes_groups marker genes with marker gene dictionaries.""" + +from __future__ import annotations + +from collections.abc import Set as AbstractSet +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd + +from .. import logging as logg +from .._utils._doctests import doctest_needs + +if TYPE_CHECKING: + from typing import Literal + + from anndata import AnnData + + _Method = Literal["overlap_count", "overlap_coef", "jaccard"] + + +def _calc_overlap_count(markers1: dict, markers2: dict): + """Calculate overlap count between the values of two dictionaries. + + Note: dict values must be sets + """ + overlaps = np.zeros((len(markers1), len(markers2))) + + for j, marker_group in enumerate(markers1): + tmp = [len(markers2[i].intersection(markers1[marker_group])) for i in markers2] + overlaps[j, :] = tmp + + return overlaps + + +def _calc_overlap_coef(markers1: dict, markers2: dict): + """Calculate overlap coefficient between the values of two dictionaries. + + Note: dict values must be sets + """ + overlap_coef = np.zeros((len(markers1), len(markers2))) + + for j, marker_group in enumerate(markers1): + tmp = [ + len(markers2[i].intersection(markers1[marker_group])) + / max(min(len(markers2[i]), len(markers1[marker_group])), 1) + for i in markers2 + ] + overlap_coef[j, :] = tmp + + return overlap_coef + + +def _calc_jaccard(markers1: dict, markers2: dict): + """Calculate jaccard index between the values of two dictionaries. + + Note: dict values must be sets + """ + jacc_results = np.zeros((len(markers1), len(markers2))) + + for j, marker_group in enumerate(markers1): + tmp = [ + len(markers2[i].intersection(markers1[marker_group])) + / len(markers2[i].union(markers1[marker_group])) + for i in markers2 + ] + jacc_results[j, :] = tmp + + return jacc_results + + +@doctest_needs("leidenalg") +def marker_gene_overlap( # noqa: PLR0912, PLR0915 + adata: AnnData, + reference_markers: dict[str, set] | dict[str, list], + *, + key: str = "rank_genes_groups", + method: _Method = "overlap_count", + normalize: Literal["reference", "data"] | None = None, + top_n_markers: int | None = None, + adj_pval_threshold: float | None = None, + key_added: str = "marker_gene_overlap", + inplace: bool = False, +): + """Calculate an overlap score between data-derived marker genes and provided markers. + + Marker gene overlap scores can be quoted as overlap counts, overlap + coefficients, or jaccard indices. The method returns a pandas dataframe + which can be used to annotate clusters based on marker gene overlaps. + + This function was written by Malte Luecken. + + Parameters + ---------- + adata + The annotated data matrix. + reference_markers + A marker gene dictionary object. Keys should be strings with the + cell identity name and values are sets or lists of strings which match + format of `adata.var_name`. + key + The key in `adata.uns` where the rank_genes_groups output is stored. + By default this is `'rank_genes_groups'`. + method + (default: `overlap_count`) + Method to calculate marker gene overlap. `'overlap_count'` uses the + intersection of the gene set, `'overlap_coef'` uses the overlap + coefficient, and `'jaccard'` uses the Jaccard index. + normalize + Normalization option for the marker gene overlap output. This parameter + can only be set when `method` is set to `'overlap_count'`. `'reference'` + normalizes the data by the total number of marker genes given in the + reference annotation per group. `'data'` normalizes the data by the + total number of marker genes used for each cluster. + top_n_markers + The number of top data-derived marker genes to use. By default the top + 100 marker genes are used. If `adj_pval_threshold` is set along with + `top_n_markers`, then `adj_pval_threshold` is ignored. + adj_pval_threshold + A significance threshold on the adjusted p-values to select marker + genes. This can only be used when adjusted p-values are calculated by + `sc.tl.rank_genes_groups()`. If `adj_pval_threshold` is set along with + `top_n_markers`, then `adj_pval_threshold` is ignored. + key_added + Name of the `.uns` field that will contain the marker overlap scores. + inplace + Return a marker gene dataframe or store it inplace in `adata.uns`. + + Returns + ------- + Returns :class:`pandas.DataFrame` if `inplace=False`, else returns an `AnnData` object where it sets the following field: + + `adata.uns[key_added]` : :class:`pandas.DataFrame` (dtype `float`) + Marker gene overlap scores. Default for `key_added` is `'marker_gene_overlap'`. + + Examples + -------- + >>> import scanpy as sc + >>> adata = sc.datasets.pbmc68k_reduced() + >>> sc.pp.pca(adata, svd_solver="arpack") + >>> sc.pp.neighbors(adata) + >>> sc.tl.leiden(adata) + >>> sc.tl.rank_genes_groups(adata, groupby="leiden") + >>> marker_genes = { + ... "CD4 T cells": {"IL7R"}, + ... "CD14+ Monocytes": {"CD14", "LYZ"}, + ... "B cells": {"MS4A1"}, + ... "CD8 T cells": {"CD8A"}, + ... "NK cells": {"GNLY", "NKG7"}, + ... "FCGR3A+ Monocytes": {"FCGR3A", "MS4A7"}, + ... "Dendritic Cells": {"FCER1A", "CST3"}, + ... "Megakaryocytes": {"PPBP"}, + ... } + >>> marker_matches = sc.tl.marker_gene_overlap(adata, marker_genes) + + """ + # Test user inputs + if inplace: + msg = ( + "Writing Pandas dataframes to h5ad is currently under development." + "\nPlease use `inplace=False`." + ) + raise NotImplementedError(msg) + + if key not in adata.uns: + msg = ( + "Could not find marker gene data. " + "Please run `sc.tl.rank_genes_groups()` first." + ) + raise ValueError(msg) + + avail_methods = {"overlap_count", "overlap_coef", "jaccard", "enrich"} + if method not in avail_methods: + msg = f"Method must be one of {avail_methods}." + raise ValueError(msg) + + if normalize == "None": + normalize = None + + avail_norm = {"reference", "data", None} + if normalize not in avail_norm: + msg = f"Normalize must be one of {avail_norm}." + raise ValueError(msg) + + if normalize is not None and method != "overlap_count": + msg = "Can only normalize with method=`overlap_count`." + raise ValueError(msg) + + if not all(isinstance(val, AbstractSet) for val in reference_markers.values()): + try: + reference_markers = { + key: set(val) for key, val in reference_markers.items() + } + except Exception as e: + msg = ( + "Please ensure that `reference_markers` contains " + "sets or lists of markers as values." + ) + raise ValueError(msg) from e + + if adj_pval_threshold is not None: + if "pvals_adj" not in adata.uns[key]: + msg = ( + "Could not find adjusted p-value data. " + "Please run `sc.tl.rank_genes_groups()` with a " + "method that outputs adjusted p-values." + ) + raise ValueError(msg) + + if adj_pval_threshold < 0: + logg.warning( + "`adj_pval_threshold` was set below 0. Threshold will be set to 0." + ) + adj_pval_threshold = 0 + elif adj_pval_threshold > 1: + logg.warning( + "`adj_pval_threshold` was set above 1. Threshold will be set to 1." + ) + adj_pval_threshold = 1 + + if top_n_markers is not None: + logg.warning( + "Both `adj_pval_threshold` and `top_n_markers` is set. " + "`adj_pval_threshold` will be ignored." + ) + + if top_n_markers is not None and top_n_markers < 1: + logg.warning( + "`top_n_markers` was set below 1. `top_n_markers` will be set to 1." + ) + top_n_markers = 1 + + # Get data-derived marker genes in a dictionary of sets + data_markers = dict() + cluster_ids = adata.uns[key]["names"].dtype.names + + for group in cluster_ids: + if top_n_markers is not None: + n_genes = min(top_n_markers, adata.uns[key]["names"].shape[0]) + data_markers[group] = set(adata.uns[key]["names"][group][:n_genes]) + elif adj_pval_threshold is not None: + n_genes = (adata.uns[key]["pvals_adj"][group] < adj_pval_threshold).sum() + data_markers[group] = set(adata.uns[key]["names"][group][:n_genes]) + if n_genes == 0: + logg.warning( + "No marker genes passed the significance threshold of " + f"{adj_pval_threshold} for cluster {group!r}." + ) + # Use top 100 markers as default if top_n_markers = None + else: + data_markers[group] = set(adata.uns[key]["names"][group][:100]) + + # Find overlaps + if method == "overlap_count": + marker_match = _calc_overlap_count(reference_markers, data_markers) + if normalize == "reference": + # Ensure rows sum to 1 + ref_lengths = np.array([ + len(reference_markers[m_group]) for m_group in reference_markers + ]) + marker_match = marker_match / ref_lengths[:, np.newaxis] + marker_match = np.nan_to_num(marker_match) + elif normalize == "data": + # Ensure columns sum to 1 + data_lengths = np.array([ + len(data_markers[dat_group]) for dat_group in data_markers + ]) + marker_match = marker_match / data_lengths + marker_match = np.nan_to_num(marker_match) + elif method == "overlap_coef": + marker_match = _calc_overlap_coef(reference_markers, data_markers) + elif method == "jaccard": + marker_match = _calc_jaccard(reference_markers, data_markers) + + # Note: + # Could add an 'enrich' option here + # (fisher's exact test or hypergeometric test), + # but that would require knowledge of the size of the space from which + # the reference marker gene set was taken. + # This is at best approximately known. + + # Create a pandas dataframe with the results + marker_groups = list(reference_markers.keys()) + clusters = list(cluster_ids) + marker_matching_df = pd.DataFrame( + marker_match, index=marker_groups, columns=clusters + ) + + # Store the results + if inplace: + adata.uns[key_added] = marker_matching_df + logg.hint(f"added\n {key_added!r}, marker overlap scores (adata.uns)") + else: + return marker_matching_df diff --git a/.venv/lib/python3.11/site-packages/scanpy/tools/_paga.py b/.venv/lib/python3.11/site-packages/scanpy/tools/_paga.py new file mode 100644 index 0000000000000000000000000000000000000000..cd4026f6770511ca807f19b3e12851a829cf3bdf --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/tools/_paga.py @@ -0,0 +1,620 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, NamedTuple + +import numpy as np +import scipy as sp +from scipy.sparse.csgraph import minimum_spanning_tree + +from .. import _utils +from .. import logging as logg +from .._compat import old_positionals +from ..neighbors import Neighbors + +if TYPE_CHECKING: + from typing import Literal + + from anndata import AnnData + +_AVAIL_MODELS = {"v1.0", "v1.2"} + + +@old_positionals("use_rna_velocity", "model", "neighbors_key", "copy") +def paga( + adata: AnnData, + groups: str | None = None, + *, + use_rna_velocity: bool = False, + model: Literal["v1.2", "v1.0"] = "v1.2", + neighbors_key: str | None = None, + copy: bool = False, +) -> AnnData | None: + """Map out the coarse-grained connectivity structures of complex manifolds :cite:p:`Wolf2019`. + + By quantifying the connectivity of partitions (groups, clusters) of the + single-cell graph, partition-based graph abstraction (PAGA) generates a much + simpler abstracted graph (*PAGA graph*) of partitions, in which edge weights + represent confidence in the presence of connections. By thresholding this + confidence in :func:`~scanpy.pl.paga`, a much simpler representation of the + manifold data is obtained, which is nonetheless faithful to the topology of + the manifold. + + The confidence should be interpreted as the ratio of the actual versus the + expected value of connections under the null model of randomly connecting + partitions. We do not provide a p-value as this null model does not + precisely capture what one would consider "connected" in real data, hence it + strongly overestimates the expected value. See an extensive discussion of + this in :cite:t:`Wolf2019`. + + .. note:: + Note that you can use the result of :func:`~scanpy.pl.paga` in + :func:`~scanpy.tl.umap` and :func:`~scanpy.tl.draw_graph` via + `init_pos='paga'` to get single-cell embeddings that are typically more + faithful to the global topology. + + Parameters + ---------- + adata + An annotated data matrix. + groups + Key for categorical in `adata.obs`. You can pass your predefined groups + by choosing any categorical annotation of observations. Default: + The first present key of `'leiden'` or `'louvain'`. + use_rna_velocity + Use RNA velocity to orient edges in the abstracted graph and estimate + transitions. Requires that `adata.uns` contains a directed single-cell + graph with key `['velocity_graph']`. This feature might be subject + to change in the future. + model + The PAGA connectivity model. + neighbors_key + If not specified, paga looks `.uns['neighbors']` for neighbors settings + and `.obsp['connectivities']`, `.obsp['distances']` for connectivities and + distances respectively (default storage places for `pp.neighbors`). + If specified, paga looks `.uns[neighbors_key]` for neighbors settings and + `.obsp[.uns[neighbors_key]['connectivities_key']]`, + `.obsp[.uns[neighbors_key]['distances_key']]` for connectivities and distances + respectively. + copy + Copy `adata` before computation and return a copy. Otherwise, perform + computation inplace and return `None`. + + Returns + ------- + Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following fields: + + `adata.uns['connectivities']` : :class:`numpy.ndarray` (dtype `float`) + The full adjacency matrix of the abstracted graph, weights correspond to + confidence in the connectivities of partitions. + `adata.uns['connectivities_tree']` : :class:`scipy.sparse.csr_matrix` (dtype `float`) + The adjacency matrix of the tree-like subgraph that best explains + the topology. + + Notes + ----- + Together with a random walk-based distance measure + (e.g. :func:`scanpy.tl.dpt`) this generates a partial coordinatization of + data useful for exploring and explaining its variation. + + .. currentmodule:: scanpy + + See Also + -------- + pl.paga + pl.paga_path + pl.paga_compare + + """ + check_neighbors = "neighbors" if neighbors_key is None else neighbors_key + if check_neighbors not in adata.uns: + msg = "You need to run `pp.neighbors` first to compute a neighborhood graph." + raise ValueError(msg) + if groups is None: + for k in ("leiden", "louvain"): + if k in adata.obs.columns: + groups = k + break + if groups is None: + msg = ( + "You need to run `tl.leiden` or `tl.louvain` to compute " + "community labels, or specify `groups='an_existing_key'`" + ) + raise ValueError(msg) + elif groups not in adata.obs.columns: + msg = f"`groups` key {groups!r} not found in `adata.obs`." + raise KeyError(msg) + + adata = adata.copy() if copy else adata + _utils.sanitize_anndata(adata) + start = logg.info("running PAGA") + paga = PAGA(adata, groups, model=model, neighbors_key=neighbors_key) + # only add if not present + if "paga" not in adata.uns: + adata.uns["paga"] = {} + if not use_rna_velocity: + paga.compute_connectivities() + adata.uns["paga"]["connectivities"] = paga.connectivities + adata.uns["paga"]["connectivities_tree"] = paga.connectivities_tree + # adata.uns['paga']['expected_n_edges_random'] = paga.expected_n_edges_random + adata.uns[f"{groups}_sizes"] = np.array(paga.ns) + else: + paga.compute_transitions() + adata.uns["paga"]["transitions_confidence"] = paga.transitions_confidence + # adata.uns['paga']['transitions_ttest'] = paga.transitions_ttest + adata.uns["paga"]["groups"] = groups + logg.info( + " finished", + time=start, + deep="added\n" + + ( + " 'paga/transitions_confidence', connectivities adjacency (adata.uns)" + # " 'paga/transitions_ttest', t-test on transitions (adata.uns)" + if use_rna_velocity + else " 'paga/connectivities', connectivities adjacency (adata.uns)\n" + " 'paga/connectivities_tree', connectivities subtree (adata.uns)" + ), + ) + return adata if copy else None + + +class PAGA: + def __init__(self, adata, groups, model="v1.2", neighbors_key=None): + assert groups in adata.obs.columns + self._adata = adata + self._neighbors = Neighbors(adata, neighbors_key=neighbors_key) + self._model = model + self._groups_key = groups + + def compute_connectivities(self): + if self._model == "v1.2": + return self._compute_connectivities_v1_2() + elif self._model == "v1.0": + return self._compute_connectivities_v1_0() + else: + msg = f"`model` {self._model} needs to be one of {_AVAIL_MODELS}." + raise ValueError(msg) + + def _compute_connectivities_v1_2(self): + import igraph + + ones = self._neighbors.distances.copy() + ones.data = np.ones(len(ones.data)) + # should be directed if we deal with distances + g = _utils.get_igraph_from_adjacency(ones, directed=True) + vc = igraph.VertexClustering( + g, membership=self._adata.obs[self._groups_key].cat.codes.values + ) + ns = vc.sizes() + n = sum(ns) + es_inner_cluster = [vc.subgraph(i).ecount() for i in range(len(ns))] + cg = vc.cluster_graph(combine_edges="sum") + inter_es = cg.get_adjacency_sparse(attribute="weight") + es = np.array(es_inner_cluster) + inter_es.sum(axis=1).A1 + inter_es = inter_es + inter_es.T # \epsilon_i + \epsilon_j + connectivities = inter_es.copy() + expected_n_edges = inter_es.copy() + inter_es = inter_es.tocoo() + for i, j, v in zip(inter_es.row, inter_es.col, inter_es.data, strict=True): + expected_random_null = (es[i] * ns[j] + es[j] * ns[i]) / (n - 1) + scaled_value = v / expected_random_null if expected_random_null != 0 else 1 + scaled_value = min(scaled_value, 1) + connectivities[i, j] = scaled_value + expected_n_edges[i, j] = expected_random_null + # set attributes + self.ns = ns + self.expected_n_edges_random = expected_n_edges + self.connectivities = connectivities + self.connectivities_tree = self._get_connectivities_tree_v1_2() + return inter_es.tocsr(), connectivities + + def _compute_connectivities_v1_0(self): + import igraph + + ones = self._neighbors.connectivities.copy() + ones.data = np.ones(len(ones.data)) + g = _utils.get_igraph_from_adjacency(ones) + vc = igraph.VertexClustering( + g, membership=self._adata.obs[self._groups_key].cat.codes.values + ) + ns = vc.sizes() + cg = vc.cluster_graph(combine_edges="sum") + inter_es = cg.get_adjacency_sparse(attribute="weight") / 2 + connectivities = inter_es.copy() + inter_es = inter_es.tocoo() + n_neighbors_sq = self._neighbors.n_neighbors**2 + for i, j, v in zip(inter_es.row, inter_es.col, inter_es.data, strict=True): + # have n_neighbors**2 inside sqrt for backwards compat + geom_mean_approx_knn = np.sqrt(n_neighbors_sq * ns[i] * ns[j]) + scaled_value = v / geom_mean_approx_knn if geom_mean_approx_knn != 0 else 1 + connectivities[i, j] = scaled_value + # set attributes + self.ns = ns + self.connectivities = connectivities + self.connectivities_tree = self._get_connectivities_tree_v1_0(inter_es) + return inter_es.tocsr(), connectivities + + def _get_connectivities_tree_v1_2(self): + inverse_connectivities = self.connectivities.copy() + inverse_connectivities.data = 1.0 / inverse_connectivities.data + connectivities_tree = minimum_spanning_tree(inverse_connectivities) + connectivities_tree_indices = [ + connectivities_tree[i].nonzero()[1] + for i in range(connectivities_tree.shape[0]) + ] + connectivities_tree = sp.sparse.lil_matrix( + self.connectivities.shape, dtype=float + ) + for i, neighbors in enumerate(connectivities_tree_indices): + if len(neighbors) > 0: + connectivities_tree[i, neighbors] = self.connectivities[i, neighbors] + return connectivities_tree.tocsr() + + def _get_connectivities_tree_v1_0(self, inter_es): + inverse_inter_es = inter_es.copy() + inverse_inter_es.data = 1.0 / inverse_inter_es.data + connectivities_tree = minimum_spanning_tree(inverse_inter_es) + connectivities_tree_indices = [ + connectivities_tree[i].nonzero()[1] + for i in range(connectivities_tree.shape[0]) + ] + connectivities_tree = sp.sparse.lil_matrix(inter_es.shape, dtype=float) + for i, neighbors in enumerate(connectivities_tree_indices): + if len(neighbors) > 0: + connectivities_tree[i, neighbors] = self.connectivities[i, neighbors] + return connectivities_tree.tocsr() + + def compute_transitions(self): + vkey = "velocity_graph" + if vkey not in self._adata.uns: + if "velocyto_transitions" in self._adata.uns: + self._adata.uns[vkey] = self._adata.uns["velocyto_transitions"] + logg.debug( + "The key 'velocyto_transitions' has been changed to 'velocity_graph'." + ) + else: + msg = ( + "The passed AnnData needs to have an `uns` annotation " + "with key 'velocity_graph' - a sparse matrix from RNA velocity." + ) + raise ValueError(msg) + if self._adata.uns[vkey].shape != (self._adata.n_obs, self._adata.n_obs): + msg = ( + f"The passed 'velocity_graph' have shape {self._adata.uns[vkey].shape} " + f"but shoud have shape {(self._adata.n_obs, self._adata.n_obs)}" + ) + raise ValueError(msg) + # restore this at some point + # if 'expected_n_edges_random' not in self._adata.uns['paga']: + # msg = 'Before running PAGA with `use_rna_velocity=True`, run it with `False`.' + # raise ValueError(msg) + import igraph + + g = _utils.get_igraph_from_adjacency( + self._adata.uns[vkey].astype("bool"), + directed=True, + ) + vc = igraph.VertexClustering( + g, membership=self._adata.obs[self._groups_key].cat.codes.values + ) + # set combine_edges to False if you want self loops + cg_full = vc.cluster_graph(combine_edges="sum") + transitions = cg_full.get_adjacency_sparse(attribute="weight") + transitions = transitions - transitions.T + transitions_conf = transitions.copy() + transitions = transitions.tocoo() + total_n = self._neighbors.n_neighbors * np.array(vc.sizes()) + # total_n_sum = sum(total_n) + # expected_n_edges_random = self._adata.uns['paga']['expected_n_edges_random'] + for i, j, v in zip( + transitions.row, transitions.col, transitions.data, strict=True + ): + # if expected_n_edges_random[i, j] != 0: + # # factor 0.5 because of asymmetry + # reference = 0.5 * expected_n_edges_random[i, j] + # else: + # # approximate + # reference = self._neighbors.n_neighbors * total_n[i] * total_n[j] / total_n_sum + reference = np.sqrt(total_n[i] * total_n[j]) + transitions_conf[i, j] = 0 if v < 0 else v / reference + transitions_conf.eliminate_zeros() + # transpose in order to match convention of stochastic matrices + # entry ij means transition from j to i + self.transitions_confidence = transitions_conf.T + + def compute_transitions_old(self): + import igraph + + g = _utils.get_igraph_from_adjacency( + self._adata.uns["velocyto_transitions"], + directed=True, + ) + vc = igraph.VertexClustering( + g, membership=self._adata.obs[self._groups_key].cat.codes.values + ) + # this stores all single-cell edges in the cluster graph + cg_full = vc.cluster_graph(combine_edges=False) + # this is the boolean version that simply counts edges in the clustered graph + g_bool = _utils.get_igraph_from_adjacency( + self._adata.uns["velocyto_transitions"].astype("bool"), + directed=True, + ) + vc_bool = igraph.VertexClustering( + g_bool, membership=self._adata.obs[self._groups_key].cat.codes.values + ) + cg_bool = vc_bool.cluster_graph(combine_edges="sum") # collapsed version + transitions = cg_bool.get_adjacency_sparse(attribute="weight") + total_n = self._neighbors.n_neighbors * np.array(vc_bool.sizes()) + transitions_ttest = transitions.copy() + transitions_confidence = transitions.copy() + from scipy.stats import ttest_1samp + + for i in range(transitions.shape[0]): + neighbors = transitions[i].nonzero()[1] + for j in neighbors: + forward = cg_full.es.select(_source=i, _target=j)["weight"] + backward = cg_full.es.select(_source=j, _target=i)["weight"] + # backward direction: add minus sign + values = np.array(list(forward) + list(-np.array(backward))) + # require some minimal number of observations + if len(values) < 5: + transitions_ttest[i, j] = 0 + transitions_ttest[j, i] = 0 + transitions_confidence[i, j] = 0 + transitions_confidence[j, i] = 0 + continue + t, prob = ttest_1samp(values, 0.0) + if t > 0: + # number of outgoing edges greater than number of ingoing edges + # i.e., transition from i to j + transitions_ttest[i, j] = -np.log10(max(prob, 1e-10)) + transitions_ttest[j, i] = 0 + else: + transitions_ttest[j, i] = -np.log10(max(prob, 1e-10)) + transitions_ttest[i, j] = 0 + # geom_mean + geom_mean = np.sqrt(total_n[i] * total_n[j]) + diff = (len(forward) - len(backward)) / geom_mean + if diff > 0: + transitions_confidence[i, j] = diff + transitions_confidence[j, i] = 0 + else: + transitions_confidence[j, i] = -diff + transitions_confidence[i, j] = 0 + transitions_ttest.eliminate_zeros() + transitions_confidence.eliminate_zeros() + # transpose in order to match convention of stochastic matrices + # entry ij means transition from j to i + self.transitions_ttest = transitions_ttest.T + self.transitions_confidence = transitions_confidence.T + + +def paga_degrees(adata: AnnData) -> list[int]: + """Compute the degree of each node in the abstracted graph. + + Parameters + ---------- + adata + Annotated data matrix. + + Returns + ------- + List of degrees for each node. + + """ + import networkx as nx + + g = nx.Graph(adata.uns["paga"]["connectivities"]) + degrees = [d for _, d in g.degree(weight="weight")] + return degrees + + +def paga_expression_entropies(adata: AnnData) -> list[float]: + """Compute the median expression entropy for each node-group. + + Parameters + ---------- + adata + Annotated data matrix. + + Returns + ------- + Entropies of median expressions for each node. + + """ + from scipy.stats import entropy + + _groups_order, groups_masks = _utils.select_groups( + adata, key=adata.uns["paga"]["groups"] + ) + entropies = [] + for mask in groups_masks: + x_mask = adata.X[mask].todense() + x_median = np.nanmedian(x_mask, axis=1, overwrite_input=True) + x_probs = (x_median - np.nanmin(x_median)) / ( + np.nanmax(x_median) - np.nanmin(x_median) + ) + entropies.append(entropy(x_probs)) + return entropies + + +class PAGAComparePathsResult(NamedTuple): + frac_steps: float + n_steps: int + frac_paths: float + n_paths: int + + +def paga_compare_paths( # noqa: PLR0912, PLR0915 + adata1: AnnData, + adata2: AnnData, + adjacency_key: str = "connectivities", + adjacency_key2: str | None = None, +) -> PAGAComparePathsResult: + """Compare paths in abstracted graphs in two datasets. + + Compute the fraction of consistent paths between leafs, a measure for the + topological similarity between graphs. + + By increasing the verbosity to level 4 and 5, the paths that do not agree + and the paths that agree are written to the output, respectively. + + The PAGA "groups key" needs to be the same in both objects. + + Parameters + ---------- + adata1, adata2 + Annotated data matrices to compare. + adjacency_key + Key for indexing the adjacency matrices in `.uns['paga']` to be used in + adata1 and adata2. + adjacency_key2 + If provided, used for adata2. + + Returns + ------- + NamedTuple with attributes + + frac_steps + fraction of consistent steps + n_steps + total number of steps in paths + frac_paths + Fraction of consistent paths + n_paths + Number of paths + + """ + import networkx as nx + + g1 = nx.Graph(adata1.uns["paga"][adjacency_key]) + g2 = nx.Graph( + adata2.uns["paga"][ + adjacency_key2 if adjacency_key2 is not None else adjacency_key + ] + ) + leaf_nodes1 = [str(x) for x in g1.nodes() if g1.degree(x) == 1] + logg.debug(f"leaf nodes in graph 1: {leaf_nodes1}") + paga_groups = adata1.uns["paga"]["groups"] + asso_groups1 = _utils.identify_groups( + adata1.obs[paga_groups].values, + adata2.obs[paga_groups].values, + ) + asso_groups2 = _utils.identify_groups( + adata2.obs[paga_groups].values, + adata1.obs[paga_groups].values, + ) + orig_names1 = adata1.obs[paga_groups].cat.categories + orig_names2 = adata2.obs[paga_groups].cat.categories + + import itertools + + n_steps = 0 + n_agreeing_steps = 0 + n_paths = 0 + n_agreeing_paths = 0 + # loop over all pairs of leaf nodes in the reference adata1 + for r, s in itertools.combinations(leaf_nodes1, r=2): + r2, s2 = asso_groups1[r][0], asso_groups1[s][0] + on1_g1, on2_g1 = (orig_names1[int(i)] for i in [r, s]) + on1_g2, on2_g2 = (orig_names2[int(i)] for i in [r2, s2]) + logg.debug( + f"compare shortest paths between leafs ({on1_g1}, {on2_g1}) " + f"in graph1 and ({on1_g2}, {on2_g2}) in graph2:" + ) + try: + path1 = [str(x) for x in nx.shortest_path(g1, int(r), int(s))] + except nx.NetworkXNoPath: + path1 = None + try: + path2 = [str(x) for x in nx.shortest_path(g2, int(r2), int(s2))] + except nx.NetworkXNoPath: + path2 = None + if path1 is None and path2 is None: + # consistent behavior + n_paths += 1 + n_agreeing_paths += 1 + n_steps += 1 + n_agreeing_steps += 1 + logg.debug("there are no connecting paths in both graphs") + continue + elif path1 is None or path2 is None: + # non-consistent result + n_paths += 1 + n_steps += 1 + continue + if len(path1) >= len(path2): + path_mapped = [asso_groups1[l] for l in path1] + path_compare = path2 + path_compare_id = 2 + path_compare_orig_names = [ + [orig_names2[int(s)] for s in l] for l in path_compare + ] + path_mapped_orig_names = [ + [orig_names2[int(s)] for s in l] for l in path_mapped + ] + else: + path_mapped = [asso_groups2[l] for l in path2] + path_compare = path1 + path_compare_id = 1 + path_compare_orig_names = [ + [orig_names1[int(s)] for s in l] for l in path_compare + ] + path_mapped_orig_names = [ + [orig_names1[int(s)] for s in l] for l in path_mapped + ] + n_agreeing_steps_path = 0 + ip_progress = 0 + for il, l in enumerate(path_compare[:-1]): + for ip, p in enumerate(path_mapped): + if ( + ip < ip_progress + or l not in p + or not ( + ip + 1 < len(path_mapped) + and path_compare[il + 1] in path_mapped[ip + 1] + ) + ): + continue + # make sure that a step backward leads us to the same value of l + # in case we "jumped" + logg.debug( + f"found matching step ({l} -> {path_compare_orig_names[il + 1]}) " + f"at position {il} in path{path_compare_id} and position {ip} in path_mapped" + ) + consistent_history = True + for iip in range(ip, ip_progress, -1): + if l not in path_mapped[iip - 1]: + consistent_history = False + if consistent_history: + # here, we take one step further back (ip_progress - 1); it's implied that this + # was ok in the previous step + poss = list(range(ip - 1, ip_progress - 2, -1)) + logg.debug( + f" step(s) backward to position(s) {poss} " + "in path_mapped are fine, too: valid step" + ) + n_agreeing_steps_path += 1 + ip_progress = ip + 1 + break + n_steps_path = len(path_compare) - 1 + n_agreeing_steps += n_agreeing_steps_path + n_steps += n_steps_path + n_paths += 1 + if n_agreeing_steps_path == n_steps_path: + n_agreeing_paths += 1 + + # only for the output, use original names + path1_orig_names = [orig_names1[int(s)] for s in path1] + path2_orig_names = [orig_names2[int(s)] for s in path2] + logg.debug( + f" path1 = {path1_orig_names},\n" + f"path_mapped = {[list(p) for p in path_mapped_orig_names]},\n" + f" path2 = {path2_orig_names},\n" + f"-> n_agreeing_steps = {n_agreeing_steps_path} / n_steps = {n_steps_path}.", + ) + return PAGAComparePathsResult( + frac_steps=n_agreeing_steps / n_steps if n_steps > 0 else np.nan, + n_steps=n_steps if n_steps > 0 else np.nan, + frac_paths=n_agreeing_paths / n_paths if n_steps > 0 else np.nan, + n_paths=n_paths if n_steps > 0 else np.nan, + ) diff --git a/.venv/lib/python3.11/site-packages/scanpy/tools/_rank_genes_groups.py b/.venv/lib/python3.11/site-packages/scanpy/tools/_rank_genes_groups.py new file mode 100644 index 0000000000000000000000000000000000000000..9bf3edc501c0eb46b0ba579eea8e83ddb8d748b2 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/tools/_rank_genes_groups.py @@ -0,0 +1,926 @@ +"""Rank genes according to differential expression.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +import numba +import numpy as np +import pandas as pd +from scipy import sparse + +from .. import _utils +from .. import logging as logg +from .._compat import CSBase, njit, old_positionals +from .._utils import ( + check_nonnegative_integers, + get_literal_vals, + raise_not_implemented_error_if_backed_type, +) +from ..get import _check_mask +from ..preprocessing._utils import _get_mean_var + +if TYPE_CHECKING: + from collections.abc import Generator, Iterable + + from anndata import AnnData + from numpy.typing import NDArray + + _CorrMethod = Literal["benjamini-hochberg", "bonferroni"] + + +# Used with get_literal_vals +_Method = Literal["logreg", "t-test", "wilcoxon", "t-test_overestim_var"] + +_CONST_MAX_SIZE = 10000000 + + +def _select_top_n(scores: NDArray, n_top: int): + n_from = scores.shape[0] + reference_indices = np.arange(n_from, dtype=int) + partition = np.argpartition(scores, -n_top)[-n_top:] + partial_indices = np.argsort(scores[partition])[::-1] + global_indices = reference_indices[partition][partial_indices] + + return global_indices + + +@njit +def rankdata(data: NDArray[np.number]) -> NDArray[np.float64]: + """Parallelized version of scipy.stats.rankdata.""" + ranked = np.empty(data.shape, dtype=np.float64) + for j in numba.prange(data.shape[1]): + arr = np.ravel(data[:, j]) + sorter = np.argsort(arr) + + arr = arr[sorter] + obs = np.concatenate((np.array([True]), arr[1:] != arr[:-1])) + + dense = np.empty(obs.size, dtype=np.int64) + dense[sorter] = obs.cumsum() + + # cumulative counts of each unique value + count = np.concatenate((np.flatnonzero(obs), np.array([len(obs)]))) + ranked[:, j] = 0.5 * (count[dense] + count[dense - 1] + 1) + + return ranked + + +@njit +def _tiecorrect(rankvals: NDArray[np.number]) -> NDArray[np.float64]: + """Parallelized version of scipy.stats.tiecorrect.""" + tc = np.ones(rankvals.shape[1], dtype=np.float64) + for j in numba.prange(rankvals.shape[1]): + arr = np.sort(np.ravel(rankvals[:, j])) + idx = np.flatnonzero( + np.concatenate((np.array([True]), arr[1:] != arr[:-1], np.array([True]))) + ) + cnt = np.diff(idx).astype(np.float64) + + size = np.float64(arr.size) + if size >= 2: + tc[j] = 1.0 - (cnt**3 - cnt).sum() / (size**3 - size) + + return tc + + +def _ranks( + x: NDArray[np.number] | CSBase, + /, + mask_obs: NDArray[np.bool_] | None = None, + mask_obs_rest: NDArray[np.bool_] | None = None, +) -> Generator[tuple[NDArray[np.float64], int, int], None, None]: + n_genes = x.shape[1] + + if isinstance(x, CSBase): + merge = lambda tpl: sparse.vstack(tpl).toarray() + adapt = lambda x: x.toarray() + else: + merge = np.vstack + adapt = lambda x: x + + masked = mask_obs is not None and mask_obs_rest is not None + + if masked: + n_cells = np.count_nonzero(mask_obs) + np.count_nonzero(mask_obs_rest) + get_chunk = lambda x, left, right: merge(( + x[mask_obs, left:right], + x[mask_obs_rest, left:right], + )) + else: + n_cells = x.shape[0] + get_chunk = lambda x, left, right: adapt(x[:, left:right]) + + # Calculate chunk frames + max_chunk = max(_CONST_MAX_SIZE // n_cells, 1) + + for left in range(0, n_genes, max_chunk): + right = min(left + max_chunk, n_genes) + + ranks = rankdata(get_chunk(x, left, right)) + yield ranks, left, right + + +class _RankGenes: + def __init__( + self, + adata: AnnData, + groups: Iterable[str] | Literal["all"], + groupby: str, + *, + mask_var: NDArray[np.bool_] | None = None, + reference: Literal["rest"] | str = "rest", + use_raw: bool = True, + layer: str | None = None, + comp_pts: bool = False, + ) -> None: + self.mask_var = mask_var + if (base := adata.uns.get("log1p", {}).get("base")) is not None: + self.expm1_func = lambda x: np.expm1(x * np.log(base)) + else: + self.expm1_func = np.expm1 + + self.groups_order, self.groups_masks_obs = _utils.select_groups( + adata, groups, groupby + ) + + # Singlet groups cause division by zero errors + invalid_groups_selected = set(self.groups_order) & set( + adata.obs[groupby].value_counts().loc[lambda x: x < 2].index + ) + + if len(invalid_groups_selected) > 0: + msg = ( + f"Could not calculate statistics for groups {', '.join(invalid_groups_selected)} " + "since they only contain one sample." + ) + raise ValueError(msg) + + adata_comp = adata + if layer is not None: + if use_raw: + msg = "Cannot specify `layer` and have `use_raw=True`." + raise ValueError(msg) + x = adata_comp.layers[layer] + else: + if use_raw and adata.raw is not None: + adata_comp = adata.raw + x = adata_comp.X + raise_not_implemented_error_if_backed_type(x, "rank_genes_groups") + + # for correct getnnz calculation + if isinstance(x, CSBase): + x.eliminate_zeros() + + if self.mask_var is not None: + self.X = x[:, self.mask_var] + self.var_names = adata_comp.var_names[self.mask_var] + + else: + self.X = x + self.var_names = adata_comp.var_names + + self.ireference = None + if reference != "rest": + self.ireference = np.where(self.groups_order == reference)[0][0] + + self.means = None + self.vars = None + + self.means_rest = None + self.vars_rest = None + + self.comp_pts = comp_pts + self.pts = None + self.pts_rest = None + + self.stats = None + + # for logreg only + self.grouping_mask = adata.obs[groupby].isin(self.groups_order) + self.grouping = adata.obs.loc[self.grouping_mask, groupby] + + def _basic_stats(self) -> None: + """Set self.{means,vars,pts}{,_rest} depending on X.""" + n_genes = self.X.shape[1] + n_groups = self.groups_masks_obs.shape[0] + + self.means = np.zeros((n_groups, n_genes)) + self.vars = np.zeros((n_groups, n_genes)) + self.pts = np.zeros((n_groups, n_genes)) if self.comp_pts else None + + if self.ireference is None: + self.means_rest = np.zeros((n_groups, n_genes)) + self.vars_rest = np.zeros((n_groups, n_genes)) + self.pts_rest = np.zeros((n_groups, n_genes)) if self.comp_pts else None + else: + mask_rest = self.groups_masks_obs[self.ireference] + x_rest = self.X[mask_rest] + self.means[self.ireference], self.vars[self.ireference] = _get_mean_var( + x_rest + ) + # deleting the next line causes a memory leak for some reason + del x_rest + + if isinstance(self.X, CSBase): + get_nonzeros = lambda x: x.getnnz(axis=0) + else: + get_nonzeros = lambda x: np.count_nonzero(x, axis=0) + + for group_index, mask_obs in enumerate(self.groups_masks_obs): + x_mask = self.X[mask_obs] + + if self.comp_pts: + self.pts[group_index] = get_nonzeros(x_mask) / x_mask.shape[0] + + if self.ireference is not None and group_index == self.ireference: + continue + + self.means[group_index], self.vars[group_index] = _get_mean_var(x_mask) + + if self.ireference is None: + mask_rest = ~mask_obs + x_rest = self.X[mask_rest] + ( + self.means_rest[group_index], + self.vars_rest[group_index], + ) = _get_mean_var(x_rest) + # this can be costly for sparse data + if self.comp_pts: + self.pts_rest[group_index] = get_nonzeros(x_rest) / x_rest.shape[0] + # deleting the next line causes a memory leak for some reason + del x_rest + + def t_test( + self, method: Literal["t-test", "t-test_overestim_var"] + ) -> Generator[tuple[int, NDArray[np.floating], NDArray[np.floating]], None, None]: + from scipy import stats + + self._basic_stats() + + for group_index, (mask_obs, mean_group, var_group) in enumerate( + zip(self.groups_masks_obs, self.means, self.vars, strict=True) + ): + if self.ireference is not None and group_index == self.ireference: + continue + + ns_group = np.count_nonzero(mask_obs) + + if self.ireference is not None: + mean_rest = self.means[self.ireference] + var_rest = self.vars[self.ireference] + ns_other = np.count_nonzero(self.groups_masks_obs[self.ireference]) + else: + mean_rest = self.means_rest[group_index] + var_rest = self.vars_rest[group_index] + ns_other = self.X.shape[0] - ns_group + + if method == "t-test": + ns_rest = ns_other + elif method == "t-test_overestim_var": + # hack for overestimating the variance for small groups + ns_rest = ns_group + else: + msg = "Method does not exist." + raise ValueError(msg) + + # TODO: Come up with better solution. Mask unexpressed genes? + # See https://github.com/scipy/scipy/issues/10269 + with np.errstate(invalid="ignore"): + scores, pvals = stats.ttest_ind_from_stats( + mean1=mean_group, + std1=np.sqrt(var_group), + nobs1=ns_group, + mean2=mean_rest, + std2=np.sqrt(var_rest), + nobs2=ns_rest, + equal_var=False, # Welch's + ) + + # I think it's only nan when means are the same and vars are 0 + scores[np.isnan(scores)] = 0 + # This also has to happen for Benjamini Hochberg + pvals[np.isnan(pvals)] = 1 + + yield group_index, scores, pvals + + def wilcoxon( + self, *, tie_correct: bool + ) -> Generator[tuple[int, NDArray[np.floating], NDArray[np.floating]], None, None]: + from scipy import stats + + self._basic_stats() + + n_genes = self.X.shape[1] + # First loop: Loop over all genes + if self.ireference is not None: + # initialize space for z-scores + scores = np.zeros(n_genes) + # initialize space for tie correction coefficients + tc_coef = np.zeros(n_genes) if tie_correct else 1 + + for group_index, mask_obs in enumerate(self.groups_masks_obs): + if group_index == self.ireference: + continue + + mask_obs_rest = self.groups_masks_obs[self.ireference] + + n_active = np.count_nonzero(mask_obs) + m_active = np.count_nonzero(mask_obs_rest) + + if n_active <= 25 or m_active <= 25: + logg.hint( + "Few observations in a group for " + "normal approximation (<=25). Lower test accuracy." + ) + + # Calculate rank sums for each chunk for the current mask + for ranks, left, right in _ranks(self.X, mask_obs, mask_obs_rest): + scores[left:right] = ranks[0:n_active, :].sum(axis=0) + if tie_correct: + tc_coef[left:right] = _tiecorrect(ranks) + + std_dev = np.sqrt( + tc_coef * n_active * m_active * (n_active + m_active + 1) / 12.0 + ) + + scores = ( + scores - (n_active * ((n_active + m_active + 1) / 2.0)) + ) / std_dev + scores[np.isnan(scores)] = 0 + pvals = 2 * stats.distributions.norm.sf(np.abs(scores)) + + yield group_index, scores, pvals + # If no reference group exists, + # ranking needs only to be done once (full mask) + else: + n_groups = self.groups_masks_obs.shape[0] + scores = np.zeros((n_groups, n_genes)) + n_cells = self.X.shape[0] + + if tie_correct: + tc_coef = np.zeros((n_groups, n_genes)) + + for ranks, left, right in _ranks(self.X): + # sum up adjusted_ranks to calculate W_m,n + for group_index, mask_obs in enumerate(self.groups_masks_obs): + scores[group_index, left:right] = ranks[mask_obs, :].sum(axis=0) + if tie_correct: + tc_coef[group_index, left:right] = _tiecorrect(ranks) + + for group_index, mask_obs in enumerate(self.groups_masks_obs): + n_active = np.count_nonzero(mask_obs) + + coef = tc_coef[group_index] if tie_correct else 1 + + std_dev = np.sqrt( + coef * n_active * (n_cells - n_active) * (n_cells + 1) / 12.0 + ) + + scores[group_index, :] = ( + scores[group_index, :] - (n_active * (n_cells + 1) / 2.0) + ) / std_dev + scores[np.isnan(scores)] = 0 + pvals = 2 * stats.distributions.norm.sf(np.abs(scores[group_index, :])) + + yield group_index, scores[group_index], pvals + + def logreg( + self, **kwds + ) -> Generator[tuple[int, NDArray[np.floating], None], None, None]: + # if reference is not set, then the groups listed will be compared to the rest + # if reference is set, then the groups listed will be compared only to the other groups listed + from sklearn.linear_model import LogisticRegression + + # Indexing with a series causes issues, possibly segfault + x = self.X[self.grouping_mask.values, :] + + if len(self.groups_order) == 1: + msg = "Cannot perform logistic regression on a single cluster." + raise ValueError(msg) + + clf = LogisticRegression(**kwds) + clf.fit(x, self.grouping.cat.codes) + scores_all = clf.coef_ + # not all codes necessarily appear in data + existing_codes = np.unique(self.grouping.cat.codes) + for igroup, cat in enumerate(self.groups_order): + if len(self.groups_order) <= 2: # binary logistic regression + scores = scores_all[0] + else: + # cat code is index of cat value in .categories + cat_code: int = np.argmax(self.grouping.cat.categories == cat) + # index of scores row is index of cat code in array of existing codes + scores_idx: int = np.argmax(existing_codes == cat_code) + scores = scores_all[scores_idx] + yield igroup, scores, None + + if len(self.groups_order) <= 2: + break + + def compute_statistics( # noqa: PLR0912 + self, + method: _Method, + *, + corr_method: _CorrMethod = "benjamini-hochberg", + n_genes_user: int | None = None, + rankby_abs: bool = False, + tie_correct: bool = False, + **kwds, + ) -> None: + if method in {"t-test", "t-test_overestim_var"}: + generate_test_results = self.t_test(method) + elif method == "wilcoxon": + generate_test_results = self.wilcoxon(tie_correct=tie_correct) + elif method == "logreg": + generate_test_results = self.logreg(**kwds) + + self.stats = None + + n_genes = self.X.shape[1] + + for group_index, scores, pvals in generate_test_results: + group_name = str(self.groups_order[group_index]) + + if n_genes_user is not None: + scores_sort = np.abs(scores) if rankby_abs else scores + global_indices = _select_top_n(scores_sort, n_genes_user) + first_col = "names" + else: + global_indices = slice(None) + first_col = "scores" + + if self.stats is None: + idx = pd.MultiIndex.from_tuples([(group_name, first_col)]) + self.stats = pd.DataFrame(columns=idx) + + if n_genes_user is not None: + self.stats[group_name, "names"] = self.var_names[global_indices] + + self.stats[group_name, "scores"] = scores[global_indices] + + if pvals is not None: + self.stats[group_name, "pvals"] = pvals[global_indices] + if corr_method == "benjamini-hochberg": + from statsmodels.stats.multitest import multipletests + + pvals[np.isnan(pvals)] = 1 + _, pvals_adj, _, _ = multipletests( + pvals, alpha=0.05, method="fdr_bh" + ) + elif corr_method == "bonferroni": + pvals_adj = np.minimum(pvals * n_genes, 1.0) + self.stats[group_name, "pvals_adj"] = pvals_adj[global_indices] + + if self.means is not None: + mean_group = self.means[group_index] + if self.ireference is None: + mean_rest = self.means_rest[group_index] + else: + mean_rest = self.means[self.ireference] + foldchanges = (self.expm1_func(mean_group) + 1e-9) / ( + self.expm1_func(mean_rest) + 1e-9 + ) # add small value to remove 0's + self.stats[group_name, "logfoldchanges"] = np.log2( + foldchanges[global_indices] + ) + + if n_genes_user is None: + self.stats.index = self.var_names + + +@old_positionals( + "mask", + "use_raw", + "groups", + "reference", + "n_genes", + "rankby_abs", + "pts", + "key_added", + "copy", + "method", + "corr_method", + "tie_correct", + "layer", +) +def rank_genes_groups( # noqa: PLR0912, PLR0913, PLR0915 + adata: AnnData, + groupby: str, + *, + mask_var: NDArray[np.bool_] | str | None = None, + use_raw: bool | None = None, + groups: Literal["all"] | Iterable[str] = "all", + reference: str = "rest", + n_genes: int | None = None, + rankby_abs: bool = False, + pts: bool = False, + key_added: str | None = None, + copy: bool = False, + method: _Method | None = None, + corr_method: _CorrMethod = "benjamini-hochberg", + tie_correct: bool = False, + layer: str | None = None, + **kwds, +) -> AnnData | None: + """Rank genes for characterizing groups. + + Expects logarithmized data. + + Parameters + ---------- + adata + Annotated data matrix. + groupby + The key of the observations grouping to consider. + mask_var + Select subset of genes to use in statistical tests. + use_raw + Use `raw` attribute of `adata` if present. The default behavior is to use `raw` if present. + layer + Key from `adata.layers` whose value will be used to perform tests on. + groups + Subset of groups, e.g. [`'g1'`, `'g2'`, `'g3'`], to which comparison + shall be restricted, or `'all'` (default), for all groups. Note that if + `reference='rest'` all groups will still be used as the reference, not + just those specified in `groups`. + reference + If `'rest'`, compare each group to the union of the rest of the group. + If a group identifier, compare with respect to this group. + n_genes + The number of genes that appear in the returned tables. + Defaults to all genes. + method + The default method is `'t-test'`, + `'t-test_overestim_var'` overestimates variance of each group, + `'wilcoxon'` uses Wilcoxon rank-sum, + `'logreg'` uses logistic regression. See :cite:t:`Ntranos2019`, + `here `__ and `here + `__, + for why this is meaningful. + corr_method + p-value correction method. + Used only for `'t-test'`, `'t-test_overestim_var'`, and `'wilcoxon'`. + tie_correct + Use tie correction for `'wilcoxon'` scores. + Used only for `'wilcoxon'`. + rankby_abs + Rank genes by the absolute value of the score, not by the + score. The returned scores are never the absolute values. + pts + Compute the fraction of cells expressing the genes. + key_added + The key in `adata.uns` information is saved to. + copy + Whether to copy `adata` or modify it inplace. + kwds + Are passed to test methods. Currently this affects only parameters that + are passed to :class:`sklearn.linear_model.LogisticRegression`. + For instance, you can pass `penalty='l1'` to try to come up with a + minimal set of genes that are good predictors (sparse solution meaning + few non-zero fitted coefficients). + + Returns + ------- + Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following fields: + + `adata.uns['rank_genes_groups' | key_added]['names']` : structured :class:`numpy.ndarray` (dtype `object`) + Structured array to be indexed by group id storing the gene + names. Ordered according to scores. + `adata.uns['rank_genes_groups' | key_added]['scores']` : structured :class:`numpy.ndarray` (dtype `object`) + Structured array to be indexed by group id storing the z-score + underlying the computation of a p-value for each gene for each + group. Ordered according to scores. + `adata.uns['rank_genes_groups' | key_added]['logfoldchanges']` : structured :class:`numpy.ndarray` (dtype `object`) + Structured array to be indexed by group id storing the log2 + fold change for each gene for each group. Ordered according to + scores. Only provided if method is 't-test' like. + Note: this is an approximation calculated from mean-log values. + `adata.uns['rank_genes_groups' | key_added]['pvals']` : structured :class:`numpy.ndarray` (dtype `float`) + p-values. + `adata.uns['rank_genes_groups' | key_added]['pvals_adj']` : structured :class:`numpy.ndarray` (dtype `float`) + Corrected p-values. + `adata.uns['rank_genes_groups' | key_added]['pts']` : :class:`pandas.DataFrame` (dtype `float`) + Fraction of cells expressing the genes for each group. + `adata.uns['rank_genes_groups' | key_added]['pts_rest']` : :class:`pandas.DataFrame` (dtype `float`) + Only if `reference` is set to `'rest'`. + Fraction of cells from the union of the rest of each group + expressing the genes. + + Notes + ----- + There are slight inconsistencies depending on whether sparse + or dense data are passed. See `here `__. + + Examples + -------- + >>> import scanpy as sc + >>> adata = sc.datasets.pbmc68k_reduced() + >>> sc.tl.rank_genes_groups(adata, "bulk_labels", method="wilcoxon") + >>> # to visualize the results + >>> sc.pl.rank_genes_groups(adata) + + """ + mask_var = _check_mask(adata, mask_var, "var") + + if use_raw is None: + use_raw = adata.raw is not None + elif use_raw is True and adata.raw is None: + msg = "Received `use_raw=True`, but `adata.raw` is empty." + raise ValueError(msg) + + if method is None: + method = "t-test" + + if "only_positive" in kwds: + rankby_abs = not kwds.pop("only_positive") # backwards compat + + start = logg.info("ranking genes") + if method not in (avail_methods := get_literal_vals(_Method)): + msg = f"Method must be one of {avail_methods}." + raise ValueError(msg) + + avail_corr = {"benjamini-hochberg", "bonferroni"} + if corr_method not in avail_corr: + msg = f"Correction method must be one of {avail_corr}." + raise ValueError(msg) + + adata = adata.copy() if copy else adata + _utils.sanitize_anndata(adata) + # for clarity, rename variable + if groups == "all": + groups_order = "all" + elif isinstance(groups, str | int): + msg = "Specify a sequence of groups" + raise ValueError(msg) + else: + groups_order = list(groups) + if isinstance(groups_order[0], int): + groups_order = [str(n) for n in groups_order] + if reference != "rest" and reference not in set(groups_order): + groups_order += [reference] + if reference != "rest" and reference not in adata.obs[groupby].cat.categories: + cats = adata.obs[groupby].cat.categories.tolist() + msg = f"reference = {reference} needs to be one of groupby = {cats}." + raise ValueError(msg) + + if key_added is None: + key_added = "rank_genes_groups" + adata.uns[key_added] = {} + adata.uns[key_added]["params"] = dict( + groupby=groupby, + reference=reference, + method=method, + use_raw=use_raw, + layer=layer, + corr_method=corr_method, + ) + + test_obj = _RankGenes( + adata, + groups_order, + groupby, + mask_var=mask_var, + reference=reference, + use_raw=use_raw, + layer=layer, + comp_pts=pts, + ) + + if check_nonnegative_integers(test_obj.X) and method != "logreg": + logg.warning( + "It seems you use rank_genes_groups on the raw count data. " + "Please logarithmize your data before calling rank_genes_groups." + ) + + # for clarity, rename variable + n_genes_user = n_genes + # make sure indices are not OoB in case there are less genes than n_genes + # defaults to all genes + if n_genes_user is None or n_genes_user > test_obj.X.shape[1]: + n_genes_user = test_obj.X.shape[1] + + logg.debug(f"consider {groupby!r} groups:") + logg.debug(f"with sizes: {np.count_nonzero(test_obj.groups_masks_obs, axis=1)}") + + test_obj.compute_statistics( + method, + corr_method=corr_method, + n_genes_user=n_genes_user, + rankby_abs=rankby_abs, + tie_correct=tie_correct, + **kwds, + ) + + if test_obj.pts is not None: + groups_names = [str(name) for name in test_obj.groups_order] + adata.uns[key_added]["pts"] = pd.DataFrame( + test_obj.pts.T, index=test_obj.var_names, columns=groups_names + ) + if test_obj.pts_rest is not None: + adata.uns[key_added]["pts_rest"] = pd.DataFrame( + test_obj.pts_rest.T, index=test_obj.var_names, columns=groups_names + ) + + test_obj.stats.columns = test_obj.stats.columns.swaplevel() + + dtypes = { + "names": "O", + "scores": "float32", + "logfoldchanges": "float32", + "pvals": "float64", + "pvals_adj": "float64", + } + + for col in test_obj.stats.columns.levels[0]: + adata.uns[key_added][col] = test_obj.stats[col].to_records( + index=False, column_dtypes=dtypes[col] + ) + + logg.info( + " finished", + time=start, + deep=( + f"added to `.uns[{key_added!r}]`\n" + " 'names', sorted np.recarray to be indexed by group ids\n" + " 'scores', sorted np.recarray to be indexed by group ids\n" + + ( + " 'logfoldchanges', sorted np.recarray to be indexed by group ids\n" + " 'pvals', sorted np.recarray to be indexed by group ids\n" + " 'pvals_adj', sorted np.recarray to be indexed by group ids" + if method in {"t-test", "t-test_overestim_var", "wilcoxon"} + else "" + ) + ), + ) + return adata if copy else None + + +def _calc_frac(x: NDArray[np.number] | CSBase, /) -> NDArray[np.float64]: + n_nonzero = ( + x.getnnz(axis=0) if isinstance(x, CSBase) else np.count_nonzero(x, axis=0) + ) + return n_nonzero / x.shape[0] + + +@old_positionals( + "key", + "groupby", + "use_raw", + "key_added", + "min_in_group_fraction", + "min_fold_change", + "max_out_group_fraction", + "compare_abs", +) +def filter_rank_genes_groups( # noqa: PLR0912 + adata: AnnData, + *, + key: str | None = None, + groupby: str | None = None, + use_raw: bool | None = None, + key_added: str = "rank_genes_groups_filtered", + min_in_group_fraction: float = 0.25, + min_fold_change: float = 1, + max_out_group_fraction: float = 0.5, + compare_abs: bool = False, +) -> None: + """Filter out genes based on two criteria. + + 1. log fold change and + 2. fraction of genes expressing the + gene within and outside the `groupby` categories. + + See :func:`~scanpy.tl.rank_genes_groups`. + + Results are stored in `adata.uns[key_added]` + (default: 'rank_genes_groups_filtered'). + + To preserve the original structure of adata.uns['rank_genes_groups'], + filtered genes are set to `NaN`. + + Parameters + ---------- + adata + key + groupby + use_raw + key_added + min_in_group_fraction + min_fold_change + max_out_group_fraction + compare_abs + If `True`, compare absolute values of log fold change with `min_fold_change`. + + Returns + ------- + Same output as :func:`scanpy.tl.rank_genes_groups` but with filtered genes names set to + `nan` + + Examples + -------- + >>> import scanpy as sc + >>> adata = sc.datasets.pbmc68k_reduced() + >>> sc.tl.rank_genes_groups(adata, "bulk_labels", method="wilcoxon") + >>> sc.tl.filter_rank_genes_groups(adata, min_fold_change=3) + >>> # visualize results + >>> sc.pl.rank_genes_groups(adata, key="rank_genes_groups_filtered") + >>> # visualize results using dotplot + >>> sc.pl.rank_genes_groups_dotplot(adata, key="rank_genes_groups_filtered") + + """ + if key is None: + key = "rank_genes_groups" + + if groupby is None: + groupby = adata.uns[key]["params"]["groupby"] + + if use_raw is None: + use_raw = adata.uns[key]["params"]["use_raw"] + + same_params = ( + adata.uns[key]["params"]["groupby"] == groupby + and adata.uns[key]["params"]["reference"] == "rest" + and adata.uns[key]["params"]["use_raw"] == use_raw + ) + + use_logfolds = same_params and "logfoldchanges" in adata.uns[key] + use_fraction = same_params and "pts_rest" in adata.uns[key] + + # convert structured numpy array into DataFrame + gene_names = pd.DataFrame(adata.uns[key]["names"]) + + fraction_in_cluster_matrix = pd.DataFrame( + np.zeros(gene_names.shape), + columns=gene_names.columns, + index=gene_names.index, + ) + fraction_out_cluster_matrix = pd.DataFrame( + np.zeros(gene_names.shape), + columns=gene_names.columns, + index=gene_names.index, + ) + + if use_logfolds: + fold_change_matrix = pd.DataFrame(adata.uns[key]["logfoldchanges"]) + else: + fold_change_matrix = pd.DataFrame( + np.zeros(gene_names.shape), + columns=gene_names.columns, + index=gene_names.index, + ) + + if (base := adata.uns.get("log1p", {}).get("base")) is not None: + expm1_func = lambda x: np.expm1(x * np.log(base)) + else: + expm1_func = np.expm1 + + logg.info( + f"Filtering genes using: " + f"min_in_group_fraction: {min_in_group_fraction} " + f"min_fold_change: {min_fold_change}, " + f"max_out_group_fraction: {max_out_group_fraction}" + ) + + for cluster in gene_names.columns: + # iterate per column + var_names = gene_names[cluster].values + + if not use_logfolds or not use_fraction: + sub_x = adata.raw[:, var_names].X if use_raw else adata[:, var_names].X + in_group = (adata.obs[groupby] == cluster).to_numpy() + x_in = sub_x[in_group] + x_out = sub_x[~in_group] + + if use_fraction: + fraction_in_cluster_matrix.loc[:, cluster] = ( + adata.uns[key]["pts"][cluster].loc[var_names].values + ) + fraction_out_cluster_matrix.loc[:, cluster] = ( + adata.uns[key]["pts_rest"][cluster].loc[var_names].values + ) + else: + fraction_in_cluster_matrix.loc[:, cluster] = _calc_frac(x_in) + fraction_out_cluster_matrix.loc[:, cluster] = _calc_frac(x_out) + + if not use_logfolds: + # compute mean value + mean_in_cluster = np.ravel(x_in.mean(0)) + mean_out_cluster = np.ravel(x_out.mean(0)) + # compute fold change + fold_change_matrix.loc[:, cluster] = np.log2( + (expm1_func(mean_in_cluster) + 1e-9) + / (expm1_func(mean_out_cluster) + 1e-9) + ) + + if compare_abs: + fold_change_matrix = fold_change_matrix.abs() + # filter original_matrix + gene_names = gene_names[ + (fraction_in_cluster_matrix > min_in_group_fraction) + & (fraction_out_cluster_matrix < max_out_group_fraction) + & (fold_change_matrix > min_fold_change) + ] + # create new structured array using 'key_added'. + adata.uns[key_added] = adata.uns[key].copy() + adata.uns[key_added]["names"] = gene_names.to_records(index=False) diff --git a/.venv/lib/python3.11/site-packages/scanpy/tools/_score_genes.py b/.venv/lib/python3.11/site-packages/scanpy/tools/_score_genes.py new file mode 100644 index 0000000000000000000000000000000000000000..d582acfb3b3a662cb52807dd789cf5d493926459 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/tools/_score_genes.py @@ -0,0 +1,331 @@ +"""Calculate scores based on the expression of gene lists.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd + +from .. import logging as logg +from .._compat import CSBase, old_positionals +from .._utils import _check_use_raw, is_backed_type +from ..get import _get_obs_rep + +if TYPE_CHECKING: + from collections.abc import Callable, Generator, Sequence + from typing import Literal + + from anndata import AnnData + from numpy.typing import DTypeLike, NDArray + + from .._utils.random import _LegacyRandom + + try: + _StrIdx = pd.Index[str] + except TypeError: # Sphinx + _StrIdx = pd.Index + _GetSubset = Callable[[_StrIdx], np.ndarray | CSBase] + + +def _sparse_nanmean(x: CSBase, /, axis: Literal[0, 1]) -> NDArray[np.float64]: + """np.nanmean equivalent for sparse matrices.""" + if not isinstance(x, CSBase): + msg = "X must be a compressed sparse matrix" + raise TypeError(msg) + + # count the number of nan elements per row/column (dep. on axis) + z = x.copy() + z.data = np.isnan(z.data) + z.eliminate_zeros() + n_elements = z.shape[axis] - z.sum(axis) + + # set the nans to 0, so that a normal .sum() works + y = x.copy() + y.data[np.isnan(y.data)] = 0 + y.eliminate_zeros() + + # the average + s = y.sum(axis, dtype="float64") # float64 for score_genes function compatibility) + m = s / n_elements + + return m + + +@old_positionals( + "ctrl_size", "gene_pool", "n_bins", "score_name", "random_state", "copy", "use_raw" +) +def score_genes( # noqa: PLR0913 + adata: AnnData, + gene_list: Sequence[str] | pd.Index[str], + *, + ctrl_as_ref: bool = True, + ctrl_size: int = 50, + gene_pool: Sequence[str] | pd.Index[str] | None = None, + n_bins: int = 25, + score_name: str = "score", + random_state: _LegacyRandom = 0, + copy: bool = False, + use_raw: bool | None = None, + layer: str | None = None, +) -> AnnData | None: + """Score a set of genes :cite:p:`Satija2015`. + + The score is the average expression of a set of genes after subtraction by + the average expression of a reference set of genes. The reference set is + randomly sampled from the `gene_pool` for each binned expression value. + + This reproduces the approach in Seurat :cite:p:`Satija2015` and has been implemented + for Scanpy by Davide Cittaro. + + Parameters + ---------- + adata + The annotated data matrix. + gene_list + The list of gene names used for score calculation. + ctrl_as_ref + Allow the algorithm to use the control genes as reference. + Will be changed to `False` in scanpy 2.0. + ctrl_size + Number of reference genes to be sampled from each bin. If `len(gene_list)` is not too + low, you can set `ctrl_size=len(gene_list)`. + gene_pool + Genes for sampling the reference set. Default is all genes. + n_bins + Number of expression level bins for sampling. + score_name + Name of the field to be added in `.obs`. + random_state + The random seed for sampling. + copy + Copy `adata` or modify it inplace. + use_raw + Whether to use `raw` attribute of `adata`. Defaults to `True` if `.raw` is present. + + .. versionchanged:: 1.4.5 + Default value changed from `False` to `None`. + layer + Key from `adata.layers` whose value will be used to perform tests on. + + Returns + ------- + Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following field: + + `adata.obs[score_name]` : :class:`numpy.ndarray` (dtype `float`) + Scores of each cell. + + Examples + -------- + See this `notebook `__. + + """ + start = logg.info(f"computing score {score_name!r}") + adata = adata.copy() if copy else adata + use_raw = _check_use_raw(adata, use_raw, layer=layer) + if is_backed_type(adata.X) and not use_raw: + msg = f"score_genes is not implemented for matrices of type {type(adata.X)}" + raise NotImplementedError(msg) + + if random_state is not None: + np.random.seed(random_state) + + gene_list, gene_pool, get_subset = _check_score_genes_args( + adata, gene_list, gene_pool, use_raw=use_raw, layer=layer + ) + del use_raw, layer, random_state + + # Trying here to match the Seurat approach in scoring cells. + # Basically we need to compare genes against random genes in a matched + # interval of expression. + + control_genes = pd.Index([], dtype="string") + for r_genes in _score_genes_bins( + gene_list, + gene_pool, + ctrl_as_ref=ctrl_as_ref, + ctrl_size=ctrl_size, + n_bins=n_bins, + get_subset=get_subset, + ): + control_genes = control_genes.union(r_genes) + + if len(control_genes) == 0: + msg = "No control genes found in any cut." + if ctrl_as_ref: + msg += " Try setting `ctrl_as_ref=False`." + raise RuntimeError(msg) + + means_list, means_control = ( + _nan_means(get_subset(genes), axis=1, dtype="float64") + for genes in (gene_list, control_genes) + ) + score = means_list - means_control + + adata.obs[score_name] = pd.Series( + np.array(score).ravel(), index=adata.obs_names, dtype="float64" + ) + + logg.info( + " finished", + time=start, + deep=( + "added\n" + f" {score_name!r}, score of gene set (adata.obs).\n" + f" {len(control_genes)} total control genes are used." + ), + ) + return adata if copy else None + + +def _check_score_genes_args( + adata: AnnData, + gene_list: pd.Index[str] | Sequence[str], + gene_pool: pd.Index[str] | Sequence[str] | None, + *, + layer: str | None, + use_raw: bool, +) -> tuple[pd.Index[str], pd.Index[str], _GetSubset]: + """Restrict `gene_list` and `gene_pool` to present genes in `adata`. + + Also returns a function to get subset of `adata.X` based on a set of genes passed. + """ + var_names = adata.raw.var_names if use_raw else adata.var_names + gene_list = pd.Index([gene_list] if isinstance(gene_list, str) else gene_list) + genes_to_ignore = gene_list.difference(var_names, sort=False) # first get missing + gene_list = gene_list.intersection(var_names) # then restrict to present + if len(genes_to_ignore) > 0: + logg.warning(f"genes are not in var_names and ignored: {genes_to_ignore}") + if len(gene_list) == 0: + msg = "No valid genes were passed for scoring." + raise ValueError(msg) + + if gene_pool is None: + gene_pool = var_names.astype("string") + else: + gene_pool = pd.Index(gene_pool, dtype="string").intersection(var_names) + if len(gene_pool) == 0: + msg = "No valid genes were passed for reference set." + raise ValueError(msg) + + def get_subset(genes: pd.Index[str]): + x = _get_obs_rep(adata, use_raw=use_raw, layer=layer) + if len(genes) == len(var_names): + return x + idx = var_names.get_indexer(genes) + return x[:, idx] + + return gene_list, gene_pool, get_subset + + +def _score_genes_bins( + gene_list: pd.Index[str], + gene_pool: pd.Index[str], + *, + ctrl_as_ref: bool, + ctrl_size: int, + n_bins: int, + get_subset: _GetSubset, +) -> Generator[pd.Index[str], None, None]: + # average expression of genes + obs_avg = pd.Series(_nan_means(get_subset(gene_pool), axis=0), index=gene_pool) + # Sometimes (and I don’t know how) missing data may be there, with NaNs for missing entries + obs_avg = obs_avg[np.isfinite(obs_avg)] + + n_items = int(np.round(len(obs_avg) / (n_bins - 1))) + obs_cut = obs_avg.rank(method="min") // n_items + keep_ctrl_in_obs_cut = False if ctrl_as_ref else obs_cut.index.isin(gene_list) + + # now pick `ctrl_size` genes from every cut + for cut in np.unique(obs_cut.loc[gene_list]): + r_genes: pd.Index[str] = obs_cut[(obs_cut == cut) & ~keep_ctrl_in_obs_cut].index + if len(r_genes) == 0: + msg = ( + f"No control genes for {cut=}. You might want to increase " + f"gene_pool size (current size: {len(gene_pool)})" + ) + logg.warning(msg) + if ctrl_size < len(r_genes): + r_genes = r_genes.to_series().sample(ctrl_size).index + if ctrl_as_ref: # otherwise `r_genes` is already filtered + r_genes = r_genes.difference(gene_list) + yield r_genes + + +def _nan_means( + x: np.ndarray | CSBase, *, axis: Literal[0, 1], dtype: DTypeLike | None = None +) -> NDArray[np.float64]: + if isinstance(x, CSBase): + return np.array(_sparse_nanmean(x, axis=axis)).flatten() + return np.nanmean(x, axis=axis, dtype=dtype) + + +@old_positionals("s_genes", "g2m_genes", "copy") +def score_genes_cell_cycle( + adata: AnnData, + *, + s_genes: Sequence[str], + g2m_genes: Sequence[str], + copy: bool = False, + **kwargs, +) -> AnnData | None: + """Score cell cycle genes :cite:p:`Satija2015`. + + Given two lists of genes associated to S phase and G2M phase, calculates + scores and assigns a cell cycle phase (G1, S or G2M). See + :func:`~scanpy.tl.score_genes` for more explanation. + + Parameters + ---------- + adata + The annotated data matrix. + s_genes + List of genes associated with S phase. + g2m_genes + List of genes associated with G2M phase. + copy + Copy `adata` or modify it inplace. + **kwargs + Are passed to :func:`~scanpy.tl.score_genes`. `ctrl_size` is not + possible, as it's set as `min(len(s_genes), len(g2m_genes))`. + + Returns + ------- + Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following fields: + + `adata.obs['S_score']` : :class:`pandas.Series` (dtype `object`) + The score for S phase for each cell. + `adata.obs['G2M_score']` : :class:`pandas.Series` (dtype `object`) + The score for G2M phase for each cell. + `adata.obs['phase']` : :class:`pandas.Series` (dtype `object`) + The cell cycle phase (`S`, `G2M` or `G1`) for each cell. + + See Also + -------- + score_genes + + Examples + -------- + See this `notebook `__. + + """ + logg.info("calculating cell cycle phase") + + adata = adata.copy() if copy else adata + ctrl_size = min(len(s_genes), len(g2m_genes)) + for genes, name in [(s_genes, "S_score"), (g2m_genes, "G2M_score")]: + score_genes(adata, genes, score_name=name, ctrl_size=ctrl_size, **kwargs) + scores = adata.obs[["S_score", "G2M_score"]] + + # default phase is S + phase = pd.Series("S", index=scores.index) + + # if G2M is higher than S, it's G2M + phase[scores["G2M_score"] > scores["S_score"]] = "G2M" + + # if all scores are negative, it's G1... + phase[np.all(scores < 0, axis=1)] = "G1" + + adata.obs["phase"] = phase + logg.hint(" 'phase', cell cycle phase (adata.obs)") + return adata if copy else None diff --git a/.venv/lib/python3.11/site-packages/scanpy/tools/_sim.py b/.venv/lib/python3.11/site-packages/scanpy/tools/_sim.py new file mode 100644 index 0000000000000000000000000000000000000000..e1435cc2d55b4948e050e58d015a288584e2f186 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/tools/_sim.py @@ -0,0 +1,1288 @@ +# Author: Alex Wolf (https://falexwolf.de) +"""Simulate Data. + +Simulate stochastic dynamic systems to model gene expression dynamics and +cause-effect data. + +Todo: +---- +Beta Version. The code will be reorganized soon. + +""" + +from __future__ import annotations + +import itertools +import shutil +import sys +from pathlib import Path +from types import MappingProxyType +from typing import TYPE_CHECKING + +import numpy as np +import scipy as sp + +from .. import _utils, readwrite +from .. import logging as logg +from .._compat import old_positionals +from .._settings import settings + +if TYPE_CHECKING: + from collections.abc import Mapping + from typing import ClassVar, Literal + + from anndata import AnnData + + +@old_positionals( + "params_file", + "tmax", + "branching", + "nrRealizations", + "noiseObs", + "noiseDyn", + "step", + "seed", + "writedir", +) +def sim( + model: Literal["krumsiek11", "toggleswitch"], + *, + params_file: bool = True, + tmax: int | None = None, + branching: bool | None = None, + nrRealizations: int | None = None, + noiseObs: float | None = None, + noiseDyn: float | None = None, + step: int | None = None, + seed: int | None = None, + writedir: Path | str | None = None, +) -> AnnData: + """Simulate dynamic gene expression data :cite:p:`Wittmann2009` :cite:p:`Wolf2018`. + + Sample from a stochastic differential equation model built from + literature-curated boolean gene regulatory networks, as suggested by + :cite:t:`Wittmann2009`. The Scanpy implementation can be found in :cite:t:`Wolf2018`. + + Parameters + ---------- + model + Model file in 'sim_models' directory. + params_file + Read default params from file. + tmax + Number of time steps per realization of time series. + branching + Only write realizations that contain new branches. + nrRealizations + Number of realizations. + noiseObs + Observatory/Measurement noise. + noiseDyn + Dynamic noise. + step + Interval for saving state of system. + seed + Seed for generation of random numbers. + writedir + Path to directory for writing output files. + + Returns + ------- + Annotated data matrix. + + Examples + -------- + See this `use case `__ + + """ + params = locals() + if params_file: + model_key = Path(model).with_suffix("").name + from .. import sim_models + + pfile_sim = Path(sim_models.__file__).parent / f"{model_key}_params.txt" + default_params = readwrite.read_params(pfile_sim) + params = _utils.update_params(default_params, params) + adata = sample_dynamic_data(**params) + adata.uns["iroot"] = 0 + return adata + + +def add_args(p): + """Update parser with tool specific arguments. + + This overwrites was is done in utils.uns_args. + """ + # dictionary for adding arguments + dadd_args = { + "--opfile": { + "default": "", + "metavar": "f", + "type": str, + "help": 'Specify a parameter file (default: "sim/${exkey}_params.txt")', + } + } + p = _utils.add_args(p, dadd_args) + return p + + +def sample_dynamic_data(**params): # noqa: PLR0912, PLR0915 + model_key = Path(params["model"]).with_suffix("").name + writedir = params.get("writedir") + if writedir is None: + writedir = settings.writedir / f"{model_key}_sim" + else: + writedir = Path(writedir) + writedir.mkdir(parents=True, exist_ok=True) + readwrite.write_params(writedir / "params.txt", params) + # init variables + tmax = params["tmax"] + branching = params["branching"] + noiseObs = params["noiseObs"] + noiseDyn = params["noiseDyn"] + nrRealizations = params["nrRealizations"] + step = params["step"] # step size for saving the figure + + nrSamples = 1 # how many files? + maxRestarts = 1000 + maxNrSamples = 1 + + # simple vector auto regressive process or + # hill kinetics process simulation + if "krumsiek11" not in model_key: + # create instance, set seed + grnsim = GRNsim(model=model_key, params=params) + nrOffEdges_list = np.zeros(nrSamples) + for sample in range(nrSamples): + # random topology / for a given edge density + if "hill" not in model_key: + Coupl = np.array(grnsim.Coupl) + for _sampleCoupl in range(10): + nrOffEdges = 0 + for gp in range(grnsim.dim): + for g in range(grnsim.dim): + # only consider off-diagonal edges + if g != gp: + Coupl[gp, g] = 0.7 if np.random.rand() < 0.4 else 0 + nrOffEdges += 1 if Coupl[gp, g] > 0 else 0 + else: + Coupl[gp, g] = 0.7 + # check that the coupling matrix does not have eigenvalues + # greater than 1, which would lead to an exploding var process + if max(sp.linalg.eig(Coupl)[0]) < 1: + break + nrOffEdges_list[sample] = nrOffEdges + grnsim.set_coupl(Coupl) + # init type + real = 0 + X0 = np.random.rand(grnsim.dim) + Xsamples = [] + for restart in range(nrRealizations + maxRestarts): + # slightly break symmetry in initial conditions + if "toggleswitch" in model_key: + X0 = np.array([ + 0.8 for i in range(grnsim.dim) + ]) + 0.01 * np.random.randn(grnsim.dim) + X = grnsim.sim_model(tmax=tmax, X0=X0, noiseDyn=noiseDyn) + # check branching + check = True + if branching: + check, Xsamples = _check_branching(X, Xsamples, restart) + if check: + real += 1 + grnsim.write_data( + X[::step], + dir=writedir, + noiseObs=noiseObs, + append=restart != 0, + branching=branching, + nrRealizations=nrRealizations, + ) + # append some zeros + if "zeros" in writedir.name and real == 2: + grnsim.write_data( + noiseDyn * np.random.randn(500, 3), + dir=writedir, + noiseObs=noiseObs, + append=restart != 0, + branching=branching, + nrRealizations=nrRealizations, + ) + if real >= nrRealizations: + break + logg.debug( + f"mean nr of offdiagonal edges {nrOffEdges_list.mean()} " + f"compared to total nr {grnsim.dim * (grnsim.dim - 1) / 2.0}" + ) + + # more complex models + else: + initType = "random" + + dim = 11 + step = 5 + + grnsim = GRNsim(dim=dim, initType=initType, model=model_key, params=params) + Xsamples = [] + for _sample in range(maxNrSamples): + # choose initial conditions such that branchings result + if initType == "branch": + X0mean = grnsim.branch_init_model1(tmax) + if X0mean is None: + grnsim.set_coupl() + continue + real = 0 + for restart in range(nrRealizations + maxRestarts): + if initType == "branch": + # vary initial conditions around mean + X0 = X0mean + (0.05 * np.random.rand(dim) - 0.025 * np.ones(dim)) + else: + # generate random initial conditions within [0.3,0.7] + X0 = 0.4 * np.random.rand(dim) + 0.3 + if model_key in [5, 6]: + X0 = np.array([0.3, 0.3, 0, 0, 0, 0]) + if model_key in [7, 8, 9, 10]: + X0 = 0.6 * np.random.rand(dim) + 0.2 + X0[2:] = np.zeros(4) + if "krumsiek11" in model_key: + X0 = np.zeros(dim) + X0[grnsim.varNames["Gata2"]] = 0.8 + X0[grnsim.varNames["Pu.1"]] = 0.8 + X0[grnsim.varNames["Cebpa"]] = 0.8 + X0 += 0.001 * np.random.randn(dim) + if False: + switch_gene = restart - (nrRealizations - dim) + if switch_gene >= dim: + break + X0[switch_gene] = 0 if X0[switch_gene] > 0.1 else 0.8 + X = grnsim.sim_model(tmax, X0=X0, noiseDyn=noiseDyn, restart=restart) + # check branching + check = True + if branching: + check, Xsamples = _check_branching(X, Xsamples, restart) + if check: + real += 1 + grnsim.write_data( + X[::step], + dir=writedir, + noiseObs=noiseObs, + append=restart != 0, + branching=branching, + nrRealizations=nrRealizations, + ) + if real >= nrRealizations: + break + # load the last simulation file + filename = max(writedir.glob("sim*.txt")) + logg.info(f"reading simulation results {filename}") + adata = readwrite._read( + filename, first_column_names=True, suppress_cache_warning=True + ) + adata.uns["tmax_write"] = tmax / step + return adata + + +def write_data( # noqa: PLR0912, PLR0913 + X, + dir=Path("sim/test"), + *, + append=False, + header="", + varNames: Mapping[str, int] = MappingProxyType({}), + Adj: np.ndarray | None = None, + Coupl: np.ndarray | None = None, + boolRules: Mapping[str, str] = MappingProxyType({}), + model="", + modelType="", + invTimeStep=1, +): + """Write simulated data. + + Accounts for saving at the same time an ID + and a model file. + """ + dir.mkdir(parents=True, exist_ok=True) + # update file with sample ids + filename = dir / "id.txt" + if filename.is_file(): + with filename.open("r") as f: + id = int(f.read()) + (0 if append else 1) + else: + id = 0 + with filename.open("w") as f: + id = f"{id:0>6}" + f.write(str(id)) + # dimension + dim = X.shape[1] + # write files with adjacancy and coupling matrices + if not append: + if False: + if Adj is not None: + # due to 'update formulation' of model, there + # is always a diagonal dependence + Adj = np.copy(Adj) + if "hill" in model: + for i in range(Adj.shape[0]): + Adj[i, i] = 1 + np.savetxt(f"{dir}/adj_{id}.txt", Adj, header=header, fmt="%d") + if Coupl is not None: + np.savetxt(f"{dir}/coupl_{id}.txt", Coupl, header=header, fmt="%10.6f") + # write model file + if varNames and Coupl is not None: + with (dir / f"model_{id}.txt").open("w") as f: + f.write('# For each "variable = ", there must be a right hand side: \n') + f.write( + "# either an empty string or a python-style logical expression \n" + ) + f.write('# involving variable names, "or", "and", "(", ")". \n') + f.write("# The order of equations matters! \n") + f.write("# \n") + f.write(f"# modelType = {modelType}\n") + f.write(f"# invTimeStep = {invTimeStep}\n") + f.write("# \n") + f.write("# boolean update rules: \n") + for k, v in boolRules.items(): + f.write(f"{k} = {v}\n") + # write coupling via names + f.write("# coupling list: \n") + names = list(varNames.keys()) + for gp in range(dim): + for g in range(dim): + if np.abs(Coupl[gp, g]) > 1e-10: + f.write( + f"{names[gp]:10} {names[g]:10} {Coupl[gp, g]:10.3} \n" + ) + # write simulated data + # the binary mode option in the following line is a fix for python 3 + # variable names + if varNames: + header += f"{'it':>2} " + for v in varNames: + header += f"{v:>7} " + with (dir / f"sim_{id}.txt").open("ab" if append else "wb") as f: + np.savetxt( + f, + np.c_[np.arange(0, X.shape[0]), X], + header=("" if append else header), + fmt=["%4.f"] + ["%7.4f" for i in range(dim)], + ) + + +class GRNsim: + """Simlulation of stochastic dynamic systems. + + Main application: simulation of gene expression dynamics. + + Also standard models are implemented. + """ + + availModels: ClassVar = dict( + krumsiek11=( + "myeloid progenitor network, Krumsiek et al., PLOS One 6, e22649, " + "\n equations from Table 1 on page 3, " + "doi:10.1371/journal.pone.0022649 \n" + ), + var="vector autoregressive process \n", + hill="process with hill kinetics \n", + ) + + writeOutputOnce = True + + def __init__( + self, + *, + dim=3, + model="ex0", + modelType="var", + initType="random", + show=False, + verbosity=0, + Coupl=None, + params=MappingProxyType({}), + ): + """Initialize. + + Params + ------ + model + either string for predefined model, + or directory with a model file and a couple matrix files + """ + # number of nodes / dimension of system + self.dim = dim if Coupl is None else Coupl.shape[0] + self.maxnpar = 1 # maximal number of parents + self.p_indep = 0.4 # fraction of independent genes + self.model = model + self.modelType = modelType + self.initType = initType # string characterizing a specific initial + self.show = show + self.verbosity = verbosity + # checks + if initType not in ["branch", "random"]: + msg = "initType must be either: branch, random" + raise RuntimeError(msg) + if model not in self.availModels: + message = "model not among predefined models \n" # noqa: F841 # TODO FIX + # read from file + from .. import sim_models + + model = Path(sim_models.__file__).parent / f"{model}.txt" + if not model.is_file(): + msg = f"Model file {model} does not exist" + raise RuntimeError(msg) + self.model = model + # set the coupling matrix, and with that the adjacency matrix + self.set_coupl(Coupl=Coupl) + # seed + np.random.seed(params["seed"]) + # header + self.header = f"model = {self.model.name} \n" + # params + self.params = params + + def sim_model(self, tmax, X0, noiseDyn=0, restart=0): + """Simulate the model.""" + self.noiseDyn = noiseDyn + X = np.zeros((tmax, self.dim)) + X[0] = X0 + noiseDyn * np.random.randn(self.dim) + # run simulation + for t in range(1, tmax): + if self.modelType == "hill": + Xdiff = self.Xdiff_hill(X[t - 1]) + elif self.modelType == "var": + Xdiff = self.Xdiff_var(X[t - 1]) + else: + msg = f"Unknown modelType {self.modelType!r}" + raise ValueError(msg) + X[t] = X[t - 1] + Xdiff + # add dynamic noise + X[t] += noiseDyn * np.random.randn(self.dim) + return X + + def Xdiff_hill(self, Xt): + """Build Xdiff from coefficients of boolean network. + + That is, using self.boolCoeff. The employed functions + are Hill type activation and deactivation functions. + + See Wittmann et al., BMC Syst. Biol. 3, 98 (2009), + doi:10.1186/1752-0509-3-98 for more details. + """ + verbosity = self.verbosity > 0 and self.writeOutputOnce + self.writeOutputOnce = False + Xdiff = np.zeros(self.dim) + for ichild, child in enumerate(self.pas.keys()): + # check whether list of parents is non-empty, + # otherwise continue + if self.pas[child]: + Xdiff_syn = 0 # synthesize term + if verbosity > 0: + Xdiff_syn_str = "" + else: + continue + # loop over all tuples for which the boolean update + # rule returns true, these are stored in self.boolCoeff + for ituple, tuple in enumerate(self.boolCoeff[child]): + Xdiff_syn_tuple = 1 + Xdiff_syn_tuple_str = "" + for iv, v in enumerate(tuple): + iparent = self.varNames[self.pas[child][iv]] + x = Xt[iparent] + threshold = 0.1 / np.abs(self.Coupl[ichild, iparent]) + Xdiff_syn_tuple *= ( + self.hill_a(x, threshold) if v else self.hill_i(x, threshold) + ) + if verbosity > 0: + Xdiff_syn_tuple_str += ( + f"{'a' if v else 'i'}" + f"({self.pas[child][iv]}, {threshold:.2})" + ) + Xdiff_syn += Xdiff_syn_tuple + if verbosity > 0: + Xdiff_syn_str += ("+" if ituple != 0 else "") + Xdiff_syn_tuple_str + # multiply with degradation term + Xdiff[ichild] = self.invTimeStep * (Xdiff_syn - Xt[ichild]) + if verbosity > 0: + Xdiff_str = ( + f"{child}_{child}-{child} = " + f"{self.invTimeStep}*({Xdiff_syn_str}-{child})" + ) + settings.m(0, Xdiff_str) + return Xdiff + + def Xdiff_var(self, Xt, verbosity=0): + # subtract the current state + Xdiff = -Xt + # add the information from the past + Xdiff += np.dot(self.Coupl, Xt) + return Xdiff + + def hill_a(self, x, threshold=0.1, power=2): + """Activating hill function.""" + x_pow = np.power(x, power) + threshold_pow = np.power(threshold, power) + return x_pow / (x_pow + threshold_pow) + + def hill_i(self, x, threshold=0.1, power=2): + """Inhibiting hill function. + + Is equivalent to 1-hill_a(self,x,power,threshold). + """ + x_pow = np.power(x, power) + threshold_pow = np.power(threshold, power) + return threshold_pow / (x_pow + threshold_pow) + + def nhill_a(self, x, threshold=0.1, power=2, ichild=2): + """Normalized activating hill function.""" # noqa: D401 + x_pow = np.power(x, power) + threshold_pow = np.power(threshold, power) + return x_pow / (x_pow + threshold_pow) * (1 + threshold_pow) + + def nhill_i(self, x, threshold=0.1, power=2): + """Normalized inhibiting hill function. + + Is equivalent to 1-nhill_a(self,x,power,threshold). + """ # noqa: D401 + x_pow = np.power(x, power) + threshold_pow = np.power(threshold, power) + return threshold_pow / (x_pow + threshold_pow) * (1 - x_pow) + + def read_model(self): + """Read the model and the couplings from the model file.""" + if self.verbosity > 0: + settings.m(0, "reading model", self.model) + # read model + boolRules = [] + for line in self.model.open(): + if line.startswith("#") and "modelType =" in line: + keyval = line + if "|" in line: + keyval, _type = line.split("|")[:2] + self.modelType = keyval.split("=")[1].strip() + if line.startswith("#") and "invTimeStep =" in line: + keyval = line + if "|" in line: + keyval, _type = line.split("|")[:2] + self.invTimeStep = float(keyval.split("=")[1].strip()) + if not line.startswith("#"): + boolRules.append([s.strip() for s in line.split("=")]) + if line.startswith("# coupling list:"): + break + self.dim = len(boolRules) + self.boolRules = dict(boolRules) + self.varNames = {s: i for i, s in enumerate(self.boolRules.keys())} + names = self.varNames + # read couplings via names + self.Coupl = np.zeros((self.dim, self.dim)) + boolContinue = True + for ( + line + ) in self.model.open(): # open(self.model.replace('/model','/couplList')): + if line.startswith("# coupling list:"): + boolContinue = False + if boolContinue: + continue + if not line.startswith("#"): + gps, gs, val = line.strip().split() + self.Coupl[int(names[gps]), int(names[gs])] = float(val) + # adjancecy matrices + self.Adj_signed = np.sign(self.Coupl) + self.Adj = np.abs(np.array(self.Adj_signed)) + # build bool coefficients (necessary for odefy type + # version of the discrete model) + self.build_boolCoeff() + + def set_coupl(self, Coupl=None) -> None: + """Construct the coupling matrix (and adjacancy matrix) from predefined models or via sampling.""" + self.varNames = {str(i): i for i in range(self.dim)} + if self.model not in self.availModels and Coupl is None: + self.read_model() + elif "var" in self.model.name: + # vector auto regressive process + self.Coupl = Coupl + self.boolRules = dict.fromkeys(self.varNames, "") + names = list(self.varNames.keys()) + for gp in range(self.dim): + pas = [ + names[g] + for g in range(self.dim) + if np.abs(self.Coupl[gp, g] > 1e-10) + ] + self.boolRules[names[gp]] = "".join( + pas[:1] + [" or " + pa for pa in pas[1:]] + ) + self.Adj_signed = np.sign(Coupl) + elif self.model in ["6", "7", "8", "9", "10"]: + self.Adj_signed = np.zeros((self.dim, self.dim)) + n_sinknodes = 2 + # sinknodes = np.random.choice(self.dim, n_sinknodes, replace=False) + sinknodes = np.array([0, 1]) + # assume sinknodes have feeback + self.Adj_signed[sinknodes, sinknodes] = np.ones(n_sinknodes) + # # allow negative feedback + # if self.model == 10: + # plus_minus = (np.random.randint(0,2,n_sinknodes) - 0.5)*2 + # self.Adj_signed[sinknodes,sinknodes] = plus_minus + leafnodes = np.array(sinknodes) + availnodes = np.array([i for i in range(self.dim) if i not in sinknodes]) + # settings.m(0,leafnodes,availnodes) + while len(availnodes) != 0: + # parent + parent_idx = np.random.choice( + np.arange(0, len(leafnodes)), size=1, replace=False + ) + parent = leafnodes[parent_idx] + # children + children_ids = np.random.choice( + np.arange(0, len(availnodes)), size=2, replace=False + ) + children = availnodes[children_ids] + settings.m(0, parent, children) + self.Adj_signed[children, parent] = np.ones(2) + if self.model == 8: + self.Adj_signed[children[0], children[1]] = -1 + if self.model in [9, 10]: + self.Adj_signed[children[0], children[1]] = -1 + self.Adj_signed[children[1], children[0]] = -1 + # update leafnodes + leafnodes = np.delete(leafnodes, parent_idx) + leafnodes = np.append(leafnodes, children) + # update availnodes + availnodes = np.delete(availnodes, children_ids) + # settings.m(0,availnodes) + # settings.m(0,leafnodes) + # settings.m(0,self.Adj) + # settings.m(0,'-') + else: + self.Adj = np.zeros((self.dim, self.dim)) + for i in range(self.dim): + indep = np.random.binomial(1, self.p_indep) + if indep == 0: + # this number includes parents (other variables) + # and the variable itself, therefore its + # self.maxnpar+2 in the following line + nr = np.random.randint(1, self.maxnpar + 2) + j_par = np.random.choice( + np.arange(0, self.dim), size=nr, replace=False + ) + self.Adj[i, j_par] = 1 + else: + self.Adj[i, i] = 1 + self.Adj = np.abs(np.array(self.Adj_signed)) + # settings.m(0,self.Adj) + + def set_coupl_old(self): + """Sample a coupling matrix using the adjacency matrix.""" + if self.model in {"krumsiek11", "var"}: + # we already built the coupling matrix in set_coupl20() + return + self.Coupl = np.zeros((self.dim, self.dim)) + for i in range(self.Adj.shape[0]): + for j, a in enumerate(self.Adj[i]): + # if there is a 1 in Adj, specify co and antiregulation + # and strength of regulation + if a != 0: + co_anti = np.random.randint(2) + # set a lower bound for the coupling parameters + # they ought not to be smaller than 0.1 + # and not be larger than 0.4 + self.Coupl[i, j] = 0.0 * np.random.rand() + 0.1 + # set sign for coupling + if co_anti == 1: + self.Coupl[i, j] *= -1 + # enforce certain requirements on models + if self.model == 1: + self.coupl_model1() + elif self.model == 5: + self.coupl_model5() + elif self.model in [6, 7]: + self.coupl_model6() + elif self.model in [8, 9, 10]: + self.coupl_model8() + # output + if self.verbosity > 1: + settings.m(0, self.Coupl) + + def coupl_model1(self): + """Enforce the following signs on the couplings. + + (Model 2 has the same couplings but arbitrary signs.) + """ + self.Coupl[0, 0] = np.abs(self.Coupl[0, 0]) + self.Coupl[0, 1] = -np.abs(self.Coupl[0, 1]) + self.Coupl[1, 1] = np.abs(self.Coupl[1, 1]) + + def coupl_model5(self): + """Toggle switch.""" + self.Coupl = -0.2 * self.Adj + self.Coupl[2, 0] *= -1 + self.Coupl[3, 0] *= -1 + self.Coupl[4, 1] *= -1 + self.Coupl[5, 1] *= -1 + + def coupl_model6(self): + """Variant of toggle switch.""" + self.Coupl = 0.5 * self.Adj_signed + + def coupl_model8(self): + """Variant of toggle switch.""" + self.Coupl = 0.5 * self.Adj_signed + # reduce the value of the coupling of the repressing genes + # otherwise completely unstable solutions are obtained + for x in np.nditer(self.Coupl, op_flags=["readwrite"]): + if x < -1e-6: + x[...] = -0.2 + + def coupl_model_krumsiek11(self): + """Variant of toggle switch.""" + self.Coupl = self.Adj_signed + + def sim_model_back_help(self, Xt, Xt1): + """Yield zero when solved for X_t given X_{t+1}.""" + return -Xt1 + Xt + self.Xdiff(Xt) + + def sim_model_backwards(self, tmax, X0): + """Simulate the model backwards in time.""" + X = np.zeros((tmax, self.dim)) + X[tmax - 1] = X0 + for t in range(tmax - 2, -1, -1): + sol = sp.optimize.root( + self.sim_model_back_help, X[t + 1], args=(X[t + 1]), method="hybr" + ) + X[t] = sol.x + return X + + def branch_init_model1(self, tmax=100): + # check whether we can define trajectories + Xfix = np.array([self.Coupl[0, 1] / self.Coupl[0, 0], 1]) + if Xfix[0] > 0.97 or Xfix[0] < 0.03: + settings.m( + 0, + "... either no fixed point in [0,1]^2! \n" + " or fixed point is too close to bounds", + ) + return None + XbackUp = self.sim_model_backwards( + tmax=tmax / 3, X0=Xfix + np.array([0.02, -0.02]) + ) + XbackDo = self.sim_model_backwards( + tmax=tmax / 3, X0=Xfix + np.array([-0.02, -0.02]) + ) + Xup = self.sim_model(tmax=tmax, X0=XbackUp[0]) + Xdo = self.sim_model(tmax=tmax, X0=XbackDo[0]) + # compute mean + X0mean = 0.5 * (Xup[0] + Xdo[0]) + if np.min(X0mean) < 0.025 or np.max(X0mean) > 0.975: + settings.m(0, "... initial point is too close to bounds") + return None + if self.show and self.verbosity > 1: + pl.figure() # noqa: F821 TODO Fix me + pl.plot(XbackUp[:, 0], ".b", XbackUp[:, 1], ".g") # noqa: F821 TODO Fix me + pl.plot(XbackDo[:, 0], ".b", XbackDo[:, 1], ".g") # noqa: F821 TODO Fix me + pl.plot(Xup[:, 0], "b", Xup[:, 1], "g") # noqa: F821 TODO Fix me + pl.plot(Xdo[:, 0], "b", Xdo[:, 1], "g") # noqa: F821 TODO Fix me + return X0mean + + def parents_from_boolRule(self, rule): + """Determine parents based on boolean updaterule. + + Returns list of parents. + """ + rule_pa = ( + rule.replace("(", "") + .replace(")", "") + .replace("or", "") + .replace("and", "") + .replace("not", "") + ) + rule_pa = rule_pa.split() + # if there are no parents, continue + if not rule_pa: + return [] + # check whether these are meaningful parents + pa_old = [] + pa_delete = [] + for pa in rule_pa: + if pa not in self.varNames: + settings.m(0, "list of available variables:") + settings.m(0, list(self.varNames.keys())) + message = ( + f"processing of rule {rule!r} yields an invalid parent: {pa} " + "| check whether the syntax is correct: \n" + 'only python expressions "(",")","or","and","not" ' + "are allowed, variable names and expressions have to be separated " + "by white spaces" + ) + raise ValueError(message) + if pa in pa_old: + pa_delete.append(pa) + for pa in pa_delete: + rule_pa.remove(pa) + return rule_pa + + def build_boolCoeff(self): + """Compute coefficients for tuple space.""" + # coefficients for hill functions from boolean update rules + self.boolCoeff = {s: [] for s in self.varNames} + # parents + self.pas = {s: [] for s in self.varNames} + for key, rule in self.boolRules.items(): + self.pas[key] = self.parents_from_boolRule(rule) + pasIndices = [self.varNames[pa] for pa in self.pas[key]] + # check whether there are coupling matrix entries for each parent + for g in range(self.dim): + if g in pasIndices: + if np.abs(self.Coupl[self.varNames[key], g]) < 1e-10: + msg = f"specify coupling value for {key} <- {g}" + raise ValueError(msg) + elif np.abs(self.Coupl[self.varNames[key], g]) > 1e-10: + msg = f"there should be no coupling value for {key} <- {g}" + raise ValueError(msg) + if self.verbosity > 1: + settings.m(0, "..." + key) + settings.m(0, rule) + settings.m(0, rule_pa) # noqa: F821 + # now evaluate coefficients + for tuple in list( + itertools.product([False, True], repeat=len(self.pas[key])) + ): + if self.process_rule(rule, self.pas[key], tuple): + self.boolCoeff[key].append(tuple) + if self.verbosity > 1: + settings.m(0, self.boolCoeff[key]) + + def process_rule(self, rule, pa, tuple): + """Process a string that denotes a boolean rule.""" + for i, v in enumerate(tuple): + rule = rule.replace(pa[i], str(v)) + return eval(rule) + + def write_data( + self, + X, + *, + dir=Path("sim/test"), + noiseObs=0.0, + append=False, + branching=False, + nrRealizations=1, + seed=0, + ): + header = self.header + tmax = int(X.shape[0]) + header += f"tmax = {tmax}\n" + header += f"branching = {branching}\n" + header += f"nrRealizations = {nrRealizations}\n" + header += f"noiseObs = {noiseObs}\n" + header += f"noiseDyn = {self.noiseDyn}\n" + header += f"seed = {seed}\n" + # add observational noise + X += noiseObs * np.random.randn(tmax, self.dim) + # call helper function + write_data( + X, + dir, + append=append, + header=header, + varNames=self.varNames, + Adj=self.Adj, + Coupl=self.Coupl, + model=self.model, + modelType=self.modelType, + boolRules=self.boolRules, + invTimeStep=self.invTimeStep, + ) + + +def _check_branching( + X: np.ndarray, Xsamples: np.ndarray, restart: int, threshold: float = 0.25 +) -> tuple[bool, list[np.ndarray]]: + """Check whether time series branches. + + Parameters + ---------- + X + current time series data. + Xsamples + list of previous branching samples. + restart + counts number of restart trials. + threshold + sets threshold for attractor identification. + + Returns + ------- + check + true if branching realization + Xsamples + updated list + + """ + check = True + Xsamples = list(Xsamples) + if restart == 0: + Xsamples.append(X) + else: + for Xcompare in Xsamples: + Xtmax_diff = np.absolute(X[-1, :] - Xcompare[-1, :]) + # If the second largest element is smaller than threshold + # set check to False, i.e. at least two elements + # need to change in order to have a branching. + # If we observe all parameters of the system, + # a new attractor state must involve changes in two + # variables. + if np.partition(Xtmax_diff, -2)[-2] < threshold: + check = False + if check: + Xsamples.append(X) + logg.debug(f"realization {restart}: {'' if check else 'no'} new branch") + return check, Xsamples + + +def check_nocycles(Adj: np.ndarray, verbosity: int = 2) -> bool: + """Check that there are no cycles in graph described by adjacancy matrix. + + Parameters + ---------- + Adj + adjancancy matrix of dimension (dim, dim) + + Returns + ------- + True if there is no cycle, False otherwise. + + """ + dim = Adj.shape[0] + for g in range(dim): + v = np.zeros(dim) + v[g] = 1 + for i in range(dim): + v = Adj.dot(v) + if v[g] > 1e-10: + if verbosity > 2: + settings.m(0, Adj) + settings.m( + 0, + "contains a cycle of length", + i + 1, + "starting from node", + g, + "-> reject", + ) + return False + return True + + +def sample_coupling_matrix( + dim: int = 3, connectivity: float = 0.5 +) -> tuple[np.ndarray, np.ndarray, np.ndarray, int]: + """Sample coupling matrix. + + Checks that returned graphs contain no self-cycles. + + Parameters + ---------- + dim + dimension of coupling matrix. + connectivity + fraction of connectivity, fully connected means 1., + not-connected means 0, in the case of fully connected, one has + dim*(dim-1)/2 edges in the graph. + + Returns + ------- + coupl + coupling matrix + adj + adjancancy matrix + adj_signed + signed adjacancy matrix + n_edges + Number of edges + + """ + for _attempt in range(max_attempt := 10): + # random topology for a given connectivity / edge density + Coupl = np.zeros((dim, dim)) + n_edges = 0 + for gp in range(dim): + for g in range(dim): + if gp == g: + continue + # need to have the factor 0.5, otherwise + # connectivity=1 would lead to dim*(dim-1) edges + if np.random.rand() < 0.5 * connectivity: + Coupl[gp, g] = 0.7 + n_edges += 1 + # obtain adjacancy matrix + Adj_signed = np.zeros((dim, dim), dtype="int_") + Adj_signed = np.sign(Coupl) + Adj = np.abs(Adj_signed) + # check for cycles and whether there is at least one edge + if check_nocycles(Adj) and n_edges > 0: + break + else: + msg = f"did not find graph without cycles after {max_attempt} trials" + raise ValueError(msg) + return Coupl, Adj, Adj_signed, n_edges + + +class StaticCauseEffect: + """Simulates static data to investigate structure learning.""" + + availModels: ClassVar = dict( + line="y = αx \n", + noise="y = noise \n", + absline="y = |x| \n", + parabola="y = αx² \n", + sawtooth="y = x - |x| \n", + tanh="y = tanh(x) \n", + combi="combinatorial regulation \n", + ) + + def __init__(self): + # define a set of available functions + self.funcs = dict( + line=lambda x: x, + noise=lambda x: 0, + absline=np.abs, + parabola=lambda x: x**2, + sawtooth=lambda x: 0.5 * x - np.floor(0.5 * x), + tanh=lambda x: np.tanh(2 * x), + ) + + def sim_givenAdj(self, Adj: np.ndarray, model="line"): + """Simulate data given only an adjacancy matrix and a model. + + The model is a bivariate funtional dependence. The adjacancy matrix + needs to be acyclic. + + Parameters + ---------- + Adj + adjacancy matrix of shape (dim,dim). + + Returns + ------- + Data array of shape (n_samples,dim). + + """ + # nice examples + examples = [ # noqa: F841 TODO We are really unsure whether this is needed. + dict( + func="sawtooth", + gdist="uniform", + sigma_glob=1.8, + sigma_noise=0.1, + ) + ] + + # nr of samples + n_samples = 100 + + # noise + sigma_glob = 1.8 + sigma_noise = 0.4 + + # coupling function / model + func = self.funcs[model] + + # glob distribution + sourcedist = "uniform" + + # loop over source nodes + dim = Adj.shape[0] + X = np.zeros((n_samples, dim)) + # source nodes have no parents themselves + nrpar = 0 + children = list(range(dim)) + parents = [] + for gp in range(dim): + if Adj[gp, :].sum() == nrpar: + if sourcedist == "gaussian": + X[:, gp] = np.random.normal(0, sigma_glob, n_samples) + if sourcedist == "uniform": + X[:, gp] = np.random.uniform(-sigma_glob, sigma_glob, n_samples) + parents.append(gp) + children.remove(gp) + + # all of the following guarantees for 3 dim, that we generate the data + # in the correct sequence + # then compute all nodes that have 1 parent, then those with 2 parents + children_sorted = [] + nrchildren_par = np.zeros(dim) + nrchildren_par[0] = len(parents) + for nrpar in range(1, dim): + # loop over child nodes + for gp in children: + if Adj[gp, :].sum() == nrpar: + children_sorted.append(gp) + nrchildren_par[nrpar] += 1 + # if there is more than a child with a single parent + # order these children (there are two in three dim) + # by distance to the source/parent + if nrchildren_par[1] > 1 and Adj[children_sorted[0], parents[0]] == 0: + help = children_sorted[0] + children_sorted[0] = children_sorted[1] + children_sorted[1] = help + + for gp in children_sorted: + for g in range(dim): + if Adj[gp, g] > 0: + X[:, gp] += 1.0 / Adj[gp, :].sum() * func(X[:, g]) + X[:, gp] += np.random.normal(0, sigma_noise, n_samples) + + # fig = pl.figure() + # fig.add_subplot(311) + # pl.plot(X[:,0],X[:,1],'.',mec='white') + # fig.add_subplot(312) + # pl.plot(X[:,1],X[:,2],'.',mec='white') + # fig.add_subplot(313) + # pl.plot(X[:,2],X[:,0],'.',mec='white') + # pl.show() + + return X + + def sim_combi(self): + """Simulate data to model combi regulation.""" + n_samples = 500 + sigma_glob = 1.8 + + X = np.zeros((n_samples, 3)) + + X[:, 0] = np.random.uniform(-sigma_glob, sigma_glob, n_samples) + X[:, 1] = np.random.uniform(-sigma_glob, sigma_glob, n_samples) + + func = self.funcs["tanh"] + + # XOR type + # X[:,2] = (func(X[:,0])*sp.stats.norm.pdf(X[:,1],0,0.2) + # + func(X[:,1])*sp.stats.norm.pdf(X[:,0],0,0.2)) + # AND type / diagonal + # X[:,2] = (func(X[:,0]+X[:,1])*sp.stats.norm.pdf(X[:,1]-X[:,0],0,0.2)) + # AND type / horizontal + X[:, 2] = func(X[:, 0]) * sp.stats.norm.cdf(X[:, 1], 1, 0.2) + + pl.scatter( # noqa: F821 TODO Fix me + X[:, 0], X[:, 1], c=X[:, 2], edgecolor="face" + ) + pl.show() # noqa: F821 TODO Fix me + + pl.plot(X[:, 1], X[:, 2], ".") # noqa: F821 TODO Fix me + pl.show() # noqa: F821 TODO Fix me + + return X + + +def sample_static_data(model, dir, verbosity=0): + # fraction of connectivity as compared to fully connected + # in one direction, which amounts to dim*(dim-1)/2 edges + connectivity = 0.8 + dim = 3 + n_Coupls = 50 + model = model.replace("static-", "") + np.random.seed(0) + + if model != "combi": + n_edges = np.zeros(n_Coupls) + for icoupl in range(n_Coupls): + _coupl, adj, _adj_signed, n_e = sample_coupling_matrix(dim, connectivity) + if verbosity > 1: + settings.m(0, icoupl) + settings.m(0, adj) + n_edges[icoupl] = n_e + # sample data + X = StaticCauseEffect().sim_givenAdj(adj, model) + write_data(X, dir, Adj=adj) + settings.m(0, "mean edge number:", n_edges.mean()) + + else: + X = StaticCauseEffect().sim_combi() + adj = np.zeros((3, 3)) + adj[2, 0] = adj[2, 1] = 0 + write_data(X, dir, Adj=adj) + + +if __name__ == "__main__": + import argparse + + # epilog = ( + # " 1: 2dim, causal direction X_1 -> X_0, constraint signs\n" + # " 2: 2dim, causal direction X_1 -> X_0, arbitrary signs\n" + # " 3: 2dim, causal direction X_1 <-> X_0, arbitrary signs\n" + # " 4: 2dim, mix of model 2 and 3\n" + # " 5: 6dim double toggle switch\n" + # " 6: two independent evolutions without repression, sync.\n" + # " 7: two independent evolutions without repression, random init\n" + # " 8: two independent evolutions directed repression, random init\n" + # " 9: two independent evolutions mutual repression, random init\n" + # " 10: two indep. evol., diff. self-loops possible, mut. repr., rand init\n" + # ) + epilog = "" + for k, v in StaticCauseEffect.availModels.items(): + epilog += f" static-{k}: {v}" + for k, v in GRNsim.availModels.items(): + epilog += f" {k}: {v}" + # command line options + p = argparse.ArgumentParser( + description=( + "Simulate stochastic discrete-time dynamical systems,\n" + "in particular gene regulatory networks." + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=( + " MODEL: specify one of the following models, or one of \n" + ' the filenames (without ".txt") in the directory "models" \n' + epilog + ), + ) + aa = p.add_argument + dir_arg = aa( + "--dir", + required=True, + type=str, + default="", + help=( + "specify directory to store data, " + ' must start with "sim/MODEL_...", see possible values for MODEL below ' + ), + ) + aa("--show", action="store_true", help="show plots") + aa( + "--verbosity", + type=int, + default=0, + help="specify integer > 0 to get more output [default 0]", + ) + args = p.parse_args() + + # run checks on output directory + dir = Path(args.dir) + if not dir.resolve().parent.name == "sim": + raise argparse.ArgumentError( + dir_arg, + "The parent directory of the --dir argument needs to be named 'sim'", + ) + else: + model = dir.name.split("_")[0] + settings.m(0, f"...model is: {model!r}") + if dir.is_dir() and "test" not in str(dir): + message = ( + f"directory {dir} already exists, " + "remove it and continue? [y/n, press enter]" + ) + if str(input(message)) != "y": + settings.m(0, " ...quit program execution") + sys.exit() + else: + settings.m(0, " ...removing directory and continuing...") + shutil.rmtree(dir) + + settings.m(0, model) + settings.m(0, dir) + + # sample data + if "static" in model: + sample_static_data(model=model, dir=dir, verbosity=args.verbosity) + else: + sample_dynamic_data(model=model, dir=dir) diff --git a/.venv/lib/python3.11/site-packages/scanpy/tools/_tsne.py b/.venv/lib/python3.11/site-packages/scanpy/tools/_tsne.py new file mode 100644 index 0000000000000000000000000000000000000000..82dd419837bdcf3258d83cc37d4061bf2c7142cc --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/tools/_tsne.py @@ -0,0 +1,195 @@ +from __future__ import annotations + +import warnings +from typing import TYPE_CHECKING + +from packaging.version import Version + +from .. import logging as logg +from .._compat import old_positionals, pkg_version +from .._settings import settings +from .._utils import _doc_params, raise_not_implemented_error_if_backed_type +from ..neighbors._doc import doc_n_pcs, doc_use_rep +from ._utils import _choose_representation + +if TYPE_CHECKING: + from anndata import AnnData + + from .._utils.random import _LegacyRandom + + +@old_positionals( + "use_rep", + "perplexity", + "early_exaggeration", + "learning_rate", + "random_state", + "use_fast_tsne", + "n_jobs", + "copy", +) +@_doc_params(doc_n_pcs=doc_n_pcs, use_rep=doc_use_rep) +def tsne( # noqa: PLR0913 + adata: AnnData, + n_pcs: int | None = None, + *, + use_rep: str | None = None, + perplexity: float = 30, + metric: str = "euclidean", + early_exaggeration: float = 12, + learning_rate: float = 1000, + random_state: _LegacyRandom = 0, + use_fast_tsne: bool = False, + n_jobs: int | None = None, + key_added: str | None = None, + copy: bool = False, +) -> AnnData | None: + r"""t-SNE :cite:p:`vanDerMaaten2008,Amir2013,Pedregosa2011`. + + t-distributed stochastic neighborhood embedding (tSNE, :cite:t:`vanDerMaaten2008`) was + proposed for visualizating single-cell data by :cite:t:`Amir2013`. Here, by default, + we use the implementation of *scikit-learn* :cite:p:`Pedregosa2011`. You can achieve + a huge speedup and better convergence if you install Multicore-tSNE_ + by :cite:t:`Ulyanov2016`, which will be automatically detected by Scanpy. + + .. _multicore-tsne: https://github.com/DmitryUlyanov/Multicore-TSNE + + Parameters + ---------- + adata + Annotated data matrix. + {doc_n_pcs} + {use_rep} + perplexity + The perplexity is related to the number of nearest neighbors that + is used in other manifold learning algorithms. Larger datasets + usually require a larger perplexity. Consider selecting a value + between 5 and 50. The choice is not extremely critical since t-SNE + is quite insensitive to this parameter. + metric + Distance metric calculate neighbors on. + early_exaggeration + Controls how tight natural clusters in the original space are in the + embedded space and how much space will be between them. For larger + values, the space between natural clusters will be larger in the + embedded space. Again, the choice of this parameter is not very + critical. If the cost function increases during initial optimization, + the early exaggeration factor or the learning rate might be too high. + learning_rate + Note that the R-package "Rtsne" uses a default of 200. + The learning rate can be a critical parameter. It should be + between 100 and 1000. If the cost function increases during initial + optimization, the early exaggeration factor or the learning rate + might be too high. If the cost function gets stuck in a bad local + minimum increasing the learning rate helps sometimes. + random_state + Change this to use different intial states for the optimization. + If `None`, the initial state is not reproducible. + n_jobs + Number of jobs for parallel computation. + `None` means using :attr:`scanpy.settings.n_jobs`. + key_added + If not specified, the embedding is stored as + :attr:`~anndata.AnnData.obsm`\ `['X_tsne']` and the the parameters in + :attr:`~anndata.AnnData.uns`\ `['tsne']`. + If specified, the embedding is stored as + :attr:`~anndata.AnnData.obsm`\ ``[key_added]`` and the the parameters in + :attr:`~anndata.AnnData.uns`\ ``[key_added]``. + copy + Return a copy instead of writing to `adata`. + + Returns + ------- + Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following fields: + + `adata.obsm['X_tsne' | key_added]` : :class:`numpy.ndarray` (dtype `float`) + tSNE coordinates of data. + `adata.uns['tsne' | key_added]` : :class:`dict` + tSNE parameters. + + """ + start = logg.info("computing tSNE") + adata = adata.copy() if copy else adata + x = _choose_representation(adata, use_rep=use_rep, n_pcs=n_pcs) + raise_not_implemented_error_if_backed_type(x, "tsne") + # params for sklearn + n_jobs = settings.n_jobs if n_jobs is None else n_jobs + params_sklearn = dict( + perplexity=perplexity, + random_state=random_state, + verbose=settings.verbosity > 3, + early_exaggeration=early_exaggeration, + learning_rate=learning_rate, + n_jobs=n_jobs, + metric=metric, + ) + if metric != "euclidean" and (pkg_version("scikit-learn") < Version("1.3.0rc1")): + params_sklearn["square_distances"] = True + + # Backwards compat handling: Remove in scanpy 1.9.0 + if n_jobs != 1 and not use_fast_tsne: + warnings.warn( + "In previous versions of scanpy, calling tsne with n_jobs > 1 would use " + "MulticoreTSNE. Now this uses the scikit-learn version of TSNE by default. " + "If you'd like the old behaviour (which is deprecated), pass " + "'use_fast_tsne=True'. Note, MulticoreTSNE is not actually faster anymore.", + UserWarning, + stacklevel=2, + ) + if use_fast_tsne: + warnings.warn( + "Argument `use_fast_tsne` is deprecated, and support for MulticoreTSNE " + "will be dropped in a future version of scanpy.", + FutureWarning, + stacklevel=2, + ) + + # deal with different tSNE implementations + if use_fast_tsne: + try: + from MulticoreTSNE import MulticoreTSNE as TSNE # noqa: N814 + + tsne = TSNE(**params_sklearn) + logg.info(" using the 'MulticoreTSNE' package by Ulyanov (2017)") + # need to transform to float64 for MulticoreTSNE... + x_tsne = tsne.fit_transform(x.astype("float64")) + except ImportError: + use_fast_tsne = False + warnings.warn( + "Could not import 'MulticoreTSNE'. Falling back to scikit-learn.", + UserWarning, + stacklevel=2, + ) + if use_fast_tsne is False: # In case MultiCore failed to import + from sklearn.manifold import TSNE + + # unfortunately, sklearn does not allow to set a minimum number + # of iterations for barnes-hut tSNE + tsne = TSNE(**params_sklearn) + logg.info(" using sklearn.manifold.TSNE") + x_tsne = tsne.fit_transform(x) + + # update AnnData instance + params = dict( + perplexity=perplexity, + early_exaggeration=early_exaggeration, + learning_rate=learning_rate, + n_jobs=n_jobs, + metric=metric, + use_rep=use_rep, + ) + key_uns, key_obsm = ("tsne", "X_tsne") if key_added is None else [key_added] * 2 + adata.obsm[key_obsm] = x_tsne # annotate samples with tSNE coordinates + adata.uns[key_uns] = dict(params={k: v for k, v in params.items() if v is not None}) + + logg.info( + " finished", + time=start, + deep=( + f"added\n" + f" {key_obsm!r}, tSNE coordinates (adata.obsm)\n" + f" {key_uns!r}, tSNE parameters (adata.uns)" + ), + ) + + return adata if copy else None diff --git a/.venv/lib/python3.11/site-packages/scanpy/tools/_umap.py b/.venv/lib/python3.11/site-packages/scanpy/tools/_umap.py new file mode 100644 index 0000000000000000000000000000000000000000..8f880bc87c4ff9b3fcd768f4e1c3e72efdaa7c36 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/tools/_umap.py @@ -0,0 +1,279 @@ +from __future__ import annotations + +import warnings +from typing import TYPE_CHECKING + +import numpy as np +from sklearn.utils import check_array, check_random_state + +from .. import logging as logg +from .._compat import old_positionals +from .._settings import settings +from .._utils import NeighborsView +from ._utils import _choose_representation, get_init_pos_from_paga + +if TYPE_CHECKING: + from typing import Literal + + from anndata import AnnData + + from .._utils.random import _LegacyRandom + + _InitPos = Literal["paga", "spectral", "random"] + + +@old_positionals( + "min_dist", + "spread", + "n_components", + "maxiter", + "alpha", + "gamma", + "negative_sample_rate", + "init_pos", + "random_state", + "a", + "b", + "copy", + "method", + "neighbors_key", +) +def umap( # noqa: PLR0913, PLR0915 + adata: AnnData, + *, + min_dist: float = 0.5, + spread: float = 1.0, + n_components: int = 2, + maxiter: int | None = None, + alpha: float = 1.0, + gamma: float = 1.0, + negative_sample_rate: int = 5, + init_pos: _InitPos | np.ndarray | None = "spectral", + random_state: _LegacyRandom = 0, + a: float | None = None, + b: float | None = None, + method: Literal["umap", "rapids"] = "umap", + key_added: str | None = None, + neighbors_key: str = "neighbors", + copy: bool = False, +) -> AnnData | None: + r"""Embed the neighborhood graph using UMAP :cite:p:`McInnes2018`. + + UMAP (Uniform Manifold Approximation and Projection) is a manifold learning + technique suitable for visualizing high-dimensional data. Besides tending to + be faster than tSNE, it optimizes the embedding such that it best reflects + the topology of the data, which we represent throughout Scanpy using a + neighborhood graph. tSNE, by contrast, optimizes the distribution of + nearest-neighbor distances in the embedding such that these best match the + distribution of distances in the high-dimensional space. + We use the implementation of umap-learn_ :cite:p:`McInnes2018`. + For a few comparisons of UMAP with tSNE, see :cite:t:`Becht2018`. + + .. _umap-learn: https://github.com/lmcinnes/umap + + Parameters + ---------- + adata + Annotated data matrix. + min_dist + The effective minimum distance between embedded points. Smaller values + will result in a more clustered/clumped embedding where nearby points on + the manifold are drawn closer together, while larger values will result + on a more even dispersal of points. The value should be set relative to + the ``spread`` value, which determines the scale at which embedded + points will be spread out. The default of in the `umap-learn` package is + 0.1. + spread + The effective scale of embedded points. In combination with `min_dist` + this determines how clustered/clumped the embedded points are. + n_components + The number of dimensions of the embedding. + maxiter + The number of iterations (epochs) of the optimization. Called `n_epochs` + in the original UMAP. + alpha + The initial learning rate for the embedding optimization. + gamma + Weighting applied to negative samples in low dimensional embedding + optimization. Values higher than one will result in greater weight + being given to negative samples. + negative_sample_rate + The number of negative edge/1-simplex samples to use per positive + edge/1-simplex sample in optimizing the low dimensional embedding. + init_pos + How to initialize the low dimensional embedding. Called `init` in the + original UMAP. Options are: + + * Any key for `adata.obsm`. + * 'paga': positions from :func:`~scanpy.pl.paga`. + * 'spectral': use a spectral embedding of the graph. + * 'random': assign initial embedding positions at random. + * A numpy array of initial embedding positions. + random_state + If `int`, `random_state` is the seed used by the random number generator; + If `RandomState` or `Generator`, `random_state` is the random number generator; + If `None`, the random number generator is the `RandomState` instance used + by `np.random`. + a + More specific parameters controlling the embedding. If `None` these + values are set automatically as determined by `min_dist` and + `spread`. + b + More specific parameters controlling the embedding. If `None` these + values are set automatically as determined by `min_dist` and + `spread`. + method + Chosen implementation. + + ``'umap'`` + Umap’s simplical set embedding. + ``'rapids'`` + GPU accelerated implementation. + + .. deprecated:: 1.10.0 + Use :func:`rapids_singlecell.tl.umap` instead. + key_added + If not specified, the embedding is stored as + :attr:`~anndata.AnnData.obsm`\ `['X_umap']` and the the parameters in + :attr:`~anndata.AnnData.uns`\ `['umap']`. + If specified, the embedding is stored as + :attr:`~anndata.AnnData.obsm`\ ``[key_added]`` and the the parameters in + :attr:`~anndata.AnnData.uns`\ ``[key_added]``. + neighbors_key + Umap looks in + :attr:`~anndata.AnnData.uns`\ ``[neighbors_key]`` for neighbors settings and + :attr:`~anndata.AnnData.obsp`\ ``[.uns[neighbors_key]['connectivities_key']]`` for connectivities. + copy + Return a copy instead of writing to adata. + + Returns + ------- + Returns `None` if `copy=False`, else returns an `AnnData` object. Sets the following fields: + + `adata.obsm['X_umap' | key_added]` : :class:`numpy.ndarray` (dtype `float`) + UMAP coordinates of data. + `adata.uns['umap' | key_added]` : :class:`dict` + UMAP parameters. + + """ + adata = adata.copy() if copy else adata + + key_obsm, key_uns = ("X_umap", "umap") if key_added is None else [key_added] * 2 + + if neighbors_key is None: # backwards compat + neighbors_key = "neighbors" + if neighbors_key not in adata.uns: + msg = f"Did not find .uns[{neighbors_key!r}]. Run `sc.pp.neighbors` first." + raise ValueError(msg) + + start = logg.info("computing UMAP") + + neighbors = NeighborsView(adata, neighbors_key) + + if "params" not in neighbors or neighbors["params"]["method"] != "umap": + logg.warning( + f'.obsp["{neighbors["connectivities_key"]}"] have not been computed using umap' + ) + + with warnings.catch_warnings(): + # umap 0.5.0 + warnings.filterwarnings("ignore", message=r"Tensorflow not installed") + import umap + + from umap.umap_ import find_ab_params, simplicial_set_embedding + + if a is None or b is None: + a, b = find_ab_params(spread, min_dist) + adata.uns[key_uns] = dict(params=dict(a=a, b=b)) + if isinstance(init_pos, str) and init_pos in adata.obsm: + init_coords = adata.obsm[init_pos] + elif isinstance(init_pos, str) and init_pos == "paga": + init_coords = get_init_pos_from_paga( + adata, random_state=random_state, neighbors_key=neighbors_key + ) + else: + init_coords = init_pos # Let umap handle it + if hasattr(init_coords, "dtype"): + init_coords = check_array(init_coords, dtype=np.float32, accept_sparse=False) + + if random_state != 0: + adata.uns[key_uns]["params"]["random_state"] = random_state + random_state = check_random_state(random_state) + + neigh_params = neighbors["params"] + x = _choose_representation( + adata, + use_rep=neigh_params.get("use_rep", None), + n_pcs=neigh_params.get("n_pcs", None), + silent=True, + ) + if method == "umap": + # the data matrix X is really only used for determining the number of connected components + # for the init condition in the UMAP embedding + default_epochs = 500 if neighbors["connectivities"].shape[0] <= 10000 else 200 + n_epochs = default_epochs if maxiter is None else maxiter + x_umap, _ = simplicial_set_embedding( + data=x, + graph=neighbors["connectivities"].tocoo(), + n_components=n_components, + initial_alpha=alpha, + a=a, + b=b, + gamma=gamma, + negative_sample_rate=negative_sample_rate, + n_epochs=n_epochs, + init=init_coords, + random_state=random_state, + metric=neigh_params.get("metric", "euclidean"), + metric_kwds=neigh_params.get("metric_kwds", {}), + densmap=False, + densmap_kwds={}, + output_dens=False, + verbose=settings.verbosity > 3, + ) + elif method == "rapids": + msg = ( + "`method='rapids'` is deprecated. " + "Use `rapids_singlecell.tl.louvain` instead." + ) + warnings.warn(msg, FutureWarning, stacklevel=2) + metric = neigh_params.get("metric", "euclidean") + if metric != "euclidean": + msg = ( + f"`sc.pp.neighbors` was called with `metric` {metric!r}, " + "but umap `method` 'rapids' only supports the 'euclidean' metric." + ) + raise ValueError(msg) + from cuml import UMAP + + n_neighbors = neighbors["params"]["n_neighbors"] + n_epochs = ( + 500 if maxiter is None else maxiter + ) # 0 is not a valid value for rapids, unlike original umap + x_contiguous = np.ascontiguousarray(x, dtype=np.float32) + umap = UMAP( + n_neighbors=n_neighbors, + n_components=n_components, + n_epochs=n_epochs, + learning_rate=alpha, + init=init_pos, + min_dist=min_dist, + spread=spread, + negative_sample_rate=negative_sample_rate, + a=a, + b=b, + verbose=settings.verbosity > 3, + random_state=random_state, + ) + x_umap = umap.fit_transform(x_contiguous) + adata.obsm[key_obsm] = x_umap # annotate samples with UMAP coordinates + logg.info( + " finished", + time=start, + deep=( + "added\n" + f" {key_obsm!r}, UMAP coordinates (adata.obsm)\n" + f" {key_uns!r}, UMAP parameters (adata.uns)" + ), + ) + return adata if copy else None diff --git a/.venv/lib/python3.11/site-packages/scanpy/tools/_utils.py b/.venv/lib/python3.11/site-packages/scanpy/tools/_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..b93399ad6057e89618077b88ce697e5484ba4c9b --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/tools/_utils.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +import warnings +from typing import TYPE_CHECKING + +import numpy as np + +from .. import logging as logg +from .._settings import settings +from .._utils import _choose_graph + +if TYPE_CHECKING: + from anndata import AnnData + + from .._compat import CSRBase, SpBase + + +def _choose_representation( + adata: AnnData, + *, + use_rep: str | None = None, + n_pcs: int | None = None, + silent: bool = False, +) -> np.ndarray | CSRBase: # TODO: what else? + verbosity = settings.verbosity + if silent and settings.verbosity > 1: + settings.verbosity = 1 + if use_rep is None and n_pcs == 0: # backwards compat for specifying `.X` + use_rep = "X" + if use_rep is None: + x = _get_pca_or_small_x(adata, n_pcs) + elif use_rep in adata.obsm and n_pcs is not None: + if n_pcs > adata.obsm[use_rep].shape[1]: + msg = ( + f"{use_rep} does not have enough Dimensions. Provide a " + "Representation with equal or more dimensions than" + "`n_pcs` or lower `n_pcs` " + ) + raise ValueError(msg) + x = adata.obsm[use_rep][:, :n_pcs] + elif use_rep in adata.obsm and n_pcs is None: + x = adata.obsm[use_rep] + elif use_rep == "X": + x = adata.X + else: + msg = f"Did not find {use_rep} in `.obsm.keys()`. You need to compute it first." + raise ValueError(msg) + settings.verbosity = verbosity # resetting verbosity + return x + + +def _get_pca_or_small_x(adata: AnnData, n_pcs: int | None) -> np.ndarray | CSRBase: + if adata.n_vars <= settings.N_PCS: + logg.info(" using data matrix X directly") + return adata.X + + if "X_pca" in adata.obsm: + if n_pcs is not None and n_pcs > adata.obsm["X_pca"].shape[1]: + msg = "`X_pca` does not have enough PCs. Rerun `sc.pp.pca` with adjusted `n_comps`." + raise ValueError(msg) + x = adata.obsm["X_pca"][:, :n_pcs] + logg.info(f" using 'X_pca' with n_pcs = {x.shape[1]}") + return x + + from ..preprocessing import pca + + warnings.warn( + f"You’re trying to run this on {adata.n_vars} dimensions of `.X`, " + "if you really want this, set `use_rep='X'`.\n " + "Falling back to preprocessing with `sc.pp.pca` and default params.", + stacklevel=3, + ) + n_pcs_pca = n_pcs if n_pcs is not None else settings.N_PCS + pca(adata, n_comps=n_pcs_pca) + return adata.obsm["X_pca"] + + +def get_init_pos_from_paga( + adata: AnnData, + adjacency: SpBase | None = None, + random_state=0, + neighbors_key: str | None = None, + obsp: str | None = None, +): + np.random.seed(random_state) + if adjacency is None: + adjacency = _choose_graph(adata, obsp, neighbors_key) + if "pos" not in adata.uns.get("paga", {}): + msg = "Plot PAGA first, so that `adata.uns['paga']['pos']` exists." + raise ValueError(msg) + + groups = adata.obs[adata.uns["paga"]["groups"]] + pos = adata.uns["paga"]["pos"] + connectivities_coarse = adata.uns["paga"]["connectivities"] + init_pos = np.ones((adjacency.shape[0], 2)) + for i, group_pos in enumerate(pos): + subset = (groups == groups.cat.categories[i]).values + neighbors = connectivities_coarse[i].nonzero() + if len(neighbors[1]) > 0: + connectivities = connectivities_coarse[i][neighbors] + nearest_neighbor = neighbors[1][np.argmax(connectivities)] + noise = np.random.random((len(subset[subset]), 2)) + dist = group_pos - pos[nearest_neighbor] + noise = noise * dist + init_pos[subset] = group_pos - 0.5 * dist + noise + else: + init_pos[subset] = group_pos + return init_pos diff --git a/.venv/lib/python3.11/site-packages/scanpy/tools/_utils_clustering.py b/.venv/lib/python3.11/site-packages/scanpy/tools/_utils_clustering.py new file mode 100644 index 0000000000000000000000000000000000000000..c64e92bf653bc43a95a9f85a37739d45e0ffd709 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scanpy/tools/_utils_clustering.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + + import numpy as np + import pandas as pd + from anndata import AnnData + from numpy.typing import NDArray + + from .._compat import CSBase + + +def rename_groups( + adata: AnnData, + restrict_key: str, + *, + key_added: str | None, + restrict_categories: Iterable[str], + restrict_indices: NDArray[np.bool_], + groups: NDArray, +) -> pd.Series[str]: + key_added = f"{restrict_key}_R" if key_added is None else key_added + all_groups = adata.obs[restrict_key].astype("U") + prefix = f"{'-'.join(restrict_categories)}," + new_groups = [prefix + g for g in groups.astype("U")] + all_groups.iloc[restrict_indices] = new_groups + return all_groups + + +def restrict_adjacency( + adata: AnnData, + restrict_key: str, + *, + restrict_categories: Sequence[str], + adjacency: CSBase, +) -> tuple[CSBase, NDArray[np.bool_]]: + if not isinstance(restrict_categories[0], str): + msg = "You need to use strings to label categories, e.g. '1' instead of 1." + raise ValueError(msg) + for c in restrict_categories: + if c not in adata.obs[restrict_key].cat.categories: + msg = f"{c!r} is not a valid category for {restrict_key!r}" + raise ValueError(msg) + restrict_indices = adata.obs[restrict_key].isin(restrict_categories).values + adjacency = adjacency[restrict_indices, :] + adjacency = adjacency[:, restrict_indices] + return adjacency, restrict_indices diff --git a/.venv/lib/python3.11/site-packages/scikit_learn-1.9.0.dist-info/INSTALLER b/.venv/lib/python3.11/site-packages/scikit_learn-1.9.0.dist-info/INSTALLER new file mode 100644 index 0000000000000000000000000000000000000000..5c69047b2eb8235994febeeae1da4a82365a240a --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scikit_learn-1.9.0.dist-info/INSTALLER @@ -0,0 +1 @@ +uv \ No newline at end of file diff --git a/.venv/lib/python3.11/site-packages/scikit_learn-1.9.0.dist-info/METADATA b/.venv/lib/python3.11/site-packages/scikit_learn-1.9.0.dist-info/METADATA new file mode 100644 index 0000000000000000000000000000000000000000..50efc499f656145fd74381aa9735d31f8eb29ee2 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scikit_learn-1.9.0.dist-info/METADATA @@ -0,0 +1,313 @@ +Metadata-Version: 2.4 +Name: scikit-learn +Version: 1.9.0 +Summary: A set of python modules for machine learning and data mining +Maintainer-Email: scikit-learn developers +License-Expression: BSD-3-Clause +License-File: COPYING +Classifier: Intended Audience :: Science/Research +Classifier: Intended Audience :: Developers +Classifier: Programming Language :: C +Classifier: Programming Language :: Python +Classifier: Topic :: Software Development +Classifier: Topic :: Scientific/Engineering +Classifier: Development Status :: 5 - Production/Stable +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX +Classifier: Operating System :: Unix +Classifier: Operating System :: MacOS +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: Implementation :: CPython +Project-URL: homepage, https://scikit-learn.org +Project-URL: source, https://github.com/scikit-learn/scikit-learn +Project-URL: download, https://pypi.org/project/scikit-learn/#files +Project-URL: tracker, https://github.com/scikit-learn/scikit-learn/issues +Project-URL: release notes, https://scikit-learn.org/stable/whats_new +Requires-Python: >=3.11 +Requires-Dist: numpy>=1.24.1 +Requires-Dist: scipy>=1.10.0 +Requires-Dist: joblib>=1.4.0 +Requires-Dist: narwhals>=2.0.1 +Requires-Dist: threadpoolctl>=3.5.0 +Provides-Extra: build +Requires-Dist: numpy>=1.24.1; extra == "build" +Requires-Dist: scipy>=1.10.0; extra == "build" +Requires-Dist: cython>=3.1.2; extra == "build" +Requires-Dist: meson-python>=0.17.1; extra == "build" +Provides-Extra: install +Requires-Dist: numpy>=1.24.1; extra == "install" +Requires-Dist: scipy>=1.10.0; extra == "install" +Requires-Dist: joblib>=1.4.0; extra == "install" +Requires-Dist: narwhals>=2.0.1; extra == "install" +Requires-Dist: threadpoolctl>=3.5.0; extra == "install" +Provides-Extra: benchmark +Requires-Dist: matplotlib>=3.6.1; extra == "benchmark" +Requires-Dist: pandas>=1.5.0; extra == "benchmark" +Requires-Dist: memory_profiler>=0.57.0; extra == "benchmark" +Provides-Extra: docs +Requires-Dist: matplotlib>=3.6.1; extra == "docs" +Requires-Dist: scikit-image>=0.22.0; extra == "docs" +Requires-Dist: pandas>=1.5.0; extra == "docs" +Requires-Dist: rich>=14.1.0; extra == "docs" +Requires-Dist: seaborn>=0.13.0; extra == "docs" +Requires-Dist: memory_profiler>=0.57.0; extra == "docs" +Requires-Dist: sphinx>=7.3.7; extra == "docs" +Requires-Dist: sphinx-copybutton>=0.5.2; extra == "docs" +Requires-Dist: sphinx-gallery>=0.17.1; extra == "docs" +Requires-Dist: numpydoc>=1.2.0; extra == "docs" +Requires-Dist: Pillow>=12.1.1; extra == "docs" +Requires-Dist: pooch>=1.8.0; extra == "docs" +Requires-Dist: sphinx-prompt>=1.4.0; extra == "docs" +Requires-Dist: sphinxext-opengraph>=0.9.1; extra == "docs" +Requires-Dist: plotly>=5.22.0; extra == "docs" +Requires-Dist: polars>=0.20.30; extra == "docs" +Requires-Dist: sphinx-design>=0.6.0; extra == "docs" +Requires-Dist: sphinxcontrib-sass>=0.3.4; extra == "docs" +Requires-Dist: pydata-sphinx-theme>=0.15.3; extra == "docs" +Requires-Dist: sphinx-remove-toctrees>=1.0.0.post1; extra == "docs" +Requires-Dist: towncrier>=24.8.0; extra == "docs" +Provides-Extra: examples +Requires-Dist: matplotlib>=3.6.1; extra == "examples" +Requires-Dist: scikit-image>=0.22.0; extra == "examples" +Requires-Dist: pandas>=1.5.0; extra == "examples" +Requires-Dist: rich>=14.1.0; extra == "examples" +Requires-Dist: seaborn>=0.13.0; extra == "examples" +Requires-Dist: pooch>=1.8.0; extra == "examples" +Requires-Dist: plotly>=5.22.0; extra == "examples" +Provides-Extra: tests +Requires-Dist: matplotlib>=3.6.1; extra == "tests" +Requires-Dist: pandas>=1.5.0; extra == "tests" +Requires-Dist: rich>=14.1.0; extra == "tests" +Requires-Dist: pytest>=7.1.2; extra == "tests" +Requires-Dist: pytest-cov>=2.9.0; extra == "tests" +Requires-Dist: ruff>=0.12.2; extra == "tests" +Requires-Dist: mypy>=1.15; extra == "tests" +Requires-Dist: pyamg>=5.0.0; extra == "tests" +Requires-Dist: polars>=0.20.30; extra == "tests" +Requires-Dist: pyarrow>=13.0.0; extra == "tests" +Requires-Dist: numpydoc>=1.2.0; extra == "tests" +Requires-Dist: pooch>=1.8.0; extra == "tests" +Provides-Extra: maintenance +Requires-Dist: conda-lock==3.0.1; extra == "maintenance" +Description-Content-Type: text/x-rst + +.. -*- mode: rst -*- + +|GitHubActions| |Codecov| |CircleCI| |Nightly wheels| |Ruff| |PythonVersion| |PyPI| |DOI| |Benchmark| + + +.. |GitHubActions| image:: https://github.com/scikit-learn/scikit-learn/actions/workflows/unit-tests.yml/badge.svg? + :target: https://github.com/scikit-learn/scikit-learn/actions/workflows/unit-tests.yml?query=branch%3Amain + +.. |CircleCI| image:: https://circleci.com/gh/scikit-learn/scikit-learn/tree/main.svg?style=shield + :target: https://circleci.com/gh/scikit-learn/scikit-learn + +.. |Codecov| image:: https://codecov.io/gh/scikit-learn/scikit-learn/branch/main/graph/badge.svg?token=Pk8G9gg3y9 + :target: https://codecov.io/gh/scikit-learn/scikit-learn + +.. |Nightly wheels| image:: https://github.com/scikit-learn/scikit-learn/actions/workflows/wheels.yml/badge.svg?event=schedule + :target: https://github.com/scikit-learn/scikit-learn/actions?query=workflow%3A%22Wheel+builder%22+event%3Aschedule + +.. |Ruff| image:: https://img.shields.io/badge/code%20style-ruff-000000.svg? + :target: https://github.com/astral-sh/ruff + +.. |PythonVersion| image:: https://img.shields.io/pypi/pyversions/scikit-learn.svg? + :target: https://pypi.org/project/scikit-learn/ + +.. |PyPI| image:: https://img.shields.io/pypi/v/scikit-learn + :target: https://pypi.org/project/scikit-learn + +.. |DOI| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.17880109.svg? + :target: https://zenodo.org/badge/latestdoi/21369/scikit-learn/scikit-learn + +.. |Benchmark| image:: https://img.shields.io/badge/Benchmarked%20by-asv-blue + :target: https://scikit-learn.org/scikit-learn-benchmarks + +.. |PythonMinVersion| replace:: 3.11 +.. |NumPyMinVersion| replace:: 1.24.1 +.. |SciPyMinVersion| replace:: 1.10.0 +.. |JoblibMinVersion| replace:: 1.4.0 +.. |NarwhalsMinVersion| replace:: 2.0.1 +.. |ThreadpoolctlMinVersion| replace:: 3.5.0 +.. |MatplotlibMinVersion| replace:: 3.6.1 +.. |Scikit-ImageMinVersion| replace:: 0.22.0 +.. |PandasMinVersion| replace:: 1.5.0 +.. |SeabornMinVersion| replace:: 0.13.0 +.. |PytestMinVersion| replace:: 7.1.2 +.. |PlotlyMinVersion| replace:: 5.22.0 + +.. image:: https://raw.githubusercontent.com/scikit-learn/scikit-learn/main/doc/logos/scikit-learn-logo.png + :target: https://scikit-learn.org/ + +**scikit-learn** is a Python module for machine learning built on top of +SciPy and is distributed under the 3-Clause BSD license. + +The project was started in 2007 by David Cournapeau as a Google Summer +of Code project, and since then many volunteers have contributed. See +the `About us `__ page +for a list of core contributors. + +It is currently maintained by a team of volunteers. + +Website: https://scikit-learn.org + +Installation +------------ + +Dependencies +~~~~~~~~~~~~ + +scikit-learn requires: + +- Python (>= |PythonMinVersion|) +- NumPy (>= |NumPyMinVersion|) +- SciPy (>= |SciPyMinVersion|) +- Narwhals (>= |NarwhalsMinVersion|) +- joblib (>= |JoblibMinVersion|) +- threadpoolctl (>= |ThreadpoolctlMinVersion|) + +======= + +Scikit-learn plotting capabilities (i.e., functions start with ``plot_`` and +classes end with ``Display``) require Matplotlib (>= |MatplotlibMinVersion|). +For running the examples Matplotlib >= |MatplotlibMinVersion| is required. +A few examples require scikit-image >= |Scikit-ImageMinVersion|, a few examples +require pandas >= |PandasMinVersion|, some examples require seaborn >= +|SeabornMinVersion| and Plotly >= |PlotlyMinVersion|. + +User installation +~~~~~~~~~~~~~~~~~ + +If you already have a working installation of NumPy and SciPy, +the easiest way to install scikit-learn is using ``pip``:: + + pip install -U scikit-learn + +or ``conda``:: + + conda install -c conda-forge scikit-learn + +The documentation includes more detailed `installation instructions `_. + + +Changelog +--------- + +See the `changelog `__ +for a history of notable changes to scikit-learn. + +Development +----------- + +We welcome new contributors of all experience levels. The scikit-learn +community goals are to be helpful, welcoming, and effective. The +`Development Guide `_ +has detailed information about contributing code, documentation, tests, and +more. We've included some basic information in this README. + +Important links +~~~~~~~~~~~~~~~ + +- Official source code repo: https://github.com/scikit-learn/scikit-learn +- Download releases: https://pypi.org/project/scikit-learn/ +- Issue tracker: https://github.com/scikit-learn/scikit-learn/issues + +Source code +~~~~~~~~~~~ + +You can check the latest sources with the command:: + + git clone https://github.com/scikit-learn/scikit-learn.git + +Contributing +~~~~~~~~~~~~ + +To learn more about making a contribution to scikit-learn, please see our +`Contributing guide +`_. + +Testing +~~~~~~~ + +After installation, you can launch the test suite from outside the source +directory (you will need to have ``pytest`` >= |PytestMinVersion| installed):: + + pytest sklearn + +See the web page https://scikit-learn.org/dev/developers/contributing.html#testing-and-improving-test-coverage +for more information. + + Random number generation can be controlled during testing by setting + the ``SKLEARN_SEED`` environment variable. + +Submitting a Pull Request +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Before opening a Pull Request, have a look at the +full Contributing page to make sure your code complies +with our guidelines: https://scikit-learn.org/stable/developers/index.html + +Project History +--------------- + +The project was started in 2007 by David Cournapeau as a Google Summer +of Code project, and since then many volunteers have contributed. See +the `About us `__ page +for a list of core contributors. + +The project is currently maintained by a team of volunteers. + +**Note**: `scikit-learn` was previously referred to as `scikits.learn`. + +Help and Support +---------------- + +Documentation +~~~~~~~~~~~~~ + +- HTML documentation (stable release): https://scikit-learn.org +- HTML documentation (development version): https://scikit-learn.org/dev/ +- FAQ: https://scikit-learn.org/stable/faq.html + +Communication +~~~~~~~~~~~~~ + +Main Channels +^^^^^^^^^^^^^ + +- **Website**: https://scikit-learn.org +- **Blog**: https://blog.scikit-learn.org +- **Mailing list**: https://mail.python.org/mailman/listinfo/scikit-learn + +Developer & Support +^^^^^^^^^^^^^^^^^^^^^^ + +- **GitHub Discussions**: https://github.com/scikit-learn/scikit-learn/discussions +- **Stack Overflow**: https://stackoverflow.com/questions/tagged/scikit-learn +- **Discord**: https://discord.gg/h9qyrK8Jc8 + +Social Media Platforms +^^^^^^^^^^^^^^^^^^^^^^ + +- **LinkedIn**: https://www.linkedin.com/company/scikit-learn +- **YouTube**: https://www.youtube.com/channel/UCJosFjYm0ZYVUARxuOZqnnw/playlists +- **Facebook**: https://www.facebook.com/scikitlearnofficial/ +- **Instagram**: https://www.instagram.com/scikitlearnofficial/ +- **TikTok**: https://www.tiktok.com/@scikit.learn +- **Bluesky**: https://bsky.app/profile/scikit-learn.org +- **Mastodon**: https://mastodon.social/@sklearn@fosstodon.org + +Resources +^^^^^^^^^ + +- **Calendar**: https://blog.scikit-learn.org/calendar/ +- **Logos & Branding**: https://github.com/scikit-learn/scikit-learn/tree/main/doc/logos + +Citation +~~~~~~~~ + +If you use scikit-learn in a scientific publication, we would appreciate citations: https://scikit-learn.org/stable/about.html#citing-scikit-learn diff --git a/.venv/lib/python3.11/site-packages/scikit_learn-1.9.0.dist-info/RECORD b/.venv/lib/python3.11/site-packages/scikit_learn-1.9.0.dist-info/RECORD new file mode 100644 index 0000000000000000000000000000000000000000..f8b093ed9e5dc6aa9792b214a82dbcff6f6f00e5 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scikit_learn-1.9.0.dist-info/RECORD @@ -0,0 +1,992 @@ +scikit_learn-1.9.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2 +scikit_learn-1.9.0.dist-info/METADATA,sha256=7mGlefQtPtq-Pkg9tINHXjP__sBNfz5pxTlSe9k-rKY,11984 +scikit_learn-1.9.0.dist-info/RECORD,, +scikit_learn-1.9.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +scikit_learn-1.9.0.dist-info/WHEEL,sha256=0k-XbnzlIttvoouTVqS5igzC3_g2SiMBxIP-1EBcy-Q,138 +scikit_learn-1.9.0.dist-info/licenses/COPYING,sha256=h1Lf6C1Kdo527jCbIC57M5BtHqkATWjMYVZDkcxrGpY,5078 +scikit_learn-1.9.0.dist-info/sboms/auditwheel.cdx.json,sha256=WZF1RfIVE_cgmVU3J6GHbXeYfwBtnMsZkbd82gIu2NA,1478 +scikit_learn.libs/libgomp-e985bcbb.so.1.0.0,sha256=pDkE5PopcwHUZA3BuzyKNIC0BvmeSY66mxkUtoqrYEo,253289 +sklearn/__check_build/__init__.py,sha256=RUwPCaMsJP6aR9xbsS9AUIcfJUmk10Ut1gLeaPv0Xuc,1880 +sklearn/__check_build/_check_build.cpython-311-x86_64-linux-gnu.so,sha256=EeSxnGr3_VV41jqMxUC7u5TUugTGdxgXyj_cNjVP-dY,40304 +sklearn/__check_build/_check_build.pyx,sha256=8uo0MEvoqggJXyJug6X1iOtrHEjEuRHEy8XK9EEEsVE,30 +sklearn/__check_build/meson.build,sha256=kYUehV7zeGx_ckXUuJZoUHqzFr_QjTkEQFpzUdCmUeM,135 +sklearn/__init__.py,sha256=zmY9_bMb29QpIWZSfkOk-GGzG0RdLuiU5yXoLB5zj3g,4520 +sklearn/_build_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/_build_utils/tempita.py,sha256=D-5VlYirbKymB12g0lRet-BHq40YXbViGh51Ngr_yi8,1684 +sklearn/_build_utils/version.py,sha256=MXulZf33cp8otqGocAwKzSBIM6MUerYtE8fxqZsAfJA,448 +sklearn/_config.py,sha256=J_BC8zvu4qJBqX1NvRjZ0mU37lEIza6C4OXtZj3IWJU,16099 +sklearn/_cyutility.cpython-311-x86_64-linux-gnu.so,sha256=DorTUTAglMb5_wBxmxUvK-Aei3SttkSSSnPZiBLxgg4,214872 +sklearn/_distributor_init.py,sha256=HJ3OJ8FgzN3a-dNHePdJd_rdMK7_GYsnqU_fe3VuipE,424 +sklearn/_isotonic.cpython-311-x86_64-linux-gnu.so,sha256=VbaNB6riuA5H5KYQ3J5URY2Nx0mI163kscBgAh26NVo,141776 +sklearn/_isotonic.pyx,sha256=L1JOQOTp5SoiZjzZKTczOtGuxilh_0bAluCk8Q9YM3Y,3733 +sklearn/_loss/__init__.py,sha256=4qOsSqP266QevVEUqnKtnDtBdBsjtwB8NWz-SsLDUfU,830 +sklearn/_loss/_loss.cpython-311-x86_64-linux-gnu.so,sha256=2b-vi7Vilqc7LPjwBwrIT1g85PhZKU8c6CG2qBWXpAU,1580473 +sklearn/_loss/_loss.pxd,sha256=8LvWX3YNUuv3E5KQtl2o68mEqzu3tFFGjk8Qn-9lnk0,4577 +sklearn/_loss/_loss.pyx.tp,sha256=FFWc5HiGoe-xEMk1VLf_oCtf8wZwJghgFmnKlArwR_Q,53678 +sklearn/_loss/link.py,sha256=c83sz8y19Gxe8kmNAqvv9bPIqibo_cK-ybRShoET22Q,7487 +sklearn/_loss/loss.py,sha256=fATgBmqMKHjEqD0DxXxDWflo5DsKmPsUAtgfVrJ5p7k,59075 +sklearn/_loss/meson.build,sha256=euUiR6guYLdWJh8MBrqm7FvJTP8Etjxcx8GT0Np_-3Q,860 +sklearn/_loss/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/_loss/tests/test_link.py,sha256=qi-RqAD7A8KM-wJixWcDp1dpCXZj5Lajb-JhpJ3Lnpk,5032 +sklearn/_loss/tests/test_loss.py,sha256=cSHR2jHmjih9BZ7fupjIy8WjBJAnPCSTqOdbd4As6Ng,55743 +sklearn/_min_dependencies.py,sha256=cQpbNHFDaUKVZ2YqL8jkUNEAnXJAI3fGU2Sd-ZfRjEg,2926 +sklearn/base.py,sha256=_13SjaUzN_gOkpJVQuagfRqknLMlt_VjbNnFbV8mtE8,49206 +sklearn/calibration.py,sha256=Z93DxZibtLfgFVtwGqNSRNgPxZfF0-wdRGFfPmCka-0,61554 +sklearn/callback/__init__.py,sha256=aXIyMYozmMJs-fr8_3ctcgPGIg3C8STX7C6LrDQWJNI,761 +sklearn/callback/_base.py,sha256=1o1Mqhcf9EJWtXyOip_xfQrBAMpVzUzBcCMY5OzDwkE,4328 +sklearn/callback/_callback_context.py,sha256=goMhfgI3QhBMfS6Zh66aN1U31zNBW8u16wXQrArbJ08,21930 +sklearn/callback/_callback_support.py,sha256=hFL5ft4zGqxO29L6O74o5rAEQ5seQr3yKUlmxS89WSA,5707 +sklearn/callback/_progressbar.py,sha256=AbkZT-84LGwnoVru412z_KHTFqy0uSfaTgkHa-_OIMI,11306 +sklearn/callback/_scoring_monitor.py,sha256=_Ajbg32ix_D9U-kDyfZJLEWPGRWMdxZW6hcshU925Rs,11691 +sklearn/callback/_transport.py,sha256=zqWrBIm9eo9-RP0ASYbG5IKLrYGWTRh7LebkLq1k_kA,7699 +sklearn/callback/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/callback/tests/_utils.py,sha256=9gslxxsa3JLe0DM4AG2njl7QDhwVGeuHjRfTrUejOnQ,14241 +sklearn/callback/tests/conftest.py,sha256=Dy_WY_wwfj0QLa3jyqbHZ-ZExMOnPqu2dYK880os35g,179 +sklearn/callback/tests/test_callback_context.py,sha256=IPFWnuL3BaEghYddQPmKuM4OdCxv-ji5GIQ7dfCDGUA,20050 +sklearn/callback/tests/test_callback_support.py,sha256=rjP1HMBAXdZYOOD0UsZrvfsuJP9JIL-iEItGfR51QrE,5052 +sklearn/callback/tests/test_pickle.py,sha256=j7Tg17RGNwBAG_2inr1DXfqYiHiyOGhO0D0MpWeEQrI,4471 +sklearn/callback/tests/test_progressbar.py,sha256=m9LkzDo-BS8R7i3LnXBpLOI1WIC29WTeioSL-9VtOoI,7888 +sklearn/callback/tests/test_scoring_monitor.py,sha256=MQSUVf0dNvN3hjSMw5HPd43BtSgi0R9IfFhbRW8zH_s,14155 +sklearn/cluster/__init__.py,sha256=fed7qyJJrpjq6UcdC7UvR3T0W8RUGHRfj8km06o_al8,1675 +sklearn/cluster/_affinity_propagation.py,sha256=zNIzFEQ592Iej0LnDaT7nIm20lGDi4vG9bS3D9hc-vY,20752 +sklearn/cluster/_agglomerative.py,sha256=qciILafS2-3C6DJ6eaGfghpCumh51bc4adTv8f45f7w,49603 +sklearn/cluster/_bicluster.py,sha256=rUZYZ1Oo2Gz6A60fRTgF6TzRIvbTWrz-IHPxMQOJ44Y,22134 +sklearn/cluster/_birch.py,sha256=kgcMlAcrZ54i3SmMB-XQwTh7f6cje_440no1oTCijeM,26062 +sklearn/cluster/_bisect_k_means.py,sha256=dd9GwrqMpAgIaSrhjRC7Dp_NQfcZIqvdkwF3vbVbp3E,19608 +sklearn/cluster/_dbscan.py,sha256=qs7vMscUul2UUKa-BOB16FETZKOFNRMB8KOB7Me6H_o,20552 +sklearn/cluster/_dbscan_inner.cpython-311-x86_64-linux-gnu.so,sha256=43wUpB8Zgo9X7DWYzWo5A64z7lSmXB-VxbvZpwEtnbs,59240 +sklearn/cluster/_dbscan_inner.pyx,sha256=388D0OV6pim9vWNoUsmSbqnlNgg8mUrF8kBP58Mone8,1324 +sklearn/cluster/_feature_agglomeration.py,sha256=805q7TJvToKzuJLrTc4MbEYoT2Jm0VS-TCr1wQiv8kM,2439 +sklearn/cluster/_hdbscan/__init__.py,sha256=vKm4xxqJIfK6Tk6By-YU03YcE6pR1X1juFrOsacfZjY,79 +sklearn/cluster/_hdbscan/_linkage.cpython-311-x86_64-linux-gnu.so,sha256=a8FnFQ0y8-CFJKIFay8zlZQpj8WXn8ZXlUgL3-8a6iY,123864 +sklearn/cluster/_hdbscan/_linkage.pyx,sha256=VEYh6Jr8JlAnQcG_apQgCi40d8e9kSMMcdhnkw1-6x4,10277 +sklearn/cluster/_hdbscan/_reachability.cpython-311-x86_64-linux-gnu.so,sha256=-zqnGqNa92t4upI-86i3mypG-pj6e4q_l7wDLPL_Jnk,212232 +sklearn/cluster/_hdbscan/_reachability.pyx,sha256=oq16fcP5kXiQUuxYDKyrsuwJTL4xPD2DoSVzhWSZeTI,7779 +sklearn/cluster/_hdbscan/_tree.cpython-311-x86_64-linux-gnu.so,sha256=TqkDnEWnfvRh8Ri_96at3sFq6lL0RSzItAyiCZ-5je4,260528 +sklearn/cluster/_hdbscan/_tree.pxd,sha256=tVftCj-QjotuQWzKqluO11S3HAzRH71XSiuNrYStmsk,2155 +sklearn/cluster/_hdbscan/_tree.pyx,sha256=OGXbAmp7P90a0ENuGaEcLCPpwiNbVHEyr07EYPjSTxM,27776 +sklearn/cluster/_hdbscan/hdbscan.py,sha256=HK0BiQZpSCG_AggWXyVZo3rOOzwjdfGCJoPMHl7ANP0,41792 +sklearn/cluster/_hdbscan/meson.build,sha256=b7tictmrUv1r6gCTC6cue3Zy45FwBMU3HfsuN_BE7oU,762 +sklearn/cluster/_hdbscan/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/cluster/_hdbscan/tests/test_reachibility.py,sha256=HCzRrBdtYARu83re_Z-Mu-hEZzhVWKNzCDpuZD_M3rM,2065 +sklearn/cluster/_hierarchical_fast.cpython-311-x86_64-linux-gnu.so,sha256=l2FIuwPiiKQrwYo5tczufix-Kz59gWnTxfBfp1_qWfA,174136 +sklearn/cluster/_hierarchical_fast.pxd,sha256=x5OaOp85uS7tm18Mzff3B6NvLRguqN0kGOCFHgO2ovA,251 +sklearn/cluster/_hierarchical_fast.pyx,sha256=DR22IMU8S7zQWhPZ-vSpfW_T4Up55JgUnGXwWe5X3Pw,15943 +sklearn/cluster/_k_means_common.cpython-311-x86_64-linux-gnu.so,sha256=ODs_K7pdz8PEoQsF_hXANyzqRov4fALy2r67_KG4eyc,281937 +sklearn/cluster/_k_means_common.pxd,sha256=6QW18TtC1wGpyTd0cdG9PxSYTiP4ZN3hj6ltJWrdaic,887 +sklearn/cluster/_k_means_common.pyx,sha256=SwCwuo2DF4R0icjK4oMljhW4v30OpwZ60GsqG94DQMQ,10212 +sklearn/cluster/_k_means_elkan.cpython-311-x86_64-linux-gnu.so,sha256=ooGrWiTvSq5F6DijwRYXy0IrKbZb9mni8cWtfhiRe28,290081 +sklearn/cluster/_k_means_elkan.pyx,sha256=z7_I9YXuva4FoqyCmmRY3SRRXXjZ6W9OrNNeA9bOTPc,28305 +sklearn/cluster/_k_means_lloyd.cpython-311-x86_64-linux-gnu.so,sha256=mN2OlVgK84GKKzH_aliOYz0qPqZV_C-5rFeYYjrkhFk,195841 +sklearn/cluster/_k_means_lloyd.pyx,sha256=iqbGbS6nkNQgxNoAHkTHX5QaacRcWVP5mJynse2Z-JQ,16580 +sklearn/cluster/_k_means_minibatch.cpython-311-x86_64-linux-gnu.so,sha256=rf8zNUFKn-y8Mz_yCE2G3vy2ibY9rMKQnt17SwiiWMo,158649 +sklearn/cluster/_k_means_minibatch.pyx,sha256=ytlKAPQuIgC54Wc8t8OlzeS8qi6HMALyKcun4lWOjR4,8156 +sklearn/cluster/_kmeans.py,sha256=fZzTx18cQGFiI_vOsjvB4RXeBDs1WrkHFomDAXVtdGw,82785 +sklearn/cluster/_mean_shift.py,sha256=cuCwDvkWQv6Ck77HCZ4QDS_GLr8Xcq2mcQR914MmO5w,20332 +sklearn/cluster/_optics.py,sha256=0h9Wgx4hZY9Si22T0B2LaQGji_mi3mk_0IKgTnzUbmo,44986 +sklearn/cluster/_spectral.py,sha256=zYz9qXgWpGRG7oI3txiiCkuxkbB4S34DGckX6k1fPAQ,30980 +sklearn/cluster/meson.build,sha256=UBtHRFqB7JvZQ3o6rP4FsLccnPlbs5WYYBXNlO1dfmQ,975 +sklearn/cluster/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/cluster/tests/common.py,sha256=1jmt9fXRXYt9TYCwJFcgDGV10slNNjJW7_2tRCSzJBY,880 +sklearn/cluster/tests/test_affinity_propagation.py,sha256=p-q92owXh0cG1oPD3d5VZOfQoZMwEeDfRlTAS25NTa0,11898 +sklearn/cluster/tests/test_bicluster.py,sha256=pifMCHZeqmzubf8xTgeIq50FzEtU51kB4fwbJw-T028,9154 +sklearn/cluster/tests/test_birch.py,sha256=1vCTQlIByLia7dwJ8Lequ6OhDuuUjnIbayPgoxk0lD0,8606 +sklearn/cluster/tests/test_bisect_k_means.py,sha256=ti8z2czRyy-tWR0p_c1HhaknRZgzhcq8S7XYz3zTCOw,5567 +sklearn/cluster/tests/test_dbscan.py,sha256=8T5QOHsOI7ZnCYcBgcRE1AMT9IUanlFImxxsr3TKi1E,15704 +sklearn/cluster/tests/test_feature_agglomeration.py,sha256=V1apZXrZLF631DBVs72OMsZFGTHmb-ZdhvwXGd1vew0,1965 +sklearn/cluster/tests/test_hdbscan.py,sha256=oVyXGkNxLMZVVe6ATHR0TI4U9cx6clvQ3we2lKcjyJs,20336 +sklearn/cluster/tests/test_hierarchical.py,sha256=jLyRQRQ_lcXQXUhKnaXWv-4TPoZWOGHGt96PiQeNhv8,33144 +sklearn/cluster/tests/test_k_means.py,sha256=M77xKbDLHNlpG3LZcN8-e1a3DWfoz00x7psQN6B-rjo,49197 +sklearn/cluster/tests/test_mean_shift.py,sha256=7fxfCCvMWQ-5PkRKcGrl48w0nOMnZvnv5nyGOvpBLHU,7196 +sklearn/cluster/tests/test_optics.py,sha256=TYCTdbrzrD7AB9zRPpkhCZSt3OhvcNuvn3pLw-bIDyk,24787 +sklearn/cluster/tests/test_spectral.py,sha256=mSI9ATIFBZH9ySu68SqFx694UVPAK5JIt5ZKpZoCh5o,11828 +sklearn/compose/__init__.py,sha256=0npaCUNc07d9cqO9ust8Grj8U86FeMfXSZ3poiXBLuk,661 +sklearn/compose/_column_transformer.py,sha256=vqU-G12pVWxLLOpr6ooQKhmqysH88EbyaajGHaPx1J8,61159 +sklearn/compose/_target.py,sha256=WSoNJqNyY5h4I33OxPp885hQL25uIs75pM8XAjpPpSw,15074 +sklearn/compose/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/compose/tests/test_column_transformer.py,sha256=clzdFr91CWINprnIw-ulAcLWYro1CaNW_o_wPmTpfNE,98554 +sklearn/compose/tests/test_target.py,sha256=OzwK81_h6he_AqlrK1_7R_pnu03IcaMkYOJfGA2uqKw,14891 +sklearn/conftest.py,sha256=mGqvO6UZ6zXP6-wzhETLuvk5ll7GMU3j4td4zEuyQbY,18166 +sklearn/covariance/__init__.py,sha256=RpHF7Hps-yr87BCDWtvjeO28a565jdeAC6k68fsIY4c,1278 +sklearn/covariance/_elliptic_envelope.py,sha256=W7kgryernB4PG9IGgGoZeE_iQKDZmTPmeynmw6RJ0XQ,9097 +sklearn/covariance/_empirical_covariance.py,sha256=zFxN4dUN6OIN_JNkMTmWTe-mT5WwRdceYKxKwKoNScg,12312 +sklearn/covariance/_graph_lasso.py,sha256=aFcoX9MIK4Y1HqYeBc6zhLjmlRFHrcWdpQyO-9MxSJA,40906 +sklearn/covariance/_robust_covariance.py,sha256=SuHPMMXa473xKOFNDkzJ0fp7ik4Dh_iA0TBS_KStbaQ,36429 +sklearn/covariance/_shrunk_covariance.py,sha256=8bPiPeAbskALu6xFvasBl8vlxSar4DB1aJv4bGmMZh0,28079 +sklearn/covariance/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/covariance/tests/test_covariance.py,sha256=MVKW6cxKSUl9TrleBJAQGLpup1a7NUtSg2JrP1J6FNU,14040 +sklearn/covariance/tests/test_elliptic_envelope.py,sha256=xCxtRYDNADB22KJURLGRqI2OoWh4LLfazpjgsIWOzH4,1587 +sklearn/covariance/tests/test_graphical_lasso.py,sha256=od09Tmq5RiLkyyDKubLc_CFRf4lTNQno2NWezpbSvas,11455 +sklearn/covariance/tests/test_robust_covariance.py,sha256=k6cLWjgJ6zLDZkQEjweBhPZ3D3KyD5ZHv7MS1ecYyT8,7516 +sklearn/cross_decomposition/__init__.py,sha256=xFhVVPMDX7e-ja5DVu_yT32m-um2cTKipgqufVANXkc,271 +sklearn/cross_decomposition/_pls.py,sha256=tRv1SmQOyZxY1ZfZjjIYIjvfnOTHfCzQQVtqwSt6U4Y,37009 +sklearn/cross_decomposition/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/cross_decomposition/tests/test_pls.py,sha256=I1IcFi9dpYmAv7PqOdUgYv9wz54He6erpzFz3vjksME,23601 +sklearn/datasets/__init__.py,sha256=aihzZGJxePU87GvoZd2dH9PWkKe_g-ZG57gW4a2BAl0,5391 +sklearn/datasets/_arff_parser.py,sha256=5LycpRgxxVWnl5NUoIKhrRCChXSU6TnzxvQT14HbjJo,19262 +sklearn/datasets/_base.py,sha256=WcOPQxGPXH0NX9yiX-8rUDB2drNFLWcwCEBQEs5G1Uo,53688 +sklearn/datasets/_california_housing.py,sha256=yHwlWNL-kzEwGreDUraSCVYl0o_EVx28oa8UAzE07pc,7237 +sklearn/datasets/_covtype.py,sha256=57A4nBMOVdgM_iSgYBsu_LT9DdEFNWP84WJ4mSo1c_8,8118 +sklearn/datasets/_kddcup99.py,sha256=eRhda8NMyqRTJT4mM8ecKEqvWaqR-ztvMoX97QaGZYg,13865 +sklearn/datasets/_lfw.py,sha256=TKA83-n-hp6E2en33VJsYiTvg7s4sAJk4klXRLpEPXM,22675 +sklearn/datasets/_olivetti_faces.py,sha256=MK4LKD3N9j2CV2t_0g17fSrQva-t3boT10qUmphgCUs,6072 +sklearn/datasets/_openml.py,sha256=jzJgxg_q2etLhNgDe7JBTCPg3NvbyEmIKg0rLBQIZ54,41840 +sklearn/datasets/_rcv1.py,sha256=OlMteSI-VFLchDTgSnlPvJgGMbz2qvh05ipidl08wxo,12055 +sklearn/datasets/_samples_generator.py,sha256=GLjreC543xBYrTtwqSbknRwB4jcmhWp15IyZ43SYqxU,77213 +sklearn/datasets/_species_distributions.py,sha256=_o8tUaXqW6iJ4nMp8qM0bbgda1euzLFLRC_mbMPj8D8,9382 +sklearn/datasets/_svmlight_format_fast.cpython-311-x86_64-linux-gnu.so,sha256=NnqJZvhJDiH8RGjxMkGZzoGOKHhxver5JYyZP4KFkWc,441016 +sklearn/datasets/_svmlight_format_fast.pyx,sha256=rPeUo9Ose06b9hpswkmkvyXesvlSMKu3Xvq_dZk3aIY,7153 +sklearn/datasets/_svmlight_format_io.py,sha256=TvTIpo3AUmpZYObk89tFeoLPaPW7STXCfJ8ls-Dk6U8,20969 +sklearn/datasets/_twenty_newsgroups.py,sha256=Z0_10hHGxGN9KjJPANEaZcF3anPvfdQ4B__tXpDl4bw,20867 +sklearn/datasets/data/__init__.py,sha256=vKm4xxqJIfK6Tk6By-YU03YcE6pR1X1juFrOsacfZjY,79 +sklearn/datasets/data/breast_cancer.csv,sha256=_tPrctBXXvYZIpP1CTxugBsUdrV30Dhr9EVVBFIhcu0,119913 +sklearn/datasets/data/diabetes_data_raw.csv.gz,sha256=f8De1XFFSxmCIQ07tD8KykTq4BoLhlSjskAivbazgAk,7073 +sklearn/datasets/data/diabetes_target.csv.gz,sha256=jlP2XrgR30PCBvNTS7OvDl_tITvDfta6NjEBV9YCOAM,1050 +sklearn/datasets/data/digits.csv.gz,sha256=CfZubeve4s0rWuWeDWq7tz_CsOAYXS4ZV-nrtR4jqiI,57523 +sklearn/datasets/data/iris.csv,sha256=8T_6j91W_Y5sjRbUCBo_vTEUvNCq5CVsQyBRac2dFEk,2734 +sklearn/datasets/data/linnerud_exercise.csv,sha256=y42MJJN2Q_okWWgu-4bF5me81t2TEJ7vgZZNnp8Rv4w,212 +sklearn/datasets/data/linnerud_physiological.csv,sha256=K_fgXBzX0K3w7KHkVpQfYkvtCk_JZpTWDQ_3hT7F_Pc,219 +sklearn/datasets/data/wine_data.csv,sha256=EOioApCLNPhuXajOli88gGaUvJhFChj2GFGvWfMkvt4,11157 +sklearn/datasets/descr/__init__.py,sha256=vKm4xxqJIfK6Tk6By-YU03YcE6pR1X1juFrOsacfZjY,79 +sklearn/datasets/descr/breast_cancer.rst,sha256=PFhVGCpE0SyR8fsnOIdB-3C0uSukD7dC3Km15ATGjxk,4794 +sklearn/datasets/descr/california_housing.rst,sha256=mDlwU6orh6xVH-4sBNTcYuUZkkbqDLCEK7KGjwyGWNY,1693 +sklearn/datasets/descr/covtype.rst,sha256=C6DmczitjtnrO-XhCIi8WqNT0uPgYnPWNYtKwJTwcn4,1191 +sklearn/datasets/descr/diabetes.rst,sha256=B9z8E5V6gkhb385Ers_7py55d1lZZtEYuB8WLLgn44E,1455 +sklearn/datasets/descr/digits.rst,sha256=jn5Y1hKVj32bDeGTHtaLIRcD7rI56Ajz2CxfCDfMAiI,2007 +sklearn/datasets/descr/iris.rst,sha256=cfhnSai8Uo0ht9sPlTMuMjDRMjGgXCcg5TeyxaqO9ek,2656 +sklearn/datasets/descr/kddcup99.rst,sha256=qRz2X8XmUh8IZKjzT1OAJd5sj91bBo0xpdcV5rS2Jko,3919 +sklearn/datasets/descr/lfw.rst,sha256=8sj8ApMwZDHabnaksL4S-WHS1V-k-divU7DGPJWP7Aw,4409 +sklearn/datasets/descr/linnerud.rst,sha256=jDI-AIsVeZZTVVWSiUztp5lEL4H2us847bgF3FSGb1s,704 +sklearn/datasets/descr/olivetti_faces.rst,sha256=i8Y7-g4fOPdLvupgJ8i_ze1pA0hGpfDgAoPCGvCPFxI,1834 +sklearn/datasets/descr/rcv1.rst,sha256=mLj4WU7aEVqaJg7hgSSe81oI74L6_pGECR72O8dEMZ4,2455 +sklearn/datasets/descr/species_distributions.rst,sha256=L80eaLcb9ymJOZyFLoQhDykU9dwiouRFRTD-_IrKFsI,1648 +sklearn/datasets/descr/twenty_newsgroups.rst,sha256=Z5efG4-mdET4H4sXgCt37IHL08EUzKbnucB190o_GD8,10923 +sklearn/datasets/descr/wine_data.rst,sha256=zs6XS-V-cnn92wnz_6zMJs8MIAh_KalkGhd1bFLiUwE,3367 +sklearn/datasets/images/README.txt,sha256=PH7xWh-iW5mNOMkhMjeGNZVare3B3PPkDmPcAJj2uPc,709 +sklearn/datasets/images/__init__.py,sha256=vKm4xxqJIfK6Tk6By-YU03YcE6pR1X1juFrOsacfZjY,79 +sklearn/datasets/images/china.jpg,sha256=g3gCWtJRnWSdAuMr2YmQ20q1cjV9nwmEHC-_u0_vrSk,196653 +sklearn/datasets/images/flower.jpg,sha256=p39uxB41Ov34vf8uqYGylVU12NgylPjPpJz05CPdVjg,142987 +sklearn/datasets/meson.build,sha256=Vx9GBA1WjNkluNaLNncDqp7NsZ6jTw3Ymw7htBHfy2M,173 +sklearn/datasets/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_1/api-v1-jd-1.json.gz,sha256=hi4IUgokM6SVo7066f2ebHxUCpxjLbKbuCUnhMva13k,1786 +sklearn/datasets/tests/data/openml/id_1/api-v1-jdf-1.json.gz,sha256=qWba1Yz1-8kUo3StVVbAQU9e2WIjftVaN5_pbjCNAN4,889 +sklearn/datasets/tests/data/openml/id_1/api-v1-jdq-1.json.gz,sha256=hKhybSw_i7ynnVTYsZEVh0SxmTFG-PCDsRGo6nhTYFc,145 +sklearn/datasets/tests/data/openml/id_1/data-v1-dl-1.arff.gz,sha256=z-iUW5SXcLDaQtr1jOZ9HF_uJc97T9FFFhg3wqvAlCk,1841 +sklearn/datasets/tests/data/openml/id_1119/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_1119/api-v1-jd-1119.json.gz,sha256=xB5fuz5ZzU3oge18j4j5sDp1DVN7pjWByv3mqv13rcE,711 +sklearn/datasets/tests/data/openml/id_1119/api-v1-jdf-1119.json.gz,sha256=gviZ7cWctB_dZxslaiKOXgbfxeJMknEudQBbJRsACGU,1108 +sklearn/datasets/tests/data/openml/id_1119/api-v1-jdl-dn-adult-census-l-2-dv-1.json.gz,sha256=Sl3DbKl1gxOXiyqdecznY8b4TV2V8VrFV7PXSC8i7iE,364 +sklearn/datasets/tests/data/openml/id_1119/api-v1-jdl-dn-adult-census-l-2-s-act-.json.gz,sha256=bsCVV4iRT6gfaY6XpNGv93PXoSXtbnacYnGgtI_EAR0,363 +sklearn/datasets/tests/data/openml/id_1119/api-v1-jdq-1119.json.gz,sha256=73y8tYwu3P6kXAWLdR-vd4PnEEYqkk6arK2NR6fp-Us,1549 +sklearn/datasets/tests/data/openml/id_1119/data-v1-dl-54002.arff.gz,sha256=aTGvJWGV_N0uR92LD57fFvvwOxmOd7cOPf2Yd83wlRU,1190 +sklearn/datasets/tests/data/openml/id_1590/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_1590/api-v1-jd-1590.json.gz,sha256=mxBa3-3GtrgvRpXKm_4jI5MDTN95gDUj85em3Fv4JNE,1544 +sklearn/datasets/tests/data/openml/id_1590/api-v1-jdf-1590.json.gz,sha256=BG9eYFZGk_DzuOOCclyAEsPgWGRxOcJGhc7JhOQPzQA,1032 +sklearn/datasets/tests/data/openml/id_1590/api-v1-jdq-1590.json.gz,sha256=RLmw0pCh4zlpWkMUOPhAgAccVjUWHDl33Rf0wnsAo0o,1507 +sklearn/datasets/tests/data/openml/id_1590/data-v1-dl-1595261.arff.gz,sha256=7h3N9Y8vEHL33RtDOIlpxRvGz-d24-lGWuanVuXdsQo,1152 +sklearn/datasets/tests/data/openml/id_2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_2/api-v1-jd-2.json.gz,sha256=pnLUNbl6YDPf0dKlyCPSN60YZRAb1eQDzZm1vguk4Ds,1363 +sklearn/datasets/tests/data/openml/id_2/api-v1-jdf-2.json.gz,sha256=wbg4en0IAUocCYB65FjKdmarijxXnL-xieCcbX3okqY,866 +sklearn/datasets/tests/data/openml/id_2/api-v1-jdl-dn-anneal-l-2-dv-1.json.gz,sha256=6QCxkHlSJP9I5GocArEAINTJhroUKIDALIbwtHLe08k,309 +sklearn/datasets/tests/data/openml/id_2/api-v1-jdl-dn-anneal-l-2-s-act-.json.gz,sha256=_2Ily5gmDKTr7AFaGidU8qew2_tNDxfc9nJ1QhVOKhA,346 +sklearn/datasets/tests/data/openml/id_2/api-v1-jdq-2.json.gz,sha256=xG9sXyIdh33mBLkGQDsgy99nTxIlvNuz4VvRiCpppHE,1501 +sklearn/datasets/tests/data/openml/id_2/data-v1-dl-1666876.arff.gz,sha256=1XsrBMrlJjBmcONRaYncoyyIwVV4EyXdrELkPcIyLDA,1855 +sklearn/datasets/tests/data/openml/id_292/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_292/api-v1-jd-292.json.gz,sha256=Hmo4152PnlOizhG2i0FTBi1OluwLNo0CsuZPGzPFFpM,551 +sklearn/datasets/tests/data/openml/id_292/api-v1-jd-40981.json.gz,sha256=wm3L4wz7ORYfMFsrPUOptQrcizaNB0lWjEcQbL2yCJc,553 +sklearn/datasets/tests/data/openml/id_292/api-v1-jdf-292.json.gz,sha256=JVwW8z7Sln_hAM2AEafmn3iWA3JLHsLs-R3-tyBnwZA,306 +sklearn/datasets/tests/data/openml/id_292/api-v1-jdf-40981.json.gz,sha256=JVwW8z7Sln_hAM2AEafmn3iWA3JLHsLs-R3-tyBnwZA,306 +sklearn/datasets/tests/data/openml/id_292/api-v1-jdl-dn-australian-l-2-dv-1-s-dact.json.gz,sha256=jvYCVCX9_F9zZVXqOFJSr1vL9iODYV24JIk2bU-WoKc,327 +sklearn/datasets/tests/data/openml/id_292/api-v1-jdl-dn-australian-l-2-dv-1.json.gz,sha256=naCemmAx0GDsQW9jmmvzSYnmyIzmQdEGIeuQa6HYwpM,99 +sklearn/datasets/tests/data/openml/id_292/api-v1-jdl-dn-australian-l-2-s-act-.json.gz,sha256=NYkNCBZcgEUmtIqtRi18zAnoCL15dbpgS9YSuWCHl6w,319 +sklearn/datasets/tests/data/openml/id_292/data-v1-dl-49822.arff.gz,sha256=t-4kravUqu1kGbQ_6dP4bVX89L7g8WmK4h2GwnATFOM,2532 +sklearn/datasets/tests/data/openml/id_3/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_3/api-v1-jd-3.json.gz,sha256=BmohZnmxl8xRlG4X7pouKCFUJZkbDOt_EJiMFPfz-Gk,2473 +sklearn/datasets/tests/data/openml/id_3/api-v1-jdf-3.json.gz,sha256=7E8ta8TfOIKwi7oBVx4HkqVveeCpItmEiXdzrNKEtCY,535 +sklearn/datasets/tests/data/openml/id_3/api-v1-jdq-3.json.gz,sha256=Ce8Zz60lxd5Ifduu88TQaMowY3d3MKKI39b1CWoMb0Y,1407 +sklearn/datasets/tests/data/openml/id_3/data-v1-dl-3.arff.gz,sha256=xj_fiGF2HxynBQn30tFpp8wFOYjHt8CcCabbYSTiCL4,19485 +sklearn/datasets/tests/data/openml/id_40589/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_40589/api-v1-jd-40589.json.gz,sha256=WdGqawLSNYwW-p5Pvv9SOjvRDr04x8NxkR-oM1573L8,598 +sklearn/datasets/tests/data/openml/id_40589/api-v1-jdf-40589.json.gz,sha256=gmurBXo5KfQRibxRr6ChdSaV5jzPIOEoymEp6eMyH8I,856 +sklearn/datasets/tests/data/openml/id_40589/api-v1-jdl-dn-emotions-l-2-dv-3.json.gz,sha256=Geayoqj-xUA8FGZCpNwuB31mo6Gsh-gjm9HdMckoq5w,315 +sklearn/datasets/tests/data/openml/id_40589/api-v1-jdl-dn-emotions-l-2-s-act-.json.gz,sha256=TaY6YBYzQLbhiSKr_n8fKnp9oj2mPCaTJJhdYf-qYHU,318 +sklearn/datasets/tests/data/openml/id_40589/api-v1-jdq-40589.json.gz,sha256=0PeXMZPrNdGemdHYvKPH86i40EEFCK80rVca7o7FqwU,913 +sklearn/datasets/tests/data/openml/id_40589/data-v1-dl-4644182.arff.gz,sha256=LEImVQgnzv81CcZxecRz4UOFzuIGU2Ni5XxeDfx3Ub8,4344 +sklearn/datasets/tests/data/openml/id_40675/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_40675/api-v1-jd-40675.json.gz,sha256=p4d3LWD7_MIaDpb9gZBvA1QuC5QtGdzJXa5HSYlTpP0,323 +sklearn/datasets/tests/data/openml/id_40675/api-v1-jdf-40675.json.gz,sha256=1I2WeXida699DTw0bjV211ibZjw2QJQvnB26duNV-qo,307 +sklearn/datasets/tests/data/openml/id_40675/api-v1-jdl-dn-glass2-l-2-dv-1-s-dact.json.gz,sha256=Ie0ezF2HSVbpUak2HyUa-yFlrdqSeYyJyl4vl66A3Y8,317 +sklearn/datasets/tests/data/openml/id_40675/api-v1-jdl-dn-glass2-l-2-dv-1.json.gz,sha256=rQpKVHdgU4D4gZzoQNu5KKPQhCZ8US9stQ1b4vfHa8I,85 +sklearn/datasets/tests/data/openml/id_40675/api-v1-jdl-dn-glass2-l-2-s-act-.json.gz,sha256=FBumMOA56kS7rvkqKI4tlk_Dqi74BalyO0qsc4ompic,88 +sklearn/datasets/tests/data/openml/id_40675/api-v1-jdq-40675.json.gz,sha256=iPzcOm_tVpfzbcJi9pv_-4FHZ84zb_KKId7zqsk3sIw,886 +sklearn/datasets/tests/data/openml/id_40675/data-v1-dl-4965250.arff.gz,sha256=VD0IhzEvQ9n2Wn4dCL54okNjafYy1zgrQTTOu1JaSKM,3000 +sklearn/datasets/tests/data/openml/id_40945/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_40945/api-v1-jd-40945.json.gz,sha256=AogsawLE4GjvKxbzfzOuPV6d0XyinQFmLGkk4WQn610,437 +sklearn/datasets/tests/data/openml/id_40945/api-v1-jdf-40945.json.gz,sha256=lfCTjf3xuH0P_E1SbyyR4JfvdolIC2k5cBJtkI8pEDA,320 +sklearn/datasets/tests/data/openml/id_40945/api-v1-jdq-40945.json.gz,sha256=nH5aRlVKtqgSGDLcDNn3pg9QNM7xpafWE0a72RJRa1Q,1042 +sklearn/datasets/tests/data/openml/id_40945/data-v1-dl-16826755.arff.gz,sha256=UW6WH1GYduX4mzOaA2SgjdZBYKw6TXbV7GKVW_1tbOU,32243 +sklearn/datasets/tests/data/openml/id_40966/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_40966/api-v1-jd-40966.json.gz,sha256=NsY8OsjJ21mRCsv0x3LNUwQMzQ6sCwRSYR3XrY2lBHQ,1660 +sklearn/datasets/tests/data/openml/id_40966/api-v1-jdf-40966.json.gz,sha256=itrI4vjLy_qWd6zdSSepYUMEZdLJlAGDIWC-RVz6ztg,3690 +sklearn/datasets/tests/data/openml/id_40966/api-v1-jdl-dn-miceprotein-l-2-dv-4.json.gz,sha256=8MIDtGJxdc679SfYGRekmZEa-RX28vRu5ySEKKlI1gM,325 +sklearn/datasets/tests/data/openml/id_40966/api-v1-jdl-dn-miceprotein-l-2-s-act-.json.gz,sha256=MBOWtKQsgUsaFQON38vPXIWQUBIxdH0NwqUAuEsv0N8,328 +sklearn/datasets/tests/data/openml/id_40966/api-v1-jdq-40966.json.gz,sha256=Pe6DmH__qOwg4js8q8ANQr63pGmva9gDkJmYwWh_pjQ,934 +sklearn/datasets/tests/data/openml/id_40966/data-v1-dl-17928620.arff.gz,sha256=HF_ZP_7H3rY6lA_WmFNN1-u32zSfwYOTAEHL8X5g4sw,6471 +sklearn/datasets/tests/data/openml/id_42074/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_42074/api-v1-jd-42074.json.gz,sha256=T8shVZW7giMyGUPw31D1pQE0Rb8YGdU9PLW_qQ2eecA,595 +sklearn/datasets/tests/data/openml/id_42074/api-v1-jdf-42074.json.gz,sha256=OLdOfwKmH_Vbz6xNhxA9W__EP-uwwBnZqqFi-PdpMGg,272 +sklearn/datasets/tests/data/openml/id_42074/api-v1-jdq-42074.json.gz,sha256=h0KnS9W8EgrNkYbIqHN8tCDtmwCfreALJOfOUhd5fyw,722 +sklearn/datasets/tests/data/openml/id_42074/data-v1-dl-21552912.arff.gz,sha256=9iPnd8CjaubIL64Qp8IIjLODKY6iRFlb-NyVRJyb5MQ,2326 +sklearn/datasets/tests/data/openml/id_42585/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_42585/api-v1-jd-42585.json.gz,sha256=fMvxOOBmOJX5z1ERNrxjlcFT9iOK8urLajZ-huFdGnE,1492 +sklearn/datasets/tests/data/openml/id_42585/api-v1-jdf-42585.json.gz,sha256=CYUEWkVMgYa05pDr77bOoe98EyksmNUKvaRwoP861CU,312 +sklearn/datasets/tests/data/openml/id_42585/api-v1-jdq-42585.json.gz,sha256=Nzbn_retMMaGdcLE5IqfsmLoAwjJCDsQDd0DOdofwoI,348 +sklearn/datasets/tests/data/openml/id_42585/data-v1-dl-21854866.arff.gz,sha256=yNAMZpBXap7Dnhy3cFThMpa-D966sPs1pkoOhie25vM,4519 +sklearn/datasets/tests/data/openml/id_561/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_561/api-v1-jd-561.json.gz,sha256=odOP3WAbZ7ucbRYVL1Pd8Wagz8_vT6hkOOiZv-RJImw,1798 +sklearn/datasets/tests/data/openml/id_561/api-v1-jdf-561.json.gz,sha256=QHQk-3nMMLjp_5CQCzvykkSsfzeX8ni1vmAoQ_lZtO4,425 +sklearn/datasets/tests/data/openml/id_561/api-v1-jdl-dn-cpu-l-2-dv-1.json.gz,sha256=BwOwriC5_3UIfcYBZA7ljxwq1naIWOohokUVHam6jkw,301 +sklearn/datasets/tests/data/openml/id_561/api-v1-jdl-dn-cpu-l-2-s-act-.json.gz,sha256=cNRZath5VHhjEJ2oZ1wreJ0H32a1Jtfry86WFsTJuUw,347 +sklearn/datasets/tests/data/openml/id_561/api-v1-jdq-561.json.gz,sha256=h0Oy2T0sYqgvtH4fvAArl-Ja3Ptb8fyya1itC-0VvUg,1074 +sklearn/datasets/tests/data/openml/id_561/data-v1-dl-52739.arff.gz,sha256=6WFCteAN_sJhewwi1xkrNAriwo7D_8OolMW-dGuXClk,3303 +sklearn/datasets/tests/data/openml/id_61/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_61/api-v1-jd-61.json.gz,sha256=pcfnmqQe9YCDj7n8GQYoDwdsR74XQf3dUATdtQDrV_4,898 +sklearn/datasets/tests/data/openml/id_61/api-v1-jdf-61.json.gz,sha256=M8vWrpRboElpNwqzVgTpNjyHJWOTSTOCtRGKidWThtY,268 +sklearn/datasets/tests/data/openml/id_61/api-v1-jdl-dn-iris-l-2-dv-1.json.gz,sha256=C84gquf9kDeW2W1bOjZ3twWPvF8_4Jlu6dSR5O4j0TI,293 +sklearn/datasets/tests/data/openml/id_61/api-v1-jdl-dn-iris-l-2-s-act-.json.gz,sha256=qfS5MXmX32PtjSuwc6OQY0TA4L4Bf9OE6uw2zti5S64,330 +sklearn/datasets/tests/data/openml/id_61/api-v1-jdq-61.json.gz,sha256=QkzUfBKlHHu42BafrID7VgHxUr14RoskHUsRW_fSLyA,1121 +sklearn/datasets/tests/data/openml/id_61/data-v1-dl-61.arff.gz,sha256=r-RzaSRgZjiYTlcyNRkQJdQZxUXTHciHTJa3L17F23M,2342 +sklearn/datasets/tests/data/openml/id_62/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/datasets/tests/data/openml/id_62/api-v1-jd-62.json.gz,sha256=fvNVGtR9SAI8Wh8c8HcEeppLlVRLuR1Khgl_i1dPjQc,656 +sklearn/datasets/tests/data/openml/id_62/api-v1-jdf-62.json.gz,sha256=SJsXcSbLfzNcsiBwkjO5RtOgrXHTi7ptSLeRhxRuWFo,817 +sklearn/datasets/tests/data/openml/id_62/api-v1-jdq-62.json.gz,sha256=J4pSpS1WnwfRTGp4d7EEdix32qxCn7H9mBegN41uxjQ,805 +sklearn/datasets/tests/data/openml/id_62/data-v1-dl-52352.arff.gz,sha256=-1gwyCES9ipADIKsHxtethwpwKfMcrpW0q7_D66KYPk,1625 +sklearn/datasets/tests/data/svmlight_classification.txt,sha256=an5ZLlFP2RJkK0iT8V6B5NLpNvZFUEzpTonY-Frcv0o,253 +sklearn/datasets/tests/data/svmlight_invalid.txt,sha256=ueCvdPekdiYpH8FAH_AW9MHiyMd9SulhrkJ8FQm3ol8,54 +sklearn/datasets/tests/data/svmlight_invalid_order.txt,sha256=xSNKVNcM7TuWkTyTZnQSTTcoBdERxUKoM2yz_gFCaHA,23 +sklearn/datasets/tests/data/svmlight_multilabel.txt,sha256=DsT6kKm83Ac7HLhmw6d6P0e2YNSdL7-ES3lgk7BozW4,104 +sklearn/datasets/tests/test_20news.py,sha256=-EdeU6SLVlTPCGtatJRplVBvPrt6AygXgeNz_9JF-8Y,5340 +sklearn/datasets/tests/test_arff_parser.py,sha256=n9WpxiBJ_AvltjDGmH8VLJyX6EXLWzhQQoGKTLYYbEI,8196 +sklearn/datasets/tests/test_base.py,sha256=CzNmjduFE_LqRn6-WtlEdhnRx_VnBYqqrVW0__7npzk,23049 +sklearn/datasets/tests/test_california_housing.py,sha256=-kGKf35jMxfB9PgvNryrL3Xqil_CVhoWFPqRGoCdBoU,1369 +sklearn/datasets/tests/test_common.py,sha256=AiysQSj7JWHFN0L66fQv7bpdLIidkaGhqwbRq1Wp2nU,4392 +sklearn/datasets/tests/test_covtype.py,sha256=rnS0G-zkPov-roszvXRwiNBG50tciwMKe-D_RKe2OYY,1757 +sklearn/datasets/tests/test_kddcup99.py,sha256=5rw4Pva1EC2CO7imU9NVe0OqTrmTCu_4hElGpvZkUfk,2601 +sklearn/datasets/tests/test_lfw.py,sha256=c1brbM3idsxRdTfpohdr9cxC7pSLW4M00DOdBXP11jA,7798 +sklearn/datasets/tests/test_olivetti_faces.py,sha256=d2r43YseviKoA9OyX6JvDyXvY8lFRfV__j5hippkYY0,919 +sklearn/datasets/tests/test_openml.py,sha256=T9YFSK7rlgfxCwz0N2AdVHSdpyvzsO5G5kUskuYHr6w,54644 +sklearn/datasets/tests/test_rcv1.py,sha256=_MI_VuGKrZIIV-WMVxOEKMh94DqzhCrxV7l1E3NGkNM,2343 +sklearn/datasets/tests/test_samples_generator.py,sha256=NATN9G6WtU-rRsUl6Q3DeqRxTtxP8ch6kzrl3ht_KLo,23972 +sklearn/datasets/tests/test_svmlight_format.py,sha256=mqKurK216uySN6hE-DAfHRt-6NHEGm4fBWyBIHpKCx0,20222 +sklearn/decomposition/__init__.py,sha256=cgUyF3qtThtUAtAyijDXfnmtm0-pF8lMmVtzrGrDZrM,1524 +sklearn/decomposition/_base.py,sha256=afjHoWx05n3coOTOR7ckOO4HyzBBijpfal4AzA8-4Bk,7314 +sklearn/decomposition/_cdnmf_fast.cpython-311-x86_64-linux-gnu.so,sha256=wI3sEWtRLAXkvifLLT0KlgIcWcnfCz9nb3LVnr7fCqg,84184 +sklearn/decomposition/_cdnmf_fast.pyx,sha256=ONJUPP9uKUn6uyUJHwHBD3qQeJmtM-7GFjFA8qCniJQ,1128 +sklearn/decomposition/_dict_learning.py,sha256=MexdDLm2AcpcmK4JENbXFpNJ8d215yf0dyiXvY53YVQ,78693 +sklearn/decomposition/_factor_analysis.py,sha256=jAEmkBefCTDEN0pko-CgQZOqH-2TiD9WdpdN5CHY7R8,15281 +sklearn/decomposition/_fastica.py,sha256=3VuSRyoAznWitdTTPWSGnt5ROCQVTfxnSyadzOktEF8,26568 +sklearn/decomposition/_incremental_pca.py,sha256=8n_jLDFALWkCEQpoxfEQWxIqYTwIe4-fFdeDiInkeYg,16333 +sklearn/decomposition/_kernel_pca.py,sha256=qjcUB_H2l0ISq_IAtbopSrznivMtWBeQS2weNMNKER8,22651 +sklearn/decomposition/_lda.py,sha256=-BUAGurrOhpvRAyB8PavZmdjCte5wnnr8FUPajnxW4Q,34215 +sklearn/decomposition/_nmf.py,sha256=8so-4kLYJ07EXpQNx0EpyNPA_rGtPVBsr-SVL7a_IZk,81640 +sklearn/decomposition/_online_lda_fast.cpython-311-x86_64-linux-gnu.so,sha256=dC_BmRWM-vZZnzc2y1WwtmDoTBTM_VwMgopFYi0gFLc,124280 +sklearn/decomposition/_online_lda_fast.pyx,sha256=X5k9YTxrcCP37rlg1PZ55yAtbCzb6kUxPK-uPfho5ZQ,2848 +sklearn/decomposition/_pca.py,sha256=ZaLN7a_cK-ztuhQNIZRhIByHd67zUjXjg6siHz33DG8,34219 +sklearn/decomposition/_sparse_pca.py,sha256=sI29oTOrvAPRnfluWbxYuvzetcleBs7FeT6n7gzqihg,17986 +sklearn/decomposition/_truncated_svd.py,sha256=rtB9ZvTjmR39q_bQkMFJzexHqmIAmE8Wl7SQ_Cz-N8M,11748 +sklearn/decomposition/meson.build,sha256=Ou8NjxEeKMUenExdqgH-7ijrec-bkKI8Q_21ScKCjzM,322 +sklearn/decomposition/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/decomposition/tests/test_dict_learning.py,sha256=gBhm1BUZCgJNA0bgdYl9ZeVMcsbbUHLrZ5ecAzIWBtI,31546 +sklearn/decomposition/tests/test_factor_analysis.py,sha256=WOiKlwnFn4WqelwLa8ERB4dZsUbjF32h3-OTtRgRzZA,4032 +sklearn/decomposition/tests/test_fastica.py,sha256=BFYeGAA9DGiZb4tVFpKi__0QtECEFk47qkPQFZ4xOus,15916 +sklearn/decomposition/tests/test_incremental_pca.py,sha256=Oa2iwpd53fnOFsVX_U6-54AFjRDS8gs84alpmqMKwOY,16897 +sklearn/decomposition/tests/test_kernel_pca.py,sha256=8-LRps6Be2Oz2hb5Ic8Q9BfQcNLNBQZjIEOTQ8MQQWQ,21023 +sklearn/decomposition/tests/test_nmf.py,sha256=9H6yCXucWk1EPvyfpw8bEaeVQ4v_6sjWmCNt1JcTrUQ,32808 +sklearn/decomposition/tests/test_online_lda.py,sha256=5vInpaydegaKgKqf_lqogY1xKcvuMCZbki6kRDROj-w,16308 +sklearn/decomposition/tests/test_pca.py,sha256=Ur5-WknJyKhwWwdfW73x3wWLYJEZiuV4Vs4vQWTFnGA,41529 +sklearn/decomposition/tests/test_sparse_pca.py,sha256=Rp4GINs-dNGyVTP3xOqfDSNGOumkYJbj7dAl8qeZMjs,12216 +sklearn/decomposition/tests/test_truncated_svd.py,sha256=_09-jzEYo2Jt3pJVDdGjBUtbsadFS6ykhb3Ody0aC8A,7544 +sklearn/discriminant_analysis.py,sha256=ejOyzWzrDS96sCvVxBPqPrAD3VmF4Yo4BeppFdvEOCc,43914 +sklearn/dummy.py,sha256=PEwe63r8IBeYH-AY6KMyVlRr_CgkgM2D8Z3T41OhN-4,24390 +sklearn/ensemble/__init__.py,sha256=_lDhDQ3hmLtRDMl_eyH4TE4cOgIZ4AKFTzcT45lfNiM,1518 +sklearn/ensemble/_bagging.py,sha256=NSawN6FjqrtRkwCT39VyvcIBtmdtGsuYLcKllQ3ujXI,53102 +sklearn/ensemble/_base.py,sha256=GD2OaDO53ZEp0ZE-ToS3bXmS-h7ubjgZQ4GHpLur5CM,10604 +sklearn/ensemble/_bootstrap.py,sha256=IfPBzyDOZfHgSztQTAcdhvZ3k15-85ejN6AgBpFOg-M,2589 +sklearn/ensemble/_forest.py,sha256=kq4nPCioCxZIXCc1tj35_oJ2stUS5Pb_SMEfBTjBMTQ,119457 +sklearn/ensemble/_gb.py,sha256=25QlHX236ClkwYd9BN19e8pyb62Y-HcMlvw9Qn1Il1o,87833 +sklearn/ensemble/_gradient_boosting.cpython-311-x86_64-linux-gnu.so,sha256=sXvQlw7JrclC8JOGOA0FM5vOZzYUV3IbDGPGxtpqze8,102064 +sklearn/ensemble/_gradient_boosting.pyx,sha256=v9_53Yyd_nvBHr8WeXPEoCplCxNvWJgtadb3IYZlw_Y,8587 +sklearn/ensemble/_hist_gradient_boosting/__init__.py,sha256=CjfoMHKJd5hxBLWAbtW-lN4WAoAjhWpw8RwtcWmuX-s,246 +sklearn/ensemble/_hist_gradient_boosting/_binning.cpython-311-x86_64-linux-gnu.so,sha256=KxdbrFksscRPJ8XZl04vSPWdNGZS7r5MBVCPlgqn95o,80281 +sklearn/ensemble/_hist_gradient_boosting/_binning.pyx,sha256=aty1ETEOpONrOm8hzkktp9RLWaDs2BBUxmgnvE7yRd4,2831 +sklearn/ensemble/_hist_gradient_boosting/_gradient_boosting.cpython-311-x86_64-linux-gnu.so,sha256=h4ISADK3M6ZVUAtTbNTrr1owgOXwneAXolqZfJXqIww,84593 +sklearn/ensemble/_hist_gradient_boosting/_gradient_boosting.pyx,sha256=7gzORy3zVvRhUUdWgIohia7rojpuSvMGVlI4LuRXd7A,2070 +sklearn/ensemble/_hist_gradient_boosting/_predictor.cpython-311-x86_64-linux-gnu.so,sha256=4uAUs_KQ4MrbMzTBPU9VkDAVZOreFuuNX0yc6ePy57o,109297 +sklearn/ensemble/_hist_gradient_boosting/_predictor.pyx,sha256=jI4-ZAjkLUepCO7_QTsaLR2CfIrE4Gdundlfhh0IJ0s,9773 +sklearn/ensemble/_hist_gradient_boosting/binning.py,sha256=en0lV5Z-iFu5yeTsrz3tWF-ch09R6_GOimWeMD11c0I,16216 +sklearn/ensemble/_hist_gradient_boosting/common.cpython-311-x86_64-linux-gnu.so,sha256=YBSjJz_z0ZD1y8aCdImrU3lWI3xi41m9eGluyGnS_mw,49616 +sklearn/ensemble/_hist_gradient_boosting/common.pxd,sha256=KYUqXgj87UE8VS4onWyMkQzxe_1KAV9gkyxrjY1spew,1310 +sklearn/ensemble/_hist_gradient_boosting/common.pyx,sha256=KEFi77pWLNL8nhYQEnWbj0HnX0hDC68aawsa9iUEyVo,1827 +sklearn/ensemble/_hist_gradient_boosting/gradient_boosting.py,sha256=gmGeOuia8WJd6h1CRdDUqeLrheDQNHkPvYmhu1HX6ZY,92859 +sklearn/ensemble/_hist_gradient_boosting/grower.py,sha256=GNjg7BTZ-TMpZStZreZ5XjTqFPk756mFc-6l9V788mM,32849 +sklearn/ensemble/_hist_gradient_boosting/histogram.cpython-311-x86_64-linux-gnu.so,sha256=IpShQYqY98LIsLBHBNy0zGA_lFJ_dSyQSGd0LVi1qBA,183505 +sklearn/ensemble/_hist_gradient_boosting/histogram.pyx,sha256=87aMXJZulvHahB3U2Xa0XXxqKvDvp13Cw8cCgkQxAzs,20816 +sklearn/ensemble/_hist_gradient_boosting/meson.build,sha256=KGiEcsqlW3nefaPVQ0HPMCk_XIxnMwc2C2vyqWgjWjg,1200 +sklearn/ensemble/_hist_gradient_boosting/predictor.py,sha256=y5RQjQ59pu8HpIHM6Z5bNfcg71igsNYEvkzKW9xYfak,5122 +sklearn/ensemble/_hist_gradient_boosting/splitting.cpython-311-x86_64-linux-gnu.so,sha256=s9hKPga-aDDHcj6CXWNtnNB99hhFFuZ6-q5kSH6asmY,220361 +sklearn/ensemble/_hist_gradient_boosting/splitting.pyx,sha256=3CN8THzjblxmY9PVh-LRmcSXgfXO_MSUZEFx7A5x2kg,52417 +sklearn/ensemble/_hist_gradient_boosting/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/ensemble/_hist_gradient_boosting/tests/test_binning.py,sha256=yZqp7lDpkGkMzPy6wTsOsDW8OQZ9IZHBhoJkv_W441g,19847 +sklearn/ensemble/_hist_gradient_boosting/tests/test_compare_lightgbm.py,sha256=0uPgr-l7XqmjaD_yw_FUOLouQbWFDd62wuAPRtYxjLU,10109 +sklearn/ensemble/_hist_gradient_boosting/tests/test_gradient_boosting.py,sha256=QGqk7V5nGZ5CHRUIkIIp22Nd1FO1v2jRlbE4Y5cVh1M,63623 +sklearn/ensemble/_hist_gradient_boosting/tests/test_grower.py,sha256=EbM52BPe-CmqAr9tbOAO9AQntXZonAyyiY6JIm2uPz8,23153 +sklearn/ensemble/_hist_gradient_boosting/tests/test_histogram.py,sha256=PBoacgv-6rOI5lTpzCyaafC9eDvyA6tb94RnDw_wLhs,8681 +sklearn/ensemble/_hist_gradient_boosting/tests/test_monotonic_constraints.py,sha256=rzrommClPNGQ_9RHzSTwmgDbIN1o1OuJ3oQi9yZyK8g,16937 +sklearn/ensemble/_hist_gradient_boosting/tests/test_predictor.py,sha256=C2TI4WeeX-hdQvRPh8Ebjb9_bwtKiCRNdcDrPfx75f8,6319 +sklearn/ensemble/_hist_gradient_boosting/tests/test_splitting.py,sha256=nkX5rAlTeO6tPR4_K4Gc9bvViPu1HUboA7-vRdiTETo,38639 +sklearn/ensemble/_hist_gradient_boosting/tests/test_warm_start.py,sha256=3Q_3ZhKf94uvmADlNMj0Vpyp7gqjDd1czBzFW8pUuAQ,7933 +sklearn/ensemble/_hist_gradient_boosting/utils.py,sha256=tR_F_bRMRQpVSh7EMQjDynUJKPKkKOFMeh1m50Tpdv4,5571 +sklearn/ensemble/_iforest.py,sha256=m-SPOznBe9nCfVegHkqQNApV2nAAS0IwOYMz6bAWTtc,24517 +sklearn/ensemble/_stacking.py,sha256=acgv28CPwthZRitxAJ72XDu9kmdjcbLCeUuUHuBZFs4,43621 +sklearn/ensemble/_voting.py,sha256=5Jt7Ck95pezebBTPeUJ1IDJQRjPVfa4voQMC0ucpy1s,25269 +sklearn/ensemble/_weight_boosting.py,sha256=LB_RkUiofDpIcY8-VnzRypWj7AY5gr38HxtQqAFDfMA,39704 +sklearn/ensemble/meson.build,sha256=7nR6oq_djKOBo-7Yxc-UmB6uWVai4w5By9i10tBX4hE,224 +sklearn/ensemble/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/ensemble/tests/test_bagging.py,sha256=5N-E3X3HZj7QNHdfmBwehKSpCovbfGcsIWIk9669hSo,38900 +sklearn/ensemble/tests/test_base.py,sha256=dCynI18UuKx7HpGwnUSjfUR2GlTfhGRmO_UA_-kDu6A,3667 +sklearn/ensemble/tests/test_bootstrap.py,sha256=qPlsSdpBebnSVppr4ipGaDAAJ-BdVTyYceE3bjdOSr8,3042 +sklearn/ensemble/tests/test_common.py,sha256=Hjogm5cgVMCp1kbxyRhHdc125s2O98nYYFFu5TDJtZ4,9191 +sklearn/ensemble/tests/test_forest.py,sha256=8pPEhuhgPlkn6mq2x8w0dCu_MHDb0NIW3nQDvOpokLA,66799 +sklearn/ensemble/tests/test_gradient_boosting.py,sha256=D8hnKPaNL3umcb3oIlmvr8v9OClTNokXuh9ul3IBjyo,58403 +sklearn/ensemble/tests/test_iforest.py,sha256=_fhFAYNm_dTEiDkeyk-kFdOgo7Bj1wFqd7zqBakd97c,13637 +sklearn/ensemble/tests/test_stacking.py,sha256=nx5nvPaGK0E7krxV_HMDdb_Cp1zmo71d0Ad8vE_1H2g,33530 +sklearn/ensemble/tests/test_voting.py,sha256=QPTeIsw6ummEIiQaUW5Coh4o8PaFQnQoSUVMKHWaqSE,27637 +sklearn/ensemble/tests/test_weight_boosting.py,sha256=VwRGOpXNlGM1e0HcOHAI_071DxOgDB7asNZZt0LFsaQ,20710 +sklearn/exceptions.py,sha256=CaaFS4DVbqhqUidiA-cMvTP6DR6qIFmEUzuiMS5HDec,7703 +sklearn/experimental/__init__.py,sha256=0SSV8qXhFfA8-T9zvuWasIT8bNbPXLUX4ZQZp0CoDzk,305 +sklearn/experimental/enable_halving_search_cv.py,sha256=ckZyUC1__tQEZSnEv_NNDRZli7U16wwPhE3CSJ5I5pA,1301 +sklearn/experimental/enable_hist_gradient_boosting.py,sha256=0vehofwAKeWQbeQO0B0E0lulIWUk1q4pwBCMFERSm3Q,826 +sklearn/experimental/enable_iterative_imputer.py,sha256=OQMyFin5BBguxNtiPL5_r23c191i8XVtnkDv67MHgwQ,779 +sklearn/experimental/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/experimental/tests/test_enable_hist_gradient_boosting.py,sha256=cAFugPf0tYSd-P2-GlcfvhG7YnKlfMoqE8Pff7yXG-4,672 +sklearn/experimental/tests/test_enable_iterative_imputer.py,sha256=LWtq99MTXXga2dq_ZcB0korId_7ctVxKtZLrFNZvFns,1689 +sklearn/experimental/tests/test_enable_successive_halving.py,sha256=MVt6aApWKiR3VnVRnY7GEoQdI8w-f2M--w60vS0B5vA,1896 +sklearn/externals/README,sha256=GFbJH7vHxxuzJLaVlul1GkfwjREK64RyEXUCWL1NSxk,270 +sklearn/externals/__init__.py,sha256=jo7XxwlsquXvHghwURnScmXn3XraDerjG1fNR_e11-U,42 +sklearn/externals/_arff.py,sha256=YXR8xgF1IxyugQV70YHNjmza2yuz86zhVM1i6AI-RSA,38341 +sklearn/externals/_array_api_compat_vendor.py,sha256=Gb7C65qVPo5gbKKlpq4jHtXWkgsN0wIrTQAaFBbais0,198 +sklearn/externals/_numpydoc/docscrape.py,sha256=yZMmCXh-wufQkSlDsApXiQkLTVJNDr_n-ffscZSLpkA,23691 +sklearn/externals/_packaging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/externals/_packaging/_structures.py,sha256=Ofe3RryZqacr5auj4s7MsEylGigfeyf8sagFvK-rPv0,2922 +sklearn/externals/_packaging/version.py,sha256=ACIAeycEss9ydd0ulNOvtZzXAwdimyWoDUP_j-IyCvc,16133 +sklearn/externals/_scipy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/externals/_scipy/sparse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/externals/_scipy/sparse/csgraph/__init__.py,sha256=GMAcZXBWt9Dp0QEOeCsQglt8CWB6_stqr7Wf_LfH0tE,34 +sklearn/externals/_scipy/sparse/csgraph/_laplacian.py,sha256=OYKtxmkzBJrTdgMcDBFNzwYQ1EsGMByn9zNLWHkCH_8,18194 +sklearn/externals/array_api_compat/LICENSE,sha256=T_2Xjj-hjQWNmMZncc_qftY0qvcCPPlhK4tV7umo8P4,1097 +sklearn/externals/array_api_compat/README.md,sha256=YjsmsQ3VNuGPaD7I6a_lvqGBVNBhm-k5ty-yWwIjjRY,67 +sklearn/externals/array_api_compat/__init__.py,sha256=XbhBzRpMRAKRuPnzAfZD4kITuXxjgafIDX4veTHtO-U,992 +sklearn/externals/array_api_compat/_internal.py,sha256=tYJkaxIxVxELYvo9JK2UgCCJBMw01JiVmomTIjVpM5U,2072 +sklearn/externals/array_api_compat/common/__init__.py,sha256=4IcMWP5rARLYe2_pgXDWEuj2YpM0c1G6Pb5pkbQ0QS8,38 +sklearn/externals/array_api_compat/common/_aliases.py,sha256=0fYL3vEwX0kRdpscJY3l7Q37kHfAlzmxUulkI-e9Sws,18988 +sklearn/externals/array_api_compat/common/_fft.py,sha256=ckCR2uHtz0iaOkcuvqVunhz1khIdxQNKuVU0x1bfrq8,4669 +sklearn/externals/array_api_compat/common/_helpers.py,sha256=1WFBA38HVEQCm9yiy3RtpxP0wIxmi2sfmYhyqgxMNBE,32556 +sklearn/externals/array_api_compat/common/_linalg.py,sha256=jsz7PpwAD2QeqJQCnYd3LwcTLm6YMjyla9_Ol70ifF4,6805 +sklearn/externals/array_api_compat/common/_typing.py,sha256=gFg3AvkvXQ0VVeiZtjejpaU5V4oCXnRlbGzNmHpLPFs,4498 +sklearn/externals/array_api_compat/cupy/__init__.py,sha256=u_lgOQrp7-pI24bZp8fNm4QfrlmcocpFL2fWX-2Tv_4,736 +sklearn/externals/array_api_compat/cupy/_aliases.py,sha256=eoR2lVB45fCMsLtGm3PnczOivUxISxlFJ2K4yV_aiZM,5122 +sklearn/externals/array_api_compat/cupy/_info.py,sha256=g3DwO5ps4bSlFU2pc_f4XTaLrkCYuSDlCw0Ql2wuqM8,10125 +sklearn/externals/array_api_compat/cupy/_typing.py,sha256=L6RUNFoy-ZwLizEqCSqHN-6GC5chKYfQL8xW4FVksKA,607 +sklearn/externals/array_api_compat/cupy/fft.py,sha256=D5HJ6rclFA7-CO_NleuCZQy2Lz7InkFuE8CHTrxrXc0,872 +sklearn/externals/array_api_compat/cupy/linalg.py,sha256=G5zEzcmqVRv8Bs_LaOXd_1QkOvSQn4c3giYvCEtt3w4,1465 +sklearn/externals/array_api_compat/dask/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/externals/array_api_compat/dask/array/__init__.py,sha256=2hRAArKNn6Zh67QpQ5yCjb3cTnJsom_85ShCFDZdz9A,677 +sklearn/externals/array_api_compat/dask/array/_aliases.py,sha256=eJkzntr9_0ew0SFBeAEe3zd1vjdsXb7wpP_HOqVl_GM,10432 +sklearn/externals/array_api_compat/dask/array/_info.py,sha256=SSClqGR_1AeTOhUPvryTpUJb4ke5jykHB-LeXw9gQmA,12241 +sklearn/externals/array_api_compat/dask/array/fft.py,sha256=dJEAXxGtfP5Z_-oR8GNuiMwHG1-Hbwax02wxmN95QjQ,333 +sklearn/externals/array_api_compat/dask/array/linalg.py,sha256=-YEIM8c0I1CUidb0DTT5QMJJRzo87N2woPSs0XI8Xh4,2139 +sklearn/externals/array_api_compat/numpy/__init__.py,sha256=S5-VzRtXS6re5bOy4xtV3RfYKk9AQwJ74vMhiiQKxJY,1064 +sklearn/externals/array_api_compat/numpy/_aliases.py,sha256=3FhudS9W4PofYTtsf6vTHEtTC_T_i2ecUE4eBMLyqrg,5666 +sklearn/externals/array_api_compat/numpy/_info.py,sha256=C7iKLQU8Q8AJCkf_ADv_UWgum8UdbagewvKGL_LmQ3I,10793 +sklearn/externals/array_api_compat/numpy/_typing.py,sha256=hXWLcYHogQcHzK1tR8B9difxm3UFlHhUUaXBHydqpyE,605 +sklearn/externals/array_api_compat/numpy/fft.py,sha256=gx6R3o_ehnMWpsvBI2y0mBNxwZP9KFLTuQ9uP4eSrMk,714 +sklearn/externals/array_api_compat/numpy/linalg.py,sha256=tZHdXpZnmWbsbeGYudpDEzDw0u65tziwya2JJd9m110,3914 +sklearn/externals/array_api_compat/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/externals/array_api_compat/torch/__init__.py,sha256=8ZAu0FQ6jpm6kVlNXD6UDIigBnyM4nC_LJTaAHz9QU0,633 +sklearn/externals/array_api_compat/torch/_aliases.py,sha256=PRCqlNel5pCaGmfj_FxcCEr_zXvZbdBBKoagerPQObs,30503 +sklearn/externals/array_api_compat/torch/_info.py,sha256=-H2xD9z9SMf3GjIOW0jeRTUOvh4s8E9p9u_4LqawRZM,11889 +sklearn/externals/array_api_compat/torch/_typing.py,sha256=-uCkuTie1g9hb4vwPLK9eEnir9Zp67wAhrfaI_o-35E,108 +sklearn/externals/array_api_compat/torch/fft.py,sha256=ijW1aJaOys2EMkF04hu3vSMQMqd2ZLUpLvAZUQZf4Lw,1833 +sklearn/externals/array_api_compat/torch/linalg.py,sha256=x-qGeIVVbNsmk9ZLSV4JHWZE4WB8HqGeHlQrhNPMGJs,4560 +sklearn/externals/array_api_extra/LICENSE,sha256=WElDmP4Uf9znamiy3s1MCM46HqI3ttZ4UAHBX4IsbtY,1097 +sklearn/externals/array_api_extra/README.md,sha256=hujBWt3i3o5AkT4rUqbVle7qQ3LhbaSwl1VYPT33rig,66 +sklearn/externals/array_api_extra/__init__.py,sha256=oXtdxF470PYOZabBWTIPWmxO_buuMRBd8VUiA-I6fag,754 +sklearn/externals/array_api_extra/_delegation.py,sha256=_OG8Oll2JnvJOezUX62-rpn8qOBdHDiKqiZ8rb_RM_4,11270 +sklearn/externals/array_api_extra/_lib/__init__.py,sha256=Mht4YV9Rpkzg5kORPlgjOOXk4y7bvdngowS6KGSHqNE,36 +sklearn/externals/array_api_extra/_lib/_at.py,sha256=JOZX0nw6UpyRKZIZRQDb1w_YSGeJ-4v1vJFdrISGI_k,15327 +sklearn/externals/array_api_extra/_lib/_backends.py,sha256=QPZvkbtVtcwr5WDseaTcTo0z6u1HqdCVd67G5ndcPwE,2168 +sklearn/externals/array_api_extra/_lib/_funcs.py,sha256=_2k3P2VqdvSHWSMfBd_XUfpb70d8klgcaGOpHPXCJoM,32635 +sklearn/externals/array_api_extra/_lib/_lazy.py,sha256=RE1DzMqj0j7GpY6GKSLyBuTL9vQaWkLSn7h-iZ9eVd8,13811 +sklearn/externals/array_api_extra/_lib/_testing.py,sha256=GtIc8GIDEqw90nn5g72gt71flj5-1BP29ZKzwoduoSQ,10290 +sklearn/externals/array_api_extra/_lib/_utils/__init__.py,sha256=8ICffM2MprXpWZd8ia0-5ZTnKtDfeZD0gExLveDrXZs,49 +sklearn/externals/array_api_extra/_lib/_utils/_compat.py,sha256=4A5a_S4qo88_H8LKmQal2e_1PVQAY1eInKDZsCgPNYU,1812 +sklearn/externals/array_api_extra/_lib/_utils/_compat.pyi,sha256=6QexmJx0rPSEtvVQAt2HbS87F8njDMsD9UhyIJsen8c,1720 +sklearn/externals/array_api_extra/_lib/_utils/_helpers.py,sha256=f3A28XBntLZxiXdXA-urUyRnS-JpD9jsT2EO7J0BldE,18965 +sklearn/externals/array_api_extra/_lib/_utils/_typing.py,sha256=0NSYWpdXH58Dx9T7HsNPM0NW2iPlTZS6xOrR-5tFOYc,228 +sklearn/externals/array_api_extra/_lib/_utils/_typing.pyi,sha256=tfmj1bK5a1RWFc8qXR10FhLlXBwzyItFrN0zcXTWntM,4665 +sklearn/externals/array_api_extra/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/externals/array_api_extra/testing.py,sha256=s2BfjND8f9GxTg9eDChRsrjsfdlJ1wyNKM9RoU865WI,14653 +sklearn/externals/conftest.py,sha256=8wfDBd_pWHl3PsD3IOGeZT4z0U-q2895fYvApMzq5gg,312 +sklearn/feature_extraction/__init__.py,sha256=y8w4moNghARZrReLo4duLEk_hT6u46bt0gbforodW28,499 +sklearn/feature_extraction/_dict_vectorizer.py,sha256=LkANHHbLfsaMluUBngW36kymRu5ZKoclRssaFVKf1bo,16065 +sklearn/feature_extraction/_hash.py,sha256=iOqnPiQXbX8GTtat5zdFivdipZOx0XzvLqSZunN1v9s,7909 +sklearn/feature_extraction/_hashing_fast.cpython-311-x86_64-linux-gnu.so,sha256=4C0SAqGjnmsRc__Lvt5gwmSyPAviby-XHkhVwfqZgak,90824 +sklearn/feature_extraction/_hashing_fast.pyx,sha256=fhv13HOJsuY7y3ongPBE7uaDkOKgHaOUFg1kZ10uyzw,3045 +sklearn/feature_extraction/_stop_words.py,sha256=ZEfwEZHSNFr0id2pPdBlZq-E9j6VFQM8S86gubzOweo,5725 +sklearn/feature_extraction/image.py,sha256=ZdFYTJ8JNpETTByhb2OWtIz6-U7emTTSWO3hGmAWSGo,23680 +sklearn/feature_extraction/meson.build,sha256=5D4WuiUPvXvqJcQj-yqpkmQ2yxJ9yVr6T-_Q5Gk0Tw8,192 +sklearn/feature_extraction/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/feature_extraction/tests/test_dict_vectorizer.py,sha256=sJfcwyId7Xjrs3aakS-HjkPvt0dzuVLqIuCQmpxnN5U,8256 +sklearn/feature_extraction/tests/test_feature_hasher.py,sha256=GahAkPTFgb0d_wC6clnMl7XJD0qgH6Lq_I3UK55a2E0,5734 +sklearn/feature_extraction/tests/test_image.py,sha256=fSAmxmp-IK4hAo-aFJjr6kQxtzb9bLstUGtuNZgA84Q,13183 +sklearn/feature_extraction/tests/test_text.py,sha256=BM1Y0Vak4JTdSsDKvdSAL1TbaMMz8KeVP2h9QW6EFhs,53293 +sklearn/feature_extraction/text.py,sha256=cvTiA2upPEZvl583IcwmDHI22raARx-nImFtYBm4mtw,77448 +sklearn/feature_selection/__init__.py,sha256=EZwpq2ZXihVbFqpQppFmpTVQiGZg8b0LFao5KHQI3YI,1316 +sklearn/feature_selection/_base.py,sha256=fzjEZ0fqtT9oGRyQkmX9z9GhiSEMr8f3z24v7ReCzbk,9770 +sklearn/feature_selection/_from_model.py,sha256=_KM20g58bFxepTzhsiciVJiWISkz2wTs4Ku6oT_PPR4,19084 +sklearn/feature_selection/_mutual_info.py,sha256=RhB6Z5jTlwZQLgztRRGatqgF4bqsrRPt1GYPVAEgeDg,20016 +sklearn/feature_selection/_rfe.py,sha256=3yNJvvOZinY367y0Th5Dv68HF3BryzIbbXlUTW8eQ7c,37055 +sklearn/feature_selection/_sequential.py,sha256=5KTmoj3LlF96R6HG4PFx3hsrqb3Glkq9910QR-fwpyM,13977 +sklearn/feature_selection/_univariate_selection.py,sha256=gWCyCrXCEuhq3uZpzFGIIJCnbrOJfhgiuqhbGE7TTfQ,40796 +sklearn/feature_selection/_variance_threshold.py,sha256=VpDylkMbA-IyBVN4I8A4x5GOzPWp-iykQE9bwIIs-dw,4688 +sklearn/feature_selection/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/feature_selection/tests/test_base.py,sha256=ythpS8iRYjFm3VP581ikkgPobz12JrlTAYBTATIKKBU,4832 +sklearn/feature_selection/tests/test_chi2.py,sha256=c6L3cs9DYulMNUTjnZJo7VURucjhUHLYzG2EaRE9N1c,3139 +sklearn/feature_selection/tests/test_feature_select.py,sha256=59hWeQqIEOZJGcE5IL5y3jMnlBwFbpuwH855OKUgpsA,32507 +sklearn/feature_selection/tests/test_from_model.py,sha256=6Kr_6AuOQs1U5r7XMTGSVty7aLtVgn8Eg3LeapsNcXs,23601 +sklearn/feature_selection/tests/test_mutual_info.py,sha256=-UfRnEIKDgD5XJKNOuPeaQydXzrsWC83qOTkYCMj1Ts,9852 +sklearn/feature_selection/tests/test_rfe.py,sha256=G0_ESXDO5s5DFPqpGRsY1OPb2GSxmUY2i1CKVISK-J8,25995 +sklearn/feature_selection/tests/test_sequential.py,sha256=9Z-naJRDVboKShzMI4xcWekQjwktpUwKT2hmaalAS3Y,10906 +sklearn/feature_selection/tests/test_variance_threshold.py,sha256=tKaSBkRgVBzo3xC0lT6nLNNzKW4M-5t_sAFJgUmr--g,2640 +sklearn/frozen/__init__.py,sha256=6Kjmv4w_cuPIWiDo0ILEvvGpDvYzyAXQxAurASg49K4,162 +sklearn/frozen/_frozen.py,sha256=zTrLEXkqxUo_MV53cCvJAeQI8VbrIz5VVAtMW0Hco-I,5015 +sklearn/frozen/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/frozen/tests/test_frozen.py,sha256=RVDaloo4F0EXQD44f8biHTREL_IqywKsipodlfECPm0,7102 +sklearn/gaussian_process/__init__.py,sha256=3kwRtbsPBi72Y0BobgTamu2HP-wVQbCkMDpchVlZQSk,401 +sklearn/gaussian_process/_gpc.py,sha256=kGKPymiyaqwsT1cwW2MCxUmVLThz5mUSCf_K-yyV_z0,39387 +sklearn/gaussian_process/_gpr.py,sha256=0TWsweOxhvCwgppv447Bh30QT5uZftr7MwohYmvdX00,28281 +sklearn/gaussian_process/kernels.py,sha256=6YUyZxTziremx8vhCp6zQJ_c7BmMFwjeIZT-l5v1T3s,85311 +sklearn/gaussian_process/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/gaussian_process/tests/_mini_sequence_kernel.py,sha256=YpD-vtJFSVdzVmJxHDmEdFGl6cOQ4J98mLpjFCFThys,1571 +sklearn/gaussian_process/tests/test_gpc.py,sha256=reNkHK4OtVCMqxkrd9LQhUzxsQiw024P5ZVhDOLXNIU,11927 +sklearn/gaussian_process/tests/test_gpr.py,sha256=Rbv4a47GoNlgu6-GCItW7VYTyG4b4dnP-LWrxsoPV6c,30834 +sklearn/gaussian_process/tests/test_kernels.py,sha256=izel3Fru6VdgNRGHxnwVqmVENxy06sYjDTF03iRI9mQ,14492 +sklearn/impute/__init__.py,sha256=aJeaxhBdoxR5WBF0xq-5qHn5GXHIS9xuABBI7P8V2jI,1073 +sklearn/impute/_base.py,sha256=hjzpYg46GAiSevnJWYme1xPdhcuETCYfiCaAMth1Yco,42890 +sklearn/impute/_iterative.py,sha256=HpgRjKiVJVvi9xbg4Uan-WojdhMlBbjoBauxOX0Z5UI,38719 +sklearn/impute/_knn.py,sha256=BLLS--modo-F2YQdPXuhQkkic7W2bWp8YnSGajX-Bys,14967 +sklearn/impute/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/impute/tests/test_base.py,sha256=aIG6RV5BXAUbNGsRawjKVGRfUNMnSxQlRMfRWnfaXes,3361 +sklearn/impute/tests/test_common.py,sha256=gaZpPMoNFomagXQxrt2zkBhJr7vQOm7UxNeRuLl1Hno,7837 +sklearn/impute/tests/test_impute.py,sha256=tryHf0v-Nthei7a0Ecnvv3HjbKkBnWMp2nI9x2wPl_o,66048 +sklearn/impute/tests/test_knn.py,sha256=4FL0dBxzW_FooUpFuzgR6uYDH2Y4l9pLGJ1zkgy9b4Q,17540 +sklearn/inspection/__init__.py,sha256=Qu5Qht03Sdi00XdUowCHz0KrJpedeqG_vtaZjFrR3Nk,557 +sklearn/inspection/_partial_dependence.py,sha256=OR54nlKi4a7Z9Tli0WE2mkIUETrdqbqghF7o5WaRcnQ,33267 +sklearn/inspection/_pd_utils.py,sha256=m01ubgd8W-ThbL95ATj7dRWK6nACermQBc0MrEPPQr8,2218 +sklearn/inspection/_permutation_importance.py,sha256=dmrURulYEFUxN41R8_6YilDlhTd0qNYoEPj_mOyk4BU,11431 +sklearn/inspection/_plot/__init__.py,sha256=vKm4xxqJIfK6Tk6By-YU03YcE6pR1X1juFrOsacfZjY,79 +sklearn/inspection/_plot/decision_boundary.py,sha256=Tqml4-Dsm4uRv08y3lu1GwpoarhAZS246dwXRQgpAZc,29725 +sklearn/inspection/_plot/partial_dependence.py,sha256=u0BkIcj_fmsXgcm3r9RJZ3x9ek73F7pM0ZS1c54C2xw,61465 +sklearn/inspection/_plot/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/inspection/_plot/tests/test_boundary_decision_display.py,sha256=a89Rs72W1Ne7gOYkjSmKoCQ9szDHSgcK6mUYhB0PcAg,33751 +sklearn/inspection/_plot/tests/test_plot_partial_dependence.py,sha256=bgbdQsV-xE2dgZweNP4asueT1VVnfjvnYrH-Rs_bdrY,41257 +sklearn/inspection/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/inspection/tests/test_partial_dependence.py,sha256=rPYcYXcDCyU9GNlZdJlAtr_MDCceLE6xx5PSfWTFW8A,40751 +sklearn/inspection/tests/test_pd_utils.py,sha256=4ftW709hRdXLjaE9J-P3FV9F-SqSgUOD8eFiyrZla4Y,1637 +sklearn/inspection/tests/test_permutation_importance.py,sha256=RsehSRAd2UG1_9F9SnAdfgH3YIrDgReKE1NA0FY39Eo,20299 +sklearn/isotonic.py,sha256=OQ5X-xdx37BNRsf09NtWYrtFMGWkT1mxlguVxJwQLKU,17387 +sklearn/kernel_approximation.py,sha256=szvSIJ7rhIyY3Hh1-1qJ9guhVAWW2KyyLAze1xnWTKc,40798 +sklearn/kernel_ridge.py,sha256=ceoe-Kd75agaQbtY28sukoPlRTdXKJAwikDmW0R05sM,9263 +sklearn/linear_model/__init__.py,sha256=D1LDJOuVciH5TbBsaiVn824N9f91Cji3rBw8oDs8Nyo,2766 +sklearn/linear_model/_base.py,sha256=WNgaP7ckBZ2y0qg0-XzgZsUSL2XeBxQtIFCWIrm1gNA,31056 +sklearn/linear_model/_bayes.py,sha256=-NVexIPo3965fvOJ4IEVXSfRPpWUk58LXt7_1ow1v3s,29065 +sklearn/linear_model/_cd_fast.cpython-311-x86_64-linux-gnu.so,sha256=CfOqOWp5y062TFWztI8b8yMe7ikciguptbi1gD8SIZc,442328 +sklearn/linear_model/_cd_fast.pyx,sha256=4_t_r212U92HqcjA-nUxtwfkLAzV-enEN5MRCpXnyb8,64285 +sklearn/linear_model/_coordinate_descent.py,sha256=l3C3yw8mjbTdOwcNlsU0xXf2fF4_o2ifN0BliB8Bs9k,120297 +sklearn/linear_model/_glm/__init__.py,sha256=fURIoi77X6Oj0-MVMdxE-6uO_fhNX7AbrxElXZUIH3Q,343 +sklearn/linear_model/_glm/_newton_solver.py,sha256=mOrGi2PI4zOdEPekxtouebkcwCAB_TImSjmcQya94Ig,25747 +sklearn/linear_model/_glm/glm.py,sha256=RS7J7oexPDKdj-xIDw54pV1yUjTYzkso1EIAsCPG6zU,33220 +sklearn/linear_model/_glm/tests/__init__.py,sha256=vKm4xxqJIfK6Tk6By-YU03YcE6pR1X1juFrOsacfZjY,79 +sklearn/linear_model/_glm/tests/test_glm.py,sha256=7DCT7mfVby4DC4myS92qHP3RD7mWSN9A25owWKyMd-0,46365 +sklearn/linear_model/_huber.py,sha256=P3QPxGnrjMEfu41qWvbVRH_kWazTxAdGeqxCUuhRb4E,12752 +sklearn/linear_model/_least_angle.py,sha256=XKamx0Vvdxgb-XJjzwu-iWxnu1RVUguy1rGbsmIb6as,83132 +sklearn/linear_model/_linear_loss.py,sha256=3ivQTbmGmlQyE2X1ll9pFnTiJ52KJ117iRpzDCk0Yhs,35681 +sklearn/linear_model/_logistic.py,sha256=F6J1nl8opItnFRNMfspq94MzVhu6-S-O6_CXENx0MhE,100507 +sklearn/linear_model/_omp.py,sha256=RvUXUhxFJDvlkks3h3XH3OqFJUQIP2oZPhy-AUhFYlM,38357 +sklearn/linear_model/_passive_aggressive.py,sha256=DDv20KC3lGPzTpvBk9mmwLu5R3f-hfNCSljDgqW4o9g,21164 +sklearn/linear_model/_perceptron.py,sha256=eZchiIJXBBYWZ9t8HI9jqoiPuy29W7kgi_tYawXep4o,7593 +sklearn/linear_model/_quantile.py,sha256=CXoxHP6lVoMIZUlVyHAplsTxfUg5cJYIal1VJY9EKek,10552 +sklearn/linear_model/_ransac.py,sha256=3Xvszieqap3XwKiFq7Ggp-9bA4WhIl87Hl0gTN5hI8s,25782 +sklearn/linear_model/_ridge.py,sha256=CcHPrKKMEkmC1S7kJY-QIt4_JP2pki865BzINNY-3jE,109185 +sklearn/linear_model/_sag.py,sha256=o_VHoPlt-prXodgIhSuiMFxH_1vFkuwc2wusa1xxGEI,12350 +sklearn/linear_model/_sag_fast.cpython-311-x86_64-linux-gnu.so,sha256=Yu3r836yAebaiHxRkxWrIyq9I4k7qIgkj-fh8iiv4H0,147064 +sklearn/linear_model/_sag_fast.pyx.tp,sha256=suqPF_FAJlrZh0itNk5u8f4vkQn9ddPzVwnFPlOD7pQ,24289 +sklearn/linear_model/_sgd_fast.cpython-311-x86_64-linux-gnu.so,sha256=W5OvXbor0aHaOty2VSWRugaLS8viAmCW_BZQW8aqrno,219248 +sklearn/linear_model/_sgd_fast.pyx.tp,sha256=GpfAzZTnK82EwT95994oOuGwV-Xg-CCbd0u9LAhZwKo,23297 +sklearn/linear_model/_stochastic_gradient.py,sha256=_gjs3NNCdjBoiRiExy3aUtpu_yYHn9S746MMPZAX4Ho,94072 +sklearn/linear_model/_theil_sen.py,sha256=9I3XXDbNzKxm9L_9xBF9Ic2KF4RMDFCqOflnxerjMfU,15729 +sklearn/linear_model/meson.build,sha256=cPjO_Gsfe67Uc-6yorjl7WuJC2nCu1Y823LERhE4wG4,930 +sklearn/linear_model/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/linear_model/tests/test_base.py,sha256=yzM0fo7Fq69XUHfQ09NnSuQRxbslptiQKYXch9B2FcA,31182 +sklearn/linear_model/tests/test_bayes.py,sha256=vFEEvafgQeerUSJaIb25yy9NhUMApLpCiHh-2jdRsnU,11933 +sklearn/linear_model/tests/test_common.py,sha256=plbMAsEQzhhPxO2Bv6fenLiGKNyiQyN_-rLXsRg_jjI,9749 +sklearn/linear_model/tests/test_coordinate_descent.py,sha256=uRMWNq50s2WVqoQdW9R6fbYaZUJp7adhfxk7JXEgkyk,62948 +sklearn/linear_model/tests/test_huber.py,sha256=au_AulAuqWt1XACGbWr5_1tw12M_g7Vi2U3LxvyflgM,7615 +sklearn/linear_model/tests/test_least_angle.py,sha256=8avmQULtpTpoA5ZfknpwoCH3tmtK7oTN-QujgY0rHdk,29667 +sklearn/linear_model/tests/test_linear_loss.py,sha256=2zMWRVfYQM_sVuvzTOYeG9Kl4N9XhyxSSbACCU_BUx4,17912 +sklearn/linear_model/tests/test_logistic.py,sha256=plOqAwCVtZj9Oifkr1n1nkrtIaJlG72fqmt3Mh2HEl4,107435 +sklearn/linear_model/tests/test_omp.py,sha256=ZG03dTxyJGmeajIo4fA8SN4Kxwz_rzcOTeEVkS6g3HY,9344 +sklearn/linear_model/tests/test_passive_aggressive.py,sha256=a4aVLNGviBgV7nDF3qvz7t1liG0GOUSYptDuvpq2kVY,11817 +sklearn/linear_model/tests/test_perceptron.py,sha256=rsNfXmS37bAZeZ04kRNhc2PXr4WjjTWDaxW_gNmMCkI,2608 +sklearn/linear_model/tests/test_quantile.py,sha256=JiOfB1V2NwuWeK-ed6hKmOpHPHj7CNEHp_ZZuGt4CZk,10689 +sklearn/linear_model/tests/test_ransac.py,sha256=ZvoKWzgJ4rK-2jdZpRFG-VtKbH-5Wz46G6NKyALq1Ug,16794 +sklearn/linear_model/tests/test_ridge.py,sha256=H-eNPdSQpLjvd3hrMwj2wvn3rXxygVe0twRzl-ifzUw,92988 +sklearn/linear_model/tests/test_sag.py,sha256=EkoGf0liH7yZ5Fc9El-rKQNDxIBy8Nif0cMel3_SqM8,25872 +sklearn/linear_model/tests/test_sgd.py,sha256=alnpZl3pyXO33A5LELMJYpauJDEggO6qZTBRNvXT5Eo,74756 +sklearn/linear_model/tests/test_sparse_coordinate_descent.py,sha256=K4gg76PSv0t6zlNP4BVMXuiluoCiUrwUK5Oz0KDrbG4,12983 +sklearn/linear_model/tests/test_theil_sen.py,sha256=oBkrrE2ZdHLA7_lRY95ikDKzrbV06RQF3eDTPXMHHkM,9938 +sklearn/manifold/__init__.py,sha256=awatz9ijXFIslLjvKtY8m66eD55AlniE8hvsKdkKQQc,735 +sklearn/manifold/_barnes_hut_tsne.cpython-311-x86_64-linux-gnu.so,sha256=S-71DJXxCAqksZuSio_6wMeeWE6txZsdV5rGBkT_Etg,113713 +sklearn/manifold/_barnes_hut_tsne.pyx,sha256=grJb3RWQxmX9ZZUxiDoyLmCgX122W1starJjEio75ss,11270 +sklearn/manifold/_classical_mds.py,sha256=3pycY7_kQpRJe9k-TjWkhAq1Kh7HemoPU5jTHdpUtYQ,6381 +sklearn/manifold/_isomap.py,sha256=PVotZf6HgNb1FEdDLnNPGENqgIavib_6XIm5AAuxP_0,15833 +sklearn/manifold/_locally_linear.py,sha256=t5u05QHo5vvoo2ckYU1CMhuRLMM6BKGdAGkZfPAS9co,30945 +sklearn/manifold/_mds.py,sha256=6tIAd1lxgTU4deV4mS6t_In_uoO8jni6DLBudnC_lSA,30244 +sklearn/manifold/_spectral_embedding.py,sha256=-i4FyQ6lESJOKJGxUpuVKWHAaB1-trwkdUzsqWf16zE,30431 +sklearn/manifold/_t_sne.py,sha256=N42DWXsm2rd_2POV5aK-O39ViXKLkbaVPbBojwxCa6E,44086 +sklearn/manifold/_utils.cpython-311-x86_64-linux-gnu.so,sha256=VCJX4yYnaqnfqVu45YrCrL5iikq43MezD1gUbpxgjys,70152 +sklearn/manifold/_utils.pyx,sha256=NIX9uNgiZZTd5hDGqK5Oxw88glmc_U7av2KoNp1DOtE,3914 +sklearn/manifold/meson.build,sha256=sCySiLhLC1RumNBhRsAFZFM7GyU88lQxCrBVcZhWgtU,314 +sklearn/manifold/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/manifold/tests/test_classical_mds.py,sha256=TCflk0PYQYNpfHmgHSxF6tR14Y1oXMTvkw1j_Ocgj7A,1980 +sklearn/manifold/tests/test_isomap.py,sha256=Wl4voE-7ZRpK6YS6JKyEpAhccA0ReBlGyLNU-p4hQWc,12074 +sklearn/manifold/tests/test_locally_linear.py,sha256=yxsUuJ7vzm2VxiLi1fuZjzICS_0mXrwIicJHLC79eDM,5772 +sklearn/manifold/tests/test_mds.py,sha256=aN_WLgItPbiANEaoWtPnsJyMZSKbPGCu-4Tu1QOvV10,9503 +sklearn/manifold/tests/test_spectral_embedding.py,sha256=dYBI89VlLvYzz5KOXfcdBQoLrsafwmgcq3Qp_xkvh30,19994 +sklearn/manifold/tests/test_t_sne.py,sha256=64WNq_uJG9XEjbBMaq3fnbBCRXwfkJQwlSQgZcfI1_Q,37737 +sklearn/meson.build,sha256=usLP3EIm1dN7P9WaHybPIp8Qz5V-oZOehCQkFp5fsc4,9554 +sklearn/metrics/__init__.py,sha256=6j8L-Ad7_wY4rNyALuFn7Z0B25As83_B2y1ewFk8Qvw,5018 +sklearn/metrics/_base.py,sha256=3kRVak7umjKZu856bOxpiEZ8ZHY-Hg4gNL05-h4DV1k,7603 +sklearn/metrics/_classification.py,sha256=4JO_dVsbWCeRx99PL_imDC-X3IO0mKfDtbXpKJw2NOc,154809 +sklearn/metrics/_dist_metrics.cpython-311-x86_64-linux-gnu.so,sha256=3_LBnolG8UzrJVWJPTC1r1_vrvsG7hx_dITY0jd3iGE,591216 +sklearn/metrics/_dist_metrics.pxd,sha256=DZ0CmmQV1vsGjithqJyjsYvRSA4fPVqH_pSyC3qkk6E,7336 +sklearn/metrics/_dist_metrics.pxd.tp,sha256=qRsu02q9W6-tKcfPgUeLAKnqTEhEHGwFBJidXfadves,4384 +sklearn/metrics/_dist_metrics.pyx.tp,sha256=maZzWrXlaOuyonZp894OyX92efwhPGbgVU-Vf3jKhXg,92227 +sklearn/metrics/_pairwise_distances_reduction/__init__.py,sha256=ne2lDNgA2AcHtHtlIMVD-QEmdrfgyLv0BYDIhnzi_ko,5177 +sklearn/metrics/_pairwise_distances_reduction/_argkmin.cpython-311-x86_64-linux-gnu.so,sha256=bIdBsrUU8ZBcnsxuPt9AQ9aikmlauyqCxDgyfpZlcVk,241177 +sklearn/metrics/_pairwise_distances_reduction/_argkmin.pxd.tp,sha256=SBYCccYJBe9Fn4JPZXjvatQcLVv8Gu_fcS4r28EVMBI,1074 +sklearn/metrics/_pairwise_distances_reduction/_argkmin.pyx.tp,sha256=yWefGG3XVcM6im3vuI1WYPPQoRckYKbcIWCJb-8KoKM,19948 +sklearn/metrics/_pairwise_distances_reduction/_argkmin_classmode.cpython-311-x86_64-linux-gnu.so,sha256=KaMCWw5__EDKng7dRcvcCeb2iD6AgkgYWICUL_Bbm24,150801 +sklearn/metrics/_pairwise_distances_reduction/_argkmin_classmode.pyx.tp,sha256=ssH6dqfbpwbaV_Sfsx9E4P5_x1vjFbAwe0SRFGHGITc,6577 +sklearn/metrics/_pairwise_distances_reduction/_base.cpython-311-x86_64-linux-gnu.so,sha256=JR1dgpEs67lURZ6AU6Yyx1N1Pq7kI547cKiMciveNNc,208881 +sklearn/metrics/_pairwise_distances_reduction/_base.pxd.tp,sha256=Ox3r7oKxBiN9BLEfZvCW3F5rWaoozXvclcVpzA0HQ9w,3613 +sklearn/metrics/_pairwise_distances_reduction/_base.pyx.tp,sha256=yXvFmawbmbTICjRlPXLfPUcFVI35xncOMexj6zGPs3I,18419 +sklearn/metrics/_pairwise_distances_reduction/_classmode.pxd,sha256=DndeCKL21LyIGbp42nlWI9CKoyErDByZyQawUagL1XE,151 +sklearn/metrics/_pairwise_distances_reduction/_datasets_pair.cpython-311-x86_64-linux-gnu.so,sha256=89Fvj4POOcSdqQ8rCe1ke3NLfY_SmMrbSSRHsVUdGA8,345120 +sklearn/metrics/_pairwise_distances_reduction/_datasets_pair.pxd.tp,sha256=xaXCGCb8g5UaSStoIYfvgbIdCPaHFglML92yYvo6-SI,1958 +sklearn/metrics/_pairwise_distances_reduction/_datasets_pair.pyx.tp,sha256=aepsWtlwfo_La5IiO0TPp4JlyKxMJ8g92FmIWYlfGrk,15098 +sklearn/metrics/_pairwise_distances_reduction/_dispatcher.py,sha256=CYObEkXBZsYZQ1zTUO_BPIO6PQCv20RPcn34CswMxDc,30032 +sklearn/metrics/_pairwise_distances_reduction/_middle_term_computer.cpython-311-x86_64-linux-gnu.so,sha256=jlBokMEBvic_lMZf2jIRQIKpa_9gU8ton7Htq4G3FAk,349824 +sklearn/metrics/_pairwise_distances_reduction/_middle_term_computer.pxd.tp,sha256=fQZcfaSd_-u1JZVOCfXtLhA0Ith4jIGM3_4zFM20cpc,5930 +sklearn/metrics/_pairwise_distances_reduction/_middle_term_computer.pyx.tp,sha256=phLBDVxBVUyacti4bGSx7VuBZoo4YlLi6uxT-V33cws,20352 +sklearn/metrics/_pairwise_distances_reduction/_radius_neighbors.cpython-311-x86_64-linux-gnu.so,sha256=tCoHal2aLJWkRnr2YY4vbZftEoTOA1F-P6xNvcAvyP8,267513 +sklearn/metrics/_pairwise_distances_reduction/_radius_neighbors.pxd.tp,sha256=iWd8KDLDpTmg8vO9Av0dkrXRQNKPMewHn0eOIte6tjk,3349 +sklearn/metrics/_pairwise_distances_reduction/_radius_neighbors.pyx.tp,sha256=hUJseGlzo4_K19hXpKTOR6AfzpH74-sUXPStLMS9wmk,19589 +sklearn/metrics/_pairwise_distances_reduction/_radius_neighbors_classmode.cpython-311-x86_64-linux-gnu.so,sha256=jpngk7QY6TQdREeUp-GGmskMzlH2zYROTTqdCD3ipf0,175505 +sklearn/metrics/_pairwise_distances_reduction/_radius_neighbors_classmode.pyx.tp,sha256=5-bjKNInpt77ha9m-ZUjbIfSdac5PMH2Iw4b5w5QUYE,7498 +sklearn/metrics/_pairwise_distances_reduction/meson.build,sha256=AcafMHYxf3fpNWmUjzYDSiVa08KTUAqD2yA5HrcBh5Q,7547 +sklearn/metrics/_pairwise_fast.cpython-311-x86_64-linux-gnu.so,sha256=4MnoAl-odQtdJAS8tR6RltXvTqY-Tfta4jQn2Hp2ACk,142369 +sklearn/metrics/_pairwise_fast.pyx,sha256=5kBvF5vyJiMxlnNFeO7BHREsjEuSt1g7HtlTp5apIQI,3472 +sklearn/metrics/_plot/__init__.py,sha256=vKm4xxqJIfK6Tk6By-YU03YcE6pR1X1juFrOsacfZjY,79 +sklearn/metrics/_plot/confusion_matrix.py,sha256=j62Powfq483yrSNP66SPpSZ5IdDswN9-TPygwb_P6YM,17363 +sklearn/metrics/_plot/det_curve.py,sha256=URtZeexzEav99uVSFA_WcqAoVGKCO_WhcbLBAliKiv8,13260 +sklearn/metrics/_plot/precision_recall_curve.py,sha256=Z2pTXjtU0i-kDh3X7z6OE9VN11-V4jfvyTn4_kx-NJk,34959 +sklearn/metrics/_plot/regression.py,sha256=I_0-wXr5CYWDrXu8otWA7wbAXvWTw3w0ilACiXKTR48,14706 +sklearn/metrics/_plot/roc_curve.py,sha256=vUHcyrF6joeU35Vpb_ppOIf1Iy5Vg9wwF9B2dVbBK3A,26469 +sklearn/metrics/_plot/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/metrics/_plot/tests/test_common_curve_display.py,sha256=w6uevAb065JgKnMuLU8qZBflIIkUqnhZOfbRG6G07E8,29969 +sklearn/metrics/_plot/tests/test_confusion_matrix_display.py,sha256=E1xT26QRcOHH8VvuLShq8ev5YQ_8bLsJmfkHW7dTiiE,13487 +sklearn/metrics/_plot/tests/test_det_curve_display.py,sha256=gntdusohSu0vWb0MMpatsNm6gtQTcnn_5tfAQRGVvl0,4329 +sklearn/metrics/_plot/tests/test_precision_recall_display.py,sha256=ZPNv7iyPaANSb1wpSmMG5DMQX4H4F2WM1pjl2G1KSr4,26401 +sklearn/metrics/_plot/tests/test_predict_error_display.py,sha256=3PnOYrgBf7bnw1zHCPWm28tVCeuZlR4hIQD2fR-9RfM,6007 +sklearn/metrics/_plot/tests/test_roc_curve_display.py,sha256=ZxpiqltDpoHDs1XaWrI9E5quVomCuTWzMwQqVulRmA4,19314 +sklearn/metrics/_ranking.py,sha256=YOpnQ6O-DJeao-REGzhZMUHw3l6QEcFyKvdUz0GmvtU,89137 +sklearn/metrics/_regression.py,sha256=JfZCgYElcIn7wTqJ2CcIYs5Pcs-yGOtn-kNwhK7QFqU,67044 +sklearn/metrics/_scorer.py,sha256=CqpYyGX96VXyrj7HHp87kjVRFkVkQnbOezpHR5DLaG8,40631 +sklearn/metrics/cluster/__init__.py,sha256=CBWZ-isDQo9pXNZpPnaS6WvTK6vQlSGPKY21To7GQUw,1509 +sklearn/metrics/cluster/_bicluster.py,sha256=y1ZKrxBLqCEvKu3eVeSu8YviHT_sOqc4ryL86bUxfro,3647 +sklearn/metrics/cluster/_expected_mutual_info_fast.cpython-311-x86_64-linux-gnu.so,sha256=TEXLF7mTDirVleP4UepYGKVbaCBW8vpou1MyxEVW2Xk,96552 +sklearn/metrics/cluster/_expected_mutual_info_fast.pyx,sha256=5PeRMnMTwjvSqfKAf9csru9FAE5CIU-3qkqLQEifka0,2692 +sklearn/metrics/cluster/_supervised.py,sha256=2mi-xvowrvgnGqjln7gZhOEfdEIHKnV45zcJwTsBha8,45062 +sklearn/metrics/cluster/_unsupervised.py,sha256=nM5_Yz3DinAww0bN5puEBhJkgEsachRzj9t61-ddeTY,17987 +sklearn/metrics/cluster/meson.build,sha256=j4LJu4kH3dRH5UZN-2B62MM4m3mXwq44hEIeBRLgd2w,164 +sklearn/metrics/cluster/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/metrics/cluster/tests/test_bicluster.py,sha256=KecSxviHfRfUMNVZ0g77Ykx96QAuKoax0YUY8paQjFg,1719 +sklearn/metrics/cluster/tests/test_common.py,sha256=F-HQx19JyjV7igJaykJQVZUHM5UBH16tB0XjTSvYpwk,9476 +sklearn/metrics/cluster/tests/test_supervised.py,sha256=5K9F4CieVdQFQSoWFUcP1nXw_y6GwtB06sMvaAdoh4o,19156 +sklearn/metrics/cluster/tests/test_unsupervised.py,sha256=eAic9M_89S8Xbk1hEX0xyIeBW2GrAwPOTpNuNob3TaU,12269 +sklearn/metrics/meson.build,sha256=6qAj9ZxvPIgl-7sPLdoKdHG3RMEj5PC0agMvWJOswH4,1511 +sklearn/metrics/pairwise.py,sha256=3t4z8g4cM3HgsUwqI7Vqx0SmAuoMK0SPufAkrtLFCXI,91986 +sklearn/metrics/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/metrics/tests/test_classification.py,sha256=N3d7z8LWJ-9WdIbnmky6Q2H1LAcxousRnCtWSsS1ugY,132130 +sklearn/metrics/tests/test_common.py,sha256=m566w1S7bimo2XdWvSWcbQ6FbHItfbHpW2ey8zgrMWw,99000 +sklearn/metrics/tests/test_dist_metrics.py,sha256=vMpc3Q1sgD6nuWNXkKVQD46eGRkzn1S_w1w72ACLP2I,14995 +sklearn/metrics/tests/test_pairwise.py,sha256=wNTVTldIkXW5xzB_fAeKf2ncdcxQT7tuHBeTnJ2cISg,65275 +sklearn/metrics/tests/test_pairwise_distances_reduction.py,sha256=t-ZNZ7RyXOkybyFvCxm7VU2UWgOjY2_roOE8UO89aig,53061 +sklearn/metrics/tests/test_ranking.py,sha256=s9LRYpHftesVpU-A8gwTCe9LEi2okFRP3iQPa3d8NdM,96441 +sklearn/metrics/tests/test_regression.py,sha256=W8spjLxk7WZO42-gVGwnkapTCbvPVJoHQ1HFzOrIJgA,25924 +sklearn/metrics/tests/test_score_objects.py,sha256=blxZqYZ9WxZqugOfOjMFV02u4Veuy2l7vbWKocXxm9M,60935 +sklearn/mixture/__init__.py,sha256=D19PUKTiyVIRUAbFKS3SeKdmrRJcMRaVW9_Pt9Ok4j4,306 +sklearn/mixture/_base.py,sha256=XQqHUmCPS-thJ2O3P9mFhqQ0RvzZVSK9eRoPWJ797nE,21162 +sklearn/mixture/_bayesian_mixture.py,sha256=An-fxkeAPGVE0rHM1NvslsCSJinEUMc2z8QRo8MnVLk,33678 +sklearn/mixture/_gaussian_mixture.py,sha256=J0q-dpgWTws8wqzqW028kchZg_yr6mgG8vrAHxf8gl4,35776 +sklearn/mixture/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/mixture/tests/test_bayesian_mixture.py,sha256=23_HO3xRwo4hMpF60yjCVixeCsx4T4bZcd_OebiqmPA,17040 +sklearn/mixture/tests/test_gaussian_mixture.py,sha256=PFZf2ukFhCc2u_QIUbP-u6tbE7KNucrQS_iX5Zyb4Mc,55798 +sklearn/mixture/tests/test_mixture.py,sha256=T1grCQjuMVDSc29oeznXMzqFv6CZDKbb_zONniPsWyI,1178 +sklearn/model_selection/__init__.py,sha256=SMqIzKKBQxkoOeWR7Y6UhrE9ON6AqIGUWzkJu4p80VU,2861 +sklearn/model_selection/_classification_threshold.py,sha256=QcFBX00w-QrYEwzRC1BR4ZxCeswGrIc9QE3H_B1sUrg,33025 +sklearn/model_selection/_plot.py,sha256=8tO-rw_vt1WGEozEFpmVRxxMQU7QuZSXNrMd326CZRw,34606 +sklearn/model_selection/_search.py,sha256=vS4kvtlg1dbOZHbVvdyGXoldnDeNZghnzAbik92OPpU,85350 +sklearn/model_selection/_search_successive_halving.py,sha256=wlxLqFbHCfNabRMEbPmnUsD5WD-RjjbbdyLZWCdH7Yc,46151 +sklearn/model_selection/_split.py,sha256=UhnxzbXgz0OUf_fQh5rGwqTCP1SYmzdUkb6TBwMaeUA,112630 +sklearn/model_selection/_validation.py,sha256=WCxiPch0rmxRuc_X4323sQRL7tudb7ubq-BwySpsNHQ,95958 +sklearn/model_selection/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/model_selection/tests/common.py,sha256=PrR7WoVcn4MdG4DPrOvuZ1jrOIZPFPok20zannr4dwI,641 +sklearn/model_selection/tests/test_classification_threshold.py,sha256=e0sMmCeQ3OsgUanaY_h4vC6smoAYXwVbQhP34HYpSdY,23816 +sklearn/model_selection/tests/test_plot.py,sha256=goA_s29K0admCpVCSnWisPzLVf5-XvbTfwxev-DcDZ8,18456 +sklearn/model_selection/tests/test_search.py,sha256=59NWOFBEGdatjILq3zKZmJ0qaEGoLnqKhMkwkoV3jHc,107010 +sklearn/model_selection/tests/test_split.py,sha256=7qzn65fCDaCvKKy3L9DXn3QqV5Nt3-ECgSYLoX84JJc,75767 +sklearn/model_selection/tests/test_successive_halving.py,sha256=liuAL9oXGDJM8a63Jpqjp_UmMTehJNnFlHZvjK08gRI,29010 +sklearn/model_selection/tests/test_validation.py,sha256=4hiFduIb4PHVaX7thT2_exMU9fvIZuYBz9q9Vg8ic3Y,93432 +sklearn/multiclass.py,sha256=vk_EUP6pQfWrHH_0eBPTIcBDRcmO62nd7_1WK3DDn7I,44511 +sklearn/multioutput.py,sha256=r_SIXBNF7MbHYlB3S7ywDcyW02pPSDPTpt82FVG0uiQ,43678 +sklearn/naive_bayes.py,sha256=7-tCfsC0mHlQqP-7xZsMHiSMH-Zgp4eXc7YI5VHrop0,57249 +sklearn/neighbors/__init__.py,sha256=dyTAH1k_d-JvVEEfEv4SVFnoUxLtiktS7PuTtveexTw,1468 +sklearn/neighbors/_ball_tree.cpython-311-x86_64-linux-gnu.so,sha256=i7amPcUoeVunXCFnol2qOsxcVfaBLgYeIYvfqvsVCz4,599064 +sklearn/neighbors/_ball_tree.pyx.tp,sha256=8SJHBN7bRfds2t7DP4T0T8hkMAGVfusnbXOHXyViaIk,9327 +sklearn/neighbors/_base.py,sha256=bggSlzm9FgANi4osL6yd6lMWoSBzxoarDcfWi3HlTGY,52454 +sklearn/neighbors/_binary_tree.pxi.tp,sha256=3DP8kd7VYVPHAR_b9IhgtH9wR2ikDsA-N8iftQfAalw,100727 +sklearn/neighbors/_classification.py,sha256=HBqVm4l4E_q3YbWhEkXUIKgYOWzGRSWwY_TBG6akGpA,35154 +sklearn/neighbors/_graph.py,sha256=TQC-dgRQhQdowBmVK1EqAusKcFVb4dmDy4ugiJ-ctLo,24684 +sklearn/neighbors/_kd_tree.cpython-311-x86_64-linux-gnu.so,sha256=tsEj5Js5qBOc7gfW8TMbejUp0jtj1uod7dl0rCVc8a0,610736 +sklearn/neighbors/_kd_tree.pyx.tp,sha256=ZM4_DcS7eUkXbpxBJ4OLXR6exLbkGUB7Kmd_Ou2H4N0,11117 +sklearn/neighbors/_kde.py,sha256=sKDGI8JsDDk3GEg_gUEImommiivS1YQ9HBYkNx-ldd8,12359 +sklearn/neighbors/_lof.py,sha256=y_e4MZtB2kEypAxtST7F5zJwLpfuZuyO8fXoRliwepU,20231 +sklearn/neighbors/_nca.py,sha256=rmnURfvzGug1R3bGhq9CcjdxOvheUyiYwGvIJdIKeLM,19943 +sklearn/neighbors/_nearest_centroid.py,sha256=poMlV4RIXXqJJKQXF4YWOz1i4EWwU3HI4M7LxUXfHW8,13095 +sklearn/neighbors/_partition_nodes.cpython-311-x86_64-linux-gnu.so,sha256=tXx63RH-PWdNtkF3qA1_FtSPABaMyZElXvcHzkhd04s,32328 +sklearn/neighbors/_partition_nodes.pxd,sha256=cvQFe3Rz2D8iCMSCd8iJX6-GKbVPfBZ6Mxi_Vvv-nng,294 +sklearn/neighbors/_partition_nodes.pyx,sha256=iJw0PB95n4VgXORPMjDzLr0DJKgdfzoz_PUKyi0MelY,4120 +sklearn/neighbors/_quad_tree.cpython-311-x86_64-linux-gnu.so,sha256=UmGq5IhDl4RBzuBCzJ-95oEvgRJVFHKFQfemwdk2Tkg,164296 +sklearn/neighbors/_quad_tree.pxd,sha256=Chmd9_VuImln5GfELWopbgn6HbB7R0Tb9uF92WVCvZs,4211 +sklearn/neighbors/_quad_tree.pyx,sha256=R7MI_7Q1DSnxiqxBUBObOcf--fs8Surcr1rDfRJRvmQ,23702 +sklearn/neighbors/_regression.py,sha256=i42QJw3sQAmRb_ZZRPgZAZaEQ7DIOb1l-2_UrP34Bic,18369 +sklearn/neighbors/_unsupervised.py,sha256=f8-RT_3E07XaaP5tvi_pEojc1RQZKUn4oxgmpyID0TU,6283 +sklearn/neighbors/meson.build,sha256=7GJzh3-VYr3zFNdAZMuTKMo57_BjkBw05Z_FIjcKCNc,1635 +sklearn/neighbors/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/neighbors/tests/test_ball_tree.py,sha256=hpoJiFpMrGxw0FEhd-KghO8zWtonaqSr1JgbKB7sdN0,7097 +sklearn/neighbors/tests/test_graph.py,sha256=QdJvyK2N138biDPhixx_Z9xbJ7R-aSxz5mhSSvh-HRg,3547 +sklearn/neighbors/tests/test_kd_tree.py,sha256=J22MAV8fgqYsk4Nr8YOfWCHq5M6ihvdHWqKS4SnsXTQ,4013 +sklearn/neighbors/tests/test_kde.py,sha256=kEZsv-8U0oWrkAVuzRidsqL5w1jQZ2b7tK9pFZYnm44,9745 +sklearn/neighbors/tests/test_lof.py,sha256=x0h5dzFQpRqSG91CviJa_cytpemWv2HPSe45leX-p60,13746 +sklearn/neighbors/tests/test_nca.py,sha256=CAT5f0TpDPc8hvpPHobj2y-41xuQDjk3d1dNmdiTeCg,19506 +sklearn/neighbors/tests/test_nearest_centroid.py,sha256=Uo0oebJiyjCnrkSx9mDN89EPbFjhmhV2c8boUEIXRyQ,7572 +sklearn/neighbors/tests/test_neighbors.py,sha256=1yF0SgDOcmHlG1guITP7L3s-vr9t5JUfgGPpmXp39q0,87700 +sklearn/neighbors/tests/test_neighbors_pipeline.py,sha256=CwllxS4T9cP2utY-xuui3GhgtjRBkA7759byS4LdQ3U,8147 +sklearn/neighbors/tests/test_neighbors_tree.py,sha256=8OagtxQTE0jNy7-rTbl4L9lEbCgarf6n_jkx1woYlOs,9297 +sklearn/neighbors/tests/test_quad_tree.py,sha256=u3G19M0YHcMf_vpQMYUIFjbUX4R47mqFHT4Bu2ItbBw,5142 +sklearn/neural_network/__init__.py,sha256=0EomfZZmwgK2ZIpqZRYxb86aNlUhOYLNfgFTwYvoIxM,320 +sklearn/neural_network/_base.py,sha256=bp4Z3TxnFtzH7VinDh9GAuywChqyz3NohImLLymG9jg,7983 +sklearn/neural_network/_multilayer_perceptron.py,sha256=emrZk7xx2NI2Go6HKZK3Mwu35BfkqI6592VFskTjfTg,66188 +sklearn/neural_network/_rbm.py,sha256=3YFB43lePY1KWA6wBCI2Ss9tE1bF3Hwyj4WM0ahBsrs,14998 +sklearn/neural_network/_stochastic_optimizers.py,sha256=ldZWuIL10VpHq4tZ2PzJrTSWzAQjdpbzx7iJEbbFyMw,8838 +sklearn/neural_network/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/neural_network/tests/test_base.py,sha256=jSriY_p7h95ngec0Iggh2IwDpeR67RirQ5-q2RrP9Zc,1566 +sklearn/neural_network/tests/test_mlp.py,sha256=AyJ9ETYyN0S8KUXtPDkivaMjjjB2ntmHFaVinxpXPoM,36960 +sklearn/neural_network/tests/test_rbm.py,sha256=GvGZfOS8rmZyfZ3hbTHRpkv5vHPD3QmzmEKviOD11lI,7979 +sklearn/neural_network/tests/test_stochastic_optimizers.py,sha256=9JhAPo1Qc0sA735qPORoKtS04bCTts9lQ65P9Qlhtyo,4137 +sklearn/pipeline.py,sha256=YAuy7_LEoPn4KsnaNd7tt958klDDP6t26RhsQjwjs5Y,88595 +sklearn/preprocessing/__init__.py,sha256=h2UUUAn3I6TgC9aVP82THREOxScGSSmxEh0feSQTdv8,1671 +sklearn/preprocessing/_csr_polynomial_expansion.cpython-311-x86_64-linux-gnu.so,sha256=_GZ0243fFCSssbdELXTgM33h9NZUEvpss_JKoNofmOs,292112 +sklearn/preprocessing/_csr_polynomial_expansion.pyx,sha256=10bF-RruxYqaTkjhZnREhaxtYOH_9iOviuEkvD0JvQM,9160 +sklearn/preprocessing/_data.py,sha256=Nh8Fi0J75J_yHHrBDNoDrFnidooTuNcan2nCe2lVgTM,130480 +sklearn/preprocessing/_discretization.py,sha256=wm3Zzrp3qzwtehrBTgOTmcBLVx0R7SVb3cUMQ2TW3HE,20409 +sklearn/preprocessing/_encoders.py,sha256=EojM4gkSsjQM7gghMxA27Gchds80ajae_ZL8lEKo5MA,68525 +sklearn/preprocessing/_function_transformer.py,sha256=bKuAloy1tjvlMQIWdFZN3h8v6_nIxyRCFwD1fuU_5cY,16893 +sklearn/preprocessing/_label.py,sha256=EDBiVtfPTBIu3AcnS7eXTdYS4l4HpB_eOMn3uAX0j8I,35342 +sklearn/preprocessing/_polynomial.py,sha256=xCzSgE6ciYiExslOTG6tMS6TtRnmtQge8fZDuvYjnNk,51003 +sklearn/preprocessing/_target_encoder.py,sha256=SSQMB24sYaq3MYwyAuxU9KY-vI54lPk58dPLYSekhOU,26763 +sklearn/preprocessing/_target_encoder_fast.cpython-311-x86_64-linux-gnu.so,sha256=7A66CM0f69fdCmAoUuMynW-vNs1lQpOcUyTtv7mUMFM,494064 +sklearn/preprocessing/_target_encoder_fast.pyx,sha256=A1EcxYZ-BBHg1nMuuRWmraYs0TG8utv4761nlTXeoKo,5947 +sklearn/preprocessing/meson.build,sha256=D5DfSN_SRseZ2iljBqU9IKnue8HeH-27TnVDTQY1uhU,357 +sklearn/preprocessing/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/preprocessing/tests/test_common.py,sha256=Ab6QMQkp4WNl9-sJJXqMHMG4ZDRBZvKLMvjoO5YB8co,6820 +sklearn/preprocessing/tests/test_data.py,sha256=TOdFaHVbGUqAPPaHXzkpBS_x6ghs_-ZmJ27v2-NXSBc,107230 +sklearn/preprocessing/tests/test_discretization.py,sha256=IgL-Owf3uTdASuDjIrJAG8BY7HJEgx6FdZsVxoeleBU,21133 +sklearn/preprocessing/tests/test_encoders.py,sha256=Yt6YJoCLGEhlVmC6m7tu47lvfl3tHUyxqLCy_FRtbHU,81901 +sklearn/preprocessing/tests/test_function_transformer.py,sha256=MOgzVuD4kNgMjlEuiNZtp__Svo5g0xZWg5BVpiilA7A,19273 +sklearn/preprocessing/tests/test_label.py,sha256=ANJe2Y-kpsRvo6G1-fuHzxpFIaX6mIIXgxpvyhjPCZk,32825 +sklearn/preprocessing/tests/test_polynomial.py,sha256=01Ufg91zFyIDYJd27bfniwRbDTL5P2cfEDnXyRGSkc0,47528 +sklearn/preprocessing/tests/test_target_encoder.py,sha256=O3tKx0mGVaAhoVKOW0Ego-2_SFgGDzVOhOTvjFL9_Hk,30306 +sklearn/random_projection.py,sha256=6kGR0s6aHtEPh_77yDlu58jRrjP0Vt4cSSUpAWNlj00,28585 +sklearn/semi_supervised/__init__.py,sha256=qQVVJBoW796P18JMGlHtG_IDLhyQesuZ8DG19ZvFQSc,481 +sklearn/semi_supervised/_label_propagation.py,sha256=A8vXb1MAMH1C4GVBBZLqfzEtDjEd4_EVjUmUhQgMBSg,21808 +sklearn/semi_supervised/_self_training.py,sha256=-MPQepL6vlOXjxgbluvardRDq2G438tFyaMaK38BWU0,20229 +sklearn/semi_supervised/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/semi_supervised/tests/test_label_propagation.py,sha256=QmGk71EsI6LPGZE6ekR_4aM0E1xwgYgLsPwqHJdKkbQ,9889 +sklearn/semi_supervised/tests/test_self_training.py,sha256=RbHoBlJYAPOhH7P8h29dnAmMRxbHhrl40PknBt1uPns,13808 +sklearn/svm/__init__.py,sha256=5sh5bCKW8Uf04s1jYqtOVH5EDnHbDEj3qJwY6eFzD_k,509 +sklearn/svm/_base.py,sha256=W6cq9ykWpeppKP0R3EjC5-b1nAlshtXkop5k3XSNtUg,45045 +sklearn/svm/_bounds.py,sha256=EycnPtNUDgmNpunVITsJ8M3noy9HgG2Ij6AN58vOGxU,3483 +sklearn/svm/_classes.py,sha256=BriXYOIrXfRBgg9TGpmJ7f3yK9XkBaaMWjP7OeYGRLk,68141 +sklearn/svm/_liblinear.cpython-311-x86_64-linux-gnu.so,sha256=OHGLKgkSUYqS2sZ8MwllnPZGW6BMcffKgA3VSqu9kCw,165832 +sklearn/svm/_liblinear.pxi,sha256=wyp36zvouzBAeQktOH5J7cLgKst5vkZfbT1kvlGx2D0,1725 +sklearn/svm/_liblinear.pyx,sha256=kfF1rBD21rcR-wn5dfLVqhzZQWvMHMZorlvm6yUKdIA,4158 +sklearn/svm/_libsvm.cpython-311-x86_64-linux-gnu.so,sha256=cY6rs7TFtgMmS_NP7I8lfCnxFuSfCnmOQROMoiB-d1w,405976 +sklearn/svm/_libsvm.pxi,sha256=cV0nEGKq3yrtKsNxHpioX0MOmwO_4dURv9gR7Ci8TKM,3186 +sklearn/svm/_libsvm.pyx,sha256=EmwIe1nFXwD4EQKK1xner4V-G1u4L5aKWukWu78kCvs,26681 +sklearn/svm/_libsvm_sparse.cpython-311-x86_64-linux-gnu.so,sha256=F0mmHVgd_D1d36KKPssMaJ2JIH2m7us_3sLiEA64lUA,337984 +sklearn/svm/_libsvm_sparse.pyx,sha256=hqo_p_Ua8MLemb_ks8CbYC67InwblaNpcuibJbVvuoI,18974 +sklearn/svm/_newrand.cpython-311-x86_64-linux-gnu.so,sha256=PX2E5QbUQQo9YTOEYNL7hNmJyB7a1NLgkqEdIHsuRhg,55352 +sklearn/svm/_newrand.pyx,sha256=9Wgz24TrfT03OhvSrJ50LOq-6dznY73cXToi_seg0hg,298 +sklearn/svm/meson.build,sha256=yS7MPRrPJ5pxtVemPkGo7wNzNVfIVzivnmP_elfNd3M,1218 +sklearn/svm/src/liblinear/COPYRIGHT,sha256=NvBI21ZR3UUPA-UTAWt3A2zJmkSmay_c7PT2QYZX4OE,1486 +sklearn/svm/src/liblinear/_cython_blas_helpers.h,sha256=x7EL4uLM9u9v0iJmEaQDFJgXEhxM-3lWQ1ax-78gtlE,458 +sklearn/svm/src/liblinear/liblinear_helper.c,sha256=9rtFOnID6rSuKKkkj1kGLhPAqbA01-pYIB_14JtlREw,6380 +sklearn/svm/src/liblinear/linear.cpp,sha256=LQTw77EcPqwpkQkGuNI-KLYpAgf8sTWgP03tH5EgLSM,62646 +sklearn/svm/src/liblinear/linear.h,sha256=w70N_Hu8NaTsmxYTffXfOTgpbK1nbcpzVAiT1OOsiNs,2458 +sklearn/svm/src/liblinear/tron.cpp,sha256=S5Qv4zXxR12JBIwrNJLRTFiEl1ub7q42BLulYFB2QA0,4952 +sklearn/svm/src/liblinear/tron.h,sha256=rX95I3vubCVFvoPaI8vE6jqdsWTOvq5GHx8FUcOiRFE,768 +sklearn/svm/src/libsvm/LIBSVM_CHANGES,sha256=n5OrHZ65A9CqDFxpGfph5_tWGAuiRhdBI0xAGWoYx9I,769 +sklearn/svm/src/libsvm/_svm_cython_blas_helpers.h,sha256=H25CeF4GM3FQq0B6u3cQp1FZGAiGlbOOhgFqn4RIAFk,217 +sklearn/svm/src/libsvm/libsvm_helper.c,sha256=fVUEDyWrrX65g3pstPpnxWdvWZlIsB4BoD4XCQ5gy-c,11718 +sklearn/svm/src/libsvm/libsvm_sparse_helper.c,sha256=fWKVM9H_TNNUcVhymn678X2PYCM4S1KrD6ArcRbdW1I,13247 +sklearn/svm/src/libsvm/libsvm_template.cpp,sha256=de-H2Nxv6VI2P8KXyAirKS8IAdtJYKfqPoDn3mMaIyM,173 +sklearn/svm/src/libsvm/svm.cpp,sha256=jpxB8uAayT4H7Olv6JQ2mc-38DI-Bx1zIfsAMqZmw9Y,69309 +sklearn/svm/src/libsvm/svm.h,sha256=Vhf4LRfqLp7dE8swI2LmAKF3lf6ZOjC6L10k1IXJ96I,6262 +sklearn/svm/src/newrand/newrand.h,sha256=VGF__VxEdrYCRWeldvGF2AQfmb6DTH2bwR3QnsAmhQg,1840 +sklearn/svm/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/svm/tests/test_bounds.py,sha256=eIXss1T8uRoXQQ9eWa1L0cqOR3v0vwRL10b7h-dO-hg,5103 +sklearn/svm/tests/test_sparse.py,sha256=iLQKLE4l2C1d8qKiiX4JKj7nnWA_eFhIkmiyGU6j4ek,16238 +sklearn/svm/tests/test_svm.py,sha256=W8luLE9PpmD9y73R4eYZ_dvwNI_zbGNbK5w0TyEo95M,53029 +sklearn/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/tests/metadata_routing_common.py,sha256=S7XbgKbG2gZmTreaaj2vgjHzcfmW8Ko6v2J-EdSWuBQ,20380 +sklearn/tests/test_base.py,sha256=btoP9eXSQqWByiZvdjOmCPCHBhPK14CsSBQ-6CmYC6o,35566 +sklearn/tests/test_build.py,sha256=5ypftmK5z24KugmewvoBgAz99HtX_09uGd0bKJC5Qqg,4170 +sklearn/tests/test_calibration.py,sha256=g2gkjbM_LHHSA-eGDGr2N9s2lRIw6BM-y36pHjSVdRI,50733 +sklearn/tests/test_check_build.py,sha256=udkTjCgk_hJbkNmyyWxCZaf3-rfGCNudkHY_dH3lXj0,300 +sklearn/tests/test_common.py,sha256=ptsTOwG0bTtVcHYUpbbP4diG7qKidY8x3dmKPhxyKsM,13655 +sklearn/tests/test_config.py,sha256=IAHH_xnSwSe9DMFlh8aKyxYJH1qGmBxrZa8czqBLDn4,5911 +sklearn/tests/test_discriminant_analysis.py,sha256=SOVBGSxHSyVTZUkd-Vz3BpXz9fqW9sHmW01fYbBYIXA,28954 +sklearn/tests/test_docstring_parameters.py,sha256=4NIn1b6isiZOap5fgsuJIF2_XeqrMBrwclcUAfUH4xw,12565 +sklearn/tests/test_docstring_parameters_consistency.py,sha256=MEG4Rut5qEKBgrkImKzRvr-iNEqZUO4iyG1SK9KYSIc,4171 +sklearn/tests/test_docstrings.py,sha256=LLp1Zgyh5pS8wzYGMVEM0sr0T-G28Orriwx4y_b3CeQ,7529 +sklearn/tests/test_dummy.py,sha256=kSSm0v9b-rcoGR5fCETFmd2qYOoBcYSUyNZ1T4nYcdY,22085 +sklearn/tests/test_init.py,sha256=drYMGOmpHv567dHS_91F_x0GsAaNyh2kXPF21nARsYY,500 +sklearn/tests/test_isotonic.py,sha256=KjvlHdoFtmaVXOzm-jZDJjiGe6GRFXeRAvv0L3SGjgY,21822 +sklearn/tests/test_kernel_approximation.py,sha256=qGwSLs6F_E2DhOwkKiIjrcNtrL9TEjd94GkG60nVFsI,18423 +sklearn/tests/test_kernel_ridge.py,sha256=qkwUUjuY5O1uMiXi9gAS-wXOCHa62F5T7VJnNdZdGOE,2888 +sklearn/tests/test_metadata_routing.py,sha256=jm84z3xWbBRk6KtLJRbVnw0WXGjO7ugV4_WcLaa1Dqs,46756 +sklearn/tests/test_metaestimators.py,sha256=lb5My0OVfboQllx8JRNb8Pkk7H6ZxzQEbriQ9VYO-2w,11585 +sklearn/tests/test_metaestimators_metadata_routing.py,sha256=YewBmzOkngxaBef6L-0MW85jgVO8SytTxtrZJ-K9lY4,34442 +sklearn/tests/test_min_dependencies_readme.py,sha256=2SAh-lHFJmRbTlkbzVbO5JtmgxeNWnSZVBTzhA79fDo,8837 +sklearn/tests/test_multiclass.py,sha256=w2sjMwzlLBKnrU-rqIbJtoxdqHJApxRSYP-jdIWX2gU,34019 +sklearn/tests/test_multioutput.py,sha256=4vL2eEtKufm7OXEE9ffTdDjvTO-0rfyG7nJhimFdOsk,30164 +sklearn/tests/test_naive_bayes.py,sha256=WgEitwVTP5Nq-sLnySsRhmj8nwEUTZQZNzWRWfPR5RU,38230 +sklearn/tests/test_pipeline.py,sha256=NjCzLz8wn6AU2_IdnvyKG93ZObez37oUEBDzPgPG8fo,90838 +sklearn/tests/test_public_functions.py,sha256=tkphumO5IntdAxLQwhBUFVqAm6WKIwiyu3OywwhLD4Y,16836 +sklearn/tests/test_random_projection.py,sha256=PHgMtjxt5qvy6IM0YY6eWmNLelxdT2H4kF8BQbUBeRc,19583 +sklearn/tree/__init__.py,sha256=yCZuIaQc5NfN_2ggm1dcueIb0RfyA1JfJr0_uKSb8V0,596 +sklearn/tree/_classes.py,sha256=NSlC5SA2ODZt-4hg7O7R7lt5UMmWkltSKCKg6rCfWro,77245 +sklearn/tree/_criterion.cpython-311-x86_64-linux-gnu.so,sha256=GAeIVSWl1TsKFNlhYz55WNLGDxnsvQMpvHFUxJpDsIw,206416 +sklearn/tree/_criterion.pxd,sha256=vXl_aqcV0omc61P0R4UHdib8JWIajrafHccVFQAgLGo,3945 +sklearn/tree/_criterion.pyx,sha256=cE9kh6zRuYu1vJQNrsmEdG7k3jXaFoh3i0b-5yI4nds,60566 +sklearn/tree/_export.py,sha256=UQbF5YCr5yCdwjA5t43PAQ_lR4CkFz3R6C6tZGBAJPk,40952 +sklearn/tree/_partitioner.cpython-311-x86_64-linux-gnu.so,sha256=9Ba0CJGuufvH5UCy38wvJlZrqdnz7EbPxQdr1YZbbFI,159024 +sklearn/tree/_partitioner.pxd,sha256=_Jp4RseKlt8m_IhSvXcBMb1wi8ClNEptIkcYgAwTioM,4934 +sklearn/tree/_partitioner.pyx,sha256=S-rZbhfSnfvOj8YCH-K58Cwd5pF6grrI3vEdizr2aHc,30240 +sklearn/tree/_reingold_tilford.py,sha256=ImilHGv15TI5inwyBar79zEy-V-TxD5A9NUk0sBM91A,5157 +sklearn/tree/_splitter.cpython-311-x86_64-linux-gnu.so,sha256=JJvHPqB53uFx691iz31Waglq29e5yDdd14ZHVh3Lq9E,132984 +sklearn/tree/_splitter.pxd,sha256=XcjhgaoRM8sz5qoisub7AeYrOGyB6xZIKIsQBlze2LQ,4377 +sklearn/tree/_splitter.pyx,sha256=h_fHLEge2g9xtf6IPiijTmlGUGzB-bO0LkC4EIIRd1I,31366 +sklearn/tree/_tree.cpython-311-x86_64-linux-gnu.so,sha256=7mB6lcHCwQEiE-mcK-OtpAkWNWAd3uxYqWzZLl9YTsQ,483728 +sklearn/tree/_tree.pxd,sha256=FN4gQ01-ZjIuNRyYMnvd_rQPnQY5YCgW6Hcwygzlz8s,5434 +sklearn/tree/_tree.pyx,sha256=UxxJN_pn0yz38Kt1F03UiT2gyM0V8kX_NqS7Pfug_ng,74335 +sklearn/tree/_utils.cpython-311-x86_64-linux-gnu.so,sha256=eEf8wZcYXAxsfszM6Wgbefm64xtb46hxGb9hVhgGB_Q,108128 +sklearn/tree/_utils.pxd,sha256=yeEcmmxoKdHVg-1RmoBK7MLY_c1fm3FK0u-D5ExCOMM,2167 +sklearn/tree/_utils.pyx,sha256=i3ROcz25tgb7gneSjAaZIk_vC1-B5h_Rsdn5PspRaBk,9851 +sklearn/tree/meson.build,sha256=h_vVyJ3Uap4rs3v7OnDqMq8gV0bxOmsYYdCZAi_G1tE,899 +sklearn/tree/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/tree/tests/test_export.py,sha256=OxxTJWvVR11TU33QaP04IhSL8sATIh2v99wwFRwqs2I,21681 +sklearn/tree/tests/test_fenwick.py,sha256=ebMhrmElQAPMggHl02PBpij0Y3JopEDzyKJktmgZMLk,1750 +sklearn/tree/tests/test_monotonic_tree.py,sha256=d8MHug249f_Biv_0dOgZK77gmgjmcPSJuDCygfrkASI,19374 +sklearn/tree/tests/test_reingold_tilford.py,sha256=gVeyJjmuToiEwXnDTpQRhjsJzWUbD0CXNha-CgXmb0I,1462 +sklearn/tree/tests/test_split.py,sha256=IwMRnTwcJweENP3z9AAzavUFYk8vKy4kBmDrXoTk_PQ,8512 +sklearn/tree/tests/test_swap.py,sha256=ThchvtRkbz5_MSUoNzka6c0it5clZJRk14a8GkyreB4,1483 +sklearn/tree/tests/test_tree.py,sha256=UWxwAY9Gdro87NM0wcTbzF4OpDJZxN14UOfLIePbAxI,107783 +sklearn/utils/__init__.py,sha256=quNBfCoKNGBhqo3tF0e1Pw6PGdgaUqKhfqE7m0AgjUI,2395 +sklearn/utils/_arpack.py,sha256=iK4Iy81p3MVIKEziGtxAaSXOtL6Pg9lQjxD4qi5svwA,1223 +sklearn/utils/_array_api.py,sha256=UDonRghmSNALARxGkp4kYrDBJoSTrUMuiOtRmZERZVI,50510 +sklearn/utils/_available_if.py,sha256=CUJT-FoWEUiSCJ7BnfBFZ__74shIuMbHSBhQpwbVgnE,2945 +sklearn/utils/_bitset.cpython-311-x86_64-linux-gnu.so,sha256=yD09Jb3J7cPk6BxZQ0E1YZKAJSHNZGNKSqIeLuuPyeo,65440 +sklearn/utils/_bitset.pxd,sha256=oAoQVBgoafdt0aX1Loc0Zqie2MC5mtuPqk86UV-r8OI,682 +sklearn/utils/_bitset.pyx,sha256=IkvRjbC1advmjdN3cKMr54Ctrd7CIc5F36CExn0Y-Qw,2092 +sklearn/utils/_blas_int.pxi.in,sha256=ecy2i0drqP_0AAKTsAzRwLCIAOd06EX66Qz2xEuAJBE,190 +sklearn/utils/_bunch.py,sha256=uwd5YCTFZOaMT6AH5bmJImWxrO3kls_zG2G3dFsbx_k,2175 +sklearn/utils/_chunking.py,sha256=mytxeABxQ60HIz8lKho0W8JfstfJ2fsawBeP5BANsnk,5457 +sklearn/utils/_cython_blas.cpython-311-x86_64-linux-gnu.so,sha256=l8V01_sawuYE8xG0HsX0X9URJcYPWbOazw3Nc8LkiCk,208304 +sklearn/utils/_cython_blas.pxd,sha256=Kx-TV-Wy3JD8JAROmcAB3623tmk01WnffCiFLResUZI,1565 +sklearn/utils/_cython_blas.pyx,sha256=fsrVFLbXJeSUzCGuP0cGflAUzg5MXrihEqRD3EFJJzE,8979 +sklearn/utils/_dataframe.py,sha256=2bAa0DbcW1UmaCBfiAkC5-h-1hRgPWQpH4GUDWySsiw,2850 +sklearn/utils/_encode.py,sha256=aoj-nt358318w-LxKAxzOZRBwAcbIpyZy1XlDZiK46I,11773 +sklearn/utils/_fast_dict.cpython-311-x86_64-linux-gnu.so,sha256=PK7eOMGMnDB1YC1Pd-Dvwr5godmQb8MDUzSTUPXp9Rk,147000 +sklearn/utils/_fast_dict.pxd,sha256=0ByuSCHaz6ECuDRfxWm7akG9Fz2JXkz5XpuPzkh10VE,529 +sklearn/utils/_fast_dict.pyx,sha256=794so_xkLNG27hrFif30xtYg-eYSeiTongxudQajZGE,4665 +sklearn/utils/_heap.cpython-311-x86_64-linux-gnu.so,sha256=EGbRN0b_4RHxUNndcDJdWgEa-h0DZ0JxKnrG0m7bB6E,27144 +sklearn/utils/_heap.pxd,sha256=OpwpWaMwhW-MzsO2kEGvl-bxpDHgTOSeyOx98xoP0Lw,269 +sklearn/utils/_heap.pyx,sha256=yrXyVjBPlrnBTSgxMzKxB_mYqT3DehmuJhfhOA_xDVw,2266 +sklearn/utils/_indexing.py,sha256=TGS5ZJfMtG5v2-kj_D5D7jKuOzUQQEMnkx7mNQcWR7Q,24040 +sklearn/utils/_isfinite.cpython-311-x86_64-linux-gnu.so,sha256=kYdRa16uVB9S3navDc3W447MAErq1U3a_3dO6JUtsOc,85048 +sklearn/utils/_isfinite.pyx,sha256=PFLLYo0BWaxpfNP6t0O6r0cLY9KXZSzHQmVYQKYbBtI,1414 +sklearn/utils/_mask.py,sha256=2lz6-GiFziQLXv8cKGVAczqZqpDlq7OsLL2YARBN9RI,5002 +sklearn/utils/_metadata_requests.py,sha256=sosLIwcbFt6ag757MdkzT59y46AtgT5Zapvf-CfQT10,68748 +sklearn/utils/_missing.py,sha256=k_iSnAultpKjFfbt6po87kWUpIvb9NRgsvdXS1DMVDI,1565 +sklearn/utils/_mocking.py,sha256=K8yrG656Qv62j7bKwkfmA2vyTEsfj8cVUynjBKMhvxM,13699 +sklearn/utils/_openmp_helpers.cpython-311-x86_64-linux-gnu.so,sha256=LZZacowenYdT04cMS_tOUUpeUB1IoXm3Udhd9otrIa4,72481 +sklearn/utils/_openmp_helpers.pxd,sha256=ORtNXjPXDBOmoHW6--54wwrMEIZptCAZ6T8CJPCuJ-0,1069 +sklearn/utils/_openmp_helpers.pyx,sha256=6NgzGt7XMaLuzqqigYqJzERWbpvW-pDJ36L8OAVfdKw,3143 +sklearn/utils/_optional_dependencies.py,sha256=LKazAnJmfnh83Q8dvyPocqYXcsP1RlBm0XqdCpZ0zJo,1735 +sklearn/utils/_param_validation.py,sha256=x3UNTzOyU2wQqlKAwAYXOCK0i0vW3SwwZfBqFr1Tu7Q,28604 +sklearn/utils/_plotting.py,sha256=ARhSR443qnNXbkeTGlqcHKrlaLT_hCvplXLiTt7lq80,17520 +sklearn/utils/_pprint.py,sha256=-Y5N2HZALiXAMkBYPvBdz9OloBtePnAjyDRNgxmMZFw,18467 +sklearn/utils/_random.cpython-311-x86_64-linux-gnu.so,sha256=gBumJLYcMhqPnjegobCCGbDcR42H7lbb3cmTuRHGfJ4,171472 +sklearn/utils/_random.pxd,sha256=pKTmH9jmBQQqiaCz5eKFcFj0eykpNZ0Yzki5WAgF7Hg,1263 +sklearn/utils/_random.pyx,sha256=p8f0kxJymf4sOjqiMNhk-WfjEwJ8qrRFyKiaubnTgJY,12592 +sklearn/utils/_repr_html/__init__.py,sha256=vKm4xxqJIfK6Tk6By-YU03YcE6pR1X1juFrOsacfZjY,79 +sklearn/utils/_repr_html/base.py,sha256=dSAjOhExR9DqisHJojb-TAY9vbMBLbkO4tM8ktyi3dU,6412 +sklearn/utils/_repr_html/common.py,sha256=YfmLfcmlNPGOQGUunrXdtlCI_f_NU46Pbg9oV6-8QXg,2654 +sklearn/utils/_repr_html/estimator.css,sha256=MRkkpeJSRsujkuHk_GBiDtJpJ0O7nkJw306SuJkEeg4,11402 +sklearn/utils/_repr_html/estimator.js,sha256=kVy2dXPclu7c8jz9OnTFmodamyvvVgmZK8u1KPCjMlI,6117 +sklearn/utils/_repr_html/estimator.py,sha256=SQWzI7YsLP10_Ocj5U9FBsTf_84EuFfzToc3N5hcWdw,21097 +sklearn/utils/_repr_html/features.css,sha256=nMtxGaIo-FsPyuorWIMDShtH01sGaIYCzNadD9z3lFI,2503 +sklearn/utils/_repr_html/features.py,sha256=9nr-seoCiDr7JQjTrb8IaDHOs8ovpSwiBhXz5HkFsyE,1824 +sklearn/utils/_repr_html/fitted_attributes.py,sha256=T0mFPYW7TPplLSQaYgBDq_Qx9mXLkCl2hXXgyyira6Q,5177 +sklearn/utils/_repr_html/params.css,sha256=2EbyXZFfeN56fDi3WJPpz862LKulyFlL8OObd39Memw,3790 +sklearn/utils/_repr_html/params.py,sha256=JOh87cdMRVe1LP0UkDrUjWZAsmIe9oWKZE1JTSkoC_s,4436 +sklearn/utils/_repr_html/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/utils/_repr_html/tests/test_attributes.py,sha256=MZmli2wq5B2Q_k5OisUU76GG80YilGFAYYHzDV0_7tM,5803 +sklearn/utils/_repr_html/tests/test_estimator.py,sha256=4dl2b2NxuryndLGI3fl3vwLyGp0PLqfcPcTYxVC3n88,22087 +sklearn/utils/_repr_html/tests/test_features.py,sha256=V8C7jrZSHLeq7Xdj8Y-3KdqhQuUhQjA3yHc-hO9i2mo,7206 +sklearn/utils/_repr_html/tests/test_js.py,sha256=vB27pS8Nt_bf-7gF6VXPRmqoFH1pR_SW8-eOXwPil8s,5756 +sklearn/utils/_repr_html/tests/test_params.py,sha256=1rXphDui9-6cJvp3AL9yu1ptu9ZPRizq-y025dF2ziI,7477 +sklearn/utils/_response.py,sha256=Eqm0UeEq2VqEoGXAujpFb9qdYKa6fdj25_QSuWJwpK8,11480 +sklearn/utils/_seq_dataset.cpython-311-x86_64-linux-gnu.so,sha256=jEQt5D2HH0z7mgqpOBWsLJeuW3JNsi4mKtEEArPDsQM,179568 +sklearn/utils/_seq_dataset.pxd.tp,sha256=Za0Ght13u7C_MvXQ53_IZGo93svk9gVY9PSdVDDQaZg,2580 +sklearn/utils/_seq_dataset.pyx.tp,sha256=6aGECtZ2n1iqFZpXKy63GFMq9r_tWzqajIFuaNpV4tU,12278 +sklearn/utils/_set_output.py,sha256=wQ-w62ijCCidfQWAXg4lwUm1etz2w6tz68mYtXA7vBQ,15011 +sklearn/utils/_show_versions.py,sha256=IIU5jDwe8rgR2VUei8tO5ldoGmpi6gApT4O5n-MfhpE,2586 +sklearn/utils/_sorting.cpython-311-x86_64-linux-gnu.so,sha256=yF1tvHH6gA5rBzZ31u4CfVWKXtvXxrQGKTyNZFgUrvM,84808 +sklearn/utils/_sorting.pxd,sha256=Cpa-TsKzpSsxhs2CnBc2sNMLFPdsvmw46A1wAndtNTo,214 +sklearn/utils/_sorting.pyx,sha256=KVTyFbV5WjvVGnkG9c0owEMl7D5jHbu6ikJYvgpCKyg,7952 +sklearn/utils/_sparse.py,sha256=hSfZ6tr4eoNFI6Grhf1VxlBU-2xMXVvQ_P4oG3Dntiw,1233 +sklearn/utils/_tags.py,sha256=2EVxQGRSg17ES-d-wnZSFWeF8uXHKMZWTvgudSdkCb8,10029 +sklearn/utils/_test_common/__init__.py,sha256=vKm4xxqJIfK6Tk6By-YU03YcE6pR1X1juFrOsacfZjY,79 +sklearn/utils/_test_common/instance_generator.py,sha256=YNoRe_uhneDYA6VcX5h4Rc3jqQHx6kCkrOMkFrmYCbs,55847 +sklearn/utils/_testing.py,sha256=xk194NG4L2Vc7pBBAHV-zE3Zi8aCujMZwcxsVHa2T1Y,54246 +sklearn/utils/_typedefs.cpython-311-x86_64-linux-gnu.so,sha256=dCOwt_xEkt67iCJBeAS-AG5K0yXLbcfgYkXj2J3g-fA,128008 +sklearn/utils/_typedefs.pxd,sha256=gew7YuCZWwpo-JWXGDIrwJ2-K_6mB-C4Ghd_Zu9Gd-o,2090 +sklearn/utils/_typedefs.pyx,sha256=rX9ZIRqg-XFgtM4L3Mh0YAsmRHSnccxdg2nEs9_2Zns,428 +sklearn/utils/_unique.py,sha256=IBhmM0fwGmuhcNjtSf_1-OryOx1plPOGwXVE4sndDyM,2972 +sklearn/utils/_user_interface.py,sha256=dzS5H5O6prEkNLholFgBLWOfFp4u0Mw61mDBQFh5KZ4,1485 +sklearn/utils/_vector_sentinel.cpython-311-x86_64-linux-gnu.so,sha256=hqkByohJfiL6_GWm03jB2hWj6iWivbFXvvfhIlErAbo,157600 +sklearn/utils/_vector_sentinel.pxd,sha256=LlBr9cqWyTl7Qs8XYkv5BpOtw3Hv60woDz_9alUy04k,302 +sklearn/utils/_vector_sentinel.pyx,sha256=H1GeEQ7qOSSwgo55sNUaiWzVb1vbAqOr03hnfR-B-o8,4458 +sklearn/utils/_weight_vector.cpython-311-x86_64-linux-gnu.so,sha256=2g2elraB0fzCUxljHUZm-ZQ_W6IAi_R6jBnx2u89sgQ,70600 +sklearn/utils/_weight_vector.pxd.tp,sha256=LL6aPNVFys4OWSC__JjCQer1-jQLjZLQF3emjvtqePQ,1461 +sklearn/utils/_weight_vector.pyx.tp,sha256=sY2zeTjm_sWOgeZkJJYxsS2IyMgFX0aHgbcUwX3sroY,7336 +sklearn/utils/arrayfuncs.cpython-311-x86_64-linux-gnu.so,sha256=6zqB_FrpsZF4stKfJnPAZWCXVF8oHceUfJuAKt_ESQg,137472 +sklearn/utils/arrayfuncs.pyx,sha256=fJEeUWjglNi9fdP6vSIxjxM58-uakBQKqLkszA6Kmsg,2921 +sklearn/utils/class_weight.py,sha256=of-rYJtxGPbOb6MFC7FcA-gxmdt7R4I4JUfAaEzDOgg,9355 +sklearn/utils/deprecation.py,sha256=C23RbEa8N37vY5-FzeBVGItFBerKJAwzFFZT84f5seA,3489 +sklearn/utils/discovery.py,sha256=JpCV8lIyuWIrMPp88Oh0mI31sPyc_kmzV20XYBoEWa0,8743 +sklearn/utils/estimator_checks.py,sha256=an4MvjYjgtik_E1dL5JpdBcC2FXc9VqThBg3rQ9tiqg,205515 +sklearn/utils/extmath.py,sha256=8Y7MaV2blJyD3Sfw6P2DknnaB7LtJz1FLt9rEsCsBB4,51713 +sklearn/utils/fixes.py,sha256=yhDdui5hwB5c3gEb5IZNgaQ_HmlCO-Z5c6P6lIEvj7k,19512 +sklearn/utils/graph.py,sha256=Who2qGq5tNKVYDvA-W89ybQqXU5gwiPEx-eCddjULWE,5714 +sklearn/utils/meson.build,sha256=e0OVs3S4_7EVvurnsvuWzYD4xo0UVHMfXwqs9vpvSxE,3224 +sklearn/utils/metadata_routing.py,sha256=ksHUMRvKGjG7Eonpzp9tVnQSrMUpOfomJ5nC4lfhV7o,590 +sklearn/utils/metaestimators.py,sha256=JfVA-W3p-v-q8hPUjRhEwnI8ZgMqJ9oR0XZnRwxBKvo,6582 +sklearn/utils/multiclass.py,sha256=hmE0MHK3K4Yl7vr3SyoBryUq_eObReZCxZ8-yJykY14,21283 +sklearn/utils/murmurhash.cpython-311-x86_64-linux-gnu.so,sha256=4PKhTtU8lHtNlZtwGEmk_oQr6f9EY11oZ4mi_QcMYPU,110736 +sklearn/utils/murmurhash.pxd,sha256=uz6-VpUEqSmOwYxwhZe0CDPUp82wzYhhO-ldwHv-ayI,882 +sklearn/utils/murmurhash.pyx,sha256=i1_wM0JgVCP2k7V9H3U3mPEo_5hrAFjmQNnv1hFDZMU,4538 +sklearn/utils/optimize.py,sha256=moGzPojqJ8rOag0pRQL8KnULMta1I93T3RKmBMYBM6o,13971 +sklearn/utils/parallel.py,sha256=55yiTOUK65N6YOuz_STCsh7z-sgFbDmgejTbmvkAWac,8102 +sklearn/utils/random.py,sha256=tnPskBTc4j_o9DT5uav_jIXyvtOqf9dTrJW5AFI3OEo,3798 +sklearn/utils/sparsefuncs.py,sha256=flet3RjGm-U54UU_473H15XZM5Z3uJ9e-kuhnNseLew,24891 +sklearn/utils/sparsefuncs_fast.cpython-311-x86_64-linux-gnu.so,sha256=ihZphkcWqnby6JNGu2E5G6sGvY1gGXpLZfAJBmwRJ-U,673560 +sklearn/utils/sparsefuncs_fast.pyx,sha256=IGL5-CBDMRj4irdbHXu0stWddrecWdj8dxfe4MWiw2Q,23100 +sklearn/utils/src/MurmurHash3.cpp,sha256=5BI_ft6ZWDOlbpDI-U1MM-bvqH5G2ssGgfLJWD5bozU,7968 +sklearn/utils/src/MurmurHash3.h,sha256=vX2iW09b4laQOwIwXSiTu14wfdkowndTzKgDAmHQPi4,1155 +sklearn/utils/stats.py,sha256=y7IgCjaALM_AgNRf8e-2zX30muo4spcJP7563_gY9Qc,9728 +sklearn/utils/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +sklearn/utils/tests/test_arpack.py,sha256=KGPsMtvLI2utRrkz_xkIK1c7n1lvlQxGsat7UXd0Xes,489 +sklearn/utils/tests/test_array_api.py,sha256=RhU7b1PebMrSF4i-xMIC2ey8Hg-DjFUZ0veTUkNOp54,39171 +sklearn/utils/tests/test_arrayfuncs.py,sha256=DGbK5ejSO-_ibZDoeM5RlDNY7a8Z8eGScnq3cThQ1Js,1326 +sklearn/utils/tests/test_bitset.py,sha256=2SVPynuTItV8irY2-jNvKrkpc9XuDm0I3boUPdRQGtg,2008 +sklearn/utils/tests/test_bunch.py,sha256=QZXKwtgneO2wcnnrbMVM_QNfVlVec8eLw0JYtL_ExMI,813 +sklearn/utils/tests/test_chunking.py,sha256=4ygjiWbrLWxqgYYKPZ2aHKRzJ93MD32kEajbgIO0C6s,2371 +sklearn/utils/tests/test_class_weight.py,sha256=K-vCvXz5GlqOl1h6QdPXRGs-svR82ql2fD6ndzPhsXI,12957 +sklearn/utils/tests/test_cython_blas.py,sha256=COhzY-WHwQLIF5qn_XvBPggWn9OfMw7J2IOLV63MKaw,6709 +sklearn/utils/tests/test_dataframe.py,sha256=11U51W6MB6kCbgOsAMnJXPnvvzs67Q9WWnN9Lh5AwGg,2578 +sklearn/utils/tests/test_deprecation.py,sha256=BRp5wLhsGtSdDt1cWwF72kqqsqbNDvggGs_kETP0g0U,2294 +sklearn/utils/tests/test_encode.py,sha256=QiiG0ArBGF7ENYrvcgPGwjYgUdn3W6Ch_GE9VEF2DWI,9603 +sklearn/utils/tests/test_estimator_checks.py,sha256=iZ_RF9ANHsOGDsTrvooLiW29GWh-p2jvRyMxGwDPUs8,63256 +sklearn/utils/tests/test_extmath.py,sha256=QdI-XwTj_rty4LdMbMVI5ccscPQLsPq_YQbeSOaP-dw,41093 +sklearn/utils/tests/test_fast_dict.py,sha256=Y4wCGUJ4Wb1SkePK4HJsqQa3iL9rTqsbByU2X3P8KQY,1355 +sklearn/utils/tests/test_fixes.py,sha256=vi2FdhpmtYoJzCukYNRUBengzDcXPKOdyGvknun0qqs,6501 +sklearn/utils/tests/test_graph.py,sha256=0FGOXawAnpEg2wYW5PEkJsLmIlz1zVTIgFP5IJqdXpc,3047 +sklearn/utils/tests/test_indexing.py,sha256=ZL72OSOef2nZDC7XShFshqrsq7wlAITKvYRy-VTy6Sw,25574 +sklearn/utils/tests/test_mask.py,sha256=eEsLP_o7OqGGFt5Kj9vnobvHw4sNaVFzHCuE4rlyEd4,537 +sklearn/utils/tests/test_metaestimators.py,sha256=x_0agW4puaVCmqPwBrk3FrWIZeK3qgM9eNJWUxYD640,2107 +sklearn/utils/tests/test_missing.py,sha256=3lPgYdyvRkzPH-Bw82N282i_5_aYN7hHK-bkoPBw_Jg,709 +sklearn/utils/tests/test_mocking.py,sha256=1R0-gSs2I7jw4R5UvapMpvj8UrDIU9ifX9dMZRh2j6w,5895 +sklearn/utils/tests/test_multiclass.py,sha256=rk9to30iMWTpwoojTRydue0zw-MIc28y9y0vktUbh7w,23724 +sklearn/utils/tests/test_murmurhash.py,sha256=b-WKvPEJmp8XiIjGVDv_c_6mGOL-nz9XOvMFNXPpXeA,2516 +sklearn/utils/tests/test_optimize.py,sha256=DQ4ZJ2eoNrr1gEdq1-24Ekl5mojbsgC6-SoVPiaEVGM,8796 +sklearn/utils/tests/test_parallel.py,sha256=FTl9M3k9Q_JU_rzP-pS4WHSFQSS8K68rAmLHTJSXUfY,7589 +sklearn/utils/tests/test_param_validation.py,sha256=4pj1mePb2BMLpG9rGjkzczdeEIflynKbfNvjS1o6vKc,24474 +sklearn/utils/tests/test_plotting.py,sha256=dRgBZt3KSpcaSYDU1WveAbnuNuduUCliDhiu2nGj0XI,20323 +sklearn/utils/tests/test_pprint.py,sha256=pRs-7uiNng1dNfccycEDZ_YYj8ha866J8YJG3Y5ZaJE,27763 +sklearn/utils/tests/test_random.py,sha256=wzhfCP5lhSm-5PaCvbcgqvsnuINkvJNVSanQZiRmc0s,7149 +sklearn/utils/tests/test_response.py,sha256=ymrOWx-jTaedaKYIaTn9IGqov-OLrgXqRPXlL8bdjl8,16662 +sklearn/utils/tests/test_seq_dataset.py,sha256=Hc-kZsFGIFbUlPBzOoIk_qKwmOXi9f01TueFe5t78AY,6051 +sklearn/utils/tests/test_set_output.py,sha256=sPtTyGoAsIXO4IkT6TYhSgXPeQu_Qt-kZ9gxErWeeos,16131 +sklearn/utils/tests/test_shortest_path.py,sha256=XN1SF7TfMo8tQCC-bUV2wK99jR32hEM7xZOl54NbIoQ,1846 +sklearn/utils/tests/test_show_versions.py,sha256=XGAOHTc4yPELnGRdrtpEpls0vrLSrED_PuZVPWZKnZw,1036 +sklearn/utils/tests/test_sorting.py,sha256=HrF_Ei5tjmO2ETNEudsHi5UcCNY1z-mQyC3rqMjevfc,2226 +sklearn/utils/tests/test_sparse.py,sha256=BSxbLsy0lvo1vgCImwG5Utw19r-5TpPzor-FjJ_3e2o,2930 +sklearn/utils/tests/test_sparsefuncs.py,sha256=j_5gvQ8rYjBH33R0VQW0sixyY1aKwmy8_Db9rGd2qPw,37252 +sklearn/utils/tests/test_stats.py,sha256=vYICphr4yInSe8SdiYDFxdcHfPmZXoY2WsTf17OIQ30,18681 +sklearn/utils/tests/test_tags.py,sha256=dFjEPp4Xb7_Nr1AzyLwtOZWk3ftqF2CkpL7tCsj90J8,4839 +sklearn/utils/tests/test_testing.py,sha256=lSXqpg77poVtVgIC4DaNcRVhj_LnV2D9DnLSSBDAnBk,33390 +sklearn/utils/tests/test_typedefs.py,sha256=gc_bm54uF15dtX5rz0Cmw4OQQhscTHACRhjdkEkMx8o,735 +sklearn/utils/tests/test_unique.py,sha256=UMMRUrDYiTzxcf49N_ddIWWyvSySFBTzrPK7JY94fGU,1820 +sklearn/utils/tests/test_user_interface.py,sha256=Pn0bUwodt-TCy7f2KdYFOXQZ-2c2BI98rhpXTpCW4uE,1772 +sklearn/utils/tests/test_validation.py,sha256=pxchVWgs41b5FwX3xQMb9S2OQgCXqskfDZOWyZLZUB0,87049 +sklearn/utils/tests/test_weight_vector.py,sha256=eay4_mfrN7vg2ZGoXmZ06cU9CLQYBJKMR_dK6s2Wyic,665 +sklearn/utils/validation.py,sha256=XV6Pt-Yas7yONUohvNfdf41fwEsObwYAdsIwPl68ybE,111786 diff --git a/.venv/lib/python3.11/site-packages/scikit_learn-1.9.0.dist-info/REQUESTED b/.venv/lib/python3.11/site-packages/scikit_learn-1.9.0.dist-info/REQUESTED new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/.venv/lib/python3.11/site-packages/scikit_learn-1.9.0.dist-info/WHEEL b/.venv/lib/python3.11/site-packages/scikit_learn-1.9.0.dist-info/WHEEL new file mode 100644 index 0000000000000000000000000000000000000000..882a79bce1466cb3ad4f11391e5208981a62c5f5 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scikit_learn-1.9.0.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: meson +Root-Is-Purelib: false +Tag: cp311-cp311-manylinux_2_27_x86_64 +Tag: cp311-cp311-manylinux_2_28_x86_64 + diff --git a/.venv/lib/python3.11/site-packages/scikit_learn-1.9.0.dist-info/licenses/COPYING b/.venv/lib/python3.11/site-packages/scikit_learn-1.9.0.dist-info/licenses/COPYING new file mode 100644 index 0000000000000000000000000000000000000000..248752081a584cba3d3f06e6979dcfaf410fa7ab --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scikit_learn-1.9.0.dist-info/licenses/COPYING @@ -0,0 +1,112 @@ +BSD 3-Clause License + +Copyright (c) 2007-2026 The scikit-learn developers. +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 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. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +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. + +---- + +This binary distribution of scikit-learn also bundles the following software: + +---- + +Name: GCC runtime library +Files: scikit_learn.libs/libgomp*.so* +Availability: https://gcc.gnu.org/git/?p=gcc.git;a=tree;f=libgomp + +GCC RUNTIME LIBRARY EXCEPTION + +Version 3.1, 31 March 2009 + +Copyright (C) 2009 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +This GCC Runtime Library Exception ("Exception") is an additional +permission under section 7 of the GNU General Public License, version +3 ("GPLv3"). It applies to a given file (the "Runtime Library") that +bears a notice placed by the copyright holder of the file stating that +the file is governed by GPLv3 along with this Exception. + +When you use GCC to compile a program, GCC may combine portions of +certain GCC header files and runtime libraries with the compiled +program. The purpose of this Exception is to allow compilation of +non-GPL (including proprietary) programs to use, in this way, the +header files and runtime libraries covered by this Exception. + +0. Definitions. + +A file is an "Independent Module" if it either requires the Runtime +Library for execution after a Compilation Process, or makes use of an +interface provided by the Runtime Library, but is not otherwise based +on the Runtime Library. + +"GCC" means a version of the GNU Compiler Collection, with or without +modifications, governed by version 3 (or a specified later version) of +the GNU General Public License (GPL) with the option of using any +subsequent versions published by the FSF. + +"GPL-compatible Software" is software whose conditions of propagation, +modification and use would permit combination with GCC in accord with +the license of GCC. + +"Target Code" refers to output from any compiler for a real or virtual +target processor architecture, in executable form or suitable for +input to an assembler, loader, linker and/or execution +phase. Notwithstanding that, Target Code does not include data in any +format that is used as a compiler intermediate representation, or used +for producing a compiler intermediate representation. + +The "Compilation Process" transforms code entirely represented in +non-intermediate languages designed for human-written code, and/or in +Java Virtual Machine byte code, into Target Code. Thus, for example, +use of source code generators and preprocessors need not be considered +part of the Compilation Process, since the Compilation Process can be +understood as starting with the output of the generators or +preprocessors. + +A Compilation Process is "Eligible" if it is done using GCC, alone or +with other GPL-compatible software, or if it is done without using any +work based on GCC. For example, using non-GPL-compatible Software to +optimize any GCC intermediate representations would not qualify as an +Eligible Compilation Process. + +1. Grant of Additional Permission. + +You have permission to propagate a work of Target Code formed by +combining the Runtime Library with Independent Modules, even if such +propagation would otherwise violate the terms of GPLv3, provided that +all Target Code was generated by Eligible Compilation Processes. You +may then convey such a combination under terms of your choice, +consistent with the licensing of the Independent Modules. + +2. No Weakening of GCC Copyleft. + +The availability of this Exception does not imply any general +presumption that third-party software is unaffected by the copyleft +requirements of the license of GCC. diff --git a/.venv/lib/python3.11/site-packages/scikit_learn-1.9.0.dist-info/sboms/auditwheel.cdx.json b/.venv/lib/python3.11/site-packages/scikit_learn-1.9.0.dist-info/sboms/auditwheel.cdx.json new file mode 100644 index 0000000000000000000000000000000000000000..b44e220642933bb4154fd0d6a564b0e0a7ed89a7 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scikit_learn-1.9.0.dist-info/sboms/auditwheel.cdx.json @@ -0,0 +1 @@ +{"bomFormat": "CycloneDX", "specVersion": "1.4", "version": 1, "metadata": {"component": {"type": "library", "bom-ref": "pkg:pypi/scikit_learn@1.9.0?file_name=scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", "name": "scikit_learn", "version": "1.9.0", "purl": "pkg:pypi/scikit_learn@1.9.0?file_name=scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl"}, "tools": [{"name": "auditwheel", "version": "6.6.0"}]}, "components": [{"type": "library", "bom-ref": "pkg:pypi/scikit_learn@1.9.0?file_name=scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", "name": "scikit_learn", "version": "1.9.0", "purl": "pkg:pypi/scikit_learn@1.9.0?file_name=scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl"}, {"type": "library", "bom-ref": "pkg:rpm/almalinux/libgomp@8.5.0-28.el8_10.alma.1#c61017c9a24eb6e1e1a3cdc9becd004a6419cbda3d54b4848b98f240a4829571", "name": "libgomp", "version": "8.5.0-28.el8_10.alma.1", "purl": "pkg:rpm/almalinux/libgomp@8.5.0-28.el8_10.alma.1"}], "dependencies": [{"ref": "pkg:pypi/scikit_learn@1.9.0?file_name=scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", "dependsOn": ["pkg:rpm/almalinux/libgomp@8.5.0-28.el8_10.alma.1#c61017c9a24eb6e1e1a3cdc9becd004a6419cbda3d54b4848b98f240a4829571"]}, {"ref": "pkg:rpm/almalinux/libgomp@8.5.0-28.el8_10.alma.1#c61017c9a24eb6e1e1a3cdc9becd004a6419cbda3d54b4848b98f240a4829571"}]} \ No newline at end of file diff --git a/.venv/lib/python3.11/site-packages/scikit_learn.libs/libgomp-e985bcbb.so.1.0.0 b/.venv/lib/python3.11/site-packages/scikit_learn.libs/libgomp-e985bcbb.so.1.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..9640ae36f6254303393cf521e6920d0790e236db --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scikit_learn.libs/libgomp-e985bcbb.so.1.0.0 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a43904e4fa297301d4640dc1bb3c8a3480b406f99e498eba9b1914b68aab604a +size 253289 diff --git a/.venv/lib/python3.11/site-packages/scipy/__config__.py b/.venv/lib/python3.11/site-packages/scipy/__config__.py new file mode 100644 index 0000000000000000000000000000000000000000..6ca9582c86e5a6146bd870d2bba94adf596913b7 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scipy/__config__.py @@ -0,0 +1,173 @@ +# This file is generated by SciPy's build process +# It contains system_info results at the time of building this package. +from enum import Enum + +__all__ = ["show"] +_built_with_meson = True + + +class DisplayModes(Enum): + stdout = "stdout" + dicts = "dicts" + + +def _cleanup(d): + """ + Removes empty values in a `dict` recursively + This ensures we remove values that Meson could not provide to CONFIG + """ + if isinstance(d, dict): + return { k: _cleanup(v) for k, v in d.items() if v != '' and _cleanup(v) != '' } + else: + return d + + +CONFIG = _cleanup( + { + "Compilers": { + "c": { + "name": "gcc", + "linker": r"ld.bfd", + "version": "14.2.1", + "commands": r"cc", + "args": r"", + "linker args": r"", + }, + "cython": { + "name": r"cython", + "linker": r"cython", + "version": r"3.2.4", + "commands": r"cython", + "args": r"", + "linker args": r"", + }, + "c++": { + "name": "gcc", + "linker": r"ld.bfd", + "version": "14.2.1", + "commands": r"c++", + "args": r"", + "linker args": r"", + }, + "fortran": { + "name": "gcc", + "linker": r"ld.bfd", + "version": "14.2.1", + "commands": r"gfortran", + "args": r"", + "linker args": r"", + }, + "pythran": { + "version": r"0.18.1", + "include directory": r"../../tmp/build-env-vm5wde73/lib/python3.11/site-packages/pythran" + }, + }, + "Machine Information": { + "host": { + "cpu": r"x86_64", + "family": r"x86_64", + "endian": r"little", + "system": r"linux", + }, + "build": { + "cpu": r"x86_64", + "family": r"x86_64", + "endian": r"little", + "system": r"linux", + }, + "cross-compiled": bool("False".lower().replace('false', '')), + }, + "Build Dependencies": { + "blas": { + "name": "scipy-openblas", + "found": bool("True".lower().replace('false', '')), + "version": "0.3.30", + "detection method": "pkgconfig", + "include directory": r"/opt/_internal/cpython-3.11.13/lib/python3.11/site-packages/scipy_openblas32/include", + "lib directory": r"/opt/_internal/cpython-3.11.13/lib/python3.11/site-packages/scipy_openblas32/lib", + "openblas configuration": r"OpenBLAS 0.3.30 DYNAMIC_ARCH NO_AFFINITY Haswell MAX_THREADS=64", + "pc file directory": r"/project/", + "has ilp64": bool(r"False".lower().replace('false', '')), + }, + "lapack": { + "name": "scipy-openblas", + "found": bool("True".lower().replace('false', '')), + "version": "0.3.30", + "detection method": "pkgconfig", + "include directory": r"/opt/_internal/cpython-3.11.13/lib/python3.11/site-packages/scipy_openblas32/include", + "lib directory": r"/opt/_internal/cpython-3.11.13/lib/python3.11/site-packages/scipy_openblas32/lib", + "openblas configuration": r"OpenBLAS 0.3.30 DYNAMIC_ARCH NO_AFFINITY Haswell MAX_THREADS=64", + "pc file directory": r"/project/", + "has ilp64": bool(r"False".lower().replace('false', '')), + }, + "pybind11": { + "name": "pybind11", + "version": "3.0.2", + "detection method": "config-tool", + "include directory": r"unknown", + }, + }, + "Python Information": { + "path": r"/tmp/build-env-vm5wde73/bin/python", + "version": "3.11", + }, + } +) + + +def _check_pyyaml(): + import yaml + + return yaml + + +def show(mode=DisplayModes.stdout.value): + """ + Show libraries and system information on which SciPy was built + and is being used + + Parameters + ---------- + mode : {`'stdout'`, `'dicts'`}, optional. + Indicates how to display the config information. + `'stdout'` prints to console, `'dicts'` returns a dictionary + of the configuration. + + Returns + ------- + out : {`dict`, `None`} + If mode is `'dicts'`, a dict is returned, else None + + Examples + -------- + >>> import scipy + >>> scipy.show_config() + ... # formatted output is printed to the console + + >>> config_dict = scipy.show_config(mode='dicts') + >>> list(config_dict.keys()) + ['Compilers', 'Machine Information', 'Build Dependencies', 'Python Information'] + + Notes + ----- + 1. The `'stdout'` mode will give more readable + output if ``pyyaml`` is installed + + """ + if mode == DisplayModes.stdout.value: + try: # Non-standard library, check import + yaml = _check_pyyaml() + + print(yaml.dump(CONFIG)) + except ModuleNotFoundError: + import warnings + import json + + warnings.warn("Install `pyyaml` for better output", stacklevel=1) + print(json.dumps(CONFIG, indent=2)) + elif mode == DisplayModes.dicts.value: + return CONFIG + else: + raise AttributeError( + f"Invalid `mode`, use one of: {', '.join([e.value for e in DisplayModes])}" + ) diff --git a/.venv/lib/python3.11/site-packages/scipy/__init__.py b/.venv/lib/python3.11/site-packages/scipy/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..cbbe00ea4b7187aa9768a15ef2ef1dd7ef5a48c3 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scipy/__init__.py @@ -0,0 +1,138 @@ +""" +SciPy: A scientific computing package for Python +================================================ + +Documentation is available in the docstrings and +online at https://docs.scipy.org/doc/scipy/ + +Subpackages +----------- +:: + + cluster --- Vector Quantization / Kmeans + constants --- Physical and mathematical constants and units + datasets --- Dataset methods + differentiate --- Finite difference differentiation tools + fft --- Discrete Fourier transforms + fftpack --- Legacy discrete Fourier transforms + integrate --- Integration routines + interpolate --- Interpolation Tools + io --- Data input and output + linalg --- Linear algebra routines + ndimage --- N-D image package + odr --- Orthogonal Distance Regression + optimize --- Optimization Tools + signal --- Signal Processing Tools + sparse --- Sparse Matrices + spatial --- Spatial data structures and algorithms + special --- Special functions + stats --- Statistical Functions + +Public API in the main SciPy namespace +-------------------------------------- +:: + + __version__ --- SciPy version string + LowLevelCallable --- Low-level callback function + show_config --- Show scipy build configuration + test --- Run scipy unittests + +""" + +import importlib as _importlib + +from numpy import __version__ as __numpy_version__ + + +try: + from scipy.__config__ import show as show_config +except ImportError as e: + msg = """Error importing SciPy: you cannot import SciPy while + being in scipy source directory; please exit the SciPy source + tree first and relaunch your Python interpreter.""" + raise ImportError(msg) from e + + +from scipy.version import version as __version__ + + +# Allow distributors to run custom init code +from . import _distributor_init +del _distributor_init + + +from scipy._lib import _pep440 +# In maintenance branch, change to np_maxversion N+3 if numpy is at N +np_minversion = '1.26.4' +np_maxversion = '2.7.0' +if (_pep440.parse(__numpy_version__) < _pep440.Version(np_minversion) or + _pep440.parse(__numpy_version__) >= _pep440.Version(np_maxversion)): + import warnings + warnings.warn(f"A NumPy version >={np_minversion} and <{np_maxversion}" + f" is required for this version of SciPy (detected " + f"version {__numpy_version__})", + UserWarning, stacklevel=2) +del _pep440 + + +# This is the first import of an extension module within SciPy. If there's +# a general issue with the install, such that extension modules are missing +# or cannot be imported, this is where we'll get a failure - so give an +# informative error message. +try: + from scipy._lib._ccallback import LowLevelCallable +except ImportError as e: + msg = "The `scipy` install you are using seems to be broken, " + \ + "(extension modules cannot be imported), " + \ + "please try reinstalling." + raise ImportError(msg) from e + + +from scipy._lib._testutils import PytestTester +test = PytestTester(__name__) +del PytestTester + + +submodules = [ + 'cluster', + 'constants', + 'datasets', + 'differentiate', + 'fft', + 'fftpack', + 'integrate', + 'interpolate', + 'io', + 'linalg', + 'ndimage', + 'odr', + 'optimize', + 'signal', + 'sparse', + 'spatial', + 'special', + 'stats' +] + +__all__ = submodules + [ + 'LowLevelCallable', + 'test', + 'show_config', + '__version__', +] + + +def __dir__(): + return __all__ + + +def __getattr__(name): + if name in submodules: + return _importlib.import_module(f'scipy.{name}') + else: + try: + return globals()[name] + except KeyError: + raise AttributeError( + f"Module 'scipy' has no attribute '{name}'" + ) diff --git a/.venv/lib/python3.11/site-packages/scipy/__pycache__/__config__.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/__pycache__/__config__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..515c4bf71a52bc118ba1a6b99a71355a66f07ef9 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/__pycache__/__config__.cpython-311.pyc differ diff --git a/.venv/lib/python3.11/site-packages/scipy/__pycache__/__init__.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a2f708638f28a17f5d92810c70c92c7fc11fae9f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/__pycache__/__init__.cpython-311.pyc differ diff --git a/.venv/lib/python3.11/site-packages/scipy/__pycache__/_distributor_init.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/__pycache__/_distributor_init.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..601a88a51eda842d1b66909fd15c1057fe738593 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/__pycache__/_distributor_init.cpython-311.pyc differ diff --git a/.venv/lib/python3.11/site-packages/scipy/__pycache__/version.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/__pycache__/version.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d34ec89c791c874e203db07aad79a864532f2423 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/__pycache__/version.cpython-311.pyc differ diff --git a/.venv/lib/python3.11/site-packages/scipy/_cyutility.cpython-311-x86_64-linux-gnu.so b/.venv/lib/python3.11/site-packages/scipy/_cyutility.cpython-311-x86_64-linux-gnu.so new file mode 100644 index 0000000000000000000000000000000000000000..b2a35db174f192ce4c77db5b342c45e37b7f8288 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scipy/_cyutility.cpython-311-x86_64-linux-gnu.so @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18a80ec93130f6bde42aa8cb2c098248979a656352f9a24d18880eb865816438 +size 214968 diff --git a/.venv/lib/python3.11/site-packages/scipy/_distributor_init.py b/.venv/lib/python3.11/site-packages/scipy/_distributor_init.py new file mode 100644 index 0000000000000000000000000000000000000000..5df134975aa27d31beaff74c3cbfd2d3fb0a55dd --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scipy/_distributor_init.py @@ -0,0 +1,18 @@ +""" Distributor init file + +Distributors: you can replace the contents of this file with your own custom +code to support particular distributions of SciPy. + +For example, this is a good place to put any checks for hardware requirements +or BLAS/LAPACK library initialization. + +The SciPy standard source distribution will not put code in this file beyond +the try-except import of `_distributor_init_local` (which is not part of a +standard source distribution), so you can safely replace this file with your +own version. +""" + +try: + from . import _distributor_init_local # noqa: F401 +except ImportError: + pass diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/__init__.py b/.venv/lib/python3.11/site-packages/scipy/_lib/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2140970015d099763797d60a98a3a3b6f4b5f219 --- /dev/null +++ b/.venv/lib/python3.11/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/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/__init__.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9169f568a23ea651d60d159b855db4f0cb45a692 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/__init__.cpython-311.pyc differ diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_array_api.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_array_api.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..20bfbc0965cb157a561004fae416109d707389fd Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_array_api.cpython-311.pyc differ diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_array_api_compat_vendor.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_array_api_compat_vendor.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..51f764218c77802343f34b7ee8364aedff05baf6 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_array_api_compat_vendor.cpython-311.pyc differ diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_array_api_override.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_array_api_override.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..17d024ec42b0dc55a30053188bd435091b7fa890 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_array_api_override.cpython-311.pyc differ diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_ccallback.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_ccallback.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86530cb803c48fce97ebaedfa880ee3f9a05d8dd Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_ccallback.cpython-311.pyc differ diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_docscrape.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_docscrape.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c45b1145b07cd6888d222ecb971628d6823abda4 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_docscrape.cpython-311.pyc differ diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_pep440.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_pep440.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b94ca55c4661ebc15ccb59e3563e0aebcd3f807b Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_pep440.cpython-311.pyc differ diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_sparse.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_sparse.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9200e1d47c785fa680b50e256e10aaf2e2d8866f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_sparse.cpython-311.pyc differ diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_testutils.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_testutils.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3fe57cc8265c2e68f3030b9e494888b1c66964c6 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_testutils.cpython-311.pyc differ diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_util.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_util.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..742219eab0ddf978355ad544b1fb26d57891fe4f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/_util.cpython-311.pyc differ diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/deprecation.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/deprecation.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b94dc9e0ab017a3fa03fef960e2dc86c825320d9 Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/__pycache__/deprecation.cpython-311.pyc differ diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/_array_api.py b/.venv/lib/python3.11/site-packages/scipy/_lib/_array_api.py new file mode 100644 index 0000000000000000000000000000000000000000..127dae4dacb11099b127fc55ce3ae8cd9d67ddeb --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scipy/_lib/_array_api.py @@ -0,0 +1,1036 @@ +"""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 +""" +import operator +import dataclasses +import functools +import textwrap + +from collections.abc import Generator +from contextlib import contextmanager +from contextvars import ContextVar +from types import ModuleType +from typing import Any, Literal, TypeAlias +from collections.abc import Iterable + +import numpy as np +import numpy.typing as npt + +from scipy._lib.array_api_compat import ( + is_array_api_obj, + is_lazy_array, + is_numpy_array, + is_cupy_array, + is_torch_array, + is_jax_array, + is_dask_array, + size as xp_size, + numpy as np_compat, + device as xp_device, + is_numpy_namespace as is_numpy, + is_cupy_namespace as is_cupy, + is_torch_namespace as is_torch, + is_jax_namespace as is_jax, + is_dask_namespace as is_dask, + is_array_api_strict_namespace as is_array_api_strict, +) +from scipy._lib.array_api_compat.common._helpers import _compat_module_name +from scipy._lib.array_api_extra.testing import lazy_xp_function +from scipy._lib._array_api_override import ( + array_namespace, SCIPY_ARRAY_API, SCIPY_DEVICE +) +from scipy._lib._docscrape import FunctionDoc +from scipy._lib import array_api_extra as xpx + + +__all__ = [ + '_asarray', 'array_namespace', 'assert_almost_equal', 'assert_array_almost_equal', + 'default_xp', 'eager_warns', 'is_lazy_array', 'is_marray', + 'is_array_api_strict', 'is_complex', 'is_cupy', 'is_jax', 'is_numpy', 'is_torch', + 'np_compat', 'get_native_namespace_name', + 'SCIPY_ARRAY_API', 'SCIPY_DEVICE', 'scipy_namespace_for', + 'xp_assert_close', 'xp_assert_equal', 'xp_assert_less', + 'xp_copy', 'xp_device', 'xp_ravel', 'xp_size', + 'xp_unsupported_param_msg', 'xp_vector_norm', 'xp_capabilities', + 'xp_result_type', 'xp_promote', + 'make_xp_test_case', 'make_xp_pytest_marks', 'make_xp_pytest_param', +] + + +Array: TypeAlias = Any # To be changed to a Protocol later (see array-api#589) +ArrayLike: TypeAlias = Array | npt.ArrayLike + + +def _check_finite(array: Array, xp: ModuleType) -> None: + """Check for NaNs or Infs.""" + if not xp.all(xp.isfinite(array)): + msg = "array must not contain infs or NaNs" + raise ValueError(msg) + +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 is_numpy(xp): + # 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) + 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 xp_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: for older NumPy versions, `np.asarray` did not support the `copy` kwarg, + # so this uses our other helper `_asarray`. + if xp is None: + xp = array_namespace(x) + + return _asarray(x, copy=True, xp=xp) + + +def _xp_copy_to_numpy(x: Array) -> np.ndarray: + """Copies a possibly on device array to a NumPy array. + + This function is intended only for converting alternative backend + arrays to numpy arrays within test code, to make it easier for use + of the alternative backend to be isolated only to the function being + tested. `_xp_copy_to_numpy` should NEVER be used except in test code + for the specific purpose mentioned above. In production code, attempts + to copy device arrays to NumPy arrays should fail, or else functions + may appear to be working on the GPU when they actually aren't. + + Parameters + ---------- + x : array + + Returns + ------- + ndarray + """ + xp = array_namespace(x) + if is_numpy(xp): + return x.copy() + if is_cupy(xp): + return x.get() + if is_torch(xp): + return x.cpu().numpy() + if is_array_api_strict(xp): + # array api strict supports multiple devices, so need to + # ensure x is on the cpu before copying to NumPy. + return np.asarray( + xp.asarray(x, device=xp.Device("CPU_DEVICE")), copy=True + ) + # Fall back to np.asarray. This works for dask.array. It + # currently works for jax.numpy, but hopefully JAX will make + # the transfer guard workable enough for use in scipy tests, in + # which case, JAX will have to be handled explicitly. + # If new backends are added, they may require explicit handling as + # well. + return np.asarray(x, copy=True) + + +_default_xp_ctxvar: ContextVar[ModuleType] = ContextVar("_default_xp") + +@contextmanager +def default_xp(xp: ModuleType) -> Generator[None, None, None]: + """In all ``xp_assert_*`` and ``assert_*`` function calls executed within this + context manager, test by default that the array namespace is + the provided across all arrays, unless one explicitly passes the ``xp=`` + parameter or ``check_namespace=False``. + + Without this context manager, the default value for `xp` is the namespace + for the desired array (the second parameter of the tests). + """ + token = _default_xp_ctxvar.set(xp) + try: + yield + finally: + _default_xp_ctxvar.reset(token) + + +def eager_warns(warning_type, *, match=None, xp): + """pytest.warns context manager if arrays of specified namespace are always eager. + + Otherwise, context manager that *ignores* specified warning. + """ + import pytest + from scipy._lib._util import ignore_warns + if is_numpy(xp) or is_array_api_strict(xp) or is_cupy(xp): + return pytest.warns(warning_type, match=match) + return ignore_warns(warning_type, match='' if match is None else match) + + +def _strict_check(actual, desired, xp, *, + check_namespace=True, check_dtype=True, check_shape=True, + check_0d=True): + __tracebackhide__ = True # Hide traceback for py.test + + if xp is None: + try: + xp = _default_xp_ctxvar.get() + except LookupError: + xp = array_namespace(desired) + + if check_namespace: + _assert_matching_namespace(actual, desired, xp) + + # only NumPy distinguishes between scalars and arrays; we do if check_0d=True. + # do this first so we can then cast to array (and thus use the array API) below. + if is_numpy(xp) and check_0d: + _msg = ("Array-ness does not match:\n Actual: " + f"{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 + + actual = xp.asarray(actual) + 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: + if is_dask(xp): + actual.compute_chunk_sizes() + desired.compute_chunk_sizes() + _msg = f"Shapes do not match.\nActual: {actual.shape}\nDesired: {desired.shape}" + assert actual.shape == desired.shape, _msg + + desired = xp.broadcast_to(desired, actual.shape) + return actual, desired, xp + + +def _assert_matching_namespace(actual, desired, xp): + __tracebackhide__ = True # Hide traceback for py.test + + desired_arr_space = array_namespace(desired) + _msg = ("Namespace of desired array does not match expectations " + "set by the `default_xp` context manager or by the `xp`" + "pytest fixture.\n" + f"Desired array's space: {desired_arr_space.__name__}\n" + f"Expected namespace: {xp.__name__}") + assert desired_arr_space == xp, _msg + + actual_arr_space = array_namespace(actual) + _msg = ("Namespace of actual and desired arrays do not match.\n" + f"Actual: {actual_arr_space.__name__}\n" + f"Desired: {xp.__name__}") + assert actual_arr_space == xp, _msg + + +def xp_assert_equal(actual, desired, *, check_namespace=True, check_dtype=True, + check_shape=True, check_0d=True, err_msg='', xp=None): + __tracebackhide__ = True # Hide traceback for py.test + + actual, desired, xp = _strict_check( + actual, desired, xp, check_namespace=check_namespace, + check_dtype=check_dtype, check_shape=check_shape, + check_0d=check_0d + ) + + 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, check_0d=True, + err_msg='', xp=None): + __tracebackhide__ = True # Hide traceback for py.test + + actual, desired, xp = _strict_check( + actual, desired, xp, + check_namespace=check_namespace, check_dtype=check_dtype, + check_shape=check_shape, check_0d=check_0d + ) + + 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_close_nulp(actual, desired, *, nulp=1, check_namespace=True, + check_dtype=True, check_shape=True, check_0d=True, + err_msg='', xp=None): + __tracebackhide__ = True # Hide traceback for py.test + + actual, desired, xp = _strict_check( + actual, desired, xp, + check_namespace=check_namespace, check_dtype=check_dtype, + check_shape=check_shape, check_0d=check_0d + ) + + actual, desired = map(_xp_copy_to_numpy, (actual, desired)) + return np.testing.assert_array_almost_equal_nulp(actual, desired, nulp=nulp) + + +def xp_assert_less(actual, desired, *, check_namespace=True, check_dtype=True, + check_shape=True, check_0d=True, err_msg='', verbose=True, xp=None): + __tracebackhide__ = True # Hide traceback for py.test + + actual, desired, xp = _strict_check( + actual, desired, xp, check_namespace=check_namespace, + check_dtype=check_dtype, check_shape=check_shape, + check_0d=check_0d + ) + + 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 assert_array_almost_equal(actual, desired, decimal=6, *args, **kwds): + """Backwards compatible replacement. In new code, use xp_assert_close instead. + """ + rtol, atol = 0, 1.5*10**(-decimal) + return xp_assert_close(actual, desired, + atol=atol, rtol=rtol, check_dtype=False, check_shape=False, + *args, **kwds) + + +def assert_almost_equal(actual, desired, decimal=7, *args, **kwds): + """Backwards compatible replacement. In new code, use xp_assert_close instead. + """ + rtol, atol = 0, 1.5*10**(-decimal) + return xp_assert_close(actual, desired, + atol=atol, rtol=rtol, check_dtype=False, check_shape=False, + *args, **kwds) + + +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_native_namespace_name(xp: ModuleType) -> str: + """Return name for native namespace (without array_api_compat prefix).""" + name = xp.__name__ + return name.removeprefix(f"{_compat_module_name()}.") + + +def scipy_namespace_for(xp: ModuleType) -> ModuleType | None: + """Return the `scipy`-like namespace of a non-NumPy backend + + That is, return the namespace corresponding with backend `xp` that contains + `scipy` sub-namespaces like `linalg` and `special`. If no such namespace + exists, return ``None``. Useful for dispatching. + """ + + 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 + + if is_torch(xp): + return xp + + return None + + +# maybe use `scipy.linalg` if/when array API support is added +def xp_vector_norm(x: Array, /, *, + axis: int | tuple[int] | None = None, + keepdims: bool = False, + ord: int | float = 2, + xp: ModuleType | None = None) -> Array: + xp = array_namespace(x) if xp is None else xp + + if SCIPY_ARRAY_API: + # check for optional `linalg` extension + if hasattr(xp, 'linalg'): + return xp.linalg.vector_norm(x, axis=axis, keepdims=keepdims, ord=ord) + else: + if ord != 2: + raise ValueError( + "only the Euclidean norm (`ord=2`) is currently supported in " + "`xp_vector_norm` for backends not implementing the `linalg` " + "extension." + ) + # return (x @ x)**0.5 + # or to get the right behavior with nd, complex arrays + return xp.sum(xp.conj(x) * x, axis=axis, keepdims=keepdims)**0.5 + else: + # to maintain backwards compatibility + return np.linalg.norm(x, ord=ord, axis=axis, keepdims=keepdims) + + +def xp_ravel(x: Array, /, *, xp: ModuleType | None = None) -> Array: + # Equivalent of np.ravel written in terms of array API + # Even though it's one line, it comes up so often that it's worth having + # this function for readability + xp = array_namespace(x) if xp is None else xp + return xp.reshape(x, (-1,)) + + +def xp_swapaxes(a, axis1, axis2, xp=None): + # Equivalent of np.swapaxes written in terms of array API + xp = array_namespace(a) if xp is None else xp + axes = list(range(a.ndim)) + axes[axis1], axes[axis2] = axes[axis2], axes[axis1] + a = xp.permute_dims(a, axes) + return a + + +# utility to find common dtype with option to force floating +def xp_result_type(*args, force_floating=False, xp): + """ + Returns the dtype that results from applying type promotion rules + (see Array API Standard Type Promotion Rules) to the arguments. Augments + standard `result_type` in a few ways: + + - There is a `force_floating` argument that ensures that the result type + is floating point, even when all args are integer. + - When a TypeError is raised (e.g. due to an unsupported promotion) + and `force_floating=True`, we define a custom rule: use the result type + of the default float and any other floats passed. See + https://github.com/scipy/scipy/pull/22695/files#r1997905891 + for rationale. + - This function accepts array-like iterables, which are immediately converted + to the namespace's arrays before result type calculation. Consequently, the + result dtype may be different when an argument is `1.` vs `[1.]`. + + Typically, this function will be called shortly after `array_namespace` + on a subset of the arguments passed to `array_namespace`. + """ + # prevent double conversion of iterable to array + # avoid `np.iterable` for torch arrays due to pytorch/pytorch#143334 + # don't use `array_api_compat.is_array_api_obj` as it returns True for NumPy scalars + args = [(_asarray(arg, subok=True, xp=xp) if is_torch_array(arg) or np.iterable(arg) + else arg) for arg in args] + args_not_none = [arg for arg in args if arg is not None] + if force_floating: + args_not_none.append(1.0) + + if is_numpy(xp) and xp.__version__ < '2.0': + # Follow NEP 50 promotion rules anyway + args_not_none = [arg.dtype if getattr(arg, 'size', 0) == 1 else arg + for arg in args_not_none] + return xp.result_type(*args_not_none) + + try: # follow library's preferred promotion rules + return xp.result_type(*args_not_none) + except TypeError: # mixed type promotion isn't defined + if not force_floating: + raise + # use `result_type` of default floating point type and any floats present + # This can be revisited, but right now, the only backends that get here + # are array-api-strict (which is not for production use) and PyTorch + # (due to data-apis/array-api-compat#279). + float_args = [] + for arg in args_not_none: + arg_array = xp.asarray(arg) if np.isscalar(arg) else arg + dtype = getattr(arg_array, 'dtype', arg) + if xp.isdtype(dtype, ('real floating', 'complex floating')): + float_args.append(arg) + return xp.result_type(*float_args, xp_default_dtype(xp)) + + +def xp_promote(*args, broadcast=False, force_floating=False, xp): + """ + Promotes elements of *args to result dtype, ignoring `None`s. + Includes options for forcing promotion to floating point and + broadcasting the arrays, again ignoring `None`s. + Type promotion rules follow `xp_result_type` instead of `xp.result_type`. + + Typically, this function will be called shortly after `array_namespace` + on a subset of the arguments passed to `array_namespace`. + + This function accepts array-like iterables, which are immediately converted + to the namespace's arrays before result type calculation. Consequently, the + result dtype may be different when an argument is `1.` vs `[1.]`. + + See Also + -------- + xp_result_type + """ + if not args: + return args + + # prevent double conversion of iterable to array + # avoid `np.iterable` for torch arrays due to pytorch/pytorch#143334 + # don't use `array_api_compat.is_array_api_obj` as it returns True for NumPy scalars + args = [(_asarray(arg, subok=True, xp=xp) if is_torch_array(arg) or np.iterable(arg) + else arg) for arg in args] + + dtype = xp_result_type(*args, force_floating=force_floating, xp=xp) + + args = [(_asarray(arg, dtype=dtype, subok=True, xp=xp) if arg is not None else arg) + for arg in args] + + if not broadcast: + return args[0] if len(args)==1 else tuple(args) + + args_not_none = [arg for arg in args if arg is not None] + + # determine result shape + shapes = {arg.shape for arg in args_not_none} + try: + shape = (np.broadcast_shapes(*shapes) if len(shapes) != 1 + else args_not_none[0].shape) + except ValueError as e: + message = "Array shapes are incompatible for broadcasting." + raise ValueError(message) from e + + out = [] + for arg in args: + if arg is None: + out.append(arg) + continue + + # broadcast only if needed + # Even if two arguments need broadcasting, this is faster than + # `broadcast_arrays`, especially since we've already determined `shape` + if arg.shape != shape: + kwargs = {'subok': True} if is_numpy(xp) else {} + arg = xp.broadcast_to(arg, shape, **kwargs) + + # This is much faster than xp.astype(arg, dtype, copy=False) + if arg.dtype != dtype: + arg = xp.astype(arg, dtype) + + out.append(arg) + + return out[0] if len(out)==1 else tuple(out) + + +def xp_float_to_complex(arr: Array, xp: ModuleType | None = None) -> Array: + xp = array_namespace(arr) if xp is None else xp + arr_dtype = arr.dtype + # The standard float dtypes are float32 and float64. + # Convert float32 to complex64, + # and float64 (and non-standard real dtypes) to complex128 + if xp.isdtype(arr_dtype, xp.float32): + arr = xp.astype(arr, xp.complex64) + elif xp.isdtype(arr_dtype, 'real floating'): + arr = xp.astype(arr, xp.complex128) + + return arr + + +def xp_default_dtype(xp): + """Query the namespace-dependent default floating-point dtype. + """ + if is_torch(xp): + # historically, we allow pytorch to keep its default of float32 + return xp.get_default_dtype() + else: + # we default to float64 + return xp.float64 + + +### MArray Helpers ### +def xp_result_device(*args): + """Return the device of an array in `args`, for the purpose of + input-output device propagation. + If there are multiple devices, return an arbitrary one. + If there are no arrays, return None (this typically happens only on NumPy). + """ + for arg in args: + # Do not do a duck-type test for the .device attribute, as many backends today + # don't have it yet. See workarouunds in array_api_compat.device(). + if is_array_api_obj(arg): + return xp_device(arg) + return None + + +# np.r_ replacement +def concat_1d(xp: ModuleType | None, *arrays: Iterable[ArrayLike]) -> Array: + """A replacement for `np.r_` as `xp.concat` does not accept python scalars + or 0-D arrays. + """ + arys = [xpx.atleast_nd(xp.asarray(a), ndim=1, xp=xp) for a in arrays] + return xp.concat(arys) + + +def is_marray(xp): + """Returns True if `xp` is an MArray namespace; False otherwise.""" + return "marray" in xp.__name__ + + +def _length_nonmasked(x, axis, keepdims=False, xp=None): + xp = array_namespace(x) if xp is None else xp + if is_marray(xp): + if np.iterable(axis): + message = '`axis` must be an integer or None for use with `MArray`.' + raise NotImplementedError(message) + return xp.astype(xp.count(x, axis=axis, keepdims=keepdims), x.dtype) + return (xp_size(x) if axis is None else + # compact way to deal with axis tuples or ints + int(np.prod(np.asarray(x.shape)[np.asarray(axis)]))) + + +def _share_masks(*args, xp): + if is_marray(xp): + mask = functools.reduce(operator.or_, (arg.mask for arg in args)) + args = [xp.asarray(arg.data, mask=mask) for arg in args] + return args[0] if len(args) == 1 else args + +### End MArray Helpers ### + + +@dataclasses.dataclass(repr=False) +class _XPSphinxCapability: + cpu: bool | None # None if not applicable + gpu: bool | None + warnings: list[str] = dataclasses.field(default_factory=list) + + def _render(self, value): + if value is None: + return "n/a" + if not value: + return "⛔" + if self.warnings: + res = "⚠️ " + '; '.join(self.warnings) + assert len(res) <= 20, "Warnings too long" + return res + return "✅" + + def __str__(self): + cpu = self._render(self.cpu) + gpu = self._render(self.gpu) + return f"{cpu:20} {gpu:20}" + + +def _make_sphinx_capabilities( + # lists of tuples [(module name, reason), ...] + skip_backends=(), xfail_backends=(), + # @pytest.mark.skip/xfail_xp_backends kwargs + cpu_only=False, np_only=False, out_of_scope=False, exceptions=(), + # xpx.lazy_xp_backends kwargs + allow_dask_compute=False, jax_jit=True, + # list of tuples [(module name, reason), ...] + warnings = (), + # unused in documentation + reason=None, +): + if out_of_scope: + return {"out_of_scope": True} + + exceptions = set(exceptions) + + # Default capabilities + capabilities = { + "numpy": _XPSphinxCapability(cpu=True, gpu=None), + "array_api_strict": _XPSphinxCapability(cpu=True, gpu=None), + "cupy": _XPSphinxCapability(cpu=None, gpu=True), + "torch": _XPSphinxCapability(cpu=True, gpu=True), + "jax.numpy": _XPSphinxCapability(cpu=True, gpu=True, + warnings=[] if jax_jit else ["no JIT"]), + # Note: Dask+CuPy is currently untested and unsupported + "dask.array": _XPSphinxCapability(cpu=True, gpu=None, + warnings=["computes graph"] if allow_dask_compute else []), + } + + # documentation doesn't display the reason + for module, _ in list(skip_backends) + list(xfail_backends): + backend = capabilities[module] + if backend.cpu is not None: + backend.cpu = False + if backend.gpu is not None: + backend.gpu = False + + for module, backend in capabilities.items(): + if np_only and module not in exceptions | {"numpy"}: + if backend.cpu is not None: + backend.cpu = False + if backend.gpu is not None: + backend.gpu = False + elif cpu_only and module not in exceptions and backend.gpu is not None: + backend.gpu = False + + for module, warning in warnings: + backend = capabilities[module] + backend.warnings.append(warning) + + return capabilities + + +def _make_capabilities_note(fun_name, capabilities, extra_note=None): + if "out_of_scope" in capabilities: + # It will be better to link to a section of the dev-arrayapi docs + # that explains what is and isn't in-scope, but such a section + # doesn't exist yet. Using :ref:`dev-arrayapi` as a placeholder. + note = f""" + **Array API Standard Support** + + `{fun_name}` is not in-scope for support of Python Array API Standard compatible + backends other than NumPy. + + See :ref:`dev-arrayapi` for more information. + """ + return textwrap.dedent(note) + + # Note: deliberately not documenting array-api-strict + note = f""" + **Array API Standard Support** + + `{fun_name}` has experimental support for Python Array API Standard compatible + backends in addition to NumPy. Please consider testing these features + by setting an environment variable ``SCIPY_ARRAY_API=1`` and providing + CuPy, PyTorch, JAX, or Dask arrays as array arguments. The following + combinations of backend and device (or other capability) are supported. + + ==================== ==================== ==================== + Library CPU GPU + ==================== ==================== ==================== + NumPy {capabilities['numpy'] } + CuPy {capabilities['cupy'] } + PyTorch {capabilities['torch'] } + JAX {capabilities['jax.numpy'] } + Dask {capabilities['dask.array'] } + ==================== ==================== ==================== + + """ + (extra_note or "") + " See :ref:`dev-arrayapi` for more information." + + return textwrap.dedent(note) + + +def xp_capabilities( + *, + # Alternative capabilities table. + # Used only for testing this decorator. + capabilities_table=None, + # Generate pytest.mark.skip/xfail_xp_backends. + # See documentation in conftest.py. + # lists of tuples [(module name, reason), ...] + skip_backends=(), xfail_backends=(), + cpu_only=False, np_only=False, reason=None, + out_of_scope=False, exceptions=(), + # lists of tuples [(module name, reason), ...] + warnings=(), + # xpx.testing.lazy_xp_function kwargs. + # Refer to array-api-extra documentation. + allow_dask_compute=False, jax_jit=True, + # Extra note to inject into the docstring + extra_note=None, +): + """Decorator for a function that states its support among various + Array API compatible backends. + + This decorator has two effects: + 1. It allows tagging tests with ``@make_xp_test_case`` or + ``make_xp_pytest_param`` (see below) to automatically generate + SKIP/XFAIL markers and perform additional backend-specific + testing, such as extra validation for Dask and JAX; + 2. It automatically adds a note to the function's docstring, containing + a table matching what has been tested. + + See Also + -------- + make_xp_test_case + make_xp_pytest_param + array_api_extra.testing.lazy_xp_function + """ + capabilities_table = (xp_capabilities_table if capabilities_table is None + else capabilities_table) + + if out_of_scope: + np_only = True + + capabilities = dict( + skip_backends=skip_backends, + xfail_backends=xfail_backends, + cpu_only=cpu_only, + np_only=np_only, + out_of_scope=out_of_scope, + reason=reason, + exceptions=exceptions, + allow_dask_compute=allow_dask_compute, + jax_jit=jax_jit, + warnings=warnings, + ) + sphinx_capabilities = _make_sphinx_capabilities(**capabilities) + + def decorator(f): + # Don't use a wrapper, as in some cases @xp_capabilities is + # applied to a ufunc + capabilities_table[f] = capabilities + note = _make_capabilities_note(f.__name__, sphinx_capabilities, extra_note) + doc = FunctionDoc(f) + doc['Notes'].append(note) + doc = str(doc).split("\n", 1)[1].lstrip(" \n") # remove signature + try: + f.__doc__ = doc + except AttributeError: + # Can't update __doc__ on ufuncs if SciPy + # was compiled against NumPy < 2.2. + pass + + return f + return decorator + + +def make_xp_test_case(*funcs, capabilities_table=None): + capabilities_table = (xp_capabilities_table if capabilities_table is None + else capabilities_table) + """Generate pytest decorator for a test function that tests functionality + of one or more Array API compatible functions. + + Read the parameters of the ``@xp_capabilities`` decorator applied to the + listed functions and: + + - Generate the ``@pytest.mark.skip_xp_backends`` and + ``@pytest.mark.xfail_xp_backends`` decorators + for the decorated test function + - Tag the function with `xpx.testing.lazy_xp_function` + + Example:: + + @make_xp_test_case(f1) + def test_f1(xp): + ... + + @make_xp_test_case(f2) + def test_f2(xp): + ... + + @make_xp_test_case(f1, f2) + def test_f1_and_f2(xp): + ... + + The above is equivalent to:: + @pytest.mark.skip_xp_backends(...) + @pytest.mark.skip_xp_backends(...) + @pytest.mark.xfail_xp_backends(...) + @pytest.mark.xfail_xp_backends(...) + def test_f1(xp): + ... + + etc., where the arguments of ``skip_xp_backends`` and ``xfail_xp_backends`` are + determined by the ``@xp_capabilities`` decorator applied to the functions. + + See Also + -------- + xp_capabilities + make_xp_pytest_marks + make_xp_pytest_param + array_api_extra.testing.lazy_xp_function + """ + marks = make_xp_pytest_marks(*funcs, capabilities_table=capabilities_table) + return lambda func: functools.reduce(lambda f, g: g(f), marks, func) + + +def make_xp_pytest_param(func, *args, capabilities_table=None): + """Variant of ``make_xp_test_case`` that returns a pytest.param for a function, + with all necessary skip_xp_backends and xfail_xp_backends marks applied:: + + @pytest.mark.parametrize( + "func", [make_xp_pytest_param(f1), make_xp_pytest_param(f2)] + ) + def test(func, xp): + ... + + The above is equivalent to:: + + @pytest.mark.parametrize( + "func", [ + pytest.param(f1, marks=[ + pytest.mark.skip_xp_backends(...), + pytest.mark.xfail_xp_backends(...), ...]), + pytest.param(f2, marks=[ + pytest.mark.skip_xp_backends(...), + pytest.mark.xfail_xp_backends(...), ...]), + ) + def test(func, xp): + ... + + Parameters + ---------- + func : Callable + Function to be tested. It must be decorated with ``@xp_capabilities``. + *args : Any, optional + Extra pytest parameters for the use case, e.g.:: + + @pytest.mark.parametrize("func,verb", [ + make_xp_pytest_param(f1, "hello"), + make_xp_pytest_param(f2, "world")]) + def test(func, verb, xp): + # iterates on (func=f1, verb="hello") + # and (func=f2, verb="world") + + See Also + -------- + xp_capabilities + make_xp_test_case + make_xp_pytest_marks + array_api_extra.testing.lazy_xp_function + """ + import pytest + + marks = make_xp_pytest_marks(func, capabilities_table=capabilities_table) + return pytest.param(func, *args, marks=marks, id=func.__name__) + + +def make_xp_pytest_marks(*funcs, capabilities_table=None): + """Variant of ``make_xp_test_case`` that returns a list of pytest marks, + which can be used with the module-level `pytestmark = ...` variable:: + + pytestmark = make_xp_pytest_marks(f1, f2) + + def test(xp): + ... + + In this example, the whole test module is dedicated to testing `f1` or `f2`, + and the two functions have the same capabilities, so it's unnecessary to + cherry-pick which test tests which function. + The above is equivalent to:: + + pytestmark = [ + pytest.mark.skip_xp_backends(...), + pytest.mark.xfail_xp_backends(...), ...]), + ] + + def test(xp): + ... + + See Also + -------- + xp_capabilities + make_xp_test_case + make_xp_pytest_param + array_api_extra.testing.lazy_xp_function + """ + capabilities_table = (xp_capabilities_table if capabilities_table is None + else capabilities_table) + import pytest + + marks = [] + for func in funcs: + capabilities = capabilities_table[func] + exceptions = capabilities['exceptions'] + reason = capabilities['reason'] + + if capabilities['cpu_only']: + marks.append(pytest.mark.skip_xp_backends( + cpu_only=True, exceptions=exceptions, reason=reason)) + if capabilities['np_only']: + marks.append(pytest.mark.skip_xp_backends( + np_only=True, exceptions=exceptions, reason=reason)) + + for mod_name, reason in capabilities['skip_backends']: + marks.append(pytest.mark.skip_xp_backends(mod_name, reason=reason)) + for mod_name, reason in capabilities['xfail_backends']: + marks.append(pytest.mark.xfail_xp_backends(mod_name, reason=reason)) + + lazy_kwargs = {k: capabilities[k] + for k in ('allow_dask_compute', 'jax_jit')} + lazy_xp_function(func, **lazy_kwargs) + + return marks + + +# Is it OK to have a dictionary that is mutated (once upon import) in many places? +xp_capabilities_table = {} # type: ignore[var-annotated] + + +def xp_device_type(a: Array) -> Literal["cpu", "cuda", None]: + if is_numpy_array(a): + return "cpu" + if is_cupy_array(a): + return "cuda" + if is_torch_array(a): + # TODO this can return other backends e.g. tpu but they're unsupported in scipy + return a.device.type + if is_jax_array(a): + # TODO this can return other backends e.g. tpu but they're unsupported in scipy + return "cuda" if (p := a.device.platform) == "gpu" else p + if is_dask_array(a): + return xp_device_type(a._meta) + # array-api-strict is a stand-in for unknown libraries; don't special-case it + return None diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/_array_api_compat_vendor.py b/.venv/lib/python3.11/site-packages/scipy/_lib/_array_api_compat_vendor.py new file mode 100644 index 0000000000000000000000000000000000000000..64d844ff46419252fd7b158700f0f4babc0f2258 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scipy/_lib/_array_api_compat_vendor.py @@ -0,0 +1,9 @@ +# DO NOT RENAME THIS FILE +# This is a hook for array_api_extra/src/array_api_extra/_lib/_compat.py +# to override functions of array_api_compat. + +from .array_api_compat import * # noqa: F403 +from ._array_api_override import array_namespace as scipy_array_namespace + +# overrides array_api_compat.array_namespace inside array-api-extra +array_namespace = scipy_array_namespace # type: ignore[assignment] diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/_array_api_docs_tables.py b/.venv/lib/python3.11/site-packages/scipy/_lib/_array_api_docs_tables.py new file mode 100644 index 0000000000000000000000000000000000000000..d3a14ee9257d58bbc558076493df5da253127d22 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scipy/_lib/_array_api_docs_tables.py @@ -0,0 +1,294 @@ +"""Generate flat tables showing Array API capabilities for use in docs. + +These tables are intended for presenting Array API capabilities across +a wide number of functions at once. Rows correspond to functions and +columns correspond to library/device/option combinations. +""" + +from collections import defaultdict +from enum import auto, Enum +from importlib import import_module +from types import ModuleType + +from scipy._lib._array_api import xp_capabilities_table +from scipy._lib._array_api import _make_sphinx_capabilities + +# For undocumented aliases of public functions which are kept around for +# backwards compatibility reasons. These should be excluded from the +# tables since they would be redundant. There are also no docs pages to +# link entries to. +ALIASES = { + "scipy.linalg": { + # Alias of scipy.linalg.solve_continuous_lyapunov + "solve_lyapunov", + }, + "scipy.ndimage": { + # Alias of scipy.ndimage.sum_labels + "sum", + }, + "scipy.special": { + # Alias of scipy.special.jv + "jn", + # Alias of scipy.special.roots_legendre + "p_roots", + # Alias of scipy.special.roots_chebyt + "t_roots", + # Alias of scipy.special.roots_chebyu + "u_roots", + # Alias of scipy.special.roots_chebyc + "c_roots", + # Alias of scipy.special.roots_chebys + "s_roots", + # Alias of scipy.special.roots_jacobi + "j_roots", + # Alias of scipy.special.roots_laguerre + "l_roots", + # Alias of scipy.special.roots_genlaguerre + "la_roots", + # Alias of scipy.special.roots_hermite + "h_roots", + # Alias of scipy.special.roots_hermitenorm + "he_roots", + # Alias of scipy.special.roots_gegenbauer + "cg_roots", + # Alias of scipy.special.roots_sh_legendre + "ps_roots", + # Alias of scipy.special.roots_sh_chebyt + "ts_roots", + # Alias of scipy.special.roots_chebyu + "us_roots", + # Alias of scipy.special.roots_sh_jacobi + "js_roots", + } +} + +# Shortened names for use in table. +BACKEND_NAMES_MAP = { + "jax.numpy": "jax", + "dask.array": "dask", +} + + +class BackendSupportStatus(Enum): + YES = auto() + NO = auto() + OUT_OF_SCOPE = auto() + UNKNOWN = auto() + + +def _process_capabilities_table_entry(entry: dict | None) -> dict[str, dict[str, bool]]: + """Returns dict showing alternative backend support in easy to consume form. + + Parameters + ---------- + entry : Optional[dict] + A dict with the structure of the values of the dict + scipy._lib._array_api.xp_capabilities_table. If None, it is + assumped that no alternative backends are supported. + Default: None. + + Returns + ------- + dict[str, dict[str, bool]] + The output dict currently has keys "cpu", "gpu", "jit" and "lazy". + The value associated to each key is itself a dict. The keys of + the inner dicts correspond to backends, with bool values stating + whether or not the backend is supported with a given device or + mode. Inapplicable backends do not appear in the inner dicts + (e.g. since cupy is gpu-only, it does not appear in the inner + dict keyed on "cpu"). Only alternative backends to NumPy are + included since NumPY support should be guaranteed. + + """ + # This is a template for the output format. If more backends and + # backend options are added, it will need to be updated manually. + # Entries start as boolean, but upon returning, will take values + # from the BackendSupportStatus Enum. + output = { + "cpu": {"torch": False, "jax": False, "dask": False}, + "gpu": {"cupy": False, "torch": False, "jax": False}, + "jit": {"jax": False}, + "lazy": {"dask": False}, + } + S = BackendSupportStatus + if entry is None: + # If there is no entry, assume no alternative backends are supported. + # If the list of supported backends will grows, this hard-coded dict + # will need to be updated. + return { + outer_key: {inner_key: S.UNKNOWN for inner_key in outer_value} + for outer_key, outer_value in output.items() + } + + if entry["out_of_scope"]: + # None is used to signify out-of-scope functions. + return { + outer_key: {inner_key: S.OUT_OF_SCOPE for inner_key in outer_value} + for outer_key, outer_value in output.items() + } + + # For now, use _make_sphinx_capabilities because that's where + # the relevant logic for determining what is and isn't + # supported based on xp_capabilities_table entries lives. + # Perhaps this logic should be decoupled from sphinx. + for backend, capabilities in _make_sphinx_capabilities(**entry).items(): + if backend in {"array_api_strict", "numpy"}: + continue + backend = BACKEND_NAMES_MAP.get(backend, backend) + cpu, gpu = capabilities.cpu, capabilities.gpu + if cpu is not None: + if backend not in output["cpu"]: + raise ValueError( + "Input capabilities table entry contains unhandled" + f" backend {backend} on cpu." + ) + output["cpu"][backend] = cpu + if gpu is not None: + if backend not in output["gpu"]: + raise ValueError( + "Input capabilities table entry contains unhandled" + f" backend {backend} on gpu." + ) + output["gpu"][backend] = gpu + if backend == "jax": + output["jit"]["jax"] = entry["jax_jit"] and output["cpu"]["jax"] + if backend == "dask.array": + support_lazy = not entry["allow_dask_compute"] and output["dask"] + output["lazy"]["dask"] = support_lazy + return { + outer_key: { + inner_key: S.YES if inner_value else S.NO + for inner_key, inner_value in outer_value.items() + } + for outer_key, outer_value in output.items() + } + + +def is_named_function_like_object(obj): + return ( + not isinstance(obj, ModuleType | type) + and callable(obj) and hasattr(obj, "__name__") + ) + + +def make_flat_capabilities_table( + modules: str | list[str], + backend_type: str, + /, + *, + capabilities_table: list[str] | None = None, +) -> list[dict[str, str]]: + """Generate full table of array api capabilities across public functions. + + Parameters + ---------- + modules : str | list[str] + A string containing single SciPy module, (e.g `scipy.stats`, `scipy.fft`) + or a list of such strings. + + backend_type : {'cpu', 'gpu', 'jit', 'lazy'} + + capabilities_table : Optional[list[str]] + Table in the form of `scipy._lib._array_api.xp_capabilities_table`. + If None, uses `scipy._lib._array_api.xp_capabilities_table`. + Default: None. + + Returns + ------- + output : list[dict[str, str]] + `output` is a table in dict format + (keys corresponding to column names). The first column is "module". + The other columns correspond to supported backends for the given + `backend_type`, e.g. jax.numpy, torch, and dask on cpu. + numpy is excluded because it should always be supported. + See the helper function + `_process_capabilities_table_entry` above). + + """ + if backend_type not in {"cpu", "gpu", "jit", "lazy"}: + raise ValueError(f"Received unhandled backend type {backend_type}") + + if isinstance(modules, str): + modules = [modules] + + if capabilities_table is None: + capabilities_table = xp_capabilities_table + + output = [] + + for module_name in modules: + module = import_module(module_name) + public_things = module.__all__ + for name in public_things: + if name in ALIASES.get(module_name, {}): + # Skip undocumented aliases that are kept + # for backwards compatibility reasons. + continue + thing = getattr(module, name) + if not is_named_function_like_object(thing): + continue + entry = xp_capabilities_table.get(thing, None) + capabilities = _process_capabilities_table_entry(entry)[backend_type] + row = {"module": module_name} + row.update({"function": name}) + row.update(capabilities) + output.append(row) + return output + + +def calculate_table_statistics( + flat_table: list[dict[str, str]] +) -> dict[str, tuple[dict[str, str], bool]]: + """Get counts of what is supported per module. + + Parameters + ---------- + flat_table : list[dict[str, str]] + A table as returned by `make_flat_capabilities_table` + + Returns + ------- + dict[str, tuple[dict[str, str], bool]] + dict mapping module names to 2-tuples containing an inner dict and a + bool. The inner dicts have a key "total" along with keys for each + backend column of the supplied flat capabilities table. The value + corresponding to total is the total count of functions in the given + module, and the value associated to the other keys is the count of + functions that support that particular backend. The bool is False if + the calculation may be innacurate due to missing xp_capabilities + decorators, and True if all functions for that particular module have + been decorated with xp_capabilities. + """ + if not flat_table: + return [] + + counter = defaultdict(lambda: defaultdict(int)) + + S = BackendSupportStatus + # Keep track of which modules have functions with missing xp_capabilities + # decorators so this information can be passed back to the caller. + missing_xp_capabilities = set() + for entry in flat_table: + entry = entry.copy() + entry.pop("function") + module = entry.pop("module") + current_counter = counter[module] + + # By design, all backends and options must be considered out-of-scope + # if one is, so just pick an arbitrary entry here to test if function is + # in-scope. + if next(iter(entry.values())) != S.OUT_OF_SCOPE: + current_counter["total"] += 1 + for key, value in entry.items(): + # Functions missing xp_capabilities will be tabulated as + # unsupported, but may actually be supported. There is a + # note about this in the documentation and this function is + # set up to return information needed to put asterisks next + # to percentages impacted by missing xp_capabilities decorators. + current_counter[key] += 1 if value == S.YES else 0 + if value == S.UNKNOWN: + missing_xp_capabilities.add(module) + return { + key: (dict(value), key not in missing_xp_capabilities) + for key, value in counter.items() + } diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/_array_api_no_0d.py b/.venv/lib/python3.11/site-packages/scipy/_lib/_array_api_no_0d.py new file mode 100644 index 0000000000000000000000000000000000000000..a6b6fe1affd172eea620760a20f185f192d37b69 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scipy/_lib/_array_api_no_0d.py @@ -0,0 +1,103 @@ +""" +Extra testing functions that forbid 0d-input, see #21044 + +While the xp_assert_* functions generally aim to follow the conventions of the +underlying `xp` library, NumPy in particular is inconsistent in its handling +of scalars vs. 0d-arrays, see https://github.com/numpy/numpy/issues/24897. + +For example, this means that the following operations (as of v2.0.1) currently +return scalars, even though a 0d-array would often be more appropriate: + + import numpy as np + np.array(0) * 2 # scalar, not 0d array + - np.array(0) # scalar, not 0d-array + np.sin(np.array(0)) # scalar, not 0d array + np.mean([1, 2, 3]) # scalar, not 0d array + +Libraries like CuPy tend to return a 0d-array in scenarios like those above, +and even `xp.asarray(0)[()]` remains a 0d-array there. To deal with the reality +of the inconsistencies present in NumPy, as well as 20+ years of code on top, +the `xp_assert_*` functions here enforce consistency in the only way that +doesn't go against the tide, i.e. by forbidding 0d-arrays as the return type. + +However, when scalars are not generally the expected NumPy return type, +it remains preferable to use the assert functions from +the `scipy._lib._array_api` module, which have less surprising behaviour. +""" +from scipy._lib._array_api import array_namespace, is_numpy +from scipy._lib._array_api import (xp_assert_close as xp_assert_close_base, + xp_assert_equal as xp_assert_equal_base, + xp_assert_less as xp_assert_less_base) + +__all__: list[str] = [] + + +def _check_scalar(actual, desired, *, xp=None, **kwargs): + __tracebackhide__ = True # Hide traceback for py.test + + if xp is None: + xp = array_namespace(actual) + + # necessary to handle non-numpy scalars, e.g. bare `0.0` has no shape + desired = xp.asarray(desired) + + # Only NumPy distinguishes between scalars and arrays; + # shape check in xp_assert_* is sufficient except for shape == () + if not (is_numpy(xp) and desired.shape == ()): + return + + _msg = ("Result is a NumPy 0d-array. Many SciPy functions intend to follow " + "the convention of many NumPy functions, returning a scalar when a " + "0d-array would be correct. The specialized `xp_assert_*` functions " + "in the `scipy._lib._array_api_no_0d` module err on the side of " + "caution and do not accept 0d-arrays by default. If the correct " + "result may legitimately be a 0d-array, pass `check_0d=True`, " + "or use the `xp_assert_*` functions from `scipy._lib._array_api`.") + assert xp.isscalar(actual), _msg + + +def xp_assert_equal(actual, desired, *, check_0d=False, **kwargs): + # in contrast to xp_assert_equal_base, this defaults to check_0d=False, + # but will do an extra check in that case, which forbids 0d-arrays for `actual` + __tracebackhide__ = True # Hide traceback for py.test + + # array-ness (check_0d == True) is taken care of by the *_base functions + if not check_0d: + _check_scalar(actual, desired, **kwargs) + return xp_assert_equal_base(actual, desired, check_0d=check_0d, **kwargs) + + +def xp_assert_close(actual, desired, *, check_0d=False, **kwargs): + # as for xp_assert_equal + __tracebackhide__ = True + + if not check_0d: + _check_scalar(actual, desired, **kwargs) + return xp_assert_close_base(actual, desired, check_0d=check_0d, **kwargs) + + +def xp_assert_less(actual, desired, *, check_0d=False, **kwargs): + # as for xp_assert_equal + __tracebackhide__ = True + + if not check_0d: + _check_scalar(actual, desired, **kwargs) + return xp_assert_less_base(actual, desired, check_0d=check_0d, **kwargs) + + +def assert_array_almost_equal(actual, desired, decimal=6, *args, **kwds): + """Backwards compatible replacement. In new code, use xp_assert_close instead. + """ + rtol, atol = 0, 1.5*10**(-decimal) + return xp_assert_close(actual, desired, + atol=atol, rtol=rtol, check_dtype=False, check_shape=False, + *args, **kwds) + + +def assert_almost_equal(actual, desired, decimal=7, *args, **kwds): + """Backwards compatible replacement. In new code, use xp_assert_close instead. + """ + rtol, atol = 0, 1.5*10**(-decimal) + return xp_assert_close(actual, desired, + atol=atol, rtol=rtol, check_dtype=False, check_shape=False, + *args, **kwds) diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/_array_api_override.py b/.venv/lib/python3.11/site-packages/scipy/_lib/_array_api_override.py new file mode 100644 index 0000000000000000000000000000000000000000..8c6965258b9b4bb35b5e5fe522b7bc1416c5ecb8 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scipy/_lib/_array_api_override.py @@ -0,0 +1,150 @@ +""" +Override functions from array_api_compat, for use by array-api-extra +and internally. + +See also _array_api_compat_vendor.py +""" +import enum +import os + +from functools import lru_cache +from types import ModuleType +from typing import Any, TypeAlias + +import numpy as np +import numpy.typing as npt + +from scipy._lib import array_api_compat +import scipy._lib.array_api_compat.numpy as np_compat +from scipy._lib.array_api_compat import is_array_api_obj, is_jax_array +from scipy._lib._sparse import SparseABC + + +Array: TypeAlias = Any # To be changed to a Protocol later (see array-api#589) +ArrayLike: TypeAlias = Array | npt.ArrayLike + +# 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") + + +class _ArrayClsInfo(enum.Enum): + skip = 0 + numpy = 1 + array_like = 2 + unknown = 3 + + +@lru_cache(100) +def _validate_array_cls(cls: type) -> _ArrayClsInfo: + if issubclass(cls, (list, tuple)): + return _ArrayClsInfo.array_like + + # this comes from `_util._asarray_validated` + if issubclass(cls, SparseABC): + msg = ('Sparse arrays/matrices are not supported by this function. ' + 'Perhaps one of the `scipy.sparse.linalg` functions ' + 'would work instead.') + raise ValueError(msg) + + if issubclass(cls, np.ma.MaskedArray): + raise TypeError("Inputs of type `numpy.ma.MaskedArray` are not supported.") + + if issubclass(cls, np.matrix): + raise TypeError("Inputs of type `numpy.matrix` are not supported.") + + if issubclass(cls, (np.ndarray, np.generic)): + return _ArrayClsInfo.numpy + + # Note: this must happen after the test for np.generic, because + # np.float64 and np.complex128 are subclasses of float and complex respectively. + # This matches the behavior of array_api_compat. + if issubclass(cls, (int, float, complex, bool, type(None))): + return _ArrayClsInfo.skip + + return _ArrayClsInfo.unknown + + +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 + ----- + Wrapper around `array_api_compat.array_namespace`. + + 1. Check for the global switch `SCIPY_ARRAY_API`. If disabled, just + return array_api_compat.numpy namespace and skip all compliance checks. + + 2. Check for known-bad array classes. + 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 + - `scipy.sparse` arrays + + 3. Coerce array-likes to NumPy arrays and check their dtype. + Note that non-scalar array-likes can't be mixed with non-NumPy Array + API objects; e.g. + + - `array_namespace([1, 2])` returns NumPy namespace; + - `array_namespace(np.asarray([1, 2], [3, 4])` returns NumPy namespace; + - `array_namespace(cp.asarray([1, 2], [3, 4])` raises an error. + """ + if not SCIPY_ARRAY_API: + # here we could wrap the namespace if needed + return np_compat + + numpy_arrays = [] + api_arrays = [] + + for array in arrays: + arr_info = _validate_array_cls(type(array)) + if arr_info is _ArrayClsInfo.skip: + pass + + elif arr_info is _ArrayClsInfo.numpy: + if array.dtype.kind in 'iufcb': # Numeric or bool + numpy_arrays.append(array) + elif array.dtype.kind == 'V' and is_jax_array(array): + # Special case for JAX zero gradient arrays; + # see array_api_compat._common._helpers._is_jax_zero_gradient_array + api_arrays.append(array) # JAX zero gradient array + else: + raise TypeError(f"An argument has dtype `{array.dtype!r}`; " + "only boolean and numerical dtypes are supported.") + + elif arr_info is _ArrayClsInfo.unknown and is_array_api_obj(array): + api_arrays.append(array) + + else: + # list, tuple, or arbitrary object + try: + array = np.asanyarray(array) + except TypeError: + raise TypeError("An argument is neither array API compatible nor " + "coercible by NumPy.") + if array.dtype.kind not in 'iufcb': # Numeric or bool + raise TypeError(f"An argument has dtype `{array.dtype!r}`; " + "only boolean and numerical dtypes are supported.") + numpy_arrays.append(array) + + # When there are exclusively NumPy and ArrayLikes, skip calling + # array_api_compat.array_namespace for performance. + if not api_arrays: + return np_compat + + # In case of mix of NumPy/ArrayLike and non-NumPy Array API arrays, + # let array_api_compat.array_namespace raise an error. + return array_api_compat.array_namespace(*numpy_arrays, *api_arrays) diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/_bunch.py b/.venv/lib/python3.11/site-packages/scipy/_lib/_bunch.py new file mode 100644 index 0000000000000000000000000000000000000000..172e7dce1b25d0a70c1810e3d133f4b6bd12a538 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scipy/_lib/_bunch.py @@ -0,0 +1,229 @@ +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__, + # _field_defaults and _replace are added to get Polars to detect + # a bunch object as a namedtuple. See gh-22450 + '_field_defaults': {}, + '_replace': None, + } + 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/.venv/lib/python3.11/site-packages/scipy/_lib/_ccallback.py b/.venv/lib/python3.11/site-packages/scipy/_lib/_ccallback.py new file mode 100644 index 0000000000000000000000000000000000000000..1980d06f5489e6633fb611c35bfb56903bd63e7f --- /dev/null +++ b/.venv/lib/python3.11/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/.venv/lib/python3.11/site-packages/scipy/_lib/_ccallback_c.cpython-311-x86_64-linux-gnu.so b/.venv/lib/python3.11/site-packages/scipy/_lib/_ccallback_c.cpython-311-x86_64-linux-gnu.so new file mode 100644 index 0000000000000000000000000000000000000000..ef5d66d61f16b89c3e7cdf4a7bbd4c30fc2114be Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/_ccallback_c.cpython-311-x86_64-linux-gnu.so differ diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/_disjoint_set.py b/.venv/lib/python3.11/site-packages/scipy/_lib/_disjoint_set.py new file mode 100644 index 0000000000000000000000000000000000000000..683c5c8e518705e710212dafc01363f92a2f947d --- /dev/null +++ b/.venv/lib/python3.11/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/.venv/lib/python3.11/site-packages/scipy/_lib/_docscrape.py b/.venv/lib/python3.11/site-packages/scipy/_lib/_docscrape.py new file mode 100644 index 0000000000000000000000000000000000000000..c74d2c20001f102c86f88c7bfb1afe731de60668 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scipy/_lib/_docscrape.py @@ -0,0 +1,761 @@ +# copied from numpydoc/docscrape.py, commit 97a6026508e0dd5382865672e9563a72cc113bd2 +"""Extract reference documentation from the NumPy source tree.""" + +import copy +import inspect +import pydoc +import re +import sys +import textwrap +from collections import namedtuple +from collections.abc import Callable, Mapping +from functools import cached_property +from warnings import warn + + +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": [], + "Attributes": [], + "Methods": [], + "Returns": [], + "Yields": [], + "Receives": [], + "Other Parameters": [], + "Raises": [], + "Warns": [], + "Warnings": [], + "See Also": [], + "Notes": [], + "References": "", + "Examples": "", + "index": {}, + } + + def __init__(self, docstring, config=None): + 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(f"Unknown section {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 ========== + if len(l2) >= 3 and (set(l2) in ({"-"}, {"="})) and len(l2) != len(l1): + snip = "\n".join(self._doc._str[:2]) + "..." + self._error_location( + f"potentially wrong underline length... \n{l1} \n{l2} in \n{snip}", + error=False, + ) + 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): + content = dedent_lines(content) + r = Reader(content) + params = [] + while not r.eof(): + header = r.read().strip() + if " : " in header: + arg_name, arg_type = header.split(" : ", maxsplit=1) + else: + # NOTE: param line with single element should never have a + # a " :" before the description line, so this should probably + # warn. + if header.endswith(" :"): + header = header[:-2] + 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(py:)?\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" + + _funcname # group for all function names + + r"(?P([,]\s+" + + _funcnamenext + + r")*)" + + r")" + + r"(?P[,\.])?" # end of "allfuncs" + + _description # Some function lists have a trailing comma (or period) '\s*' + ) + + # 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 + + """ + + content = dedent_lines(content) + + items = [] + + def parse_item_name(text): + """Match ':role:`name`' or 'name'.""" + m = self._func_rgx.match(text) + if not m: + self._error_location(f"Error parsing See Also entry {line!r}") + 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: + self._error_location(f"Error parsing See Also entry {line!r}") + 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_yields = "Yields" in section_names + # We could do more tests, but we are not. Arbitrarily. + 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 in %s" + % (section, "\n".join(self._doc._str)) + ) + + 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 + + @property + def _obj(self): + if hasattr(self, "_cls"): + return self._cls + elif hasattr(self, "_f"): + return self._f + return None + + def _error_location(self, msg, error=True): + if self._obj is not None: + # we know where the docs came from: + try: + filename = inspect.getsourcefile(self._obj) + except TypeError: + filename = None + # Make UserWarning more descriptive via object introspection. + # Skip if introspection fails + name = getattr(self._obj, "__name__", None) + if name is None: + name = getattr(getattr(self._obj, "__class__", None), "__name__", None) + if name is not None: + msg += f" in the docstring of {name}" + msg += f" in {filename}." if filename else "" + 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): + return [" " * indent + line for line in doc] + + def _str_signature(self): + if self["Signature"]: + return [self["Signature"].replace("*", r"\*")] + [""] + return [""] + + def _str_summary(self): + if self["Summary"]: + return self["Summary"] + [""] + return [] + + def _str_extended_summary(self): + if self["Extended Summary"]: + return self["Extended Summary"] + [""] + 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 = f"`{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 += [f".. index:: {default_index}"] + for section, references in idx.items(): + if section == "default": + continue + output_index = True + out += [f" :{section}: {', '.join(references)}"] + if output_index: + return out + return "" + + def __str__(self, func_role=""): + out = [] + out += self._str_signature() + out += self._str_summary() + out += self._str_extended_summary() + out += self._str_param_list("Parameters") + for param_list in ("Attributes", "Methods"): + out += self._str_param_list(param_list) + for param_list in ( + "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) + out += self._str_index() + return "\n".join(out) + + +def dedent_lines(lines): + """Deindent a list of lines maximally""" + return textwrap.dedent("\n".join(lines)).split("\n") + + +class FunctionDoc(NumpyDocString): + def __init__(self, func, role="func", doc=None, config=None): + 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 "" + if config is None: + config = {} + 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(f"Warning: invalid role {self._role}") + out += f".. {roles.get(self._role, '')}:: {func_name}\n \n\n" + + out += super().__str__(func_role=self._role) + return out + + +class ObjDoc(NumpyDocString): + def __init__(self, obj, doc=None, config=None): + self._f = obj + if config is None: + config = {} + NumpyDocString.__init__(self, doc, config=config) + + +class ClassDoc(NumpyDocString): + extra_public_methods = ["__call__"] + + def __init__(self, cls, doc=None, modulename="", func_doc=FunctionDoc, config=None): + if not inspect.isclass(cls) and cls is not None: + raise ValueError(f"Expected a class or None, but got {cls!r}") + self._cls = cls + + if "sphinx" in sys.modules: + from sphinx.ext.autodoc import ALL + else: + ALL = object() + + if config is None: + config = {} + 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 not self._should_skip_member(name, self._cls) + and ( + func is None + or isinstance(func, property | cached_property) + or inspect.isdatadescriptor(func) + ) + and self._is_show_member(name) + ) + ] + + @staticmethod + def _should_skip_member(name, klass): + return ( + # Namedtuples should skip everything in their ._fields as the + # docstrings for each of the members is: "Alias for field number X" + issubclass(klass, tuple) + and hasattr(klass, "_asdict") + and hasattr(klass, "_fields") + and name in klass._fields + ) + + def _is_show_member(self, name): + return ( + # show all class members + self.show_inherited_members + # or class member is not inherited + or name in self._cls.__dict__ + ) + + +def get_doc_object( + obj, + what=None, + doc=None, + config=None, + class_doc=ClassDoc, + func_doc=FunctionDoc, + obj_doc=ObjDoc, +): + if what is None: + if inspect.isclass(obj): + what = "class" + elif inspect.ismodule(obj): + what = "module" + elif isinstance(obj, Callable): + what = "function" + else: + what = "object" + if config is None: + config = {} + + if what == "class": + return class_doc(obj, func_doc=func_doc, doc=doc, config=config) + elif what in ("function", "method"): + return func_doc(obj, doc=doc, config=config) + else: + if doc is None: + doc = pydoc.getdoc(obj) + return obj_doc(obj, doc, config=config) \ No newline at end of file diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/_elementwise_iterative_method.py b/.venv/lib/python3.11/site-packages/scipy/_lib/_elementwise_iterative_method.py new file mode 100644 index 0000000000000000000000000000000000000000..14c5c1949ddf407e1aecce0a51a1a834647ac523 --- /dev/null +++ b/.venv/lib/python3.11/site-packages/scipy/_lib/_elementwise_iterative_method.py @@ -0,0 +1,346 @@ +# `_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, +# `scipy.differentiate.derivative` for finite difference based differentiation. + +import math +import numpy as np +from ._util import _RichResult, _call_callback_maybe_halt +from ._array_api import array_namespace, xp_size, xp_result_type +import scipy._lib.array_api_extra as xpx + +_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, xp=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. + xp : namespace + Namespace of array arguments in `xs`. + + 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) if xp is None else xp + + # 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. + xat = xp_result_type(*xs, force_floating=True, xp=xp) + xas = xp.broadcast_arrays(*xs, *args) # broadcast and rename + 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`. All arrays are + subject to being "compressed" if `preserve_shape is False`; nest + arrays that should not be compressed inside another object (e.g. + `dict` or `_RichResult`). + 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. + preserve_shape : bool, default: False + Whether to compress the attributes of `work` (to avoid unnecessary + computation on elements that have already converged). + + 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, xp.asarray(_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 = xpx.at(work.status)[:].set(_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(): + # `continued_fraction` hacks `n`; improve if this becomes a problem + if key in {'args', 'n'}: + continue + work[key] = val[proceed] if getattr(val, 'ndim', 0) > 0 else 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 = xpx.at(active_mask)[active].set(True) + active_mask = active_mask & mask + for key, val in update_dict.items(): + val = val[active_mask] if getattr(val, 'ndim', 0) > 0 else val + res[key] = xpx.at(res[key])[active_mask].set(val) + else: + active_mask = active[mask] + for key, val in update_dict.items(): + val = val[mask] if getattr(val, 'ndim', 0) > 0 else val + res[key] = xpx.at(res[key])[active_mask].set(val) + else: + for key, val in update_dict.items(): + if preserve_shape and getattr(val, 'ndim', 0) > 0: + val = val[active] + res[key] = xpx.at(res[key])[active].set(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/.venv/lib/python3.11/site-packages/scipy/_lib/_fpumode.cpython-311-x86_64-linux-gnu.so b/.venv/lib/python3.11/site-packages/scipy/_lib/_fpumode.cpython-311-x86_64-linux-gnu.so new file mode 100644 index 0000000000000000000000000000000000000000..31363be4f0f920fb1e587e039ead9ded94ec439f Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/_fpumode.cpython-311-x86_64-linux-gnu.so differ diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/_gcutils.py b/.venv/lib/python3.11/site-packages/scipy/_lib/_gcutils.py new file mode 100644 index 0000000000000000000000000000000000000000..854ae36228614f3eb8849e9f95abf0dd387b5d35 --- /dev/null +++ b/.venv/lib/python3.11/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/.venv/lib/python3.11/site-packages/scipy/_lib/_pep440.py b/.venv/lib/python3.11/site-packages/scipy/_lib/_pep440.py new file mode 100644 index 0000000000000000000000000000000000000000..d546e32a0349461a0aab76bfb4636ebf25227ca0 --- /dev/null +++ b/.venv/lib/python3.11/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/.venv/lib/python3.11/site-packages/scipy/_lib/_public_api.py b/.venv/lib/python3.11/site-packages/scipy/_lib/_public_api.py
new file mode 100644
index 0000000000000000000000000000000000000000..0bfd6c7710381e16c78be017756de614057b6028
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/_public_api.py
@@ -0,0 +1,56 @@
+"""PUBLIC_MODULES was once included in scipy._lib.tests.test_public_api.
+
+It has been separated into this file so that this list of public modules
+could be used when generating tables showing support for alternative
+array API backends across modules in
+scipy/doc/source/array_api_capabilities.py.
+"""
+
+# 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",
+    "differentiate",
+    "fft",
+    "fftpack",
+    "integrate",
+    "interpolate",
+    "io",
+    "io.arff",
+    "io.matlab",
+    "io.wavfile",
+    "linalg",
+    "linalg.blas",
+    "linalg.cython_blas",
+    "linalg.lapack",
+    "linalg.cython_lapack",
+    "linalg.interpolative",
+    "ndimage",
+    "odr",
+    "optimize",
+    "optimize.elementwise",
+    "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"
+]]
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/_sparse.py b/.venv/lib/python3.11/site-packages/scipy/_lib/_sparse.py
new file mode 100644
index 0000000000000000000000000000000000000000..e7deb32dd6e5d19bb8fceae4a7d22f2c6eddda7a
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/_sparse.py
@@ -0,0 +1,41 @@
+from abc import ABC
+
+__all__ = ["SparseABC", "issparse"]
+
+
+class SparseABC(ABC):
+    pass
+
+
+def issparse(x):
+    """Is `x` of a sparse array or sparse matrix type?
+
+    Parameters
+    ----------
+    x
+        object to check for being a sparse array or sparse matrix
+
+    Returns
+    -------
+    bool
+        True if `x` is a sparse array or a sparse matrix, False otherwise
+
+    Notes
+    -----
+    Use `isinstance(x, sp.sparse.sparray)` to check between an array or matrix.
+    Use `a.format` to check the sparse format, e.g. `a.format == 'csr'`.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.sparse import csr_array, csr_matrix, issparse
+    >>> issparse(csr_matrix([[5]]))
+    True
+    >>> issparse(csr_array([[5]]))
+    True
+    >>> issparse(np.array([[5]]))
+    False
+    >>> issparse(5)
+    False
+    """
+    return isinstance(x, SparseABC)
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/_test_ccallback.cpython-311-x86_64-linux-gnu.so b/.venv/lib/python3.11/site-packages/scipy/_lib/_test_ccallback.cpython-311-x86_64-linux-gnu.so
new file mode 100644
index 0000000000000000000000000000000000000000..ac785b06a2f9175d91d17a39f2cfdc429744358d
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/_test_ccallback.cpython-311-x86_64-linux-gnu.so differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/_test_deprecation_call.cpython-311-x86_64-linux-gnu.so b/.venv/lib/python3.11/site-packages/scipy/_lib/_test_deprecation_call.cpython-311-x86_64-linux-gnu.so
new file mode 100644
index 0000000000000000000000000000000000000000..bcead9c69ede1bd8b9bfe9e54e450e73b4f6ded0
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/_test_deprecation_call.cpython-311-x86_64-linux-gnu.so differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/_test_deprecation_def.cpython-311-x86_64-linux-gnu.so b/.venv/lib/python3.11/site-packages/scipy/_lib/_test_deprecation_def.cpython-311-x86_64-linux-gnu.so
new file mode 100644
index 0000000000000000000000000000000000000000..aa04fee13d193011441b3be00e247f93b57c1fff
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/_test_deprecation_def.cpython-311-x86_64-linux-gnu.so differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/_testutils.py b/.venv/lib/python3.11/site-packages/scipy/_lib/_testutils.py
new file mode 100644
index 0000000000000000000000000000000000000000..442bb539db3bbedf0a64876fea124c107f3d0383
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/_testutils.py
@@ -0,0 +1,373 @@
+"""
+Generic test utilities.
+
+"""
+
+import inspect
+import os
+import re
+import shutil
+import subprocess
+import sys
+import sysconfig
+import threading
+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 extra_argv is None:
+            extra_argv = []
+        pytest_args += extra_argv
+        if any(arg == "-m" or arg == "--markers" for arg in extra_argv):
+            # Likely conflict with default --mode=fast
+            raise ValueError("Must specify -m before --")
+
+        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")
+
+    # Make safe for being called by multiple threads within one test
+    tmp_path = tmp_path / str(threading.get_ident())
+
+    # 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")
+
+
+def _run_concurrent_barrier(n_workers, fn, *args, **kwargs):
+    """
+    Run a given function concurrently across a given number of threads.
+
+    This is equivalent to using a ThreadPoolExecutor, but using the threading
+    primitives instead. This function ensures that the closure passed by
+    parameter gets called concurrently by setting up a barrier before it gets
+    called before any of the threads.
+
+    Arguments
+    ---------
+    n_workers: int
+        Number of concurrent threads to spawn.
+    fn: callable
+        Function closure to execute concurrently. Its first argument will
+        be the thread id.
+    *args: tuple
+        Variable number of positional arguments to pass to the function.
+    **kwargs: dict
+        Keyword arguments to pass to the function.
+    """
+    barrier = threading.Barrier(n_workers)
+
+    def closure(i, *args, **kwargs):
+        barrier.wait()
+        fn(i, *args, **kwargs)
+
+    workers = []
+    for i in range(0, n_workers):
+        workers.append(threading.Thread(
+            target=closure,
+            args=(i,) + args, kwargs=kwargs))
+
+    for worker in workers:
+        worker.start()
+
+    for worker in workers:
+        worker.join()
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/_tmpdirs.py b/.venv/lib/python3.11/site-packages/scipy/_lib/_tmpdirs.py
new file mode 100644
index 0000000000000000000000000000000000000000..0f9fd546a9d2ae3e9a20c0684f79eb0b3d61ee92
--- /dev/null
+++ b/.venv/lib/python3.11/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/.venv/lib/python3.11/site-packages/scipy/_lib/_uarray/LICENSE b/.venv/lib/python3.11/site-packages/scipy/_lib/_uarray/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..5f2b90a026aaecbdc090b3d3234954ab29fce8ae
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/_uarray/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2018, Quansight-Labs
+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 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.
+
+* Neither the name of the copyright holder nor the names of its
+  contributors may be used to endorse or promote products derived from
+  this software without specific prior written permission.
+
+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.
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/_uarray/__init__.py b/.venv/lib/python3.11/site-packages/scipy/_lib/_uarray/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..91afdcedb180599a41758cdd8c03416cf6c20d76
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/_uarray/__init__.py
@@ -0,0 +1,116 @@
+"""
+.. note:
+    If you are looking for overrides for NumPy-specific methods, see the
+    documentation for :obj:`unumpy`. This page explains how to write
+    back-ends and multimethods.
+
+``uarray`` is built around a back-end protocol, and overridable multimethods.
+It is necessary to define multimethods for back-ends to be able to override them.
+See the documentation of :obj:`generate_multimethod` on how to write multimethods.
+
+
+
+Let's start with the simplest:
+
+``__ua_domain__`` defines the back-end *domain*. The domain consists of period-
+separated string consisting of the modules you extend plus the submodule. For
+example, if a submodule ``module2.submodule`` extends ``module1``
+(i.e., it exposes dispatchables marked as types available in ``module1``),
+then the domain string should be ``"module1.module2.submodule"``.
+
+
+For the purpose of this demonstration, we'll be creating an object and setting
+its attributes directly. However, note that you can use a module or your own type
+as a backend as well.
+
+>>> class Backend: pass
+>>> be = Backend()
+>>> be.__ua_domain__ = "ua_examples"
+
+It might be useful at this point to sidetrack to the documentation of
+:obj:`generate_multimethod` to find out how to generate a multimethod
+overridable by :obj:`uarray`. Needless to say, writing a backend and
+creating multimethods are mostly orthogonal activities, and knowing
+one doesn't necessarily require knowledge of the other, although it
+is certainly helpful. We expect core API designers/specifiers to write the
+multimethods, and implementors to override them. But, as is often the case,
+similar people write both.
+
+Without further ado, here's an example multimethod:
+
+>>> import uarray as ua
+>>> from uarray import Dispatchable
+>>> def override_me(a, b):
+...   return Dispatchable(a, int),
+>>> def override_replacer(args, kwargs, dispatchables):
+...     return (dispatchables[0], args[1]), {}
+>>> overridden_me = ua.generate_multimethod(
+...     override_me, override_replacer, "ua_examples"
+... )
+
+Next comes the part about overriding the multimethod. This requires
+the ``__ua_function__`` protocol, and the ``__ua_convert__``
+protocol. The ``__ua_function__`` protocol has the signature
+``(method, args, kwargs)`` where ``method`` is the passed
+multimethod, ``args``/``kwargs`` specify the arguments and ``dispatchables``
+is the list of converted dispatchables passed in.
+
+>>> def __ua_function__(method, args, kwargs):
+...     return method.__name__, args, kwargs
+>>> be.__ua_function__ = __ua_function__
+
+The other protocol of interest is the ``__ua_convert__`` protocol. It has the
+signature ``(dispatchables, coerce)``. When ``coerce`` is ``False``, conversion
+between the formats should ideally be an ``O(1)`` operation, but it means that
+no memory copying should be involved, only views of the existing data.
+
+>>> def __ua_convert__(dispatchables, coerce):
+...     for d in dispatchables:
+...         if d.type is int:
+...             if coerce and d.coercible:
+...                 yield str(d.value)
+...             else:
+...                 yield d.value
+>>> be.__ua_convert__ = __ua_convert__
+
+Now that we have defined the backend, the next thing to do is to call the multimethod.
+
+>>> with ua.set_backend(be):
+...      overridden_me(1, "2")
+('override_me', (1, '2'), {})
+
+Note that the marked type has no effect on the actual type of the passed object.
+We can also coerce the type of the input.
+
+>>> with ua.set_backend(be, coerce=True):
+...     overridden_me(1, "2")
+...     overridden_me(1.0, "2")
+('override_me', ('1', '2'), {})
+('override_me', ('1.0', '2'), {})
+
+Another feature is that if you remove ``__ua_convert__``, the arguments are not
+converted at all and it's up to the backend to handle that.
+
+>>> del be.__ua_convert__
+>>> with ua.set_backend(be):
+...     overridden_me(1, "2")
+('override_me', (1, '2'), {})
+
+You also have the option to return ``NotImplemented``, in which case processing moves on
+to the next back-end, which in this case, doesn't exist. The same applies to
+``__ua_convert__``.
+
+>>> be.__ua_function__ = lambda *a, **kw: NotImplemented
+>>> with ua.set_backend(be):
+...     overridden_me(1, "2")
+Traceback (most recent call last):
+    ...
+uarray.BackendNotImplementedError: ...
+
+The last possibility is if we don't have ``__ua_convert__``, in which case the job is
+left up to ``__ua_function__``, but putting things back into arrays after conversion
+will not be possible.
+"""
+
+from ._backend import *
+__version__ = '0.8.8.dev0+aa94c5a4.scipy'
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/_uarray/_backend.py b/.venv/lib/python3.11/site-packages/scipy/_lib/_uarray/_backend.py
new file mode 100644
index 0000000000000000000000000000000000000000..87ea71965761ca6916485828ee23d3402bfb1b7d
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/_uarray/_backend.py
@@ -0,0 +1,707 @@
+import typing
+import types
+import inspect
+import functools
+from . import _uarray
+import copyreg
+import pickle
+import contextlib
+import threading
+
+from ._uarray import (  # type: ignore
+    BackendNotImplementedError,
+    _Function,
+    _SkipBackendContext,
+    _SetBackendContext,
+    _BackendState,
+)
+
+__all__ = [
+    "set_backend",
+    "set_global_backend",
+    "skip_backend",
+    "register_backend",
+    "determine_backend",
+    "determine_backend_multi",
+    "clear_backends",
+    "create_multimethod",
+    "generate_multimethod",
+    "_Function",
+    "BackendNotImplementedError",
+    "Dispatchable",
+    "wrap_single_convertor",
+    "wrap_single_convertor_instance",
+    "all_of_type",
+    "mark_as",
+    "set_state",
+    "get_state",
+    "reset_state",
+    "_BackendState",
+    "_SkipBackendContext",
+    "_SetBackendContext",
+]
+
+ArgumentExtractorType = typing.Callable[..., tuple["Dispatchable", ...]]
+ArgumentReplacerType = typing.Callable[
+    [tuple, dict, tuple], tuple[tuple, dict]
+]
+
+def unpickle_function(mod_name, qname, self_):
+    import importlib
+
+    try:
+        module = importlib.import_module(mod_name)
+        qname = qname.split(".")
+        func = module
+        for q in qname:
+            func = getattr(func, q)
+
+        if self_ is not None:
+            func = types.MethodType(func, self_)
+
+        return func
+    except (ImportError, AttributeError) as e:
+        from pickle import UnpicklingError
+
+        raise UnpicklingError from e
+
+
+def pickle_function(func):
+    mod_name = getattr(func, "__module__", None)
+    qname = getattr(func, "__qualname__", None)
+    self_ = getattr(func, "__self__", None)
+
+    try:
+        test = unpickle_function(mod_name, qname, self_)
+    except pickle.UnpicklingError:
+        test = None
+
+    if test is not func:
+        raise pickle.PicklingError(
+            f"Can't pickle {func}: it's not the same object as {test}"
+        )
+
+    return unpickle_function, (mod_name, qname, self_)
+
+
+def pickle_state(state):
+    return _uarray._BackendState._unpickle, state._pickle()
+
+
+def pickle_set_backend_context(ctx):
+    return _SetBackendContext, ctx._pickle()
+
+
+def pickle_skip_backend_context(ctx):
+    return _SkipBackendContext, ctx._pickle()
+
+
+copyreg.pickle(_Function, pickle_function)
+copyreg.pickle(_uarray._BackendState, pickle_state)
+copyreg.pickle(_SetBackendContext, pickle_set_backend_context)
+copyreg.pickle(_SkipBackendContext, pickle_skip_backend_context)
+
+
+def get_state():
+    """
+    Returns an opaque object containing the current state of all the backends.
+
+    Can be used for synchronization between threads/processes.
+
+    See Also
+    --------
+    set_state
+        Sets the state returned by this function.
+    """
+    return _uarray.get_state()
+
+
+@contextlib.contextmanager
+def reset_state():
+    """
+    Returns a context manager that resets all state once exited.
+
+    See Also
+    --------
+    set_state
+        Context manager that sets the backend state.
+    get_state
+        Gets a state to be set by this context manager.
+    """
+    with set_state(get_state()):
+        yield
+
+
+@contextlib.contextmanager
+def set_state(state):
+    """
+    A context manager that sets the state of the backends to one returned by :obj:`get_state`.
+
+    See Also
+    --------
+    get_state
+        Gets a state to be set by this context manager.
+    """  # noqa: E501
+    old_state = get_state()
+    _uarray.set_state(state)
+    try:
+        yield
+    finally:
+        _uarray.set_state(old_state, True)
+
+
+def create_multimethod(*args, **kwargs):
+    """
+    Creates a decorator for generating multimethods.
+
+    This function creates a decorator that can be used with an argument
+    extractor in order to generate a multimethod. Other than for the
+    argument extractor, all arguments are passed on to
+    :obj:`generate_multimethod`.
+
+    See Also
+    --------
+    generate_multimethod
+        Generates a multimethod.
+    """
+
+    def wrapper(a):
+        return generate_multimethod(a, *args, **kwargs)
+
+    return wrapper
+
+
+def generate_multimethod(
+    argument_extractor: ArgumentExtractorType,
+    argument_replacer: ArgumentReplacerType,
+    domain: str,
+    default: typing.Callable | None = None,
+):
+    """
+    Generates a multimethod.
+
+    Parameters
+    ----------
+    argument_extractor : ArgumentExtractorType
+        A callable which extracts the dispatchable arguments. Extracted arguments
+        should be marked by the :obj:`Dispatchable` class. It has the same signature
+        as the desired multimethod.
+    argument_replacer : ArgumentReplacerType
+        A callable with the signature (args, kwargs, dispatchables), which should also
+        return an (args, kwargs) pair with the dispatchables replaced inside the
+        args/kwargs.
+    domain : str
+        A string value indicating the domain of this multimethod.
+    default: Optional[Callable], optional
+        The default implementation of this multimethod, where ``None`` (the default)
+        specifies there is no default implementation.
+
+    Examples
+    --------
+    In this example, ``a`` is to be dispatched over, so we return it, while marking it
+    as an ``int``.
+    The trailing comma is needed because the args have to be returned as an iterable.
+
+    >>> def override_me(a, b):
+    ...   return Dispatchable(a, int),
+
+    Next, we define the argument replacer that replaces the dispatchables inside
+    args/kwargs with the supplied ones.
+
+    >>> def override_replacer(args, kwargs, dispatchables):
+    ...     return (dispatchables[0], args[1]), {}
+
+    Next, we define the multimethod.
+
+    >>> overridden_me = generate_multimethod(
+    ...     override_me, override_replacer, "ua_examples"
+    ... )
+
+    Notice that there's no default implementation, unless you supply one.
+
+    >>> overridden_me(1, "a")
+    Traceback (most recent call last):
+        ...
+    uarray.BackendNotImplementedError: ...
+
+    >>> overridden_me2 = generate_multimethod(
+    ...     override_me, override_replacer, "ua_examples", default=lambda x, y: (x, y)
+    ... )
+    >>> overridden_me2(1, "a")
+    (1, 'a')
+
+    See Also
+    --------
+    uarray
+        See the module documentation for how to override the method by creating
+        backends.
+    """
+    kw_defaults, arg_defaults, opts = get_defaults(argument_extractor)
+    ua_func = _Function(
+        argument_extractor,
+        argument_replacer,
+        domain,
+        arg_defaults,
+        kw_defaults,
+        default,
+    )
+
+    return functools.update_wrapper(ua_func, argument_extractor)
+
+
+def set_backend(backend, coerce=False, only=False):
+    """
+    A context manager that sets the preferred backend.
+
+    Parameters
+    ----------
+    backend
+        The backend to set.
+    coerce
+        Whether or not to coerce to a specific backend's types. Implies ``only``.
+    only
+        Whether or not this should be the last backend to try.
+
+    See Also
+    --------
+    skip_backend: A context manager that allows skipping of backends.
+    set_global_backend: Set a single, global backend for a domain.
+    """
+    tid = threading.get_native_id()
+    try:
+        return backend.__ua_cache__[tid, "set", coerce, only]
+    except AttributeError:
+        backend.__ua_cache__ = {}
+    except KeyError:
+        pass
+
+    ctx = _SetBackendContext(backend, coerce, only)
+    backend.__ua_cache__[tid, "set", coerce, only] = ctx
+    return ctx
+
+
+def skip_backend(backend):
+    """
+    A context manager that allows one to skip a given backend from processing
+    entirely. This allows one to use another backend's code in a library that
+    is also a consumer of the same backend.
+
+    Parameters
+    ----------
+    backend
+        The backend to skip.
+
+    See Also
+    --------
+    set_backend: A context manager that allows setting of backends.
+    set_global_backend: Set a single, global backend for a domain.
+    """
+    tid = threading.get_native_id()
+    try:
+        return backend.__ua_cache__[tid, "skip"]
+    except AttributeError:
+        backend.__ua_cache__ = {}
+    except KeyError:
+        pass
+
+    ctx = _SkipBackendContext(backend)
+    backend.__ua_cache__[tid, "skip"] = ctx
+    return ctx
+
+
+def get_defaults(f):
+    sig = inspect.signature(f)
+    kw_defaults = {}
+    arg_defaults = []
+    opts = set()
+    for k, v in sig.parameters.items():
+        if v.default is not inspect.Parameter.empty:
+            kw_defaults[k] = v.default
+        if v.kind in (
+            inspect.Parameter.POSITIONAL_ONLY,
+            inspect.Parameter.POSITIONAL_OR_KEYWORD,
+        ):
+            arg_defaults.append(v.default)
+        opts.add(k)
+
+    return kw_defaults, tuple(arg_defaults), opts
+
+
+def set_global_backend(backend, coerce=False, only=False, *, try_last=False):
+    """
+    This utility method replaces the default backend for permanent use. It
+    will be tried in the list of backends automatically, unless the
+    ``only`` flag is set on a backend. This will be the first tried
+    backend outside the :obj:`set_backend` context manager.
+
+    Note that this method is not thread-safe.
+
+    .. warning::
+        We caution library authors against using this function in
+        their code. We do *not* support this use-case. This function
+        is meant to be used only by users themselves, or by a reference
+        implementation, if one exists.
+
+    Parameters
+    ----------
+    backend
+        The backend to register.
+    coerce : bool
+        Whether to coerce input types when trying this backend.
+    only : bool
+        If ``True``, no more backends will be tried if this fails.
+        Implied by ``coerce=True``.
+    try_last : bool
+        If ``True``, the global backend is tried after registered backends.
+
+    See Also
+    --------
+    set_backend: A context manager that allows setting of backends.
+    skip_backend: A context manager that allows skipping of backends.
+    """
+    _uarray.set_global_backend(backend, coerce, only, try_last)
+
+
+def register_backend(backend):
+    """
+    This utility method sets registers backend for permanent use. It
+    will be tried in the list of backends automatically, unless the
+    ``only`` flag is set on a backend.
+
+    Note that this method is not thread-safe.
+
+    Parameters
+    ----------
+    backend
+        The backend to register.
+    """
+    _uarray.register_backend(backend)
+
+
+def clear_backends(domain, registered=True, globals=False):
+    """
+    This utility method clears registered backends.
+
+    .. warning::
+        We caution library authors against using this function in
+        their code. We do *not* support this use-case. This function
+        is meant to be used only by users themselves.
+
+    .. warning::
+        Do NOT use this method inside a multimethod call, or the
+        program is likely to crash.
+
+    Parameters
+    ----------
+    domain : Optional[str]
+        The domain for which to de-register backends. ``None`` means
+        de-register for all domains.
+    registered : bool
+        Whether or not to clear registered backends. See :obj:`register_backend`.
+    globals : bool
+        Whether or not to clear global backends. See :obj:`set_global_backend`.
+
+    See Also
+    --------
+    register_backend : Register a backend globally.
+    set_global_backend : Set a global backend.
+    """
+    _uarray.clear_backends(domain, registered, globals)
+
+
+class Dispatchable:
+    """
+    A utility class which marks an argument with a specific dispatch type.
+
+
+    Attributes
+    ----------
+    value
+        The value of the Dispatchable.
+
+    type
+        The type of the Dispatchable.
+
+    Examples
+    --------
+    >>> x = Dispatchable(1, str)
+    >>> x
+    , value=1>
+
+    See Also
+    --------
+    all_of_type
+        Marks all unmarked parameters of a function.
+
+    mark_as
+        Allows one to create a utility function to mark as a given type.
+    """
+
+    def __init__(self, value, dispatch_type, coercible=True):
+        self.value = value
+        self.type = dispatch_type
+        self.coercible = coercible
+
+    def __getitem__(self, index):
+        return (self.type, self.value)[index]
+
+    def __str__(self):
+        return f"<{type(self).__name__}: type={self.type!r}, value={self.value!r}>"
+
+    __repr__ = __str__
+
+
+def mark_as(dispatch_type):
+    """
+    Creates a utility function to mark something as a specific type.
+
+    Examples
+    --------
+    >>> mark_int = mark_as(int)
+    >>> mark_int(1)
+    , value=1>
+    """
+    return functools.partial(Dispatchable, dispatch_type=dispatch_type)
+
+
+def all_of_type(arg_type):
+    """
+    Marks all unmarked arguments as a given type.
+
+    Examples
+    --------
+    >>> @all_of_type(str)
+    ... def f(a, b):
+    ...     return a, Dispatchable(b, int)
+    >>> f('a', 1)
+    (, value='a'>,
+     , value=1>)
+    """
+
+    def outer(func):
+        @functools.wraps(func)
+        def inner(*args, **kwargs):
+            extracted_args = func(*args, **kwargs)
+            return tuple(
+                Dispatchable(arg, arg_type)
+                if not isinstance(arg, Dispatchable)
+                else arg
+                for arg in extracted_args
+            )
+
+        return inner
+
+    return outer
+
+
+def wrap_single_convertor(convert_single):
+    """
+    Wraps a ``__ua_convert__`` defined for a single element to all elements.
+    If any of them return ``NotImplemented``, the operation is assumed to be
+    undefined.
+
+    Accepts a signature of (value, type, coerce).
+    """
+
+    @functools.wraps(convert_single)
+    def __ua_convert__(dispatchables, coerce):
+        converted = []
+        for d in dispatchables:
+            c = convert_single(d.value, d.type, coerce and d.coercible)
+
+            if c is NotImplemented:
+                return NotImplemented
+
+            converted.append(c)
+
+        return converted
+
+    return __ua_convert__
+
+
+def wrap_single_convertor_instance(convert_single):
+    """
+    Wraps a ``__ua_convert__`` defined for a single element to all elements.
+    If any of them return ``NotImplemented``, the operation is assumed to be
+    undefined.
+
+    Accepts a signature of (value, type, coerce).
+    """
+
+    @functools.wraps(convert_single)
+    def __ua_convert__(self, dispatchables, coerce):
+        converted = []
+        for d in dispatchables:
+            c = convert_single(self, d.value, d.type, coerce and d.coercible)
+
+            if c is NotImplemented:
+                return NotImplemented
+
+            converted.append(c)
+
+        return converted
+
+    return __ua_convert__
+
+
+def determine_backend(value, dispatch_type, *, domain, only=True, coerce=False):
+    """Set the backend to the first active backend that supports ``value``
+
+    This is useful for functions that call multimethods without any dispatchable
+    arguments. You can use :func:`determine_backend` to ensure the same backend
+    is used everywhere in a block of multimethod calls.
+
+    Parameters
+    ----------
+    value
+        The value being tested
+    dispatch_type
+        The dispatch type associated with ``value``, aka
+        ":ref:`marking `".
+    domain: string
+        The domain to query for backends and set.
+    coerce: bool
+        Whether or not to allow coercion to the backend's types. Implies ``only``.
+    only: bool
+        Whether or not this should be the last backend to try.
+
+    See Also
+    --------
+    set_backend: For when you know which backend to set
+
+    Notes
+    -----
+
+    Support is determined by the ``__ua_convert__`` protocol. Backends not
+    supporting the type must return ``NotImplemented`` from their
+    ``__ua_convert__`` if they don't support input of that type.
+
+    Examples
+    --------
+
+    Suppose we have two backends ``BackendA`` and ``BackendB`` each supporting
+    different types, ``TypeA`` and ``TypeB``. Neither supporting the other type:
+
+    >>> with ua.set_backend(ex.BackendA):
+    ...     ex.call_multimethod(ex.TypeB(), ex.TypeB())
+    Traceback (most recent call last):
+        ...
+    uarray.BackendNotImplementedError: ...
+
+    Now consider a multimethod that creates a new object of ``TypeA``, or
+    ``TypeB`` depending on the active backend.
+
+    >>> with ua.set_backend(ex.BackendA), ua.set_backend(ex.BackendB):
+    ...         res = ex.creation_multimethod()
+    ...         ex.call_multimethod(res, ex.TypeA())
+    Traceback (most recent call last):
+        ...
+    uarray.BackendNotImplementedError: ...
+
+    ``res`` is an object of ``TypeB`` because ``BackendB`` is set in the
+    innermost with statement. So, ``call_multimethod`` fails since the types
+    don't match.
+
+    Instead, we need to first find a backend suitable for all of our objects.
+
+    >>> with ua.set_backend(ex.BackendA), ua.set_backend(ex.BackendB):
+    ...     x = ex.TypeA()
+    ...     with ua.determine_backend(x, "mark", domain="ua_examples"):
+    ...         res = ex.creation_multimethod()
+    ...         ex.call_multimethod(res, x)
+    TypeA
+
+    """
+    dispatchables = (Dispatchable(value, dispatch_type, coerce),)
+    backend = _uarray.determine_backend(domain, dispatchables, coerce)
+
+    return set_backend(backend, coerce=coerce, only=only)
+
+
+def determine_backend_multi(
+    dispatchables, *, domain, only=True, coerce=False, **kwargs
+):
+    """Set a backend supporting all ``dispatchables``
+
+    This is useful for functions that call multimethods without any dispatchable
+    arguments. You can use :func:`determine_backend_multi` to ensure the same
+    backend is used everywhere in a block of multimethod calls involving
+    multiple arrays.
+
+    Parameters
+    ----------
+    dispatchables: Sequence[Union[uarray.Dispatchable, Any]]
+        The dispatchables that must be supported
+    domain: string
+        The domain to query for backends and set.
+    coerce: bool
+        Whether or not to allow coercion to the backend's types. Implies ``only``.
+    only: bool
+        Whether or not this should be the last backend to try.
+    dispatch_type: Optional[Any]
+        The default dispatch type associated with ``dispatchables``, aka
+        ":ref:`marking `".
+
+    See Also
+    --------
+    determine_backend: For a single dispatch value
+    set_backend: For when you know which backend to set
+
+    Notes
+    -----
+
+    Support is determined by the ``__ua_convert__`` protocol. Backends not
+    supporting the type must return ``NotImplemented`` from their
+    ``__ua_convert__`` if they don't support input of that type.
+
+    Examples
+    --------
+
+    :func:`determine_backend` allows the backend to be set from a single
+    object. :func:`determine_backend_multi` allows multiple objects to be
+    checked simultaneously for support in the backend. Suppose we have a
+    ``BackendAB`` which supports ``TypeA`` and ``TypeB`` in the same call,
+    and a ``BackendBC`` that doesn't support ``TypeA``.
+
+    >>> with ua.set_backend(ex.BackendAB), ua.set_backend(ex.BackendBC):
+    ...     a, b = ex.TypeA(), ex.TypeB()
+    ...     with ua.determine_backend_multi(
+    ...         [ua.Dispatchable(a, "mark"), ua.Dispatchable(b, "mark")],
+    ...         domain="ua_examples"
+    ...     ):
+    ...         res = ex.creation_multimethod()
+    ...         ex.call_multimethod(res, a, b)
+    TypeA
+
+    This won't call ``BackendBC`` because it doesn't support ``TypeA``.
+
+    We can also use leave out the ``ua.Dispatchable`` if we specify the
+    default ``dispatch_type`` for the ``dispatchables`` argument.
+
+    >>> with ua.set_backend(ex.BackendAB), ua.set_backend(ex.BackendBC):
+    ...     a, b = ex.TypeA(), ex.TypeB()
+    ...     with ua.determine_backend_multi(
+    ...         [a, b], dispatch_type="mark", domain="ua_examples"
+    ...     ):
+    ...         res = ex.creation_multimethod()
+    ...         ex.call_multimethod(res, a, b)
+    TypeA
+
+    """
+    if "dispatch_type" in kwargs:
+        disp_type = kwargs.pop("dispatch_type")
+        dispatchables = tuple(
+            d if isinstance(d, Dispatchable) else Dispatchable(d, disp_type)
+            for d in dispatchables
+        )
+    else:
+        dispatchables = tuple(dispatchables)
+        if not all(isinstance(d, Dispatchable) for d in dispatchables):
+            raise TypeError("dispatchables must be instances of uarray.Dispatchable")
+
+    if len(kwargs) != 0:
+        raise TypeError(f"Received unexpected keyword arguments: {kwargs}")
+
+    backend = _uarray.determine_backend(domain, dispatchables, coerce)
+
+    return set_backend(backend, coerce=coerce, only=only)
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/_uarray/_uarray.cpython-311-x86_64-linux-gnu.so b/.venv/lib/python3.11/site-packages/scipy/_lib/_uarray/_uarray.cpython-311-x86_64-linux-gnu.so
new file mode 100644
index 0000000000000000000000000000000000000000..f5e17f86dd4dae54f55d4eab4c0f6e185e120867
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/_uarray/_uarray.cpython-311-x86_64-linux-gnu.so
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:df29055dbb67bcbbf69e2eeb2c00a92912477a8b86706066a797a277fe75e146
+size 111000
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/_util.py b/.venv/lib/python3.11/site-packages/scipy/_lib/_util.py
new file mode 100644
index 0000000000000000000000000000000000000000..fd8130ad008d6eaa0cc022a4d6f50da2dab47f96
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/_util.py
@@ -0,0 +1,1251 @@
+import re
+from contextlib import contextmanager
+import functools
+import operator
+import warnings
+import numbers
+from collections import namedtuple
+import inspect
+import math
+import os
+import sys
+import textwrap
+from types import ModuleType
+from typing import Literal, TypeAlias, TypeVar
+
+import numpy as np
+from scipy._lib._array_api import (Array, array_namespace, is_lazy_array, is_numpy,
+                                   is_marray, xp_size, xp_result_device, xp_result_type)
+from scipy._lib._docscrape import FunctionDoc, Parameter
+from scipy._lib._sparse import issparse
+
+from numpy.exceptions import AxisError
+
+
+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 = int | np.integer
+DecimalNumber = float | np.floating | np.integer
+
+copy_if_needed: bool | None
+
+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
+
+
+# Wrapped function for inspect.signature for compatibility with Python 3.14+
+# See gh-23913
+#
+# PEP 649/749 allows for underfined annotations at runtime, and added the
+# `annotation_format` parameter to handle these cases.
+# `annotationlib.Format.FORWARDREF` is the closest to previous behavior,
+# returning ForwardRef objects fornew undefined annotations cases.
+#
+# Consider dropping this wrapper when support for Python 3.13 is dropped.
+if sys.version_info >= (3, 14):
+    import annotationlib
+    def wrapped_inspect_signature(callable):
+        """Get a signature object for the passed callable."""
+        return inspect.signature(callable,
+                                 annotation_format=annotationlib.Format.FORWARDREF)
+else:
+    wrapped_inspect_signature = inspect.signature
+
+
+_RNG: TypeAlias = np.random.Generator | np.random.RandomState
+SeedType: TypeAlias = IntNumber | _RNG | None
+
+GeneratorType = TypeVar("GeneratorType", bound=_RNG)
+
+
+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
+
+
+_rng_desc = (
+    r"""If `rng` is passed by keyword, types other than `numpy.random.Generator` are
+    passed to `numpy.random.default_rng` to instantiate a ``Generator``.
+    If `rng` is already a ``Generator`` instance, then the provided instance is
+    used. Specify `rng` for repeatable function behavior.
+
+    If this argument is passed by position or `{old_name}` is passed by keyword,
+    legacy behavior for the argument `{old_name}` applies:
+
+    - If `{old_name}` is None (or `numpy.random`), the `numpy.random.RandomState`
+      singleton is used.
+    - If `{old_name}` is an int, a new ``RandomState`` instance is used,
+      seeded with `{old_name}`.
+    - If `{old_name}` is already a ``Generator`` or ``RandomState`` instance then
+      that instance is used.
+
+    .. versionchanged:: 1.15.0
+        As part of the `SPEC-007 `_
+        transition from use of `numpy.random.RandomState` to
+        `numpy.random.Generator`, this keyword was changed from `{old_name}` to `rng`.
+        For an interim period, both keywords will continue to work, although only one
+        may be specified at a time. After the interim period, function calls using the
+        `{old_name}` keyword will emit warnings. The behavior of both `{old_name}` and
+        `rng` are outlined above, but only the `rng` keyword should be used in new code.
+        """
+)
+
+
+# SPEC 7
+def _transition_to_rng(old_name, *, position_num=None, end_version=None,
+                       replace_doc=True):
+    """Example decorator to transition from old PRNG usage to new `rng` behavior
+
+    Suppose the decorator is applied to a function that used to accept parameter
+    `old_name='random_state'` either by keyword or as a positional argument at
+    `position_num=1`. At the time of application, the name of the argument in the
+    function signature is manually changed to the new name, `rng`. If positional
+    use was allowed before, this is not changed.*
+
+    - If the function is called with both `random_state` and `rng`, the decorator
+      raises an error.
+    - If `random_state` is provided as a keyword argument, the decorator passes
+      `random_state` to the function's `rng` argument as a keyword. If `end_version`
+      is specified, the decorator will emit a `DeprecationWarning` about the
+      deprecation of keyword `random_state`.
+    - If `random_state` is provided as a positional argument, the decorator passes
+      `random_state` to the function's `rng` argument by position. If `end_version`
+      is specified, the decorator will emit a `FutureWarning` about the changing
+      interpretation of the argument.
+    - If `rng` is provided as a keyword argument, the decorator validates `rng` using
+      `numpy.random.default_rng` before passing it to the function.
+    - If `end_version` is specified and neither `random_state` nor `rng` is provided
+      by the user, the decorator checks whether `np.random.seed` has been used to set
+      the global seed. If so, it emits a `FutureWarning`, noting that usage of
+      `numpy.random.seed` will eventually have no effect. Either way, the decorator
+      calls the function without explicitly passing the `rng` argument.
+
+    If `end_version` is specified, a user must pass `rng` as a keyword to avoid
+    warnings.
+
+    After the deprecation period, the decorator can be removed, and the function
+    can simply validate the `rng` argument by calling `np.random.default_rng(rng)`.
+
+    * A `FutureWarning` is emitted when the PRNG argument is used by
+      position. It indicates that the "Hinsen principle" (same
+      code yielding different results in two versions of the software)
+      will be violated, unless positional use is deprecated. Specifically:
+
+      - If `None` is passed by position and `np.random.seed` has been used,
+        the function will change from being seeded to being unseeded.
+      - If an integer is passed by position, the random stream will change.
+      - If `np.random` or an instance of `RandomState` is passed by position,
+        an error will be raised.
+
+      We suggest that projects consider deprecating positional use of
+      `random_state`/`rng` (i.e., change their function signatures to
+      ``def my_func(..., *, rng=None)``); that might not make sense
+      for all projects, so this SPEC does not make that
+      recommendation, neither does this decorator enforce it.
+
+    Parameters
+    ----------
+    old_name : str
+        The old name of the PRNG argument (e.g. `seed` or `random_state`).
+    position_num : int, optional
+        The (0-indexed) position of the old PRNG argument (if accepted by position).
+        Maintainers are welcome to eliminate this argument and use, for example,
+        `inspect`, if preferred.
+    end_version : str, optional
+        The full version number of the library when the behavior described in
+        `DeprecationWarning`s and `FutureWarning`s will take effect. If left
+        unspecified, no warnings will be emitted by the decorator.
+    replace_doc : bool, default: True
+        Whether the decorator should replace the documentation for parameter `rng` with
+        `_rng_desc` (defined above), which documents both new `rng` keyword behavior
+        and typical legacy `random_state`/`seed` behavior. If True, manually replace
+        the first paragraph of the function's old `random_state`/`seed` documentation
+        with the desired *final* `rng` documentation; this way, no changes to
+        documentation are needed when the decorator is removed. Documentation of `rng`
+        after the first blank line is preserved. Use False if the function's old
+        `random_state`/`seed` behavior does not match that described by `_rng_desc`.
+
+    """
+    NEW_NAME = "rng"
+
+    cmn_msg = (
+        "To silence this warning and ensure consistent behavior in SciPy "
+        f"{end_version}, control the RNG using argument `{NEW_NAME}`. Arguments passed "
+        f"to keyword `{NEW_NAME}` will be validated by `np.random.default_rng`, so the "
+        "behavior corresponding with a given value may change compared to use of "
+        f"`{old_name}`. For example, "
+        "1) `None` will result in unpredictable random numbers, "
+        "2) an integer will result in a different stream of random numbers, (with the "
+        "same distribution), and "
+        "3) `np.random` or `RandomState` instances will result in an error. "
+        "See the documentation of `default_rng` for more information."
+    )
+
+    def decorator(fun):
+        @functools.wraps(fun)
+        def wrapper(*args, **kwargs):
+            # Determine how PRNG was passed
+            as_old_kwarg = old_name in kwargs
+            as_new_kwarg = NEW_NAME in kwargs
+            as_pos_arg = position_num is not None and len(args) >= position_num + 1
+            emit_warning = end_version is not None
+
+            # Can only specify PRNG one of the three ways
+            if int(as_old_kwarg) + int(as_new_kwarg) + int(as_pos_arg) > 1:
+                message = (
+                    f"{fun.__name__}() got multiple values for "
+                    f"argument now known as `{NEW_NAME}`. Specify one of "
+                    f"`{NEW_NAME}` or `{old_name}`."
+                )
+                raise TypeError(message)
+
+            # Check whether global random state has been set
+            global_seed_set = np.random.mtrand._rand._bit_generator._seed_seq is None
+
+            if as_old_kwarg:  # warn about deprecated use of old kwarg
+                kwargs[NEW_NAME] = kwargs.pop(old_name)
+                if emit_warning:
+                    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}. "
+                    ) + cmn_msg
+                    warnings.warn(message, DeprecationWarning, stacklevel=2)
+
+            elif as_pos_arg:
+                # Warn about changing meaning of positional arg
+
+                # Note that this decorator does not deprecate positional use of the
+                # argument; it only warns that the behavior will change in the future.
+                # Simultaneously transitioning to keyword-only use is another option.
+
+                arg = args[position_num]
+                # If the argument is None and the global seed wasn't set, or if the
+                # argument is one of a few new classes, the user will not notice change
+                # in behavior.
+                ok_classes = (
+                    np.random.Generator,
+                    np.random.SeedSequence,
+                    np.random.BitGenerator,
+                )
+                if (arg is None and not global_seed_set) or isinstance(arg, ok_classes):
+                    pass
+                elif emit_warning:
+                    message = (
+                        f"Positional use of `{NEW_NAME}` (formerly known as "
+                        f"`{old_name}`) is still allowed, but the behavior is "
+                        "changing: the argument will be normalized using "
+                        f"`np.random.default_rng` beginning in SciPy {end_version}, "
+                        "and the resulting `Generator` will be used to generate "
+                        "random numbers."
+                    ) + cmn_msg
+                    warnings.warn(message, FutureWarning, stacklevel=2)
+
+            elif as_new_kwarg:  # no warnings; this is the preferred use
+                # After the removal of the decorator, normalization with
+                # np.random.default_rng will be done inside the decorated function
+                kwargs[NEW_NAME] = np.random.default_rng(kwargs[NEW_NAME])
+
+            elif global_seed_set and emit_warning:
+                # Emit FutureWarning if `np.random.seed` was used and no PRNG was passed
+                message = (
+                    "The NumPy global RNG was seeded by calling "
+                    f"`np.random.seed`. Beginning in {end_version}, this "
+                    "function will no longer use the global RNG."
+                ) + cmn_msg
+                warnings.warn(message, FutureWarning, stacklevel=2)
+
+            return fun(*args, **kwargs)
+
+        # Add the old parameter name to the function signature
+        wrapped_signature = inspect.signature(fun)
+        wrapper.__signature__ = wrapped_signature.replace(parameters=[
+            *wrapped_signature.parameters.values(),
+            inspect.Parameter(old_name, inspect.Parameter.KEYWORD_ONLY, default=None),
+        ])
+
+        if replace_doc:
+            doc = FunctionDoc(wrapper)
+            parameter_names = [param.name for param in doc['Parameters']]
+            if 'rng' in parameter_names:
+                _type = "{None, int, `numpy.random.Generator`}, optional"
+                _desc = _rng_desc.replace("{old_name}", old_name)
+                old_doc = doc['Parameters'][parameter_names.index('rng')].desc
+                old_doc_keep = old_doc[old_doc.index("") + 1:] if "" in old_doc else []
+                new_doc = [_desc] + old_doc_keep
+                _rng_parameter_doc = Parameter('rng', _type, new_doc)
+                doc['Parameters'][parameter_names.index('rng')] = _rng_parameter_doc
+                doc = str(doc).split("\n", 1)[1].lstrip(" \n")  # remove signature
+                wrapper.__doc__ = str(doc)
+        return wrapper
+
+    return decorator
+
+
+# copy-pasted from scikit-learn utils/validation.py
+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:
+        if issparse(a):
+            msg = ('Sparse arrays/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 = wrapped_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 _ScalarFunctionWrapper:
+    """
+    Object to wrap scalar user function, allowing picklability
+    """
+    def __init__(self, f, args=None):
+        self.f = f
+        self.args = [] if args is None else args
+        self.nfev = 0
+
+    def __call__(self, x):
+        # Send a copy because the user may overwrite it.
+        # The user of this class might want `x` to remain unchanged.
+        fx = self.f(np.copy(x), *self.args)
+        self.nfev += 1
+
+        # Make sure the function returns a true scalar
+        if not np.isscalar(fx):
+            try:
+                fx = np.asarray(fx).item()
+            except (TypeError, ValueError) as e:
+                raise ValueError(
+                    "The user-provided objective function "
+                    "must return a scalar value."
+                ) from e
+        return fx
+
+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 get_context, get_start_method
+
+            method = get_start_method(allow_none=True)
+
+            if method is None and os.name=='posix' and sys.version_info < (3, 14):
+                # Python 3.13 and older used "fork" on posix, which can lead to
+                # deadlocks. This backports that fix to older Python versions.
+                method = 'forkserver'
+
+            # user supplies a number
+            if int(pool) == -1:
+                # use as many processors as possible
+                self.pool = get_context(method=method).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 = get_context(method=method).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 _workers_wrapper(func):
+    """
+    Wrapper to deal with setup-cleanup of workers outside a user function via a
+    ContextManager. It saves having to do the setup/tear down with within that
+    function, which can be messy.
+    """
+    @functools.wraps(func)
+    def inner(*args, **kwds):
+        kwargs = kwds.copy()
+        if 'workers' not in kwargs:
+            _workers = map
+        elif 'workers' in kwargs and kwargs['workers'] is None:
+            _workers = map
+        else:
+            _workers = kwargs['workers']
+
+        with MapWrapper(_workers) as mf:
+            kwargs['workers'] = mf
+            return func(*args, **kwargs)
+
+    return inner
+
+
+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, np.random.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
+
+
+@contextmanager
+def ignore_warns(expected_warning, *, match=None):
+    with warnings.catch_warnings():
+        warnings.filterwarnings("ignore", match, expected_warning)
+        yield
+
+
+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 _contains_nan(
+    a: Array,
+    nan_policy: Literal["propagate", "raise", "omit"] = "propagate",
+    *,
+    xp_omit_okay: bool = False,
+    xp: ModuleType | None = None,
+) -> Array | bool:
+    # Regarding `xp_omit_okay`: Temporarily, while `_axis_nan_policy` does not
+    # handle non-NumPy arrays, most functions that call `_contains_nan` want
+    # it to raise an error if `nan_policy='omit'` and `xp` is not `np`.
+    # Some functions support `nan_policy='omit'` natively, so setting this to
+    # `True` prevents the error from being raised.
+    policies = {"propagate", "raise", "omit"}
+    if nan_policy not in policies:
+        msg = f"nan_policy must be one of {policies}."
+        raise ValueError(msg)
+
+    if xp_size(a) == 0:
+        return False
+
+    if xp is None:
+        xp = array_namespace(a)
+
+    if xp.isdtype(a.dtype, "real floating"):
+        # Faster and less memory-intensive than xp.any(xp.isnan(a)), and unlike other
+        # reductions, `max`/`min` won't return NaN unless there is a NaN in the data.
+        contains_nan = xp.isnan(xp.max(a))
+    elif xp.isdtype(a.dtype, "complex floating"):
+        # Typically `real` and `imag` produce views; otherwise, `xp.any(xp.isnan(a))`
+        # would be more efficient.
+        contains_nan = xp.isnan(xp.max(xp.real(a))) | xp.isnan(xp.max(xp.imag(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
+        return False
+
+    # The implicit call to bool(contains_nan) must happen after testing
+    # nan_policy to prevent lazy and device-bound xps from raising in the
+    # default policy='propagate' case.
+    if nan_policy == 'raise':
+        if is_lazy_array(a):
+            msg = "nan_policy='raise' is not supported for lazy arrays."
+            raise TypeError(msg)
+        if contains_nan:
+            msg = "The input contains nan values"
+            raise ValueError(msg)
+    elif nan_policy == 'omit' and not xp_omit_okay and not is_numpy(xp):
+        if is_lazy_array(a):
+            msg = "nan_policy='omit' is not supported for lazy arrays."
+            raise TypeError(msg)
+
+    return contains_nan
+
+
+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, shape=(), xp=None):
+    xp = array_namespace(*data) if xp is None else xp
+    # Get NaN of appropriate dtype for data
+    dtype = xp_result_type(*data, force_floating=True, xp=xp)
+    device = xp_result_device(*data)
+    res = xp.full(shape, xp.nan, dtype=dtype, device=device)
+    if not shape:
+        res = res[()]
+    # whenever mdhaber/marray#89 is resolved, could just return `res`
+    return res.data if is_marray(xp) else res
+
+
+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
+
+
+_batch_note = """
+The documentation is written assuming array arguments are of specified
+"core" shapes. However, array argument(s) of this function may have additional
+"batch" dimensions prepended to the core shape. In this case, the array is treated
+as a batch of lower-dimensional slices; see :ref:`linalg_batch` for details.
+Note that calls with zero-size batches are unsupported and will raise a ``ValueError``.
+"""
+
+
+def _apply_over_batch(*argdefs):
+    """
+    Factory for decorator that applies a function over batched arguments.
+
+    Array arguments may have any number of core dimensions (typically 0,
+    1, or 2) and any broadcastable batch shapes. There may be any
+    number of array outputs of any number of dimensions. Assumptions
+    right now - which are satisfied by all functions of interest in `linalg` -
+    are that all array inputs are consecutive keyword or positional arguments,
+    and that the wrapped function returns either a single array or a tuple of
+    arrays. It's only as general as it needs to be right now - it can be extended.
+
+    Parameters
+    ----------
+    *argdefs : tuple of (str, int)
+        Definitions of array arguments: the keyword name of the argument, and
+        the number of core dimensions.
+
+    Example:
+    --------
+    `linalg.eig` accepts two matrices as the first two arguments `a` and `b`, where
+    `b` is optional, and returns one array or a tuple of arrays, depending on the
+    values of other positional or keyword arguments. To generate a wrapper that applies
+    the function over batches of `a` and optionally `b` :
+
+    >>> _apply_over_batch(('a', 2), ('b', 2))
+    """
+    names, ndims = list(zip(*argdefs))
+    n_arrays = len(names)
+
+    def decorator(f):
+        @functools.wraps(f)
+        def wrapper(*args, **kwargs):
+            args = list(args)
+
+            # Ensure all arrays in `arrays`, other arguments in `other_args`/`kwargs`
+            arrays, other_args = args[:n_arrays], args[n_arrays:]
+            for i, name in enumerate(names):
+                if name in kwargs:
+                    if i + 1 <= len(args):
+                        raise ValueError(f'{f.__name__}() got multiple values '
+                                         f'for argument `{name}`.')
+                    else:
+                        arrays.append(kwargs.pop(name))
+
+            xp = array_namespace(*arrays)
+
+            # Determine core and batch shapes
+            batch_shapes = []
+            core_shapes = []
+            for i, (array, ndim) in enumerate(zip(arrays, ndims)):
+                array = None if array is None else xp.asarray(array)
+                shape = () if array is None else array.shape
+
+                if ndim == "1|2":  # special case for `solve`, etc.
+                    ndim = 2 if array.ndim >= 2 else 1
+
+                arrays[i] = array
+                batch_shapes.append(shape[:-ndim] if ndim > 0 else shape)
+                core_shapes.append(shape[-ndim:] if ndim > 0 else ())
+
+            # Early exit if call is not batched
+            if not any(batch_shapes):
+                return f(*arrays, *other_args, **kwargs)
+
+            # Determine broadcasted batch shape
+            batch_shape = np.broadcast_shapes(*batch_shapes)  # Gives OK error message
+
+            # We can't support zero-size batches right now because without data with
+            # which to call the function, the decorator doesn't even know the *number*
+            # of outputs, let alone their core shapes or dtypes.
+            if math.prod(batch_shape) == 0:
+                message = f'`{f.__name__}` does not support zero-size batches.'
+                raise ValueError(message)
+
+            # Broadcast arrays to appropriate shape
+            for i, (array, core_shape) in enumerate(zip(arrays, core_shapes)):
+                if array is None:
+                    continue
+                arrays[i] = xp.broadcast_to(array, batch_shape + core_shape)
+
+            # Main loop
+            results = []
+            for index in np.ndindex(batch_shape):
+                result = f(*((array[index] if array is not None else None)
+                             for array in arrays), *other_args, **kwargs)
+                # Assume `result` is either a tuple or single array. This is easily
+                # generalized by allowing the contributor to pass an `unpack_result`
+                # callable to the decorator factory.
+                result = (result,) if not isinstance(result, tuple) else result
+                results.append(result)
+            results = list(zip(*results))
+
+            # Reshape results
+            for i, result in enumerate(results):
+                result = xp.stack(result)
+                core_shape = result.shape[1:]
+                results[i] = xp.reshape(result, batch_shape + core_shape)
+
+            # Assume `result` should be a single array if there is only one element or
+            # a `tuple` otherwise. This is easily generalized by allowing the
+            # contributor to pass an `pack_result` callable to the decorator factory.
+            return results[0] if len(results) == 1 else results
+
+        doc = FunctionDoc(wrapper)
+        doc['Extended Summary'].append(_batch_note.rstrip())
+        wrapper.__doc__ = str(doc).split("\n", 1)[1].lstrip(" \n")  # remove signature
+
+        return wrapper
+    return decorator
+
+
+def np_vecdot(x1, x2, /, *, axis=-1):
+    # `np.vecdot` has advantages (e.g. see gh-22462), so let's use it when
+    # available. As functions are translated to Array API, `np_vecdot` can be
+    # replaced with `xp.vecdot`.
+    if np.__version__ > "2.0":
+        return np.vecdot(x1, x2, axis=axis)
+    else:
+        # of course there are other fancy ways of doing this (e.g. `einsum`)
+        # but let's keep it simple since it's temporary
+        return np.sum(x1 * x2, axis=axis)
+
+
+def _dedent_for_py313(s):
+    """Apply textwrap.dedent to s for Python versions 3.13 or later."""
+    return s if sys.version_info < (3, 13) else textwrap.dedent(s)
+
+
+def broadcastable(shape_a: tuple[int, ...], shape_b: tuple[int, ...]) -> bool:
+    """Check if two shapes are broadcastable."""
+    return all(
+        (m == n) or (m == 1) or (n == 1) for m, n in zip(shape_a[::-1], shape_b[::-1])
+    )
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/__init__.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4abca400a24f71dd59876488e4cf9fde89de9489
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/__init__.py
@@ -0,0 +1,22 @@
+"""
+NumPy Array API compatibility library
+
+This is a small wrapper around NumPy, CuPy, JAX, sparse and others that are
+compatible with the Array API standard https://data-apis.org/array-api/latest/.
+See also NEP 47 https://numpy.org/neps/nep-0047-array-api-standard.html.
+
+Unlike array_api_strict, this is not a strict minimal implementation of the
+Array API, but rather just an extension of the main NumPy namespace with
+changes needed to be compliant with the Array API. See
+https://numpy.org/doc/stable/reference/array_api.html for a full list of
+changes. In particular, unlike array_api_strict, this package does not use a
+separate Array object, but rather just uses numpy.ndarray directly.
+
+Library authors using the Array API may wish to test against array_api_strict
+to ensure they are not using functionality outside of the standard, but prefer
+this implementation for the default when working with NumPy arrays.
+
+"""
+__version__ = '1.13.0'
+
+from .common import *  # noqa: F401, F403
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/__pycache__/__init__.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..204d7f1ccba47b86d6330fd4e73d97c465984828
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/__pycache__/__init__.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/__pycache__/_internal.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/__pycache__/_internal.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ca212c155fa5c4ebffd4f89d814d4cadf2842c97
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/__pycache__/_internal.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/_internal.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/_internal.py
new file mode 100644
index 0000000000000000000000000000000000000000..baa39ded8decf2837540837ebc7b49c7b6971b82
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/_internal.py
@@ -0,0 +1,77 @@
+"""
+Internal helpers
+"""
+
+import importlib
+from collections.abc import Callable
+from functools import wraps
+from inspect import signature
+from types import ModuleType
+from typing import TypeVar
+
+_T = TypeVar("_T")
+
+
+def get_xp(xp: ModuleType) -> Callable[[Callable[..., _T]], Callable[..., _T]]:
+    """
+    Decorator to automatically replace xp with the corresponding array module.
+
+    Use like
+
+    import numpy as np
+
+    @get_xp(np)
+    def func(x, /, xp, kwarg=None):
+        return xp.func(x, kwarg=kwarg)
+
+    Note that xp must be a keyword argument and come after all non-keyword
+    arguments.
+
+    """
+
+    def inner(f: Callable[..., _T], /) -> Callable[..., _T]:
+        @wraps(f)
+        def wrapped_f(*args: object, **kwargs: object) -> object:
+            return f(*args, xp=xp, **kwargs)
+
+        sig = signature(f)
+        new_sig = sig.replace(
+            parameters=[par for i, par in sig.parameters.items() if i != "xp"]
+        )
+
+        if wrapped_f.__doc__ is None:
+            wrapped_f.__doc__ = f"""\
+Array API compatibility wrapper for {f.__name__}.
+
+See the corresponding documentation in NumPy/CuPy and/or the array API
+specification for more details.
+
+"""
+        wrapped_f.__signature__ = new_sig  # type: ignore[attr-defined] # pyright: ignore[reportAttributeAccessIssue]
+        return wrapped_f  # type: ignore[return-value] # pyright: ignore[reportReturnType]
+
+    return inner
+
+
+def clone_module(mod_name: str, globals_: dict[str, object]) -> list[str]:
+    """Import everything from module, updating globals().
+    Returns __all__.
+    """
+    mod = importlib.import_module(mod_name)
+    # Neither of these two methods is sufficient by itself,
+    # depending on various idiosyncrasies of the libraries we're wrapping.
+    objs = {}
+    exec(f"from {mod.__name__} import *", objs)
+
+    for n in dir(mod):
+        if not n.startswith("_") and hasattr(mod, n):
+            objs[n] = getattr(mod, n)
+
+    globals_.update(objs)
+    return list(objs)
+
+
+__all__ = ["get_xp", "clone_module"]
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__init__.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..8236080738175f0fb7d1f47fcc52522306b43050
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__init__.py
@@ -0,0 +1 @@
+from ._helpers import *  # noqa: F403
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__pycache__/__init__.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..17e14f3c27223dd2f7eaa77aa08df1493d665bdb
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__pycache__/__init__.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__pycache__/_aliases.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__pycache__/_aliases.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..4e17fd6a215d88ae2ec3ae9f0da09919445212e0
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__pycache__/_aliases.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__pycache__/_fft.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__pycache__/_fft.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..d3af9c0a23cfdc631d221ebb0317d83b3378a7d1
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__pycache__/_fft.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__pycache__/_helpers.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__pycache__/_helpers.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..8cf254f9ec170a68292e4d7ac962566876c238f0
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__pycache__/_helpers.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__pycache__/_linalg.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__pycache__/_linalg.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..90f03dbedf5fda279845c833bf25d796b872a2c7
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__pycache__/_linalg.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__pycache__/_typing.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__pycache__/_typing.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..e1b4c9daf17a02eaccb97e5a6fa2eb61ac63e78f
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/__pycache__/_typing.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/_aliases.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/_aliases.py
new file mode 100644
index 0000000000000000000000000000000000000000..3587ef16fa18b22081770ecbd13335a4fed5ac0c
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/_aliases.py
@@ -0,0 +1,702 @@
+"""
+These are functions that are just aliases of existing functions in NumPy.
+"""
+
+from __future__ import annotations
+
+import inspect
+from collections.abc import Sequence
+from typing import TYPE_CHECKING, Any, NamedTuple, cast
+
+from ._helpers import _check_device, array_namespace
+from ._helpers import device as _get_device
+from ._helpers import is_cupy_namespace
+from ._typing import Array, Device, DType, Namespace
+
+if TYPE_CHECKING:
+    # TODO: import from typing (requires Python >=3.13)
+    from typing_extensions import TypeIs
+
+# These functions are modified from the NumPy versions.
+
+# Creation functions add the device keyword (which does nothing for NumPy and Dask)
+
+
+def arange(
+    start: float,
+    /,
+    stop: float | None = None,
+    step: float = 1,
+    *,
+    xp: Namespace,
+    dtype: DType | None = None,
+    device: Device | None = None,
+    **kwargs: object,
+) -> Array:
+    _check_device(xp, device)
+    return xp.arange(start, stop=stop, step=step, dtype=dtype, **kwargs)
+
+
+def empty(
+    shape: int | tuple[int, ...],
+    xp: Namespace,
+    *,
+    dtype: DType | None = None,
+    device: Device | None = None,
+    **kwargs: object,
+) -> Array:
+    _check_device(xp, device)
+    return xp.empty(shape, dtype=dtype, **kwargs)
+
+
+def empty_like(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    dtype: DType | None = None,
+    device: Device | None = None,
+    **kwargs: object,
+) -> Array:
+    _check_device(xp, device)
+    return xp.empty_like(x, dtype=dtype, **kwargs)
+
+
+def eye(
+    n_rows: int,
+    n_cols: int | None = None,
+    /,
+    *,
+    xp: Namespace,
+    k: int = 0,
+    dtype: DType | None = None,
+    device: Device | None = None,
+    **kwargs: object,
+) -> Array:
+    _check_device(xp, device)
+    return xp.eye(n_rows, M=n_cols, k=k, dtype=dtype, **kwargs)
+
+
+def full(
+    shape: int | tuple[int, ...],
+    fill_value: complex,
+    xp: Namespace,
+    *,
+    dtype: DType | None = None,
+    device: Device | None = None,
+    **kwargs: object,
+) -> Array:
+    _check_device(xp, device)
+    return xp.full(shape, fill_value, dtype=dtype, **kwargs)
+
+
+def full_like(
+    x: Array,
+    /,
+    fill_value: complex,
+    *,
+    xp: Namespace,
+    dtype: DType | None = None,
+    device: Device | None = None,
+    **kwargs: object,
+) -> Array:
+    _check_device(xp, device)
+    return xp.full_like(x, fill_value, dtype=dtype, **kwargs)
+
+
+def linspace(
+    start: float,
+    stop: float,
+    /,
+    num: int,
+    *,
+    xp: Namespace,
+    dtype: DType | None = None,
+    device: Device | None = None,
+    endpoint: bool = True,
+    **kwargs: object,
+) -> Array:
+    _check_device(xp, device)
+    return xp.linspace(start, stop, num, dtype=dtype, endpoint=endpoint, **kwargs)
+
+
+def ones(
+    shape: int | tuple[int, ...],
+    xp: Namespace,
+    *,
+    dtype: DType | None = None,
+    device: Device | None = None,
+    **kwargs: object,
+) -> Array:
+    _check_device(xp, device)
+    return xp.ones(shape, dtype=dtype, **kwargs)
+
+
+def ones_like(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    dtype: DType | None = None,
+    device: Device | None = None,
+    **kwargs: object,
+) -> Array:
+    _check_device(xp, device)
+    return xp.ones_like(x, dtype=dtype, **kwargs)
+
+
+def zeros(
+    shape: int | tuple[int, ...],
+    xp: Namespace,
+    *,
+    dtype: DType | None = None,
+    device: Device | None = None,
+    **kwargs: object,
+) -> Array:
+    _check_device(xp, device)
+    return xp.zeros(shape, dtype=dtype, **kwargs)
+
+
+def zeros_like(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    dtype: DType | None = None,
+    device: Device | None = None,
+    **kwargs: object,
+) -> Array:
+    _check_device(xp, device)
+    return xp.zeros_like(x, dtype=dtype, **kwargs)
+
+
+# np.unique() is split into four functions in the array API:
+# unique_all, unique_counts, unique_inverse, and unique_values (this is done
+# to remove polymorphic return types).
+
+# The functions here return namedtuples (np.unique() returns a normal
+# tuple).
+
+
+# Note that these named tuples aren't actually part of the standard namespace,
+# but I don't see any issue with exporting the names here regardless.
+class UniqueAllResult(NamedTuple):
+    values: Array
+    indices: Array
+    inverse_indices: Array
+    counts: Array
+
+
+class UniqueCountsResult(NamedTuple):
+    values: Array
+    counts: Array
+
+
+class UniqueInverseResult(NamedTuple):
+    values: Array
+    inverse_indices: Array
+
+
+def _unique_kwargs(xp: Namespace) -> dict[str, bool]:
+    # Older versions of NumPy and CuPy do not have equal_nan. Rather than
+    # trying to parse version numbers, just check if equal_nan is in the
+    # signature.
+    s = inspect.signature(xp.unique)
+    if "equal_nan" in s.parameters:
+        return {"equal_nan": False}
+    return {}
+
+
+def unique_all(x: Array, /, xp: Namespace) -> UniqueAllResult:
+    kwargs = _unique_kwargs(xp)
+    values, indices, inverse_indices, counts = xp.unique(
+        x,
+        return_counts=True,
+        return_index=True,
+        return_inverse=True,
+        **kwargs,
+    )
+    # np.unique() flattens inverse indices, but they need to share x's shape
+    # See https://github.com/numpy/numpy/issues/20638
+    inverse_indices = inverse_indices.reshape(x.shape)
+    return UniqueAllResult(
+        values,
+        indices,
+        inverse_indices,
+        counts,
+    )
+
+
+def unique_counts(x: Array, /, xp: Namespace) -> UniqueCountsResult:
+    kwargs = _unique_kwargs(xp)
+    res = xp.unique(
+        x, return_counts=True, return_index=False, return_inverse=False, **kwargs
+    )
+
+    return UniqueCountsResult(*res)
+
+
+def unique_inverse(x: Array, /, xp: Namespace) -> UniqueInverseResult:
+    kwargs = _unique_kwargs(xp)
+    values, inverse_indices = xp.unique(
+        x,
+        return_counts=False,
+        return_index=False,
+        return_inverse=True,
+        **kwargs,
+    )
+    # xp.unique() flattens inverse indices, but they need to share x's shape
+    # See https://github.com/numpy/numpy/issues/20638
+    inverse_indices = inverse_indices.reshape(x.shape)
+    return UniqueInverseResult(values, inverse_indices)
+
+
+def unique_values(x: Array, /, xp: Namespace) -> Array:
+    kwargs = _unique_kwargs(xp)
+    return xp.unique(
+        x,
+        return_counts=False,
+        return_index=False,
+        return_inverse=False,
+        **kwargs,
+    )
+
+
+# These functions have different keyword argument names
+
+
+def std(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    axis: int | tuple[int, ...] | None = None,
+    correction: float = 0.0,  # correction instead of ddof
+    keepdims: bool = False,
+    **kwargs: object,
+) -> Array:
+    return xp.std(x, axis=axis, ddof=correction, keepdims=keepdims, **kwargs)
+
+
+def var(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    axis: int | tuple[int, ...] | None = None,
+    correction: float = 0.0,  # correction instead of ddof
+    keepdims: bool = False,
+    **kwargs: object,
+) -> Array:
+    return xp.var(x, axis=axis, ddof=correction, keepdims=keepdims, **kwargs)
+
+
+# cumulative_sum is renamed from cumsum, and adds the include_initial keyword
+# argument
+
+
+def cumulative_sum(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    axis: int | None = None,
+    dtype: DType | None = None,
+    include_initial: bool = False,
+    **kwargs: object,
+) -> Array:
+    wrapped_xp = array_namespace(x)
+
+    # TODO: The standard is not clear about what should happen when x.ndim == 0.
+    if axis is None:
+        if x.ndim > 1:
+            raise ValueError(
+                "axis must be specified in cumulative_sum for more than one dimension"
+            )
+        axis = 0
+
+    res = xp.cumsum(x, axis=axis, dtype=dtype, **kwargs)
+
+    # np.cumsum does not support include_initial
+    if include_initial:
+        initial_shape = list(x.shape)
+        initial_shape[axis] = 1
+        res = xp.concatenate(
+            [
+                wrapped_xp.zeros(
+                    shape=initial_shape, dtype=res.dtype, device=_get_device(res)
+                ),
+                res,
+            ],
+            axis=axis,
+        )
+    return res
+
+
+def cumulative_prod(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    axis: int | None = None,
+    dtype: DType | None = None,
+    include_initial: bool = False,
+    **kwargs: object,
+) -> Array:
+    wrapped_xp = array_namespace(x)
+
+    if axis is None:
+        if x.ndim > 1:
+            raise ValueError(
+                "axis must be specified in cumulative_prod for more than one dimension"
+            )
+        axis = 0
+
+    res = xp.cumprod(x, axis=axis, dtype=dtype, **kwargs)
+
+    # np.cumprod does not support include_initial
+    if include_initial:
+        initial_shape = list(x.shape)
+        initial_shape[axis] = 1
+        res = xp.concatenate(
+            [
+                wrapped_xp.ones(
+                    shape=initial_shape, dtype=res.dtype, device=_get_device(res)
+                ),
+                res,
+            ],
+            axis=axis,
+        )
+    return res
+
+
+# The min and max argument names in clip are different and not optional in numpy, and type
+# promotion behavior is different.
+def clip(
+    x: Array,
+    /,
+    min: float | Array | None = None,
+    max: float | Array | None = None,
+    *,
+    xp: Namespace,
+    # TODO: np.clip has other ufunc kwargs
+    out: Array | None = None,
+) -> Array:
+    def _isscalar(a: object) -> TypeIs[float | None]:
+        return isinstance(a, int | float) or a is None
+
+    min_shape = () if _isscalar(min) else min.shape
+    max_shape = () if _isscalar(max) else max.shape
+
+    wrapped_xp = array_namespace(x)
+
+    result_shape = xp.broadcast_shapes(x.shape, min_shape, max_shape)
+
+    # np.clip does type promotion but the array API clip requires that the
+    # output have the same dtype as x. We do this instead of just downcasting
+    # the result of xp.clip() to handle some corner cases better (e.g.,
+    # avoiding uint64 -> float64 promotion).
+
+    # Note: cases where min or max overflow (integer) or round (float) in the
+    # wrong direction when downcasting to x.dtype are unspecified. This code
+    # just does whatever NumPy does when it downcasts in the assignment, but
+    # other behavior could be preferred, especially for integers. For example,
+    # this code produces:
+
+    # >>> clip(asarray(0, dtype=int8), asarray(128, dtype=int16), None)
+    # -128
+
+    # but an answer of 0 might be preferred. See
+    # https://github.com/numpy/numpy/issues/24976 for more discussion on this issue.
+
+    # At least handle the case of Python integers correctly (see
+    # https://github.com/numpy/numpy/pull/26892).
+    if wrapped_xp.isdtype(x.dtype, "integral"):
+        if type(min) is int and min <= wrapped_xp.iinfo(x.dtype).min:
+            min = None
+        if type(max) is int and max >= wrapped_xp.iinfo(x.dtype).max:
+            max = None
+
+    dev = _get_device(x)
+    if out is None:
+        out = wrapped_xp.empty(result_shape, dtype=x.dtype, device=dev)
+    assert out is not None  # workaround for a type-narrowing issue in pyright
+    out[()] = x
+
+    if min is not None:
+        a = wrapped_xp.asarray(min, dtype=x.dtype, device=dev)
+        a = xp.broadcast_to(a, result_shape)
+        ia = (out < a) | xp.isnan(a)
+        out[ia] = a[ia]
+
+    if max is not None:
+        b = wrapped_xp.asarray(max, dtype=x.dtype, device=dev)
+        b = xp.broadcast_to(b, result_shape)
+        ib = (out > b) | xp.isnan(b)
+        out[ib] = b[ib]
+
+    # Return a scalar for 0-D
+    return out[()]
+
+
+# Unlike transpose(), the axes argument to permute_dims() is required.
+def permute_dims(x: Array, /, axes: tuple[int, ...], xp: Namespace) -> Array:
+    return xp.transpose(x, axes)
+
+
+# np.reshape calls the keyword argument 'newshape' instead of 'shape'
+def reshape(
+    x: Array,
+    /,
+    shape: tuple[int, ...],
+    xp: Namespace,
+    *,
+    copy: bool | None = None,
+    **kwargs: object,
+) -> Array:
+    if copy is True:
+        x = x.copy()
+    elif copy is False:
+        y = x.view()
+        y.shape = shape
+        return y
+    return xp.reshape(x, shape, **kwargs)
+
+
+# The descending keyword is new in sort and argsort, and 'kind' replaced with
+# 'stable'
+def argsort(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    axis: int = -1,
+    descending: bool = False,
+    stable: bool = True,
+    **kwargs: object,
+) -> Array:
+    # Note: this keyword argument is different, and the default is different.
+    # We set it in kwargs like this because numpy.sort uses kind='quicksort'
+    # as the default whereas cupy.sort uses kind=None.
+    if stable:
+        kwargs["kind"] = "stable"
+    if not descending:
+        res = xp.argsort(x, axis=axis, **kwargs)
+    else:
+        # As NumPy has no native descending sort, we imitate it here. Note that
+        # simply flipping the results of xp.argsort(x, ...) would not
+        # respect the relative order like it would in native descending sorts.
+        res = xp.flip(
+            xp.argsort(xp.flip(x, axis=axis), axis=axis, **kwargs),
+            axis=axis,
+        )
+        # Rely on flip()/argsort() to validate axis
+        normalised_axis = axis if axis >= 0 else x.ndim + axis
+        max_i = x.shape[normalised_axis] - 1
+        res = max_i - res
+    return res
+
+
+def sort(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    axis: int = -1,
+    descending: bool = False,
+    stable: bool = True,
+    **kwargs: object,
+) -> Array:
+    # Note: this keyword argument is different, and the default is different.
+    # We set it in kwargs like this because numpy.sort uses kind='quicksort'
+    # as the default whereas cupy.sort uses kind=None.
+    if stable:
+        kwargs["kind"] = "stable"
+    res = xp.sort(x, axis=axis, **kwargs)
+    if descending:
+        res = xp.flip(res, axis=axis)
+    return res
+
+
+# nonzero should error for zero-dimensional arrays
+def nonzero(x: Array, /, xp: Namespace, **kwargs: object) -> tuple[Array, ...]:
+    if x.ndim == 0:
+        raise ValueError("nonzero() does not support zero-dimensional arrays")
+    return xp.nonzero(x, **kwargs)
+
+
+# linear algebra functions
+
+
+def matmul(x1: Array, x2: Array, /, xp: Namespace, **kwargs: object) -> Array:
+    return xp.matmul(x1, x2, **kwargs)
+
+
+# Unlike transpose, matrix_transpose only transposes the last two axes.
+def matrix_transpose(x: Array, /, xp: Namespace) -> Array:
+    if x.ndim < 2:
+        raise ValueError("x must be at least 2-dimensional for matrix_transpose")
+    return xp.swapaxes(x, -1, -2)
+
+
+def tensordot(
+    x1: Array,
+    x2: Array,
+    /,
+    xp: Namespace,
+    *,
+    axes: int | tuple[Sequence[int], Sequence[int]] = 2,
+    **kwargs: object,
+) -> Array:
+    return xp.tensordot(x1, x2, axes=axes, **kwargs)
+
+
+def vecdot(x1: Array, x2: Array, /, xp: Namespace, *, axis: int = -1) -> Array:
+    if x1.shape[axis] != x2.shape[axis]:
+        raise ValueError("x1 and x2 must have the same size along the given axis")
+
+    if hasattr(xp, "broadcast_tensors"):
+        _broadcast = xp.broadcast_tensors
+    else:
+        _broadcast = xp.broadcast_arrays
+
+    x1_ = xp.moveaxis(x1, axis, -1)
+    x2_ = xp.moveaxis(x2, axis, -1)
+    x1_, x2_ = _broadcast(x1_, x2_)
+
+    res = xp.conj(x1_[..., None, :]) @ x2_[..., None]
+    return res[..., 0, 0]
+
+
+# isdtype is a new function in the 2022.12 array API specification.
+
+
+def isdtype(
+    dtype: DType,
+    kind: DType | str | tuple[DType | str, ...],
+    xp: Namespace,
+    *,
+    _tuple: bool = True,  # Disallow nested tuples
+) -> bool:
+    """
+    Returns a boolean indicating whether a provided dtype is of a specified data type ``kind``.
+
+    Note that outside of this function, this compat library does not yet fully
+    support complex numbers.
+
+    See
+    https://data-apis.org/array-api/latest/API_specification/generated/array_api.isdtype.html
+    for more details
+    """
+    if isinstance(kind, tuple) and _tuple:
+        return any(
+            isdtype(dtype, k, xp, _tuple=False)
+            for k in cast("tuple[DType | str, ...]", kind)
+        )
+    elif isinstance(kind, str):
+        if kind == "bool":
+            return dtype == xp.bool_
+        elif kind == "signed integer":
+            return xp.issubdtype(dtype, xp.signedinteger)
+        elif kind == "unsigned integer":
+            return xp.issubdtype(dtype, xp.unsignedinteger)
+        elif kind == "integral":
+            return xp.issubdtype(dtype, xp.integer)
+        elif kind == "real floating":
+            return xp.issubdtype(dtype, xp.floating)
+        elif kind == "complex floating":
+            return xp.issubdtype(dtype, xp.complexfloating)
+        elif kind == "numeric":
+            return xp.issubdtype(dtype, xp.number)
+        else:
+            raise ValueError(f"Unrecognized data type kind: {kind!r}")
+    else:
+        # This will allow things that aren't required by the spec, like
+        # isdtype(np.float64, float) or isdtype(np.int64, 'l'). Should we be
+        # more strict here to match the type annotation? Note that the
+        # array_api_strict implementation will be very strict.
+        return dtype == kind
+
+
+# unstack is a new function in the 2023.12 array API standard
+def unstack(x: Array, /, xp: Namespace, *, axis: int = 0) -> tuple[Array, ...]:
+    if x.ndim == 0:
+        raise ValueError("Input array must be at least 1-d.")
+    return tuple(xp.moveaxis(x, axis, 0))
+
+
+# numpy 1.26 does not use the standard definition for sign on complex numbers
+
+
+def sign(x: Array, /, xp: Namespace, **kwargs: object) -> Array:
+    if isdtype(x.dtype, "complex floating", xp=xp):
+        out = (x / xp.abs(x, **kwargs))[...]
+        # sign(0) = 0 but the above formula would give nan
+        out[x == 0j] = 0j
+    else:
+        out = xp.sign(x, **kwargs)
+    # CuPy sign() does not propagate nans. See
+    # https://github.com/data-apis/array-api-compat/issues/136
+    if is_cupy_namespace(xp) and isdtype(x.dtype, "real floating", xp=xp):
+        out[xp.isnan(x)] = xp.nan
+    return out[()]
+
+
+def finfo(type_: DType | Array, /, xp: Namespace) -> Any:
+    # It is surprisingly difficult to recognize a dtype apart from an array.
+    # np.int64 is not the same as np.asarray(1).dtype!
+    try:
+        return xp.finfo(type_)
+    except (ValueError, TypeError):
+        return xp.finfo(type_.dtype)
+
+
+def iinfo(type_: DType | Array, /, xp: Namespace) -> Any:
+    try:
+        return xp.iinfo(type_)
+    except (ValueError, TypeError):
+        return xp.iinfo(type_.dtype)
+
+
+__all__ = [
+    "arange",
+    "empty",
+    "empty_like",
+    "eye",
+    "full",
+    "full_like",
+    "linspace",
+    "ones",
+    "ones_like",
+    "zeros",
+    "zeros_like",
+    "UniqueAllResult",
+    "UniqueCountsResult",
+    "UniqueInverseResult",
+    "unique_all",
+    "unique_counts",
+    "unique_inverse",
+    "unique_values",
+    "std",
+    "var",
+    "cumulative_sum",
+    "cumulative_prod",
+    "clip",
+    "permute_dims",
+    "reshape",
+    "argsort",
+    "sort",
+    "nonzero",
+    "matmul",
+    "matrix_transpose",
+    "tensordot",
+    "vecdot",
+    "isdtype",
+    "unstack",
+    "sign",
+    "finfo",
+    "iinfo",
+]
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/_fft.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/_fft.py
new file mode 100644
index 0000000000000000000000000000000000000000..18839d37f84949ec2bf851b548c2e13b5179e04e
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/_fft.py
@@ -0,0 +1,213 @@
+from __future__ import annotations
+
+from collections.abc import Sequence
+from typing import Literal, TypeAlias
+
+from ._typing import Array, Device, DType, Namespace
+
+_Norm: TypeAlias = Literal["backward", "ortho", "forward"]
+
+# Note: NumPy fft functions improperly upcast float32 and complex64 to
+# complex128, which is why we require wrapping them all here.
+
+def fft(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    n: int | None = None,
+    axis: int = -1,
+    norm: _Norm = "backward",
+) -> Array:
+    res = xp.fft.fft(x, n=n, axis=axis, norm=norm)
+    if x.dtype in [xp.float32, xp.complex64]:
+        return res.astype(xp.complex64)
+    return res
+
+def ifft(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    n: int | None = None,
+    axis: int = -1,
+    norm: _Norm = "backward",
+) -> Array:
+    res = xp.fft.ifft(x, n=n, axis=axis, norm=norm)
+    if x.dtype in [xp.float32, xp.complex64]:
+        return res.astype(xp.complex64)
+    return res
+
+def fftn(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    s: Sequence[int] | None = None,
+    axes: Sequence[int] | None = None,
+    norm: _Norm = "backward",
+) -> Array:
+    res = xp.fft.fftn(x, s=s, axes=axes, norm=norm)
+    if x.dtype in [xp.float32, xp.complex64]:
+        return res.astype(xp.complex64)
+    return res
+
+def ifftn(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    s: Sequence[int] | None = None,
+    axes: Sequence[int] | None = None,
+    norm: _Norm = "backward",
+) -> Array:
+    res = xp.fft.ifftn(x, s=s, axes=axes, norm=norm)
+    if x.dtype in [xp.float32, xp.complex64]:
+        return res.astype(xp.complex64)
+    return res
+
+def rfft(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    n: int | None = None,
+    axis: int = -1,
+    norm: _Norm = "backward",
+) -> Array:
+    res = xp.fft.rfft(x, n=n, axis=axis, norm=norm)
+    if x.dtype == xp.float32:
+        return res.astype(xp.complex64)
+    return res
+
+def irfft(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    n: int | None = None,
+    axis: int = -1,
+    norm: _Norm = "backward",
+) -> Array:
+    res = xp.fft.irfft(x, n=n, axis=axis, norm=norm)
+    if x.dtype == xp.complex64:
+        return res.astype(xp.float32)
+    return res
+
+def rfftn(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    s: Sequence[int] | None = None,
+    axes: Sequence[int] | None = None,
+    norm: _Norm = "backward",
+) -> Array:
+    res = xp.fft.rfftn(x, s=s, axes=axes, norm=norm)
+    if x.dtype == xp.float32:
+        return res.astype(xp.complex64)
+    return res
+
+def irfftn(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    s: Sequence[int] | None = None,
+    axes: Sequence[int] | None = None,
+    norm: _Norm = "backward",
+) -> Array:
+    res = xp.fft.irfftn(x, s=s, axes=axes, norm=norm)
+    if x.dtype == xp.complex64:
+        return res.astype(xp.float32)
+    return res
+
+def hfft(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    n: int | None = None,
+    axis: int = -1,
+    norm: _Norm = "backward",
+) -> Array:
+    res = xp.fft.hfft(x, n=n, axis=axis, norm=norm)
+    if x.dtype in [xp.float32, xp.complex64]:
+        return res.astype(xp.float32)
+    return res
+
+def ihfft(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    n: int | None = None,
+    axis: int = -1,
+    norm: _Norm = "backward",
+) -> Array:
+    res = xp.fft.ihfft(x, n=n, axis=axis, norm=norm)
+    if x.dtype in [xp.float32, xp.complex64]:
+        return res.astype(xp.complex64)
+    return res
+
+def fftfreq(
+    n: int,
+    /,
+    xp: Namespace,
+    *,
+    d: float = 1.0,
+    dtype: DType | None = None,
+    device: Device | None = None,
+) -> Array:
+    if device not in ["cpu", None]:
+        raise ValueError(f"Unsupported device {device!r}")
+    res = xp.fft.fftfreq(n, d=d)
+    if dtype is not None:
+        return res.astype(dtype)
+    return res
+
+def rfftfreq(
+    n: int,
+    /,
+    xp: Namespace,
+    *,
+    d: float = 1.0,
+    dtype: DType | None = None,
+    device: Device | None = None,
+) -> Array:
+    if device not in ["cpu", None]:
+        raise ValueError(f"Unsupported device {device!r}")
+    res = xp.fft.rfftfreq(n, d=d)
+    if dtype is not None:
+        return res.astype(dtype)
+    return res
+
+def fftshift(
+    x: Array, /, xp: Namespace, *, axes: int | Sequence[int] | None = None
+) -> Array:
+    return xp.fft.fftshift(x, axes=axes)
+
+def ifftshift(
+    x: Array, /, xp: Namespace, *, axes: int | Sequence[int] | None = None
+) -> Array:
+    return xp.fft.ifftshift(x, axes=axes)
+
+__all__ = [
+    "fft",
+    "ifft",
+    "fftn",
+    "ifftn",
+    "rfft",
+    "irfft",
+    "rfftn",
+    "irfftn",
+    "hfft",
+    "ihfft",
+    "fftfreq",
+    "rfftfreq",
+    "fftshift",
+    "ifftshift",
+]
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/_helpers.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/_helpers.py
new file mode 100644
index 0000000000000000000000000000000000000000..8194a083db92fc2c09f93062d4b13d23ed0b611f
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/_helpers.py
@@ -0,0 +1,1079 @@
+"""
+Various helper functions which are not part of the spec.
+
+Functions which start with an underscore are for internal use only but helpers
+that are in __all__ are intended as additional helper functions for use by end
+users of the compat library.
+"""
+
+from __future__ import annotations
+
+import enum
+import inspect
+import math
+import sys
+import warnings
+from collections.abc import Collection, Hashable
+from functools import lru_cache
+from typing import (
+    TYPE_CHECKING,
+    Any,
+    Final,
+    Literal,
+    SupportsIndex,
+    TypeAlias,
+    TypeGuard,
+    cast,
+    overload,
+)
+
+from ._typing import Array, Device, HasShape, Namespace, SupportsArrayNamespace
+
+if TYPE_CHECKING:
+    import cupy as cp
+    import dask.array as da
+    import jax
+    import ndonnx as ndx
+    import numpy as np
+    import numpy.typing as npt
+    import sparse
+    import torch
+
+    # TODO: import from typing (requires Python >=3.13)
+    from typing_extensions import TypeIs
+
+    _ZeroGradientArray: TypeAlias = npt.NDArray[np.void]
+
+    _ArrayApiObj: TypeAlias = (
+        npt.NDArray[Any]
+        | cp.ndarray
+        | da.Array
+        | jax.Array
+        | ndx.Array
+        | sparse.SparseArray
+        | torch.Tensor
+        | SupportsArrayNamespace[Any]
+    )
+
+_API_VERSIONS_OLD: Final = frozenset({"2021.12", "2022.12", "2023.12"})
+_API_VERSIONS: Final = _API_VERSIONS_OLD | frozenset({"2024.12"})
+
+
+@lru_cache(100)
+def _issubclass_fast(cls: type, modname: str, clsname: str) -> bool:
+    try:
+        mod = sys.modules[modname]
+    except KeyError:
+        return False
+    parent_cls = getattr(mod, clsname)
+    return issubclass(cls, parent_cls)
+
+
+def _is_jax_zero_gradient_array(x: object) -> TypeGuard[_ZeroGradientArray]:
+    """Return True if `x` is a zero-gradient array.
+
+    These arrays are a design quirk of Jax that may one day be removed.
+    See https://github.com/google/jax/issues/20620.
+    """
+    # Fast exit
+    try:
+        dtype = x.dtype  # type: ignore[attr-defined]
+    except AttributeError:
+        return False
+    cls = cast(Hashable, type(dtype))
+    if not _issubclass_fast(cls, "numpy.dtypes", "VoidDType"):
+        return False
+
+    if "jax" not in sys.modules:
+        return False
+
+    import jax
+    # jax.float0 is a np.dtype([('float0', 'V')])
+    return dtype == jax.float0
+
+
+def is_numpy_array(x: object) -> TypeIs[npt.NDArray[Any]]:
+    """
+    Return True if `x` is a NumPy array.
+
+    This function does not import NumPy if it has not already been imported
+    and is therefore cheap to use.
+
+    This also returns True for `ndarray` subclasses and NumPy scalar objects.
+
+    See Also
+    --------
+
+    array_namespace
+    is_array_api_obj
+    is_cupy_array
+    is_torch_array
+    is_ndonnx_array
+    is_dask_array
+    is_jax_array
+    is_pydata_sparse_array
+    """
+    # TODO: Should we reject ndarray subclasses?
+    cls = cast(Hashable, type(x))
+    return (
+        _issubclass_fast(cls, "numpy", "ndarray") 
+        or _issubclass_fast(cls, "numpy", "generic")
+    ) and not _is_jax_zero_gradient_array(x)
+
+
+def is_cupy_array(x: object) -> bool:
+    """
+    Return True if `x` is a CuPy array.
+
+    This function does not import CuPy if it has not already been imported
+    and is therefore cheap to use.
+
+    This also returns True for `cupy.ndarray` subclasses and CuPy scalar objects.
+
+    See Also
+    --------
+
+    array_namespace
+    is_array_api_obj
+    is_numpy_array
+    is_torch_array
+    is_ndonnx_array
+    is_dask_array
+    is_jax_array
+    is_pydata_sparse_array
+    """
+    cls = cast(Hashable, type(x))
+    return _issubclass_fast(cls, "cupy", "ndarray")
+
+
+def is_torch_array(x: object) -> TypeIs[torch.Tensor]:
+    """
+    Return True if `x` is a PyTorch tensor.
+
+    This function does not import PyTorch if it has not already been imported
+    and is therefore cheap to use.
+
+    See Also
+    --------
+
+    array_namespace
+    is_array_api_obj
+    is_numpy_array
+    is_cupy_array
+    is_dask_array
+    is_jax_array
+    is_pydata_sparse_array
+    """
+    cls = cast(Hashable, type(x))
+    return _issubclass_fast(cls, "torch", "Tensor")
+
+
+def is_ndonnx_array(x: object) -> TypeIs[ndx.Array]:
+    """
+    Return True if `x` is a ndonnx Array.
+
+    This function does not import ndonnx if it has not already been imported
+    and is therefore cheap to use.
+
+    See Also
+    --------
+
+    array_namespace
+    is_array_api_obj
+    is_numpy_array
+    is_cupy_array
+    is_ndonnx_array
+    is_dask_array
+    is_jax_array
+    is_pydata_sparse_array
+    """
+    cls = cast(Hashable, type(x))
+    return _issubclass_fast(cls, "ndonnx", "Array")
+
+
+def is_dask_array(x: object) -> TypeIs[da.Array]:
+    """
+    Return True if `x` is a dask.array Array.
+
+    This function does not import dask if it has not already been imported
+    and is therefore cheap to use.
+
+    See Also
+    --------
+
+    array_namespace
+    is_array_api_obj
+    is_numpy_array
+    is_cupy_array
+    is_torch_array
+    is_ndonnx_array
+    is_jax_array
+    is_pydata_sparse_array
+    """
+    cls = cast(Hashable, type(x))
+    return _issubclass_fast(cls, "dask.array", "Array")
+
+
+def is_jax_array(x: object) -> TypeIs[jax.Array]:
+    """
+    Return True if `x` is a JAX array.
+
+    This function does not import JAX if it has not already been imported
+    and is therefore cheap to use.
+
+
+    See Also
+    --------
+
+    array_namespace
+    is_array_api_obj
+    is_numpy_array
+    is_cupy_array
+    is_torch_array
+    is_ndonnx_array
+    is_dask_array
+    is_pydata_sparse_array
+    """
+    cls = cast(Hashable, type(x))
+    # We test for jax.core.Tracer here to identify jax arrays during jit tracing. From jax 0.8.2 on,
+    # tracers are not a subclass of jax.Array anymore. Note that tracers can also represent
+    # non-array values and a fully correct implementation would need to use isinstance checks. Since
+    # we use hash-based caching with type names as keys, we cannot use instance checks without
+    # losing performance here. For more information, see
+    # https://github.com/data-apis/array-api-compat/pull/369 and the corresponding issue.
+    return (
+        _issubclass_fast(cls, "jax", "Array")
+        or _issubclass_fast(cls, "jax.core", "Tracer")
+        or _is_jax_zero_gradient_array(x)
+    )
+
+
+def is_pydata_sparse_array(x: object) -> TypeIs[sparse.SparseArray]:
+    """
+    Return True if `x` is an array from the `sparse` package.
+
+    This function does not import `sparse` if it has not already been imported
+    and is therefore cheap to use.
+
+
+    See Also
+    --------
+
+    array_namespace
+    is_array_api_obj
+    is_numpy_array
+    is_cupy_array
+    is_torch_array
+    is_ndonnx_array
+    is_dask_array
+    is_jax_array
+    """
+    # TODO: Account for other backends.
+    cls = cast(Hashable, type(x))
+    return _issubclass_fast(cls, "sparse", "SparseArray")
+
+
+def is_array_api_obj(x: object) -> TypeGuard[_ArrayApiObj]:
+    """
+    Return True if `x` is an array API compatible array object.
+
+    See Also
+    --------
+
+    array_namespace
+    is_numpy_array
+    is_cupy_array
+    is_torch_array
+    is_ndonnx_array
+    is_dask_array
+    is_jax_array
+    """
+    return (
+        hasattr(x, '__array_namespace__') 
+        or _is_array_api_cls(cast(Hashable, type(x)))
+    )
+
+
+@lru_cache(100)
+def _is_array_api_cls(cls: type) -> bool:
+    return (
+        # TODO: drop support for numpy<2 which didn't have __array_namespace__
+        _issubclass_fast(cls, "numpy", "ndarray")
+        or _issubclass_fast(cls, "numpy", "generic")
+        or _issubclass_fast(cls, "cupy", "ndarray")
+        or _issubclass_fast(cls, "torch", "Tensor")
+        or _issubclass_fast(cls, "dask.array", "Array")
+        or _issubclass_fast(cls, "sparse", "SparseArray")
+        # TODO: drop support for jax<0.4.32 which didn't have __array_namespace__
+        or _issubclass_fast(cls, "jax", "Array")
+        or _issubclass_fast(cls, "jax.core", "Tracer")  # see is_jax_array for limitations
+    )
+
+
+def _compat_module_name() -> str:
+    assert __name__.endswith(".common._helpers")
+    return __name__.removesuffix(".common._helpers")
+
+
+@lru_cache(100)
+def is_numpy_namespace(xp: Namespace) -> bool:
+    """
+    Returns True if `xp` is a NumPy namespace.
+
+    This includes both NumPy itself and the version wrapped by array-api-compat.
+
+    See Also
+    --------
+
+    array_namespace
+    is_cupy_namespace
+    is_torch_namespace
+    is_ndonnx_namespace
+    is_dask_namespace
+    is_jax_namespace
+    is_pydata_sparse_namespace
+    is_array_api_strict_namespace
+    """
+    return xp.__name__ in {"numpy", _compat_module_name() + ".numpy"}
+
+
+@lru_cache(100)
+def is_cupy_namespace(xp: Namespace) -> bool:
+    """
+    Returns True if `xp` is a CuPy namespace.
+
+    This includes both CuPy itself and the version wrapped by array-api-compat.
+
+    See Also
+    --------
+
+    array_namespace
+    is_numpy_namespace
+    is_torch_namespace
+    is_ndonnx_namespace
+    is_dask_namespace
+    is_jax_namespace
+    is_pydata_sparse_namespace
+    is_array_api_strict_namespace
+    """
+    return xp.__name__ in {"cupy", _compat_module_name() + ".cupy"}
+
+
+@lru_cache(100)
+def is_torch_namespace(xp: Namespace) -> bool:
+    """
+    Returns True if `xp` is a PyTorch namespace.
+
+    This includes both PyTorch itself and the version wrapped by array-api-compat.
+
+    See Also
+    --------
+
+    array_namespace
+    is_numpy_namespace
+    is_cupy_namespace
+    is_ndonnx_namespace
+    is_dask_namespace
+    is_jax_namespace
+    is_pydata_sparse_namespace
+    is_array_api_strict_namespace
+    """
+    return xp.__name__ in {"torch", _compat_module_name() + ".torch"}
+
+
+def is_ndonnx_namespace(xp: Namespace) -> bool:
+    """
+    Returns True if `xp` is an NDONNX namespace.
+
+    See Also
+    --------
+
+    array_namespace
+    is_numpy_namespace
+    is_cupy_namespace
+    is_torch_namespace
+    is_dask_namespace
+    is_jax_namespace
+    is_pydata_sparse_namespace
+    is_array_api_strict_namespace
+    """
+    return xp.__name__ == "ndonnx"
+
+
+@lru_cache(100)
+def is_dask_namespace(xp: Namespace) -> bool:
+    """
+    Returns True if `xp` is a Dask namespace.
+
+    This includes both ``dask.array`` itself and the version wrapped by array-api-compat.
+
+    See Also
+    --------
+
+    array_namespace
+    is_numpy_namespace
+    is_cupy_namespace
+    is_torch_namespace
+    is_ndonnx_namespace
+    is_jax_namespace
+    is_pydata_sparse_namespace
+    is_array_api_strict_namespace
+    """
+    return xp.__name__ in {"dask.array", _compat_module_name() + ".dask.array"}
+
+
+def is_jax_namespace(xp: Namespace) -> bool:
+    """
+    Returns True if `xp` is a JAX namespace.
+
+    This includes ``jax.numpy`` and ``jax.experimental.array_api`` which existed in
+    older versions of JAX.
+
+    See Also
+    --------
+
+    array_namespace
+    is_numpy_namespace
+    is_cupy_namespace
+    is_torch_namespace
+    is_ndonnx_namespace
+    is_dask_namespace
+    is_pydata_sparse_namespace
+    is_array_api_strict_namespace
+    """
+    return xp.__name__ in {"jax.numpy", "jax.experimental.array_api"}
+
+
+def is_pydata_sparse_namespace(xp: Namespace) -> bool:
+    """
+    Returns True if `xp` is a pydata/sparse namespace.
+
+    See Also
+    --------
+
+    array_namespace
+    is_numpy_namespace
+    is_cupy_namespace
+    is_torch_namespace
+    is_ndonnx_namespace
+    is_dask_namespace
+    is_jax_namespace
+    is_array_api_strict_namespace
+    """
+    return xp.__name__ == "sparse"
+
+
+def is_array_api_strict_namespace(xp: Namespace) -> bool:
+    """
+    Returns True if `xp` is an array-api-strict namespace.
+
+    See Also
+    --------
+
+    array_namespace
+    is_numpy_namespace
+    is_cupy_namespace
+    is_torch_namespace
+    is_ndonnx_namespace
+    is_dask_namespace
+    is_jax_namespace
+    is_pydata_sparse_namespace
+    """
+    return xp.__name__ == "array_api_strict"
+
+
+def _check_api_version(api_version: str | None) -> None:
+    if api_version in _API_VERSIONS_OLD:
+        warnings.warn(
+            f"The {api_version} version of the array API specification was requested but the returned namespace is actually version 2024.12"
+        )
+    elif api_version is not None and api_version not in _API_VERSIONS:
+        raise ValueError(
+            "Only the 2024.12 version of the array API specification is currently supported"
+        )
+
+
+class _ClsToXPInfo(enum.Enum):
+    SCALAR = 0
+    MAYBE_JAX_ZERO_GRADIENT = 1
+
+
+@lru_cache(100)
+def _cls_to_namespace(
+    cls: type,
+    api_version: str | None,
+    use_compat: bool | None,
+) -> tuple[Namespace | None, _ClsToXPInfo | None]:
+    if use_compat not in (None, True, False):
+        raise ValueError("use_compat must be None, True, or False")
+    _use_compat = use_compat in (None, True)
+    cls_ = cast(Hashable, cls)  # Make mypy happy
+
+    if (
+        _issubclass_fast(cls_, "numpy", "ndarray") 
+        or _issubclass_fast(cls_, "numpy", "generic")
+    ):
+        if use_compat is True:
+            _check_api_version(api_version)
+            from .. import numpy as xp
+        elif use_compat is False:
+            import numpy as xp  # type: ignore[no-redef]
+        else:
+            # NumPy 2.0+ have __array_namespace__; however they are not
+            # yet fully array API compatible.
+            from .. import numpy as xp  # type: ignore[no-redef]
+        return xp, _ClsToXPInfo.MAYBE_JAX_ZERO_GRADIENT
+
+    # Note: this must happen _after_ the test for np.generic,
+    # because np.float64 and np.complex128 are subclasses of float and complex.
+    if issubclass(cls, int | float | complex | type(None)):
+        return None, _ClsToXPInfo.SCALAR
+
+    if _issubclass_fast(cls_, "cupy", "ndarray"):
+        if _use_compat:
+            _check_api_version(api_version)
+            from .. import cupy as xp  # type: ignore[no-redef]
+        else:
+            import cupy as xp  # type: ignore[no-redef]
+        return xp, None
+
+    if _issubclass_fast(cls_, "torch", "Tensor"):
+        if _use_compat:
+            _check_api_version(api_version)
+            from .. import torch as xp  # type: ignore[no-redef]
+        else:
+            import torch as xp  # type: ignore[no-redef]
+        return xp, None
+
+    if _issubclass_fast(cls_, "dask.array", "Array"):
+        if _use_compat:
+            _check_api_version(api_version)
+            from ..dask import array as xp  # type: ignore[no-redef]
+        else:
+            import dask.array as xp  # type: ignore[no-redef]
+        return xp, None
+
+    # Backwards compatibility for jax<0.4.32
+    if _issubclass_fast(cls_, "jax", "Array"):
+        return _jax_namespace(api_version, use_compat), None
+
+    return None, None
+
+
+def _jax_namespace(api_version: str | None, use_compat: bool | None) -> Namespace:
+    if use_compat:
+        raise ValueError("JAX does not have an array-api-compat wrapper")
+    import jax.numpy as jnp
+    if not hasattr(jnp, "__array_namespace_info__"):
+        # JAX v0.4.32 and newer implements the array API directly in jax.numpy.
+        # For older JAX versions, it is available via jax.experimental.array_api.
+        # jnp.Array objects gain the __array_namespace__ method.
+        import jax.experimental.array_api  # noqa: F401
+    # Test api_version
+    return jnp.empty(0).__array_namespace__(api_version=api_version)
+
+
+def array_namespace(
+    *xs: Array | complex | None,
+    api_version: str | None = None,
+    use_compat: bool | None = None,
+) -> Namespace:
+    """
+    Get the array API compatible namespace for the arrays `xs`.
+
+    Parameters
+    ----------
+    xs: arrays
+        one or more arrays. xs can also be Python scalars (bool, int, float,
+        complex, or None), which are ignored.
+
+    api_version: str
+        The newest version of the spec that you need support for (currently
+        the compat library wrapped APIs support v2024.12).
+
+    use_compat: bool or None
+        If None (the default), the native namespace will be returned if it is
+        already array API compatible, otherwise a compat wrapper is used. If
+        True, the compat library wrapped library will be returned. If False,
+        the native library namespace is returned.
+
+    Returns
+    -------
+
+    out: namespace
+        The array API compatible namespace corresponding to the arrays in `xs`.
+
+    Raises
+    ------
+    TypeError
+        If `xs` contains arrays from different array libraries or contains a
+        non-array.
+
+
+    Typical usage is to pass the arguments of a function to
+    `array_namespace()` at the top of a function to get the corresponding
+    array API namespace:
+
+    .. code:: python
+
+       def your_function(x, y):
+           xp = array_api_compat.array_namespace(x, y)
+           # Now use xp as the array library namespace
+           return xp.mean(x, axis=0) + 2*xp.std(y, axis=0)
+
+
+    Wrapped array namespaces can also be imported directly. For example,
+    `array_namespace(np.array(...))` will return `array_api_compat.numpy`.
+    This function will also work for any array library not wrapped by
+    array-api-compat if it explicitly defines `__array_namespace__
+    `__
+    (the wrapped namespace is always preferred if it exists).
+
+    See Also
+    --------
+
+    is_array_api_obj
+    is_numpy_array
+    is_cupy_array
+    is_torch_array
+    is_dask_array
+    is_jax_array
+    is_pydata_sparse_array
+
+    """
+    namespaces: set[Namespace] = set()
+    for x in xs:
+        xp, info = _cls_to_namespace(cast(Hashable, type(x)), api_version, use_compat)
+        if info is _ClsToXPInfo.SCALAR:
+            continue
+
+        if (
+            info is _ClsToXPInfo.MAYBE_JAX_ZERO_GRADIENT
+            and _is_jax_zero_gradient_array(x)
+        ):
+            xp = _jax_namespace(api_version, use_compat)
+
+        if xp is None:
+            get_ns = getattr(x, "__array_namespace__", None)
+            if get_ns is None:
+                raise TypeError(f"{type(x).__name__} is not a supported array type")
+            if use_compat:
+                raise ValueError(
+                    "The given array does not have an array-api-compat wrapper"
+                )
+            xp = get_ns(api_version=api_version)
+
+        namespaces.add(xp)
+
+    try:
+        (xp,) = namespaces
+        return xp
+    except ValueError:
+        if not namespaces:
+            raise TypeError(
+                "array_namespace requires at least one non-scalar array input"
+            )
+        raise TypeError(f"Multiple namespaces for array inputs: {namespaces}")
+
+
+# backwards compatibility alias
+get_namespace = array_namespace
+
+
+def _check_device(bare_xp: Namespace, device: Device) -> None:  # pyright: ignore[reportUnusedFunction]
+    """
+    Validate dummy device on device-less array backends.
+
+    Notes
+    -----
+    This function is also invoked by CuPy, which does have multiple devices
+    if there are multiple GPUs available.
+    However, CuPy multi-device support is currently impossible
+    without using the global device or a context manager:
+
+    https://github.com/data-apis/array-api-compat/pull/293
+    """
+    if bare_xp is sys.modules.get("numpy"):
+        if device not in ("cpu", None):
+            raise ValueError(f"Unsupported device for NumPy: {device!r}")
+
+    elif bare_xp is sys.modules.get("dask.array"):
+        if device not in ("cpu", _DASK_DEVICE, None):
+            raise ValueError(f"Unsupported device for Dask: {device!r}")
+
+
+# Placeholder object to represent the dask device
+# when the array backend is not the CPU.
+# (since it is not easy to tell which device a dask array is on)
+class _dask_device:
+    def __repr__(self) -> Literal["DASK_DEVICE"]:
+        return "DASK_DEVICE"
+
+
+_DASK_DEVICE = _dask_device()
+
+
+# device() is not on numpy.ndarray or dask.array and to_device() is not on numpy.ndarray
+# or cupy.ndarray. They are not included in array objects of this library
+# because this library just reuses the respective ndarray classes without
+# wrapping or subclassing them. These helper functions can be used instead of
+# the wrapper functions for libraries that need to support both NumPy/CuPy and
+# other libraries that use devices.
+def device(x: _ArrayApiObj, /) -> Device:
+    """
+    Hardware device the array data resides on.
+
+    This is equivalent to `x.device` according to the `standard
+    `__.
+    This helper is included because some array libraries either do not have
+    the `device` attribute or include it with an incompatible API.
+
+    Parameters
+    ----------
+    x: array
+        array instance from an array API compatible library.
+
+    Returns
+    -------
+    out: device
+        a ``device`` object (see the `Device Support `__
+        section of the array API specification).
+
+    Notes
+    -----
+
+    For NumPy the device is always `"cpu"`. For Dask, the device is always a
+    special `DASK_DEVICE` object.
+
+    See Also
+    --------
+
+    to_device : Move array data to a different device.
+
+    """
+    if is_numpy_array(x):
+        return "cpu"
+    elif is_dask_array(x):
+        # Peek at the metadata of the Dask array to determine type
+        if is_numpy_array(x._meta):
+            # Must be on CPU since backed by numpy
+            return "cpu"
+        return _DASK_DEVICE
+    elif is_jax_array(x):
+        # FIXME Jitted JAX arrays do not have a device attribute
+        #       https://github.com/jax-ml/jax/issues/26000
+        #       Return None in this case. Note that this workaround breaks
+        #       the standard and will result in new arrays being created on the
+        #       default device instead of the same device as the input array(s).
+        x_device = getattr(x, "device", None)
+        # Older JAX releases had .device() as a method, which has been replaced
+        # with a property in accordance with the standard.
+        if inspect.ismethod(x_device):
+            return x_device()
+        else:
+            return x_device
+    elif is_pydata_sparse_array(x):
+        # `sparse` will gain `.device`, so check for this first.
+        x_device = getattr(x, "device", None)
+        if x_device is not None:
+            return x_device
+        # Everything but DOK has this attr.
+        try:
+            inner = x.data  # pyright: ignore
+        except AttributeError:
+            return "cpu"
+        # Return the device of the constituent array
+        return device(inner)  # pyright: ignore
+    return x.device  # type: ignore  # pyright: ignore
+
+
+# Prevent shadowing, used below
+_device = device
+
+
+# Based on cupy.array_api.Array.to_device
+def _cupy_to_device(
+    x: cp.ndarray,
+    device: Device,
+    /,
+    stream: int | Any | None = None,
+) -> cp.ndarray:
+    import cupy as cp
+
+    if device == "cpu":
+        # allowing us to use `to_device(x, "cpu")`
+        # is useful for portable test swapping between
+        # host and device backends
+        return x.get()
+    if not isinstance(device, cp.cuda.Device):
+        raise TypeError(f"Unsupported device type {device!r}")
+
+    if stream is None:
+        with device:
+            return cp.asarray(x)
+
+    # stream can be an int as specified in __dlpack__, or a CuPy stream
+    if isinstance(stream, int):
+        stream = cp.cuda.ExternalStream(stream)
+    elif not isinstance(stream, cp.cuda.Stream):
+        raise TypeError(f"Unsupported stream type {stream!r}")
+
+    with device, stream:
+        return cp.asarray(x)
+
+
+def _torch_to_device(
+    x: torch.Tensor,
+    device: torch.device | str | int,
+    /,
+    stream: int | Any | None = None,
+) -> torch.Tensor:
+    if stream is not None:
+        raise NotImplementedError
+    return x.to(device)
+
+
+def to_device(x: Array, device: Device, /, *, stream: int | Any | None = None) -> Array:
+    """
+    Copy the array from the device on which it currently resides to the specified ``device``.
+
+    This is equivalent to `x.to_device(device, stream=stream)` according to
+    the `standard
+    `__.
+    This helper is included because some array libraries do not have the
+    `to_device` method.
+
+    Parameters
+    ----------
+
+    x: array
+        array instance from an array API compatible library.
+
+    device: device
+        a ``device`` object (see the `Device Support `__
+        section of the array API specification).
+
+    stream: int | Any | None
+        stream object to use during copy. In addition to the types supported
+        in ``array.__dlpack__``, implementations may choose to support any
+        library-specific stream object with the caveat that any code using
+        such an object would not be portable.
+
+    Returns
+    -------
+
+    out: array
+        an array with the same data and data type as ``x`` and located on the
+        specified ``device``.
+
+    Notes
+    -----
+
+    For NumPy, this function effectively does nothing since the only supported
+    device is the CPU. For CuPy, this method supports CuPy CUDA
+    :external+cupy:class:`Device ` and
+    :external+cupy:class:`Stream ` objects. For PyTorch,
+    this is the same as :external+torch:meth:`x.to(device) `
+    (the ``stream`` argument is not supported in PyTorch).
+
+    See Also
+    --------
+
+    device : Hardware device the array data resides on.
+
+    """
+    if is_numpy_array(x):
+        if stream is not None:
+            raise ValueError("The stream argument to to_device() is not supported")
+        if device == "cpu":
+            return x
+        raise ValueError(f"Unsupported device {device!r}")
+    elif is_cupy_array(x):
+        # cupy does not yet have to_device
+        return _cupy_to_device(x, device, stream=stream)
+    elif is_torch_array(x):
+        return _torch_to_device(x, device, stream=stream)
+    elif is_dask_array(x):
+        if stream is not None:
+            raise ValueError("The stream argument to to_device() is not supported")
+        # TODO: What if our array is on the GPU already?
+        if device == "cpu":
+            return x
+        raise ValueError(f"Unsupported device {device!r}")
+    elif is_jax_array(x):
+        if not hasattr(x, "__array_namespace__"):
+            # In JAX v0.4.31 and older, this import adds to_device method to x...
+            import jax.experimental.array_api  # noqa: F401  # pyright: ignore
+
+            # ... but only on eager JAX. It won't work inside jax.jit.
+            if not hasattr(x, "to_device"):
+                return x
+        return x.to_device(device, stream=stream)
+    elif is_pydata_sparse_array(x) and device == _device(x):
+        # Perform trivial check to return the same array if
+        # device is same instead of err-ing.
+        return x
+    return x.to_device(device, stream=stream)  # pyright: ignore
+
+
+@overload
+def size(x: HasShape[Collection[SupportsIndex]]) -> int: ...
+@overload
+def size(x: HasShape[Collection[SupportsIndex | None]]) -> int | None: ...
+def size(x: HasShape[Collection[SupportsIndex | None]]) -> int | None:
+    """
+    Return the total number of elements of x.
+
+    This is equivalent to `x.size` according to the `standard
+    `__.
+
+    This helper is included because PyTorch defines `size` in an
+    :external+torch:meth:`incompatible way `.
+    It also fixes dask.array's behaviour which returns nan for unknown sizes, whereas
+    the standard requires None.
+    """
+    # Lazy API compliant arrays, such as ndonnx, can contain None in their shape
+    if None in x.shape:
+        return None
+    out = math.prod(cast("Collection[SupportsIndex]", x.shape))
+    # dask.array.Array.shape can contain NaN
+    return None if math.isnan(out) else out
+
+
+@lru_cache(100)
+def _is_writeable_cls(cls: type) -> bool | None:
+    if (
+        _issubclass_fast(cls, "numpy", "generic")
+        or _issubclass_fast(cls, "jax", "Array")
+        or _issubclass_fast(cls, "jax.core", "Tracer")  # see is_jax_array for limitations
+        or _issubclass_fast(cls, "sparse", "SparseArray")
+    ):
+        return False
+    if _is_array_api_cls(cls):
+        return True
+    return None
+
+
+def is_writeable_array(x: object) -> TypeGuard[_ArrayApiObj]:
+    """
+    Return False if ``x.__setitem__`` is expected to raise; True otherwise.
+    Return False if `x` is not an array API compatible object.
+
+    Warning
+    -------
+    As there is no standard way to check if an array is writeable without actually
+    writing to it, this function blindly returns True for all unknown array types.
+    """
+    cls = cast(Hashable, type(x))
+    if _issubclass_fast(cls, "numpy", "ndarray"):
+        return cast("npt.NDArray", x).flags.writeable
+    res = _is_writeable_cls(cls)
+    if res is not None:
+        return res
+    return hasattr(x, '__array_namespace__')
+
+
+@lru_cache(100)
+def _is_lazy_cls(cls: type) -> bool | None:
+    if (
+        _issubclass_fast(cls, "numpy", "ndarray")
+        or _issubclass_fast(cls, "numpy", "generic")
+        or _issubclass_fast(cls, "cupy", "ndarray")
+        or _issubclass_fast(cls, "torch", "Tensor")
+        or _issubclass_fast(cls, "sparse", "SparseArray")
+    ):
+        return False
+    if (
+        _issubclass_fast(cls, "jax", "Array")
+        or _issubclass_fast(cls, "jax.core", "Tracer")  # see is_jax_array for limitations
+        or _issubclass_fast(cls, "dask.array", "Array")
+        or _issubclass_fast(cls, "ndonnx", "Array")
+    ):
+        return True
+    return  None
+
+
+def is_lazy_array(x: object) -> TypeGuard[_ArrayApiObj]:
+    """Return True if x is potentially a future or it may be otherwise impossible or
+    expensive to eagerly read its contents, regardless of their size, e.g. by
+    calling ``bool(x)`` or ``float(x)``.
+
+    Return False otherwise; e.g. ``bool(x)`` etc. is guaranteed to succeed and to be
+    cheap as long as the array has the right dtype and size.
+
+    Note
+    ----
+    This function errs on the side of caution for array types that may or may not be
+    lazy, e.g. JAX arrays, by always returning True for them.
+    """
+    # **JAX note:** while it is possible to determine if you're inside or outside
+    # jax.jit by testing the subclass of a jax.Array object, as well as testing bool()
+    # as we do below for unknown arrays, this is not recommended by JAX best practices.
+
+    # **Dask note:** Dask eagerly computes the graph on __bool__, __float__, and so on.
+    # This behaviour, while impossible to change without breaking backwards
+    # compatibility, is highly detrimental to performance as the whole graph will end
+    # up being computed multiple times.
+
+    # Note: skipping reclassification of JAX zero gradient arrays, as one will
+    # exclusively get them once they leave a jax.grad JIT context.
+    cls = cast(Hashable, type(x))
+    res = _is_lazy_cls(cls)
+    if res is not None:
+        return res
+
+    if not hasattr(x, "__array_namespace__"):
+        return False
+
+    # Unknown Array API compatible object. Note that this test may have dire consequences
+    # in terms of performance, e.g. for a lazy object that eagerly computes the graph
+    # on __bool__ (dask is one such example, which however is special-cased above).
+
+    # Select a single point of the array
+    s = size(cast("HasShape[Collection[SupportsIndex | None]]", x))
+    if s is None:
+        return True
+    xp = array_namespace(x)
+    if s > 1:
+        x = xp.reshape(x, (-1,))[0]
+    # Cast to dtype=bool and deal with size 0 arrays
+    x = xp.any(x)
+
+    try:
+        bool(x)
+        return False
+    # The Array API standard dictactes that __bool__ should raise TypeError if the
+    # output cannot be defined.
+    # Here we allow for it to raise arbitrary exceptions, e.g. like Dask does.
+    except Exception:
+        return True
+
+
+__all__ = [
+    "array_namespace",
+    "device",
+    "get_namespace",
+    "is_array_api_obj",
+    "is_array_api_strict_namespace",
+    "is_cupy_array",
+    "is_cupy_namespace",
+    "is_dask_array",
+    "is_dask_namespace",
+    "is_jax_array",
+    "is_jax_namespace",
+    "is_numpy_array",
+    "is_numpy_namespace",
+    "is_torch_array",
+    "is_torch_namespace",
+    "is_ndonnx_array",
+    "is_ndonnx_namespace",
+    "is_pydata_sparse_array",
+    "is_pydata_sparse_namespace",
+    "is_writeable_array",
+    "is_lazy_array",
+    "size",
+    "to_device",
+]
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/_linalg.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/_linalg.py
new file mode 100644
index 0000000000000000000000000000000000000000..69672af768d065a3a7507e0a5ea84d6238ff174d
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/_linalg.py
@@ -0,0 +1,230 @@
+from __future__ import annotations
+
+import math
+from typing import Literal, NamedTuple, cast
+
+import numpy as np
+
+if np.__version__[0] == "2":
+    from numpy.lib.array_utils import normalize_axis_tuple
+else:
+    from numpy.core.numeric import normalize_axis_tuple  # type: ignore[no-redef]
+
+from .._internal import get_xp
+from ._aliases import isdtype, matmul, matrix_transpose, tensordot, vecdot
+from ._typing import Array, DType, JustFloat, JustInt, Namespace
+
+
+# These are in the main NumPy namespace but not in numpy.linalg
+def cross(
+    x1: Array,
+    x2: Array,
+    /,
+    xp: Namespace,
+    *,
+    axis: int = -1,
+    **kwargs: object,
+) -> Array:
+    return xp.cross(x1, x2, axis=axis, **kwargs)
+
+def outer(x1: Array, x2: Array, /, xp: Namespace, **kwargs: object) -> Array:
+    return xp.outer(x1, x2, **kwargs)
+
+class EighResult(NamedTuple):
+    eigenvalues: Array
+    eigenvectors: Array
+
+class QRResult(NamedTuple):
+    Q: Array
+    R: Array
+
+class SlogdetResult(NamedTuple):
+    sign: Array
+    logabsdet: Array
+
+class SVDResult(NamedTuple):
+    U: Array
+    S: Array
+    Vh: Array
+
+# These functions are the same as their NumPy counterparts except they return
+# a namedtuple.
+def eigh(x: Array, /, xp: Namespace, **kwargs: object) -> EighResult:
+    return EighResult(*xp.linalg.eigh(x, **kwargs))
+
+def qr(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    mode: Literal["reduced", "complete"] = "reduced",
+    **kwargs: object,
+) -> QRResult:
+    return QRResult(*xp.linalg.qr(x, mode=mode, **kwargs))
+
+def slogdet(x: Array, /, xp: Namespace, **kwargs: object) -> SlogdetResult:
+    return SlogdetResult(*xp.linalg.slogdet(x, **kwargs))
+
+def svd(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    full_matrices: bool = True,
+    **kwargs: object,
+) -> SVDResult:
+    return SVDResult(*xp.linalg.svd(x, full_matrices=full_matrices, **kwargs))
+
+# These functions have additional keyword arguments
+
+# The upper keyword argument is new from NumPy
+def cholesky(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    upper: bool = False,
+    **kwargs: object,
+) -> Array:
+    L = xp.linalg.cholesky(x, **kwargs)
+    if upper:
+        U = get_xp(xp)(matrix_transpose)(L)
+        if get_xp(xp)(isdtype)(U.dtype, 'complex floating'):
+            U = xp.conj(U)  # pyright: ignore[reportConstantRedefinition]
+        return U
+    return L
+
+# The rtol keyword argument of matrix_rank() and pinv() is new from NumPy.
+# Note that it has a different semantic meaning from tol and rcond.
+def matrix_rank(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    rtol: float | Array | None = None,
+    **kwargs: object,
+) -> Array:
+    # this is different from xp.linalg.matrix_rank, which supports 1
+    # dimensional arrays.
+    if x.ndim < 2:
+        raise xp.linalg.LinAlgError("1-dimensional array given. Array must be at least two-dimensional")
+    S: Array = get_xp(xp)(svdvals)(x, **kwargs)
+    if rtol is None:
+        tol = S.max(axis=-1, keepdims=True) * max(x.shape[-2:]) * xp.finfo(S.dtype).eps
+    else:
+        # this is different from xp.linalg.matrix_rank, which does not
+        # multiply the tolerance by the largest singular value.
+        tol = S.max(axis=-1, keepdims=True)*xp.asarray(rtol)[..., xp.newaxis]
+    return xp.count_nonzero(S > tol, axis=-1)
+
+def pinv(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    rtol: float | Array | None = None,
+    **kwargs: object,
+) -> Array:
+    # this is different from xp.linalg.pinv, which does not multiply the
+    # default tolerance by max(M, N).
+    if rtol is None:
+        rtol = max(x.shape[-2:]) * xp.finfo(x.dtype).eps
+    return xp.linalg.pinv(x, rcond=rtol, **kwargs)
+
+# These functions are new in the array API spec
+
+def matrix_norm(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    keepdims: bool = False,
+    ord: Literal[1, 2, -1, -2] | JustFloat | Literal["fro", "nuc"] | None = "fro",
+) -> Array:
+    return xp.linalg.norm(x, axis=(-2, -1), keepdims=keepdims, ord=ord)
+
+# svdvals is not in NumPy (but it is in SciPy). It is equivalent to
+# xp.linalg.svd(compute_uv=False).
+def svdvals(x: Array, /, xp: Namespace) -> Array | tuple[Array, ...]:
+    return xp.linalg.svd(x, compute_uv=False)
+
+def vector_norm(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    axis: int | tuple[int, ...] | None = None,
+    keepdims: bool = False,
+    ord: JustInt | JustFloat = 2,
+) -> Array:
+    # xp.linalg.norm tries to do a matrix norm whenever axis is a 2-tuple or
+    # when axis=None and the input is 2-D, so to force a vector norm, we make
+    # it so the input is 1-D (for axis=None), or reshape so that norm is done
+    # on a single dimension.
+    if axis is None:
+        # Note: xp.linalg.norm() doesn't handle 0-D arrays
+        _x = x.ravel()
+        _axis = 0
+    elif isinstance(axis, tuple):
+        # Note: The axis argument supports any number of axes, whereas
+        # xp.linalg.norm() only supports a single axis for vector norm.
+        normalized_axis = cast(
+            "tuple[int, ...]",
+            normalize_axis_tuple(axis, x.ndim),  # pyright: ignore[reportCallIssue]
+        )
+        rest = tuple(i for i in range(x.ndim) if i not in normalized_axis)
+        newshape = axis + rest
+        _x = xp.transpose(x, newshape).reshape(
+            (math.prod([x.shape[i] for i in axis]), *[x.shape[i] for i in rest]))
+        _axis = 0
+    else:
+        _x = x
+        _axis = axis
+
+    res = xp.linalg.norm(_x, axis=_axis, ord=ord)
+
+    if keepdims:
+        # We can't reuse xp.linalg.norm(keepdims) because of the reshape hacks
+        # above to avoid matrix norm logic.
+        shape = list(x.shape)
+        axes = cast(
+            "tuple[int, ...]",
+            normalize_axis_tuple(  # pyright: ignore[reportCallIssue]
+                range(x.ndim) if axis is None else axis,
+                x.ndim,
+            ),
+        )
+        for i in axes:
+            shape[i] = 1
+        res = xp.reshape(res, tuple(shape))
+
+    return res
+
+# xp.diagonal and xp.trace operate on the first two axes whereas these
+# operates on the last two
+
+def diagonal(x: Array, /, xp: Namespace, *, offset: int = 0, **kwargs: object) -> Array:
+    return xp.diagonal(x, offset=offset, axis1=-2, axis2=-1, **kwargs)
+
+def trace(
+    x: Array,
+    /,
+    xp: Namespace,
+    *,
+    offset: int = 0,
+    dtype: DType | None = None,
+    **kwargs: object,
+) -> Array:
+    return xp.asarray(
+        xp.trace(x, offset=offset, dtype=dtype, axis1=-2, axis2=-1, **kwargs)
+    )
+
+__all__ = ['cross', 'matmul', 'outer', 'tensordot', 'EighResult',
+           'QRResult', 'SlogdetResult', 'SVDResult', 'eigh', 'qr', 'slogdet',
+           'svd', 'cholesky', 'matrix_rank', 'pinv', 'matrix_norm',
+           'matrix_transpose', 'svdvals', 'vecdot', 'vector_norm', 'diagonal',
+           'trace']
+
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/_typing.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/_typing.py
new file mode 100644
index 0000000000000000000000000000000000000000..11b00bd10395f43bf71e008d09aad5dffaf2f98a
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/common/_typing.py
@@ -0,0 +1,189 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from types import ModuleType as Namespace
+from typing import (
+    TYPE_CHECKING,
+    Literal,
+    Protocol,
+    TypeAlias,
+    TypedDict,
+    TypeVar,
+    final,
+)
+
+if TYPE_CHECKING:
+    from _typeshed import Incomplete
+
+    SupportsBufferProtocol: TypeAlias = Incomplete
+    Array: TypeAlias = Incomplete
+    Device: TypeAlias = Incomplete
+    DType: TypeAlias = Incomplete
+else:
+    SupportsBufferProtocol = object
+    Array = object
+    Device = object
+    DType = object
+
+
+_T_co = TypeVar("_T_co", covariant=True)
+
+
+# These "Just" types are equivalent to the `Just` type from the `optype` library,
+# apart from them not being `@runtime_checkable`.
+# - docs: https://github.com/jorenham/optype/blob/master/README.md#just
+# - code: https://github.com/jorenham/optype/blob/master/optype/_core/_just.py
+@final
+class JustInt(Protocol):  # type: ignore[misc]
+    @property  # type: ignore[override]
+    def __class__(self, /) -> type[int]: ...
+    @__class__.setter
+    def __class__(self, value: type[int], /) -> None: ...  # pyright: ignore[reportIncompatibleMethodOverride]
+
+
+@final
+class JustFloat(Protocol):  # type: ignore[misc]
+    @property  # type: ignore[override]
+    def __class__(self, /) -> type[float]: ...
+    @__class__.setter
+    def __class__(self, value: type[float], /) -> None: ...  # pyright: ignore[reportIncompatibleMethodOverride]
+
+
+@final
+class JustComplex(Protocol):  # type: ignore[misc]
+    @property  # type: ignore[override]
+    def __class__(self, /) -> type[complex]: ...
+    @__class__.setter
+    def __class__(self, value: type[complex], /) -> None: ...  # pyright: ignore[reportIncompatibleMethodOverride]
+
+
+class NestedSequence(Protocol[_T_co]):
+    def __getitem__(self, key: int, /) -> _T_co | NestedSequence[_T_co]: ...
+    def __len__(self, /) -> int: ...
+
+
+class SupportsArrayNamespace(Protocol[_T_co]):
+    def __array_namespace__(self, /, *, api_version: str | None) -> _T_co: ...
+
+
+class HasShape(Protocol[_T_co]):
+    @property
+    def shape(self, /) -> _T_co: ...
+
+
+# Return type of `__array_namespace_info__.default_dtypes`
+Capabilities = TypedDict(
+    "Capabilities",
+    {
+        "boolean indexing": bool,
+        "data-dependent shapes": bool,
+        "max dimensions": int,
+    },
+)
+
+# Return type of `__array_namespace_info__.default_dtypes`
+DefaultDTypes = TypedDict(
+    "DefaultDTypes",
+    {
+        "real floating": DType,
+        "complex floating": DType,
+        "integral": DType,
+        "indexing": DType,
+    },
+)
+
+
+_DTypeKind: TypeAlias = Literal[
+    "bool",
+    "signed integer",
+    "unsigned integer",
+    "integral",
+    "real floating",
+    "complex floating",
+    "numeric",
+]
+# Type of the `kind` parameter in `__array_namespace_info__.dtypes`
+DTypeKind: TypeAlias = _DTypeKind | tuple[_DTypeKind, ...]
+
+
+# `__array_namespace_info__.dtypes(kind="bool")`
+class DTypesBool(TypedDict):
+    bool: DType
+
+
+# `__array_namespace_info__.dtypes(kind="signed integer")`
+class DTypesSigned(TypedDict):
+    int8: DType
+    int16: DType
+    int32: DType
+    int64: DType
+
+
+# `__array_namespace_info__.dtypes(kind="unsigned integer")`
+class DTypesUnsigned(TypedDict):
+    uint8: DType
+    uint16: DType
+    uint32: DType
+    uint64: DType
+
+
+# `__array_namespace_info__.dtypes(kind="integral")`
+class DTypesIntegral(DTypesSigned, DTypesUnsigned):
+    pass
+
+
+# `__array_namespace_info__.dtypes(kind="real floating")`
+class DTypesReal(TypedDict):
+    float32: DType
+    float64: DType
+
+
+# `__array_namespace_info__.dtypes(kind="complex floating")`
+class DTypesComplex(TypedDict):
+    complex64: DType
+    complex128: DType
+
+
+# `__array_namespace_info__.dtypes(kind="numeric")`
+class DTypesNumeric(DTypesIntegral, DTypesReal, DTypesComplex):
+    pass
+
+
+# `__array_namespace_info__.dtypes(kind=None)` (default)
+class DTypesAll(DTypesBool, DTypesNumeric):
+    pass
+
+
+# `__array_namespace_info__.dtypes(kind=?)` (fallback)
+DTypesAny: TypeAlias = Mapping[str, DType]
+
+
+__all__ = [
+    "Array",
+    "Capabilities",
+    "DType",
+    "DTypeKind",
+    "DTypesAny",
+    "DTypesAll",
+    "DTypesBool",
+    "DTypesNumeric",
+    "DTypesIntegral",
+    "DTypesSigned",
+    "DTypesUnsigned",
+    "DTypesReal",
+    "DTypesComplex",
+    "DefaultDTypes",
+    "Device",
+    "HasShape",
+    "Namespace",
+    "JustInt",
+    "JustFloat",
+    "JustComplex",
+    "NestedSequence",
+    "SupportsArrayNamespace",
+    "SupportsBufferProtocol",
+]
+
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/cupy/__init__.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/cupy/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..af003c5adaa52992bb673259d882098a499753e9
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/cupy/__init__.py
@@ -0,0 +1,24 @@
+from typing import Final
+from cupy import * # noqa: F403
+
+# from cupy import * doesn't overwrite these builtin names
+from cupy import abs, max, min, round # noqa: F401
+
+# These imports may overwrite names from the import * above.
+from ._aliases import * # noqa: F403
+from ._info import __array_namespace_info__  # noqa: F401
+
+# See the comment in the numpy __init__.py
+__import__(__package__ + '.linalg')
+__import__(__package__ + '.fft')
+
+__array_api_version__: Final = '2024.12'
+
+__all__ = sorted(
+    {name for name in globals() if not name.startswith("__")}
+    - {"Final", "_aliases", "_info", "_typing"}
+    | {"__array_api_version__", "__array_namespace_info__", "linalg", "fft"}
+)
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/cupy/_aliases.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/cupy/_aliases.py
new file mode 100644
index 0000000000000000000000000000000000000000..2e512fc896399bae466f3b9a7803ce280c06f551
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/cupy/_aliases.py
@@ -0,0 +1,168 @@
+from __future__ import annotations
+
+from builtins import bool as py_bool
+
+import cupy as cp
+
+from ..common import _aliases, _helpers
+from ..common._typing import NestedSequence, SupportsBufferProtocol
+from .._internal import get_xp
+from ._typing import Array, Device, DType
+
+bool = cp.bool_
+
+# Basic renames
+acos = cp.arccos
+acosh = cp.arccosh
+asin = cp.arcsin
+asinh = cp.arcsinh
+atan = cp.arctan
+atan2 = cp.arctan2
+atanh = cp.arctanh
+bitwise_left_shift = cp.left_shift
+bitwise_invert = cp.invert
+bitwise_right_shift = cp.right_shift
+concat = cp.concatenate
+pow = cp.power
+
+arange = get_xp(cp)(_aliases.arange)
+empty = get_xp(cp)(_aliases.empty)
+empty_like = get_xp(cp)(_aliases.empty_like)
+eye = get_xp(cp)(_aliases.eye)
+full = get_xp(cp)(_aliases.full)
+full_like = get_xp(cp)(_aliases.full_like)
+linspace = get_xp(cp)(_aliases.linspace)
+ones = get_xp(cp)(_aliases.ones)
+ones_like = get_xp(cp)(_aliases.ones_like)
+zeros = get_xp(cp)(_aliases.zeros)
+zeros_like = get_xp(cp)(_aliases.zeros_like)
+UniqueAllResult = get_xp(cp)(_aliases.UniqueAllResult)
+UniqueCountsResult = get_xp(cp)(_aliases.UniqueCountsResult)
+UniqueInverseResult = get_xp(cp)(_aliases.UniqueInverseResult)
+unique_all = get_xp(cp)(_aliases.unique_all)
+unique_counts = get_xp(cp)(_aliases.unique_counts)
+unique_inverse = get_xp(cp)(_aliases.unique_inverse)
+unique_values = get_xp(cp)(_aliases.unique_values)
+std = get_xp(cp)(_aliases.std)
+var = get_xp(cp)(_aliases.var)
+cumulative_sum = get_xp(cp)(_aliases.cumulative_sum)
+cumulative_prod = get_xp(cp)(_aliases.cumulative_prod)
+clip = get_xp(cp)(_aliases.clip)
+permute_dims = get_xp(cp)(_aliases.permute_dims)
+reshape = get_xp(cp)(_aliases.reshape)
+argsort = get_xp(cp)(_aliases.argsort)
+sort = get_xp(cp)(_aliases.sort)
+nonzero = get_xp(cp)(_aliases.nonzero)
+matmul = get_xp(cp)(_aliases.matmul)
+matrix_transpose = get_xp(cp)(_aliases.matrix_transpose)
+tensordot = get_xp(cp)(_aliases.tensordot)
+sign = get_xp(cp)(_aliases.sign)
+finfo = get_xp(cp)(_aliases.finfo)
+iinfo = get_xp(cp)(_aliases.iinfo)
+
+
+# asarray also adds the copy keyword, which is not present in numpy 1.0.
+def asarray(
+    obj: Array | complex | NestedSequence[complex] | SupportsBufferProtocol,
+    /,
+    *,
+    dtype: DType | None = None,
+    device: Device | None = None,
+    copy: py_bool | None = None,
+    **kwargs: object,
+) -> Array:
+    """
+    Array API compatibility wrapper for asarray().
+
+    See the corresponding documentation in the array library and/or the array API
+    specification for more details.
+    """
+    with cp.cuda.Device(device):
+        if copy is None:
+            return cp.asarray(obj, dtype=dtype, **kwargs)
+        else:
+            res = cp.array(obj, dtype=dtype, copy=copy, **kwargs)
+            if not copy and res is not obj:
+                raise ValueError("Unable to avoid copy while creating an array as requested")
+            return res
+
+
+def astype(
+    x: Array,
+    dtype: DType,
+    /,
+    *,
+    copy: py_bool = True,
+    device: Device | None = None,
+) -> Array:
+    if device is None:
+        return x.astype(dtype=dtype, copy=copy)
+    out = _helpers.to_device(x.astype(dtype=dtype, copy=False), device)
+    return out.copy() if copy and out is x else out
+
+
+# cupy.count_nonzero does not have keepdims
+def count_nonzero(
+    x: Array,
+    axis: int | tuple[int, ...] | None = None,
+    keepdims: py_bool = False,
+) -> Array:
+   result = cp.count_nonzero(x, axis)
+   if keepdims:
+       if axis is None:
+            return cp.reshape(result, [1]*x.ndim)
+       return cp.expand_dims(result, axis)
+   return result
+
+# ceil, floor, and trunc return integers for integer inputs
+
+def ceil(x: Array, /) -> Array:
+    if cp.issubdtype(x.dtype, cp.integer):
+        return x.copy()
+    return cp.ceil(x)
+
+
+def floor(x: Array, /) -> Array:
+    if cp.issubdtype(x.dtype, cp.integer):
+        return x.copy()
+    return cp.floor(x)
+
+
+def trunc(x: Array, /) -> Array:
+    if cp.issubdtype(x.dtype, cp.integer):
+        return x.copy()
+    return cp.trunc(x)
+
+
+# take_along_axis: axis defaults to -1 but in cupy (and numpy) axis is a required arg
+def take_along_axis(x: Array, indices: Array, /, *, axis: int = -1) -> Array:
+    return cp.take_along_axis(x, indices, axis=axis)
+
+
+# These functions are completely new here. If the library already has them
+# (i.e., numpy 2.0), use the library version instead of our wrapper.
+if hasattr(cp, 'vecdot'):
+    vecdot = cp.vecdot
+else:
+    vecdot = get_xp(cp)(_aliases.vecdot)
+
+if hasattr(cp, 'isdtype'):
+    isdtype = cp.isdtype
+else:
+    isdtype = get_xp(cp)(_aliases.isdtype)
+
+if hasattr(cp, 'unstack'):
+    unstack = cp.unstack
+else:
+    unstack = get_xp(cp)(_aliases.unstack)
+
+__all__ = _aliases.__all__ + ['asarray', 'astype',
+                              'acos', 'acosh', 'asin', 'asinh', 'atan',
+                              'atan2', 'atanh', 'bitwise_left_shift',
+                              'bitwise_invert', 'bitwise_right_shift',
+                              'bool', 'concat', 'count_nonzero', 'pow', 'sign',
+                              'ceil', 'floor', 'trunc', 'take_along_axis']
+
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/cupy/_info.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/cupy/_info.py
new file mode 100644
index 0000000000000000000000000000000000000000..78e48a3358cf547a9a77c5ebdc95fa81c3d4e144
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/cupy/_info.py
@@ -0,0 +1,336 @@
+"""
+Array API Inspection namespace
+
+This is the namespace for inspection functions as defined by the array API
+standard. See
+https://data-apis.org/array-api/latest/API_specification/inspection.html for
+more details.
+
+"""
+from cupy import (
+    dtype,
+    cuda,
+    bool_ as bool,
+    intp,
+    int8,
+    int16,
+    int32,
+    int64,
+    uint8,
+    uint16,
+    uint32,
+    uint64,
+    float32,
+    float64,
+    complex64,
+    complex128,
+)
+
+
+class __array_namespace_info__:
+    """
+    Get the array API inspection namespace for CuPy.
+
+    The array API inspection namespace defines the following functions:
+
+    - capabilities()
+    - default_device()
+    - default_dtypes()
+    - dtypes()
+    - devices()
+
+    See
+    https://data-apis.org/array-api/latest/API_specification/inspection.html
+    for more details.
+
+    Returns
+    -------
+    info : ModuleType
+        The array API inspection namespace for CuPy.
+
+    Examples
+    --------
+    >>> info = xp.__array_namespace_info__()
+    >>> info.default_dtypes()
+    {'real floating': cupy.float64,
+     'complex floating': cupy.complex128,
+     'integral': cupy.int64,
+     'indexing': cupy.int64}
+
+    """
+
+    __module__ = 'cupy'
+
+    def capabilities(self):
+        """
+        Return a dictionary of array API library capabilities.
+
+        The resulting dictionary has the following keys:
+
+        - **"boolean indexing"**: boolean indicating whether an array library
+          supports boolean indexing. Always ``True`` for CuPy.
+
+        - **"data-dependent shapes"**: boolean indicating whether an array
+          library supports data-dependent output shapes. Always ``True`` for
+          CuPy.
+
+        See
+        https://data-apis.org/array-api/latest/API_specification/generated/array_api.info.capabilities.html
+        for more details.
+
+        See Also
+        --------
+        __array_namespace_info__.default_device,
+        __array_namespace_info__.default_dtypes,
+        __array_namespace_info__.dtypes,
+        __array_namespace_info__.devices
+
+        Returns
+        -------
+        capabilities : dict
+            A dictionary of array API library capabilities.
+
+        Examples
+        --------
+        >>> info = xp.__array_namespace_info__()
+        >>> info.capabilities()
+        {'boolean indexing': True,
+         'data-dependent shapes': True,
+         'max dimensions': 64}
+
+        """
+        return {
+            "boolean indexing": True,
+            "data-dependent shapes": True,
+            "max dimensions": 64,
+        }
+
+    def default_device(self):
+        """
+        The default device used for new CuPy arrays.
+
+        See Also
+        --------
+        __array_namespace_info__.capabilities,
+        __array_namespace_info__.default_dtypes,
+        __array_namespace_info__.dtypes,
+        __array_namespace_info__.devices
+
+        Returns
+        -------
+        device : Device
+            The default device used for new CuPy arrays.
+
+        Examples
+        --------
+        >>> info = xp.__array_namespace_info__()
+        >>> info.default_device()
+        Device(0)
+
+        Notes
+        -----
+        This method returns the static default device when CuPy is initialized.
+        However, the *current* device used by creation functions (``empty`` etc.)
+        can be changed globally or with a context manager.
+
+        See Also
+        --------
+        https://github.com/data-apis/array-api/issues/835
+        """
+        return cuda.Device(0)
+
+    def default_dtypes(self, *, device=None):
+        """
+        The default data types used for new CuPy arrays.
+
+        For CuPy, this always returns the following dictionary:
+
+        - **"real floating"**: ``cupy.float64``
+        - **"complex floating"**: ``cupy.complex128``
+        - **"integral"**: ``cupy.intp``
+        - **"indexing"**: ``cupy.intp``
+
+        Parameters
+        ----------
+        device : str, optional
+            The device to get the default data types for.
+
+        Returns
+        -------
+        dtypes : dict
+            A dictionary describing the default data types used for new CuPy
+            arrays.
+
+        See Also
+        --------
+        __array_namespace_info__.capabilities,
+        __array_namespace_info__.default_device,
+        __array_namespace_info__.dtypes,
+        __array_namespace_info__.devices
+
+        Examples
+        --------
+        >>> info = xp.__array_namespace_info__()
+        >>> info.default_dtypes()
+        {'real floating': cupy.float64,
+         'complex floating': cupy.complex128,
+         'integral': cupy.int64,
+         'indexing': cupy.int64}
+
+        """
+        # TODO: Does this depend on device?
+        return {
+            "real floating": dtype(float64),
+            "complex floating": dtype(complex128),
+            "integral": dtype(intp),
+            "indexing": dtype(intp),
+        }
+
+    def dtypes(self, *, device=None, kind=None):
+        """
+        The array API data types supported by CuPy.
+
+        Note that this function only returns data types that are defined by
+        the array API.
+
+        Parameters
+        ----------
+        device : str, optional
+            The device to get the data types for.
+        kind : str or tuple of str, optional
+            The kind of data types to return. If ``None``, all data types are
+            returned. If a string, only data types of that kind are returned.
+            If a tuple, a dictionary containing the union of the given kinds
+            is returned. The following kinds are supported:
+
+            - ``'bool'``: boolean data types (i.e., ``bool``).
+            - ``'signed integer'``: signed integer data types (i.e., ``int8``,
+              ``int16``, ``int32``, ``int64``).
+            - ``'unsigned integer'``: unsigned integer data types (i.e.,
+              ``uint8``, ``uint16``, ``uint32``, ``uint64``).
+            - ``'integral'``: integer data types. Shorthand for ``('signed
+              integer', 'unsigned integer')``.
+            - ``'real floating'``: real-valued floating-point data types
+              (i.e., ``float32``, ``float64``).
+            - ``'complex floating'``: complex floating-point data types (i.e.,
+              ``complex64``, ``complex128``).
+            - ``'numeric'``: numeric data types. Shorthand for ``('integral',
+              'real floating', 'complex floating')``.
+
+        Returns
+        -------
+        dtypes : dict
+            A dictionary mapping the names of data types to the corresponding
+            CuPy data types.
+
+        See Also
+        --------
+        __array_namespace_info__.capabilities,
+        __array_namespace_info__.default_device,
+        __array_namespace_info__.default_dtypes,
+        __array_namespace_info__.devices
+
+        Examples
+        --------
+        >>> info = xp.__array_namespace_info__()
+        >>> info.dtypes(kind='signed integer')
+        {'int8': cupy.int8,
+         'int16': cupy.int16,
+         'int32': cupy.int32,
+         'int64': cupy.int64}
+
+        """
+        # TODO: Does this depend on device?
+        if kind is None:
+            return {
+                "bool": dtype(bool),
+                "int8": dtype(int8),
+                "int16": dtype(int16),
+                "int32": dtype(int32),
+                "int64": dtype(int64),
+                "uint8": dtype(uint8),
+                "uint16": dtype(uint16),
+                "uint32": dtype(uint32),
+                "uint64": dtype(uint64),
+                "float32": dtype(float32),
+                "float64": dtype(float64),
+                "complex64": dtype(complex64),
+                "complex128": dtype(complex128),
+            }
+        if kind == "bool":
+            return {"bool": bool}
+        if kind == "signed integer":
+            return {
+                "int8": dtype(int8),
+                "int16": dtype(int16),
+                "int32": dtype(int32),
+                "int64": dtype(int64),
+            }
+        if kind == "unsigned integer":
+            return {
+                "uint8": dtype(uint8),
+                "uint16": dtype(uint16),
+                "uint32": dtype(uint32),
+                "uint64": dtype(uint64),
+            }
+        if kind == "integral":
+            return {
+                "int8": dtype(int8),
+                "int16": dtype(int16),
+                "int32": dtype(int32),
+                "int64": dtype(int64),
+                "uint8": dtype(uint8),
+                "uint16": dtype(uint16),
+                "uint32": dtype(uint32),
+                "uint64": dtype(uint64),
+            }
+        if kind == "real floating":
+            return {
+                "float32": dtype(float32),
+                "float64": dtype(float64),
+            }
+        if kind == "complex floating":
+            return {
+                "complex64": dtype(complex64),
+                "complex128": dtype(complex128),
+            }
+        if kind == "numeric":
+            return {
+                "int8": dtype(int8),
+                "int16": dtype(int16),
+                "int32": dtype(int32),
+                "int64": dtype(int64),
+                "uint8": dtype(uint8),
+                "uint16": dtype(uint16),
+                "uint32": dtype(uint32),
+                "uint64": dtype(uint64),
+                "float32": dtype(float32),
+                "float64": dtype(float64),
+                "complex64": dtype(complex64),
+                "complex128": dtype(complex128),
+            }
+        if isinstance(kind, tuple):
+            res = {}
+            for k in kind:
+                res.update(self.dtypes(kind=k))
+            return res
+        raise ValueError(f"unsupported kind: {kind!r}")
+
+    def devices(self):
+        """
+        The devices supported by CuPy.
+
+        Returns
+        -------
+        devices : list[Device]
+            The devices supported by CuPy.
+
+        See Also
+        --------
+        __array_namespace_info__.capabilities,
+        __array_namespace_info__.default_device,
+        __array_namespace_info__.default_dtypes,
+        __array_namespace_info__.dtypes
+
+        """
+        return [cuda.Device(i) for i in range(cuda.runtime.getDeviceCount())]
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/cupy/_typing.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/cupy/_typing.py
new file mode 100644
index 0000000000000000000000000000000000000000..e5c202dc53e097936671115943072ad58dd8d389
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/cupy/_typing.py
@@ -0,0 +1,30 @@
+from __future__ import annotations
+
+__all__ = ["Array", "DType", "Device"]
+
+from typing import TYPE_CHECKING
+
+import cupy as cp
+from cupy import ndarray as Array
+from cupy.cuda.device import Device
+
+if TYPE_CHECKING:
+    # NumPy 1.x on Python 3.10 fails to parse np.dtype[]
+    DType = cp.dtype[
+        cp.intp
+        | cp.int8
+        | cp.int16
+        | cp.int32
+        | cp.int64
+        | cp.uint8
+        | cp.uint16
+        | cp.uint32
+        | cp.uint64
+        | cp.float32
+        | cp.float64
+        | cp.complex64
+        | cp.complex128
+        | cp.bool_
+    ]
+else:
+    DType = cp.dtype
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/cupy/fft.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/cupy/fft.py
new file mode 100644
index 0000000000000000000000000000000000000000..53a9a45438651bc2adc7bc19884fe9361ef99580
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/cupy/fft.py
@@ -0,0 +1,36 @@
+from cupy.fft import *  # noqa: F403
+
+# cupy.fft doesn't have __all__. If it is added, replace this with
+#
+# from cupy.fft import __all__ as linalg_all
+_n: dict[str, object] = {}
+exec("from cupy.fft import *", _n)
+del _n["__builtins__"]
+fft_all = list(_n)
+del _n
+
+from ..common import _fft
+from .._internal import get_xp
+
+import cupy as cp
+
+fft = get_xp(cp)(_fft.fft)
+ifft = get_xp(cp)(_fft.ifft)
+fftn = get_xp(cp)(_fft.fftn)
+ifftn = get_xp(cp)(_fft.ifftn)
+rfft = get_xp(cp)(_fft.rfft)
+irfft = get_xp(cp)(_fft.irfft)
+rfftn = get_xp(cp)(_fft.rfftn)
+irfftn = get_xp(cp)(_fft.irfftn)
+hfft = get_xp(cp)(_fft.hfft)
+ihfft = get_xp(cp)(_fft.ihfft)
+fftfreq = get_xp(cp)(_fft.fftfreq)
+rfftfreq = get_xp(cp)(_fft.rfftfreq)
+fftshift = get_xp(cp)(_fft.fftshift)
+ifftshift = get_xp(cp)(_fft.ifftshift)
+
+__all__ = fft_all + _fft.__all__
+
+def __dir__() -> list[str]:
+    return __all__
+
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/cupy/linalg.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/cupy/linalg.py
new file mode 100644
index 0000000000000000000000000000000000000000..da301574728a70665dbb6728488ac2ce75e619e0
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/cupy/linalg.py
@@ -0,0 +1,47 @@
+from cupy.linalg import * # noqa: F403
+# cupy.linalg doesn't have __all__. If it is added, replace this with
+#
+# from cupy.linalg import __all__ as linalg_all
+_n: dict[str, object] = {}
+exec('from cupy.linalg import *', _n)
+del _n['__builtins__']
+linalg_all = list(_n)
+del _n
+
+from ..common import _linalg
+from .._internal import get_xp
+
+import cupy as cp
+
+# These functions are in both the main and linalg namespaces
+from ._aliases import matmul, matrix_transpose, tensordot, vecdot # noqa: F401
+
+cross = get_xp(cp)(_linalg.cross)
+outer = get_xp(cp)(_linalg.outer)
+EighResult = _linalg.EighResult
+QRResult = _linalg.QRResult
+SlogdetResult = _linalg.SlogdetResult
+SVDResult = _linalg.SVDResult
+eigh = get_xp(cp)(_linalg.eigh)
+qr = get_xp(cp)(_linalg.qr)
+slogdet = get_xp(cp)(_linalg.slogdet)
+svd = get_xp(cp)(_linalg.svd)
+cholesky = get_xp(cp)(_linalg.cholesky)
+matrix_rank = get_xp(cp)(_linalg.matrix_rank)
+pinv = get_xp(cp)(_linalg.pinv)
+matrix_norm = get_xp(cp)(_linalg.matrix_norm)
+svdvals = get_xp(cp)(_linalg.svdvals)
+diagonal = get_xp(cp)(_linalg.diagonal)
+trace = get_xp(cp)(_linalg.trace)
+
+# These functions are completely new here. If the library already has them
+# (i.e., numpy 2.0), use the library version instead of our wrapper.
+if hasattr(cp.linalg, 'vector_norm'):
+    vector_norm = cp.linalg.vector_norm
+else:
+    vector_norm = get_xp(cp)(_linalg.vector_norm)
+
+__all__ = linalg_all + _linalg.__all__
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/dask/__init__.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/dask/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/dask/array/__init__.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/dask/array/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..f78aa8b378444aa486519b06f6d81daa45cb8f67
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/dask/array/__init__.py
@@ -0,0 +1,26 @@
+from typing import Final
+
+from ..._internal import clone_module
+
+__all__ = clone_module("dask.array", globals())
+
+# These imports may overwrite names from the import * above.
+from . import _aliases
+from ._aliases import *  # type: ignore[assignment] # noqa: F403
+from ._info import __array_namespace_info__  # noqa: F401
+
+__array_api_version__: Final = "2024.12"
+del Final
+
+# See the comment in the numpy __init__.py
+__import__(__package__ + '.linalg')
+__import__(__package__ + '.fft')
+
+__all__ = sorted(
+    set(__all__)
+    | set(_aliases.__all__)
+    | {"__array_api_version__", "__array_namespace_info__", "linalg", "fft"}
+)
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/dask/array/_aliases.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/dask/array/_aliases.py
new file mode 100644
index 0000000000000000000000000000000000000000..54d323b2a5b6fd933ce8db39e584dff4877a2daa
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/dask/array/_aliases.py
@@ -0,0 +1,369 @@
+# pyright: reportPrivateUsage=false
+# pyright: reportUnknownArgumentType=false
+# pyright: reportUnknownMemberType=false
+# pyright: reportUnknownVariableType=false
+
+from __future__ import annotations
+
+from builtins import bool as py_bool
+from collections.abc import Callable
+from typing import TYPE_CHECKING, Any
+
+if TYPE_CHECKING:
+    from typing_extensions import TypeIs
+
+import dask.array as da
+import numpy as np
+from numpy import bool_ as bool
+from numpy import (
+    can_cast,
+    complex64,
+    complex128,
+    float32,
+    float64,
+    int8,
+    int16,
+    int32,
+    int64,
+    result_type,
+    uint8,
+    uint16,
+    uint32,
+    uint64,
+)
+
+from ..._internal import get_xp
+from ...common import _aliases, _helpers, array_namespace
+from ...common._typing import (
+    Array,
+    Device,
+    DType,
+    NestedSequence,
+    SupportsBufferProtocol,
+)
+
+isdtype = get_xp(np)(_aliases.isdtype)
+unstack = get_xp(da)(_aliases.unstack)
+
+
+# da.astype doesn't respect copy=True
+def astype(
+    x: Array,
+    dtype: DType,
+    /,
+    *,
+    copy: py_bool = True,
+    device: Device | None = None,
+) -> Array:
+    """
+    Array API compatibility wrapper for astype().
+
+    See the corresponding documentation in the array library and/or the array API
+    specification for more details.
+    """
+    # TODO: respect device keyword?
+    _helpers._check_device(da, device)
+
+    if not copy and dtype == x.dtype:
+        return x
+    x = x.astype(dtype)
+    return x.copy() if copy else x
+
+
+# Common aliases
+
+
+# This arange func is modified from the common one to
+# not pass stop/step as keyword arguments, which will cause
+# an error with dask
+def arange(
+    start: float,
+    /,
+    stop: float | None = None,
+    step: float = 1,
+    *,
+    dtype: DType | None = None,
+    device: Device | None = None,
+    **kwargs: object,
+) -> Array:
+    """
+    Array API compatibility wrapper for arange().
+
+    See the corresponding documentation in the array library and/or the array API
+    specification for more details.
+    """
+    # TODO: respect device keyword?
+    _helpers._check_device(da, device)
+
+    args: list[Any] = [start]
+    if stop is not None:
+        args.append(stop)
+    else:
+        # stop is None, so start is actually stop
+        # prepend the default value for start which is 0
+        args.insert(0, 0)
+    args.append(step)
+
+    return da.arange(*args, dtype=dtype, **kwargs)
+
+
+eye = get_xp(da)(_aliases.eye)
+linspace = get_xp(da)(_aliases.linspace)
+UniqueAllResult = get_xp(da)(_aliases.UniqueAllResult)
+UniqueCountsResult = get_xp(da)(_aliases.UniqueCountsResult)
+UniqueInverseResult = get_xp(da)(_aliases.UniqueInverseResult)
+unique_all = get_xp(da)(_aliases.unique_all)
+unique_counts = get_xp(da)(_aliases.unique_counts)
+unique_inverse = get_xp(da)(_aliases.unique_inverse)
+unique_values = get_xp(da)(_aliases.unique_values)
+permute_dims = get_xp(da)(_aliases.permute_dims)
+std = get_xp(da)(_aliases.std)
+var = get_xp(da)(_aliases.var)
+cumulative_sum = get_xp(da)(_aliases.cumulative_sum)
+cumulative_prod = get_xp(da)(_aliases.cumulative_prod)
+empty = get_xp(da)(_aliases.empty)
+empty_like = get_xp(da)(_aliases.empty_like)
+full = get_xp(da)(_aliases.full)
+full_like = get_xp(da)(_aliases.full_like)
+ones = get_xp(da)(_aliases.ones)
+ones_like = get_xp(da)(_aliases.ones_like)
+zeros = get_xp(da)(_aliases.zeros)
+zeros_like = get_xp(da)(_aliases.zeros_like)
+reshape = get_xp(da)(_aliases.reshape)
+matrix_transpose = get_xp(da)(_aliases.matrix_transpose)
+vecdot = get_xp(da)(_aliases.vecdot)
+nonzero = get_xp(da)(_aliases.nonzero)
+matmul = get_xp(np)(_aliases.matmul)
+tensordot = get_xp(np)(_aliases.tensordot)
+sign = get_xp(np)(_aliases.sign)
+finfo = get_xp(np)(_aliases.finfo)
+iinfo = get_xp(np)(_aliases.iinfo)
+
+
+# asarray also adds the copy keyword, which is not present in numpy 1.0.
+def asarray(
+    obj: Array | complex | NestedSequence[complex] | SupportsBufferProtocol,
+    /,
+    *,
+    dtype: DType | None = None,
+    device: Device | None = None,
+    copy: py_bool | None = None,
+    **kwargs: object,
+) -> Array:
+    """
+    Array API compatibility wrapper for asarray().
+
+    See the corresponding documentation in the array library and/or the array API
+    specification for more details.
+    """
+    # TODO: respect device keyword?
+    _helpers._check_device(da, device)
+
+    if isinstance(obj, da.Array):
+        if dtype is not None and dtype != obj.dtype:
+            if copy is False:
+                raise ValueError("Unable to avoid copy when changing dtype")
+            obj = obj.astype(dtype)
+        return obj.copy() if copy else obj  # pyright: ignore[reportAttributeAccessIssue]
+
+    if copy is False:
+        raise ValueError(
+            "Unable to avoid copy when converting a non-dask object to dask"
+        )
+
+    # copy=None to be uniform across dask < 2024.12 and >= 2024.12
+    # see https://github.com/dask/dask/pull/11524/
+    obj = np.array(obj, dtype=dtype, copy=True)
+    return da.from_array(obj)
+
+
+# Element wise aliases
+from dask.array import arccos as acos
+from dask.array import arccosh as acosh
+from dask.array import arcsin as asin
+from dask.array import arcsinh as asinh
+from dask.array import arctan as atan
+from dask.array import arctan2 as atan2
+from dask.array import arctanh as atanh
+
+# Other
+from dask.array import concatenate as concat
+from dask.array import invert as bitwise_invert
+from dask.array import left_shift as bitwise_left_shift
+from dask.array import power as pow
+from dask.array import right_shift as bitwise_right_shift
+
+
+# dask.array.clip does not work unless all three arguments are provided.
+# Furthermore, the masking workaround in common._aliases.clip cannot work with
+# dask (meaning uint64 promoting to float64 is going to just be unfixed for
+# now).
+def clip(
+    x: Array,
+    /,
+    min: float | Array | None = None,
+    max: float | Array | None = None,
+) -> Array:
+    """
+    Array API compatibility wrapper for clip().
+
+    See the corresponding documentation in the array library and/or the array API
+    specification for more details.
+    """
+
+    def _isscalar(a: float | Array | None, /) -> TypeIs[float | None]:
+        return a is None or isinstance(a, (int, float))
+
+    min_shape = () if _isscalar(min) else min.shape
+    max_shape = () if _isscalar(max) else max.shape
+
+    # TODO: This won't handle dask unknown shapes
+    result_shape = np.broadcast_shapes(x.shape, min_shape, max_shape)
+
+    if min is not None:
+        min = da.broadcast_to(da.asarray(min), result_shape)
+    if max is not None:
+        max = da.broadcast_to(da.asarray(max), result_shape)
+
+    if min is None and max is None:
+        return da.positive(x)
+
+    if min is None:
+        return astype(da.minimum(x, max), x.dtype)
+    if max is None:
+        return astype(da.maximum(x, min), x.dtype)
+
+    return astype(da.minimum(da.maximum(x, min), max), x.dtype)
+
+
+def _ensure_single_chunk(x: Array, axis: int) -> tuple[Array, Callable[[Array], Array]]:
+    """
+    Make sure that Array is not broken into multiple chunks along axis.
+
+    Returns
+    -------
+    x : Array
+        The input Array with a single chunk along axis.
+    restore : Callable[Array, Array]
+        function to apply to the output to rechunk it back into reasonable chunks
+    """
+    if axis < 0:
+        axis += x.ndim
+    if x.numblocks[axis] < 2:
+        return x, lambda x: x
+
+    # Break chunks on other axes in an attempt to keep chunk size low
+    x = x.rechunk({i: -1 if i == axis else "auto" for i in range(x.ndim)})
+
+    # Rather than reconstructing the original chunks, which can be a
+    # very expensive affair, just break down oversized chunks without
+    # incurring in any transfers over the network.
+    # This has the downside of a risk of overchunking if the array is
+    # then used in operations against other arrays that match the
+    # original chunking pattern.
+    return x, lambda x: x.rechunk()
+
+
+def sort(
+    x: Array,
+    /,
+    *,
+    axis: int = -1,
+    descending: py_bool = False,
+    stable: py_bool = True,
+) -> Array:
+    """
+    Array API compatibility layer around the lack of sort() in Dask.
+
+    Warnings
+    --------
+    This function temporarily rechunks the array along `axis` to a single chunk.
+    This can be extremely inefficient and can lead to out-of-memory errors.
+
+    See the corresponding documentation in the array library and/or the array API
+    specification for more details.
+    """
+    x, restore = _ensure_single_chunk(x, axis)
+
+    meta_xp = array_namespace(x._meta)
+    x = da.map_blocks(
+        meta_xp.sort,
+        x,
+        axis=axis,
+        meta=x._meta,
+        dtype=x.dtype,
+        descending=descending,
+        stable=stable,
+    )
+
+    return restore(x)
+
+
+def argsort(
+    x: Array,
+    /,
+    *,
+    axis: int = -1,
+    descending: py_bool = False,
+    stable: py_bool = True,
+) -> Array:
+    """
+    Array API compatibility layer around the lack of argsort() in Dask.
+
+    See the corresponding documentation in the array library and/or the array API
+    specification for more details.
+
+    Warnings
+    --------
+    This function temporarily rechunks the array along `axis` into a single chunk.
+    This can be extremely inefficient and can lead to out-of-memory errors.
+    """
+    x, restore = _ensure_single_chunk(x, axis)
+
+    meta_xp = array_namespace(x._meta)
+    dtype = meta_xp.argsort(x._meta).dtype
+    meta = meta_xp.astype(x._meta, dtype)
+    x = da.map_blocks(
+        meta_xp.argsort,
+        x,
+        axis=axis,
+        meta=meta,
+        dtype=dtype,
+        descending=descending,
+        stable=stable,
+    )
+
+    return restore(x)
+
+
+# dask.array.count_nonzero does not have keepdims
+def count_nonzero(
+    x: Array,
+    axis: int | None = None,
+    keepdims: py_bool = False,
+) -> Array:
+    result = da.count_nonzero(x, axis)
+    if keepdims:
+        if axis is None:
+            return da.reshape(result, [1] * x.ndim)
+        return da.expand_dims(result, axis)
+    return result
+
+
+__all__ = [
+    "count_nonzero",
+    "bool",
+    "int8", "int16", "int32", "int64",
+    "uint8", "uint16", "uint32", "uint64",
+    "float32", "float64",
+    "complex64", "complex128",
+    "asarray", "astype", "can_cast", "result_type",
+    "pow",
+    "concat",
+    "acos", "acosh", "asin", "asinh", "atan", "atan2", "atanh",
+    "bitwise_left_shift", "bitwise_right_shift", "bitwise_invert",
+]  # fmt: skip
+__all__ += _aliases.__all__
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/dask/array/_info.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/dask/array/_info.py
new file mode 100644
index 0000000000000000000000000000000000000000..2f39fc4b17ef730b8063b8ff10146731924244ba
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/dask/array/_info.py
@@ -0,0 +1,407 @@
+"""
+Array API Inspection namespace
+
+This is the namespace for inspection functions as defined by the array API
+standard. See
+https://data-apis.org/array-api/latest/API_specification/inspection.html for
+more details.
+
+"""
+
+# pyright: reportPrivateUsage=false
+
+from __future__ import annotations
+
+from typing import Literal, TypeAlias, overload
+
+import dask.array as da
+from numpy import bool_ as bool
+from numpy import (
+    complex64,
+    complex128,
+    dtype,
+    float32,
+    float64,
+    int8,
+    int16,
+    int32,
+    int64,
+    intp,
+    uint8,
+    uint16,
+    uint32,
+    uint64,
+)
+
+from ...common._helpers import _DASK_DEVICE, _check_device, _dask_device
+from ...common._typing import (
+    Capabilities,
+    DefaultDTypes,
+    DType,
+    DTypeKind,
+    DTypesAll,
+    DTypesAny,
+    DTypesBool,
+    DTypesComplex,
+    DTypesIntegral,
+    DTypesNumeric,
+    DTypesReal,
+    DTypesSigned,
+    DTypesUnsigned,
+)
+Device: TypeAlias = Literal["cpu"] | _dask_device
+
+
+class __array_namespace_info__:
+    """
+    Get the array API inspection namespace for Dask.
+
+    The array API inspection namespace defines the following functions:
+
+    - capabilities()
+    - default_device()
+    - default_dtypes()
+    - dtypes()
+    - devices()
+
+    See
+    https://data-apis.org/array-api/latest/API_specification/inspection.html
+    for more details.
+
+    Returns
+    -------
+    info : ModuleType
+        The array API inspection namespace for Dask.
+
+    Examples
+    --------
+    >>> info = xp.__array_namespace_info__()
+    >>> info.default_dtypes()
+    {'real floating': dask.float64,
+     'complex floating': dask.complex128,
+     'integral': dask.int64,
+     'indexing': dask.int64}
+
+    """
+
+    __module__ = "dask.array"
+
+    def capabilities(self) -> Capabilities:
+        """
+        Return a dictionary of array API library capabilities.
+
+        The resulting dictionary has the following keys:
+
+        - **"boolean indexing"**: boolean indicating whether an array library
+          supports boolean indexing.
+
+          Dask support boolean indexing as long as both the index
+          and the indexed arrays have known shapes.
+          Note however that the output .shape and .size properties
+          will contain a non-compliant math.nan instead of None.
+
+        - **"data-dependent shapes"**: boolean indicating whether an array
+          library supports data-dependent output shapes.
+
+          Dask implements unique_values et.al.
+          Note however that the output .shape and .size properties
+          will contain a non-compliant math.nan instead of None.
+
+        - **"max dimensions"**: integer indicating the maximum number of
+          dimensions supported by the array library.
+
+        See
+        https://data-apis.org/array-api/latest/API_specification/generated/array_api.info.capabilities.html
+        for more details.
+
+        See Also
+        --------
+        __array_namespace_info__.default_device,
+        __array_namespace_info__.default_dtypes,
+        __array_namespace_info__.dtypes,
+        __array_namespace_info__.devices
+
+        Returns
+        -------
+        capabilities : dict
+            A dictionary of array API library capabilities.
+
+        Examples
+        --------
+        >>> info = xp.__array_namespace_info__()
+        >>> info.capabilities()
+        {'boolean indexing': True,
+         'data-dependent shapes': True,
+         'max dimensions': 64}
+
+        """
+        return {
+            "boolean indexing": True,
+            "data-dependent shapes": True,
+            "max dimensions": 64,
+        }
+
+    def default_device(self) -> Device:
+        """
+        The default device used for new Dask arrays.
+
+        For Dask, this always returns ``'cpu'``.
+
+        See Also
+        --------
+        __array_namespace_info__.capabilities,
+        __array_namespace_info__.default_dtypes,
+        __array_namespace_info__.dtypes,
+        __array_namespace_info__.devices
+
+        Returns
+        -------
+        device : Device
+            The default device used for new Dask arrays.
+
+        Examples
+        --------
+        >>> info = xp.__array_namespace_info__()
+        >>> info.default_device()
+        'cpu'
+
+        """
+        return "cpu"
+
+    def default_dtypes(self, /, *, device: Device | None = None) -> DefaultDTypes:
+        """
+        The default data types used for new Dask arrays.
+
+        For Dask, this always returns the following dictionary:
+
+        - **"real floating"**: ``numpy.float64``
+        - **"complex floating"**: ``numpy.complex128``
+        - **"integral"**: ``numpy.intp``
+        - **"indexing"**: ``numpy.intp``
+
+        Parameters
+        ----------
+        device : str, optional
+            The device to get the default data types for.
+
+        Returns
+        -------
+        dtypes : dict
+            A dictionary describing the default data types used for new Dask
+            arrays.
+
+        See Also
+        --------
+        __array_namespace_info__.capabilities,
+        __array_namespace_info__.default_device,
+        __array_namespace_info__.dtypes,
+        __array_namespace_info__.devices
+
+        Examples
+        --------
+        >>> info = xp.__array_namespace_info__()
+        >>> info.default_dtypes()
+        {'real floating': dask.float64,
+         'complex floating': dask.complex128,
+         'integral': dask.int64,
+         'indexing': dask.int64}
+
+        """
+        _check_device(da, device)
+        return {
+            "real floating": dtype(float64),
+            "complex floating": dtype(complex128),
+            "integral": dtype(intp),
+            "indexing": dtype(intp),
+        }
+
+    @overload
+    def dtypes(
+        self, /, *, device: Device | None = None, kind: None = None
+    ) -> DTypesAll: ...
+    @overload
+    def dtypes(
+        self, /, *, device: Device | None = None, kind: Literal["bool"]
+    ) -> DTypesBool: ...
+    @overload
+    def dtypes(
+        self, /, *, device: Device | None = None, kind: Literal["signed integer"]
+    ) -> DTypesSigned: ...
+    @overload
+    def dtypes(
+        self, /, *, device: Device | None = None, kind: Literal["unsigned integer"]
+    ) -> DTypesUnsigned: ...
+    @overload
+    def dtypes(
+        self, /, *, device: Device | None = None, kind: Literal["integral"]
+    ) -> DTypesIntegral: ...
+    @overload
+    def dtypes(
+        self, /, *, device: Device | None = None, kind: Literal["real floating"]
+    ) -> DTypesReal: ...
+    @overload
+    def dtypes(
+        self, /, *, device: Device | None = None, kind: Literal["complex floating"]
+    ) -> DTypesComplex: ...
+    @overload
+    def dtypes(
+        self, /, *, device: Device | None = None, kind: Literal["numeric"]
+    ) -> DTypesNumeric: ...
+    def dtypes(
+        self, /, *, device: Device | None = None, kind: DTypeKind | None = None
+    ) -> DTypesAny:
+        """
+        The array API data types supported by Dask.
+
+        Note that this function only returns data types that are defined by
+        the array API.
+
+        Parameters
+        ----------
+        device : str, optional
+            The device to get the data types for.
+        kind : str or tuple of str, optional
+            The kind of data types to return. If ``None``, all data types are
+            returned. If a string, only data types of that kind are returned.
+            If a tuple, a dictionary containing the union of the given kinds
+            is returned. The following kinds are supported:
+
+            - ``'bool'``: boolean data types (i.e., ``bool``).
+            - ``'signed integer'``: signed integer data types (i.e., ``int8``,
+              ``int16``, ``int32``, ``int64``).
+            - ``'unsigned integer'``: unsigned integer data types (i.e.,
+              ``uint8``, ``uint16``, ``uint32``, ``uint64``).
+            - ``'integral'``: integer data types. Shorthand for ``('signed
+              integer', 'unsigned integer')``.
+            - ``'real floating'``: real-valued floating-point data types
+              (i.e., ``float32``, ``float64``).
+            - ``'complex floating'``: complex floating-point data types (i.e.,
+              ``complex64``, ``complex128``).
+            - ``'numeric'``: numeric data types. Shorthand for ``('integral',
+              'real floating', 'complex floating')``.
+
+        Returns
+        -------
+        dtypes : dict
+            A dictionary mapping the names of data types to the corresponding
+            Dask data types.
+
+        See Also
+        --------
+        __array_namespace_info__.capabilities,
+        __array_namespace_info__.default_device,
+        __array_namespace_info__.default_dtypes,
+        __array_namespace_info__.devices
+
+        Examples
+        --------
+        >>> info = xp.__array_namespace_info__()
+        >>> info.dtypes(kind='signed integer')
+        {'int8': dask.int8,
+         'int16': dask.int16,
+         'int32': dask.int32,
+         'int64': dask.int64}
+
+        """
+        _check_device(da, device)
+        if kind is None:
+            return {
+                "bool": dtype(bool),
+                "int8": dtype(int8),
+                "int16": dtype(int16),
+                "int32": dtype(int32),
+                "int64": dtype(int64),
+                "uint8": dtype(uint8),
+                "uint16": dtype(uint16),
+                "uint32": dtype(uint32),
+                "uint64": dtype(uint64),
+                "float32": dtype(float32),
+                "float64": dtype(float64),
+                "complex64": dtype(complex64),
+                "complex128": dtype(complex128),
+            }
+        if kind == "bool":
+            return {"bool": bool}
+        if kind == "signed integer":
+            return {
+                "int8": dtype(int8),
+                "int16": dtype(int16),
+                "int32": dtype(int32),
+                "int64": dtype(int64),
+            }
+        if kind == "unsigned integer":
+            return {
+                "uint8": dtype(uint8),
+                "uint16": dtype(uint16),
+                "uint32": dtype(uint32),
+                "uint64": dtype(uint64),
+            }
+        if kind == "integral":
+            return {
+                "int8": dtype(int8),
+                "int16": dtype(int16),
+                "int32": dtype(int32),
+                "int64": dtype(int64),
+                "uint8": dtype(uint8),
+                "uint16": dtype(uint16),
+                "uint32": dtype(uint32),
+                "uint64": dtype(uint64),
+            }
+        if kind == "real floating":
+            return {
+                "float32": dtype(float32),
+                "float64": dtype(float64),
+            }
+        if kind == "complex floating":
+            return {
+                "complex64": dtype(complex64),
+                "complex128": dtype(complex128),
+            }
+        if kind == "numeric":
+            return {
+                "int8": dtype(int8),
+                "int16": dtype(int16),
+                "int32": dtype(int32),
+                "int64": dtype(int64),
+                "uint8": dtype(uint8),
+                "uint16": dtype(uint16),
+                "uint32": dtype(uint32),
+                "uint64": dtype(uint64),
+                "float32": dtype(float32),
+                "float64": dtype(float64),
+                "complex64": dtype(complex64),
+                "complex128": dtype(complex128),
+            }
+        if isinstance(kind, tuple):
+            res: dict[str, DType] = {}
+            for k in kind:
+                res.update(self.dtypes(kind=k))
+            return res
+        raise ValueError(f"unsupported kind: {kind!r}")
+
+    def devices(self) -> list[Device]:
+        """
+        The devices supported by Dask.
+
+        For Dask, this always returns ``['cpu', DASK_DEVICE]``.
+
+        Returns
+        -------
+        devices : list[Device]
+            The devices supported by Dask.
+
+        See Also
+        --------
+        __array_namespace_info__.capabilities,
+        __array_namespace_info__.default_device,
+        __array_namespace_info__.default_dtypes,
+        __array_namespace_info__.dtypes
+
+        Examples
+        --------
+        >>> info = xp.__array_namespace_info__()
+        >>> info.devices()
+        ['cpu', DASK_DEVICE]
+
+        """
+        return ["cpu", _DASK_DEVICE]
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/dask/array/fft.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/dask/array/fft.py
new file mode 100644
index 0000000000000000000000000000000000000000..44b68e733984f1f2da11cdd3b892e84b7bd5e797
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/dask/array/fft.py
@@ -0,0 +1,16 @@
+from ..._internal import clone_module
+
+__all__ = clone_module("dask.array.fft", globals())
+
+from ...common import _fft
+from ..._internal import get_xp
+
+import dask.array as da
+
+fftfreq = get_xp(da)(_fft.fftfreq)
+rfftfreq = get_xp(da)(_fft.rfftfreq)
+
+__all__ += ["fftfreq", "rfftfreq"]
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/dask/array/linalg.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/dask/array/linalg.py
new file mode 100644
index 0000000000000000000000000000000000000000..6b3c10117b10b9a8b3bb494415f6711bd3db19d1
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/dask/array/linalg.py
@@ -0,0 +1,64 @@
+from __future__ import annotations
+
+from typing import Literal
+
+import dask.array as da
+
+# The `matmul` and `tensordot` functions are in both the main and linalg namespaces
+from dask.array import matmul, outer, tensordot
+
+# Exports
+from ..._internal import clone_module, get_xp
+from ...common import _linalg
+from ...common._typing import Array
+
+__all__ = clone_module("dask.array.linalg", globals())
+
+from ._aliases import matrix_transpose, vecdot
+
+EighResult = _linalg.EighResult
+QRResult = _linalg.QRResult
+SlogdetResult = _linalg.SlogdetResult
+SVDResult = _linalg.SVDResult
+# TODO: use the QR wrapper once dask
+# supports the mode keyword on QR
+# https://github.com/dask/dask/issues/10388
+#qr = get_xp(da)(_linalg.qr)
+def qr(  # type: ignore[no-redef]
+    x: Array,
+    mode: Literal["reduced", "complete"] = "reduced",
+    **kwargs: object,
+) -> QRResult:
+    if mode != "reduced":
+        raise ValueError("dask arrays only support using mode='reduced'")
+    return QRResult(*da.linalg.qr(x, **kwargs))
+trace = get_xp(da)(_linalg.trace)
+cholesky = get_xp(da)(_linalg.cholesky)
+matrix_rank = get_xp(da)(_linalg.matrix_rank)
+matrix_norm = get_xp(da)(_linalg.matrix_norm)
+
+
+# Wrap the svd functions to not pass full_matrices to dask
+# when full_matrices=False (as that is the default behavior for dask),
+# and dask doesn't have the full_matrices keyword
+def svd(x: Array, full_matrices: bool = True, **kwargs: object) -> SVDResult:  # type: ignore[no-redef]
+    if full_matrices:
+        raise ValueError("full_matrics=True is not supported by dask.")
+    return da.linalg.svd(x, coerce_signs=False, **kwargs)
+
+def svdvals(x: Array) -> Array:
+    # TODO: can't avoid computing U or V for dask
+    _, s, _ =  svd(x)
+    return s
+
+vector_norm = get_xp(da)(_linalg.vector_norm)
+diagonal = get_xp(da)(_linalg.diagonal)
+
+__all__ += ["trace", "outer", "matmul", "tensordot",
+            "matrix_transpose", "vecdot", "EighResult",
+            "QRResult", "SlogdetResult", "SVDResult", "qr",
+            "cholesky", "matrix_rank", "matrix_norm", "svdvals",
+            "vector_norm", "diagonal"]
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__init__.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..23379e44db6e70eae64d4987d90d5b5bc6f8d51c
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__init__.py
@@ -0,0 +1,38 @@
+# ruff: noqa: PLC0414
+from typing import Final
+
+from .._internal import clone_module
+
+# This needs to be loaded explicitly before cloning
+import numpy.typing  # noqa: F401
+
+__all__ = clone_module("numpy", globals())
+
+# These imports may overwrite names from the import * above.
+from . import _aliases
+from ._aliases import *  # type: ignore[assignment,no-redef] # noqa: F403
+from ._info import __array_namespace_info__  # noqa: F401
+
+# Don't know why, but we have to do an absolute import to import linalg. If we
+# instead do
+#
+# from . import linalg
+#
+# It doesn't overwrite np.linalg from above. The import is generated
+# dynamically so that the library can be vendored.
+__import__(__package__ + ".linalg")
+
+__import__(__package__ + ".fft")
+
+from .linalg import matrix_transpose, vecdot  # type: ignore[no-redef]  # noqa: F401
+
+__array_api_version__: Final = "2024.12"
+
+__all__ = sorted(
+    set(__all__) 
+    | set(_aliases.__all__) 
+    | {"__array_api_version__", "__array_namespace_info__", "linalg", "fft"}
+)
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__pycache__/__init__.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..4003d6c324a6c6fdd02c5067166593f4141cb43a
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__pycache__/__init__.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__pycache__/_aliases.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__pycache__/_aliases.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..51bfea03aacf5295988a6394b22108081d3742ce
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__pycache__/_aliases.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__pycache__/_info.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__pycache__/_info.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..3c4bdd62356575a722f0091d690e6b16525fa30f
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__pycache__/_info.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__pycache__/_typing.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__pycache__/_typing.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b6969bf215732b59654b445c1f9025b98ca7d361
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__pycache__/_typing.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__pycache__/fft.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__pycache__/fft.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..9dacfb8b35e8d5a4567645f31261436d7fdaadd6
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__pycache__/fft.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__pycache__/linalg.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__pycache__/linalg.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..7b9051cff0fec0163fdea99c2f24299351f8ba57
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/__pycache__/linalg.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/_aliases.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/_aliases.py
new file mode 100644
index 0000000000000000000000000000000000000000..87b3c2f398af09fc8065aa05a1bfccd679e639d4
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/_aliases.py
@@ -0,0 +1,191 @@
+# pyright: reportPrivateUsage=false
+from __future__ import annotations
+
+from builtins import bool as py_bool
+from typing import Any, cast
+
+import numpy as np
+
+from .._internal import get_xp
+from ..common import _aliases, _helpers
+from ..common._typing import NestedSequence, SupportsBufferProtocol
+from ._typing import Array, Device, DType
+
+bool = np.bool_
+
+# Basic renames
+acos = np.arccos
+acosh = np.arccosh
+asin = np.arcsin
+asinh = np.arcsinh
+atan = np.arctan
+atan2 = np.arctan2
+atanh = np.arctanh
+bitwise_left_shift = np.left_shift
+bitwise_invert = np.invert
+bitwise_right_shift = np.right_shift
+concat = np.concatenate
+pow = np.power
+
+arange = get_xp(np)(_aliases.arange)
+empty = get_xp(np)(_aliases.empty)
+empty_like = get_xp(np)(_aliases.empty_like)
+eye = get_xp(np)(_aliases.eye)
+full = get_xp(np)(_aliases.full)
+full_like = get_xp(np)(_aliases.full_like)
+linspace = get_xp(np)(_aliases.linspace)
+ones = get_xp(np)(_aliases.ones)
+ones_like = get_xp(np)(_aliases.ones_like)
+zeros = get_xp(np)(_aliases.zeros)
+zeros_like = get_xp(np)(_aliases.zeros_like)
+UniqueAllResult = get_xp(np)(_aliases.UniqueAllResult)
+UniqueCountsResult = get_xp(np)(_aliases.UniqueCountsResult)
+UniqueInverseResult = get_xp(np)(_aliases.UniqueInverseResult)
+unique_all = get_xp(np)(_aliases.unique_all)
+unique_counts = get_xp(np)(_aliases.unique_counts)
+unique_inverse = get_xp(np)(_aliases.unique_inverse)
+unique_values = get_xp(np)(_aliases.unique_values)
+std = get_xp(np)(_aliases.std)
+var = get_xp(np)(_aliases.var)
+cumulative_sum = get_xp(np)(_aliases.cumulative_sum)
+cumulative_prod = get_xp(np)(_aliases.cumulative_prod)
+clip = get_xp(np)(_aliases.clip)
+permute_dims = get_xp(np)(_aliases.permute_dims)
+reshape = get_xp(np)(_aliases.reshape)
+argsort = get_xp(np)(_aliases.argsort)
+sort = get_xp(np)(_aliases.sort)
+nonzero = get_xp(np)(_aliases.nonzero)
+matmul = get_xp(np)(_aliases.matmul)
+matrix_transpose = get_xp(np)(_aliases.matrix_transpose)
+tensordot = get_xp(np)(_aliases.tensordot)
+sign = get_xp(np)(_aliases.sign)
+finfo = get_xp(np)(_aliases.finfo)
+iinfo = get_xp(np)(_aliases.iinfo)
+
+
+# asarray also adds the copy keyword, which is not present in numpy 1.0.
+# asarray() is different enough between numpy, cupy, and dask, the logic
+# complicated enough that it's easier to define it separately for each module
+# rather than trying to combine everything into one function in common/
+def asarray(
+    obj: Array | complex | NestedSequence[complex] | SupportsBufferProtocol,
+    /,
+    *,
+    dtype: DType | None = None,
+    device: Device | None = None,
+    copy: py_bool | None = None,
+    **kwargs: Any,
+) -> Array:
+    """
+    Array API compatibility wrapper for asarray().
+
+    See the corresponding documentation in the array library and/or the array API
+    specification for more details.
+    """
+    _helpers._check_device(np, device)
+
+    # None is unsupported in NumPy 1.0, but we can use an internal enum
+    # False in NumPy 1.0 means None in NumPy 2.0 and in the Array API
+    if copy is None:
+        copy = np._CopyMode.IF_NEEDED  # type: ignore[assignment,attr-defined]
+    elif copy is False:
+        copy = np._CopyMode.NEVER  # type: ignore[assignment,attr-defined]
+
+    return np.array(obj, copy=copy, dtype=dtype, **kwargs)
+
+
+def astype(
+    x: Array,
+    dtype: DType,
+    /,
+    *,
+    copy: py_bool = True,
+    device: Device | None = None,
+) -> Array:
+    _helpers._check_device(np, device)
+    return x.astype(dtype=dtype, copy=copy)
+
+
+# count_nonzero returns a python int for axis=None and keepdims=False
+# https://github.com/numpy/numpy/issues/17562
+def count_nonzero(
+    x: Array,
+    axis: int | tuple[int, ...] | None = None,
+    keepdims: py_bool = False,
+) -> Array:
+    # NOTE: this is currently incorrectly typed in numpy, but will be fixed in
+    # numpy 2.2.5 and 2.3.0: https://github.com/numpy/numpy/pull/28750
+    result = cast("Any", np.count_nonzero(x, axis=axis, keepdims=keepdims))  # pyright: ignore[reportArgumentType, reportCallIssue]
+    if axis is None and not keepdims:
+        return np.asarray(result)
+    return result
+
+
+# take_along_axis: axis defaults to -1 but in numpy axis is a required arg
+def take_along_axis(x: Array, indices: Array, /, *, axis: int = -1) -> Array:
+    return np.take_along_axis(x, indices, axis=axis)
+
+
+# ceil, floor, and trunc return integers for integer inputs in NumPy < 2
+
+def ceil(x: Array, /) -> Array:
+    if np.__version__ < '2' and np.issubdtype(x.dtype, np.integer):
+        return x.copy()
+    return np.ceil(x)
+
+
+def floor(x: Array, /) -> Array:
+    if np.__version__ < '2' and np.issubdtype(x.dtype, np.integer):
+        return x.copy()
+    return np.floor(x)
+
+
+def trunc(x: Array, /) -> Array:
+    if np.__version__ < '2' and np.issubdtype(x.dtype, np.integer):
+        return x.copy()
+    return np.trunc(x)
+
+
+# These functions are completely new here. If the library already has them
+# (i.e., numpy 2.0), use the library version instead of our wrapper.
+if hasattr(np, "vecdot"):
+    vecdot = np.vecdot
+else:
+    vecdot = get_xp(np)(_aliases.vecdot)  # type: ignore[assignment]
+
+if hasattr(np, "isdtype"):
+    isdtype = np.isdtype
+else:
+    isdtype = get_xp(np)(_aliases.isdtype)
+
+if hasattr(np, "unstack"):
+    unstack = np.unstack
+else:
+    unstack = get_xp(np)(_aliases.unstack)
+
+__all__ = _aliases.__all__ + [
+    "asarray",
+    "astype",
+    "acos",
+    "acosh",
+    "asin",
+    "asinh",
+    "atan",
+    "atan2",
+    "atanh",
+    "ceil",
+    "floor",
+    "trunc",
+    "bitwise_left_shift",
+    "bitwise_invert",
+    "bitwise_right_shift",
+    "bool",
+    "concat",
+    "count_nonzero",
+    "pow",
+    "take_along_axis"
+]
+
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/_info.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/_info.py
new file mode 100644
index 0000000000000000000000000000000000000000..c625c13e36942286a83ee3290c93208d8818bedd
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/_info.py
@@ -0,0 +1,367 @@
+"""
+Array API Inspection namespace
+
+This is the namespace for inspection functions as defined by the array API
+standard. See
+https://data-apis.org/array-api/latest/API_specification/inspection.html for
+more details.
+
+"""
+from __future__ import annotations
+
+from numpy import bool_ as bool
+from numpy import (
+    complex64,
+    complex128,
+    dtype,
+    float32,
+    float64,
+    int8,
+    int16,
+    int32,
+    int64,
+    intp,
+    uint8,
+    uint16,
+    uint32,
+    uint64,
+)
+
+from ..common._typing import DefaultDTypes
+from ._typing import Device, DType
+
+
+class __array_namespace_info__:
+    """
+    Get the array API inspection namespace for NumPy.
+
+    The array API inspection namespace defines the following functions:
+
+    - capabilities()
+    - default_device()
+    - default_dtypes()
+    - dtypes()
+    - devices()
+
+    See
+    https://data-apis.org/array-api/latest/API_specification/inspection.html
+    for more details.
+
+    Returns
+    -------
+    info : ModuleType
+        The array API inspection namespace for NumPy.
+
+    Examples
+    --------
+    >>> info = np.__array_namespace_info__()
+    >>> info.default_dtypes()
+    {'real floating': numpy.float64,
+     'complex floating': numpy.complex128,
+     'integral': numpy.int64,
+     'indexing': numpy.int64}
+
+    """
+
+    __module__ = 'numpy'
+
+    def capabilities(self):
+        """
+        Return a dictionary of array API library capabilities.
+
+        The resulting dictionary has the following keys:
+
+        - **"boolean indexing"**: boolean indicating whether an array library
+          supports boolean indexing. Always ``True`` for NumPy.
+
+        - **"data-dependent shapes"**: boolean indicating whether an array
+          library supports data-dependent output shapes. Always ``True`` for
+          NumPy.
+
+        See
+        https://data-apis.org/array-api/latest/API_specification/generated/array_api.info.capabilities.html
+        for more details.
+
+        See Also
+        --------
+        __array_namespace_info__.default_device,
+        __array_namespace_info__.default_dtypes,
+        __array_namespace_info__.dtypes,
+        __array_namespace_info__.devices
+
+        Returns
+        -------
+        capabilities : dict
+            A dictionary of array API library capabilities.
+
+        Examples
+        --------
+        >>> info = np.__array_namespace_info__()
+        >>> info.capabilities()
+        {'boolean indexing': True,
+         'data-dependent shapes': True,
+         'max dimensions': 64}
+
+        """
+        return {
+            "boolean indexing": True,
+            "data-dependent shapes": True,
+            "max dimensions": 64,
+        }
+
+    def default_device(self):
+        """
+        The default device used for new NumPy arrays.
+
+        For NumPy, this always returns ``'cpu'``.
+
+        See Also
+        --------
+        __array_namespace_info__.capabilities,
+        __array_namespace_info__.default_dtypes,
+        __array_namespace_info__.dtypes,
+        __array_namespace_info__.devices
+
+        Returns
+        -------
+        device : Device
+            The default device used for new NumPy arrays.
+
+        Examples
+        --------
+        >>> info = np.__array_namespace_info__()
+        >>> info.default_device()
+        'cpu'
+
+        """
+        return "cpu"
+
+    def default_dtypes(
+        self,
+        *,
+        device: Device | None = None,
+    ) -> DefaultDTypes:
+        """
+        The default data types used for new NumPy arrays.
+
+        For NumPy, this always returns the following dictionary:
+
+        - **"real floating"**: ``numpy.float64``
+        - **"complex floating"**: ``numpy.complex128``
+        - **"integral"**: ``numpy.intp``
+        - **"indexing"**: ``numpy.intp``
+
+        Parameters
+        ----------
+        device : str, optional
+            The device to get the default data types for. For NumPy, only
+            ``'cpu'`` is allowed.
+
+        Returns
+        -------
+        dtypes : dict
+            A dictionary describing the default data types used for new NumPy
+            arrays.
+
+        See Also
+        --------
+        __array_namespace_info__.capabilities,
+        __array_namespace_info__.default_device,
+        __array_namespace_info__.dtypes,
+        __array_namespace_info__.devices
+
+        Examples
+        --------
+        >>> info = np.__array_namespace_info__()
+        >>> info.default_dtypes()
+        {'real floating': numpy.float64,
+         'complex floating': numpy.complex128,
+         'integral': numpy.int64,
+         'indexing': numpy.int64}
+
+        """
+        if device not in ["cpu", None]:
+            raise ValueError(
+                'Device not understood. Only "cpu" is allowed, but received:'
+                f' {device}'
+            )
+        return {
+            "real floating": dtype(float64),
+            "complex floating": dtype(complex128),
+            "integral": dtype(intp),
+            "indexing": dtype(intp),
+        }
+
+    def dtypes(
+        self,
+        *,
+        device: Device | None = None,
+        kind: str | tuple[str, ...] | None = None,
+    ) -> dict[str, DType]:
+        """
+        The array API data types supported by NumPy.
+
+        Note that this function only returns data types that are defined by
+        the array API.
+
+        Parameters
+        ----------
+        device : str, optional
+            The device to get the data types for. For NumPy, only ``'cpu'`` is
+            allowed.
+        kind : str or tuple of str, optional
+            The kind of data types to return. If ``None``, all data types are
+            returned. If a string, only data types of that kind are returned.
+            If a tuple, a dictionary containing the union of the given kinds
+            is returned. The following kinds are supported:
+
+            - ``'bool'``: boolean data types (i.e., ``bool``).
+            - ``'signed integer'``: signed integer data types (i.e., ``int8``,
+              ``int16``, ``int32``, ``int64``).
+            - ``'unsigned integer'``: unsigned integer data types (i.e.,
+              ``uint8``, ``uint16``, ``uint32``, ``uint64``).
+            - ``'integral'``: integer data types. Shorthand for ``('signed
+              integer', 'unsigned integer')``.
+            - ``'real floating'``: real-valued floating-point data types
+              (i.e., ``float32``, ``float64``).
+            - ``'complex floating'``: complex floating-point data types (i.e.,
+              ``complex64``, ``complex128``).
+            - ``'numeric'``: numeric data types. Shorthand for ``('integral',
+              'real floating', 'complex floating')``.
+
+        Returns
+        -------
+        dtypes : dict
+            A dictionary mapping the names of data types to the corresponding
+            NumPy data types.
+
+        See Also
+        --------
+        __array_namespace_info__.capabilities,
+        __array_namespace_info__.default_device,
+        __array_namespace_info__.default_dtypes,
+        __array_namespace_info__.devices
+
+        Examples
+        --------
+        >>> info = np.__array_namespace_info__()
+        >>> info.dtypes(kind='signed integer')
+        {'int8': numpy.int8,
+         'int16': numpy.int16,
+         'int32': numpy.int32,
+         'int64': numpy.int64}
+
+        """
+        if device not in ["cpu", None]:
+            raise ValueError(
+                'Device not understood. Only "cpu" is allowed, but received:'
+                f' {device}'
+            )
+        if kind is None:
+            return {
+                "bool": dtype(bool),
+                "int8": dtype(int8),
+                "int16": dtype(int16),
+                "int32": dtype(int32),
+                "int64": dtype(int64),
+                "uint8": dtype(uint8),
+                "uint16": dtype(uint16),
+                "uint32": dtype(uint32),
+                "uint64": dtype(uint64),
+                "float32": dtype(float32),
+                "float64": dtype(float64),
+                "complex64": dtype(complex64),
+                "complex128": dtype(complex128),
+            }
+        if kind == "bool":
+            return {"bool": dtype(bool)}
+        if kind == "signed integer":
+            return {
+                "int8": dtype(int8),
+                "int16": dtype(int16),
+                "int32": dtype(int32),
+                "int64": dtype(int64),
+            }
+        if kind == "unsigned integer":
+            return {
+                "uint8": dtype(uint8),
+                "uint16": dtype(uint16),
+                "uint32": dtype(uint32),
+                "uint64": dtype(uint64),
+            }
+        if kind == "integral":
+            return {
+                "int8": dtype(int8),
+                "int16": dtype(int16),
+                "int32": dtype(int32),
+                "int64": dtype(int64),
+                "uint8": dtype(uint8),
+                "uint16": dtype(uint16),
+                "uint32": dtype(uint32),
+                "uint64": dtype(uint64),
+            }
+        if kind == "real floating":
+            return {
+                "float32": dtype(float32),
+                "float64": dtype(float64),
+            }
+        if kind == "complex floating":
+            return {
+                "complex64": dtype(complex64),
+                "complex128": dtype(complex128),
+            }
+        if kind == "numeric":
+            return {
+                "int8": dtype(int8),
+                "int16": dtype(int16),
+                "int32": dtype(int32),
+                "int64": dtype(int64),
+                "uint8": dtype(uint8),
+                "uint16": dtype(uint16),
+                "uint32": dtype(uint32),
+                "uint64": dtype(uint64),
+                "float32": dtype(float32),
+                "float64": dtype(float64),
+                "complex64": dtype(complex64),
+                "complex128": dtype(complex128),
+            }
+        if isinstance(kind, tuple):
+            res: dict[str, DType] = {}
+            for k in kind:
+                res.update(self.dtypes(kind=k))
+            return res
+        raise ValueError(f"unsupported kind: {kind!r}")
+
+    def devices(self) -> list[Device]:
+        """
+        The devices supported by NumPy.
+
+        For NumPy, this always returns ``['cpu']``.
+
+        Returns
+        -------
+        devices : list[Device]
+            The devices supported by NumPy.
+
+        See Also
+        --------
+        __array_namespace_info__.capabilities,
+        __array_namespace_info__.default_device,
+        __array_namespace_info__.default_dtypes,
+        __array_namespace_info__.dtypes
+
+        Examples
+        --------
+        >>> info = np.__array_namespace_info__()
+        >>> info.devices()
+        ['cpu']
+
+        """
+        return ["cpu"]
+
+
+__all__ = ["__array_namespace_info__"]
+
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/_typing.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/_typing.py
new file mode 100644
index 0000000000000000000000000000000000000000..b5fa188c52b69c16a1fa088026720fee9390e640
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/_typing.py
@@ -0,0 +1,29 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any, Literal, TypeAlias
+
+import numpy as np
+
+Device: TypeAlias = Literal["cpu"]
+
+if TYPE_CHECKING:
+
+    # NumPy 1.x on Python 3.10 fails to parse np.dtype[]
+    DType: TypeAlias = np.dtype[
+        np.bool_
+        | np.integer[Any]
+        | np.float32
+        | np.float64
+        | np.complex64
+        | np.complex128
+    ]
+    Array: TypeAlias = np.ndarray[Any, DType]
+else:
+    DType: TypeAlias = np.dtype
+    Array: TypeAlias = np.ndarray
+
+__all__ = ["Array", "DType", "Device"]
+
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/fft.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/fft.py
new file mode 100644
index 0000000000000000000000000000000000000000..a492feb8cf69085a3a6617fbee13a8ac236701fa
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/fft.py
@@ -0,0 +1,30 @@
+import numpy as np
+
+from .._internal import clone_module
+
+__all__ = clone_module("numpy.fft", globals())
+
+from .._internal import get_xp
+from ..common import _fft
+
+fft = get_xp(np)(_fft.fft)
+ifft = get_xp(np)(_fft.ifft)
+fftn = get_xp(np)(_fft.fftn)
+ifftn = get_xp(np)(_fft.ifftn)
+rfft = get_xp(np)(_fft.rfft)
+irfft = get_xp(np)(_fft.irfft)
+rfftn = get_xp(np)(_fft.rfftn)
+irfftn = get_xp(np)(_fft.irfftn)
+hfft = get_xp(np)(_fft.hfft)
+ihfft = get_xp(np)(_fft.ihfft)
+fftfreq = get_xp(np)(_fft.fftfreq)
+rfftfreq = get_xp(np)(_fft.rfftfreq)
+fftshift = get_xp(np)(_fft.fftshift)
+ifftshift = get_xp(np)(_fft.ifftshift)
+
+
+__all__ = sorted(set(__all__) | set(_fft.__all__))
+
+def __dir__() -> list[str]:
+    return __all__
+
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/linalg.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/linalg.py
new file mode 100644
index 0000000000000000000000000000000000000000..7168441c7517ee2d8e4b03d534531a832d292f43
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/numpy/linalg.py
@@ -0,0 +1,128 @@
+# pyright: reportAttributeAccessIssue=false
+# pyright: reportUnknownArgumentType=false
+# pyright: reportUnknownMemberType=false
+# pyright: reportUnknownVariableType=false
+
+from __future__ import annotations
+
+import numpy as np
+
+from .._internal import clone_module, get_xp
+from ..common import _linalg
+
+__all__ = clone_module("numpy.linalg", globals())
+
+# These functions are in both the main and linalg namespaces
+from ._aliases import matmul, matrix_transpose, tensordot, vecdot  # noqa: F401
+from ._typing import Array
+
+cross = get_xp(np)(_linalg.cross)
+outer = get_xp(np)(_linalg.outer)
+EighResult = _linalg.EighResult
+QRResult = _linalg.QRResult
+SlogdetResult = _linalg.SlogdetResult
+SVDResult = _linalg.SVDResult
+eigh = get_xp(np)(_linalg.eigh)
+qr = get_xp(np)(_linalg.qr)
+slogdet = get_xp(np)(_linalg.slogdet)
+svd = get_xp(np)(_linalg.svd)
+cholesky = get_xp(np)(_linalg.cholesky)
+matrix_rank = get_xp(np)(_linalg.matrix_rank)
+pinv = get_xp(np)(_linalg.pinv)
+matrix_norm = get_xp(np)(_linalg.matrix_norm)
+svdvals = get_xp(np)(_linalg.svdvals)
+diagonal = get_xp(np)(_linalg.diagonal)
+trace = get_xp(np)(_linalg.trace)
+
+# Note: unlike np.linalg.solve, the array API solve() only accepts x2 as a
+# vector when it is exactly 1-dimensional. All other cases treat x2 as a stack
+# of matrices. The np.linalg.solve behavior of allowing stacks of both
+# matrices and vectors is ambiguous c.f.
+# https://github.com/numpy/numpy/issues/15349 and
+# https://github.com/data-apis/array-api/issues/285.
+
+# To workaround this, the below is the code from np.linalg.solve except
+# only calling solve1 in the exactly 1D case.
+
+
+# This code is here instead of in common because it is numpy specific. Also
+# note that CuPy's solve() does not currently support broadcasting (see
+# https://github.com/cupy/cupy/blob/main/cupy/cublas.py#L43).
+def solve(x1: Array, x2: Array, /) -> Array:
+    try:
+        from numpy.linalg._linalg import (  # type: ignore[attr-defined]
+            _assert_stacked_2d,
+            _assert_stacked_square,
+            _commonType,
+            _makearray,
+            _raise_linalgerror_singular,
+            isComplexType,
+        )
+    except ImportError:
+        from numpy.linalg.linalg import (  # type: ignore[attr-defined]
+            _assert_stacked_2d,
+            _assert_stacked_square,
+            _commonType,
+            _makearray,
+            _raise_linalgerror_singular,
+            isComplexType,
+        )
+    from numpy.linalg import _umath_linalg
+
+    x1, _ = _makearray(x1)
+    _assert_stacked_2d(x1)
+    _assert_stacked_square(x1)
+    x2, wrap = _makearray(x2)
+    t, result_t = _commonType(x1, x2)
+
+    # This part is different from np.linalg.solve
+    gufunc: np.ufunc
+    if x2.ndim == 1:
+        gufunc = _umath_linalg.solve1
+    else:
+        gufunc = _umath_linalg.solve
+
+    # This does nothing currently but is left in because it will be relevant
+    # when complex dtype support is added to the spec in 2022.
+    signature = "DD->D" if isComplexType(t) else "dd->d"
+    with np.errstate(
+        call=_raise_linalgerror_singular,
+        invalid="call",
+        over="ignore",
+        divide="ignore",
+        under="ignore",
+    ):
+        r: Array = gufunc(x1, x2, signature=signature)
+
+    return wrap(r.astype(result_t, copy=False))
+
+
+# These functions are completely new here. If the library already has them
+# (i.e., numpy 2.0), use the library version instead of our wrapper.
+if hasattr(np.linalg, "vector_norm"):
+    vector_norm = np.linalg.vector_norm
+else:
+    vector_norm = get_xp(np)(_linalg.vector_norm)
+
+
+_all = [
+    "LinAlgError",
+    "cond",
+    "det",
+    "eig",
+    "eigvals",
+    "eigvalsh",
+    "inv",
+    "lstsq",
+    "matrix_power",
+    "multi_dot",
+    "norm",
+    "solve", 
+    "tensorinv",
+    "tensorsolve",
+    "vector_norm",
+]
+__all__ = sorted(set(__all__) | set(_linalg.__all__) | set(_all))
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/torch/__init__.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/torch/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..6cbb6ec264869090e888e6631df86ab2e99ca36a
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/torch/__init__.py
@@ -0,0 +1,25 @@
+from typing import Final
+
+from .._internal import clone_module
+
+__all__ = clone_module("torch", globals())
+
+# These imports may overwrite names from the import * above.
+from . import _aliases
+from ._aliases import * # noqa: F403
+from ._info import __array_namespace_info__  # noqa: F401
+
+# See the comment in the numpy __init__.py
+__import__(__package__ + '.linalg')
+__import__(__package__ + '.fft')
+
+__array_api_version__: Final = '2024.12'
+
+__all__ = sorted(
+    set(__all__)
+    | set(_aliases.__all__)
+    | {"__array_api_version__", "__array_namespace_info__", "linalg", "fft"}
+)
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/torch/_aliases.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/torch/_aliases.py
new file mode 100644
index 0000000000000000000000000000000000000000..4e8533f95e839f7ccfce2ef83a810edf2cf9662c
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/torch/_aliases.py
@@ -0,0 +1,875 @@
+from __future__ import annotations
+
+from collections.abc import Sequence
+from functools import reduce as _reduce, wraps as _wraps
+from builtins import all as _builtin_all, any as _builtin_any
+from typing import Any, Literal
+
+import torch
+
+from .._internal import get_xp
+from ..common import _aliases
+from ..common._typing import NestedSequence, SupportsBufferProtocol
+from ._typing import Array, Device, DType
+
+_int_dtypes = {
+    torch.uint8,
+    torch.int8,
+    torch.int16,
+    torch.int32,
+    torch.int64,
+}
+try:
+    # torch >=2.3
+    _int_dtypes |= {torch.uint16, torch.uint32, torch.uint64}
+except AttributeError:
+    pass
+
+
+_array_api_dtypes = {
+    torch.bool,
+    *_int_dtypes,
+    torch.float32,
+    torch.float64,
+    torch.complex64,
+    torch.complex128,
+}
+
+_promotion_table = {
+    # ints
+    (torch.int8, torch.int16): torch.int16,
+    (torch.int8, torch.int32): torch.int32,
+    (torch.int8, torch.int64): torch.int64,
+    (torch.int16, torch.int32): torch.int32,
+    (torch.int16, torch.int64): torch.int64,
+    (torch.int32, torch.int64): torch.int64,
+    # ints and uints (mixed sign)
+    (torch.uint8, torch.int8): torch.int16,
+    (torch.uint8, torch.int16): torch.int16,
+    (torch.uint8, torch.int32): torch.int32,
+    (torch.uint8, torch.int64): torch.int64,
+    # floats
+    (torch.float32, torch.float64): torch.float64,
+    # complexes
+    (torch.complex64, torch.complex128): torch.complex128,
+    # Mixed float and complex
+    (torch.float32, torch.complex64): torch.complex64,
+    (torch.float32, torch.complex128): torch.complex128,
+    (torch.float64, torch.complex64): torch.complex128,
+    (torch.float64, torch.complex128): torch.complex128,
+}
+
+_promotion_table.update({(b, a): c for (a, b), c in _promotion_table.items()})
+_promotion_table.update({(a, a): a for a in _array_api_dtypes})
+
+
+def _two_arg(f):
+    @_wraps(f)
+    def _f(x1, x2, /, **kwargs):
+        x1, x2 = _fix_promotion(x1, x2)
+        return f(x1, x2, **kwargs)
+    if _f.__doc__ is None:
+        _f.__doc__ = f"""\
+Array API compatibility wrapper for torch.{f.__name__}.
+
+See the corresponding PyTorch documentation and/or the array API specification
+for more details.
+
+"""
+    return _f
+
+def _fix_promotion(x1, x2, only_scalar=True):
+    if not isinstance(x1, torch.Tensor) or not isinstance(x2, torch.Tensor):
+        return x1, x2
+    if x1.dtype not in _array_api_dtypes or x2.dtype not in _array_api_dtypes:
+        return x1, x2
+    # If an argument is 0-D pytorch downcasts the other argument
+    if not only_scalar or x1.shape == ():
+        dtype = result_type(x1, x2)
+        x2 = x2.to(dtype)
+    if not only_scalar or x2.shape == ():
+        dtype = result_type(x1, x2)
+        x1 = x1.to(dtype)
+    return x1, x2
+
+
+_py_scalars = (bool, int, float, complex)
+
+
+def result_type(*arrays_and_dtypes: Array | DType | complex) -> DType:
+    num = len(arrays_and_dtypes)
+
+    if num == 0:
+        raise ValueError("At least one array or dtype must be provided")
+
+    elif num == 1:
+        x = arrays_and_dtypes[0]
+        if isinstance(x, torch.dtype):
+            return x
+        return x.dtype
+
+    if num == 2:
+        x, y = arrays_and_dtypes
+        return _result_type(x, y)
+
+    else:
+        # sort scalars so that they are treated last
+        scalars, others = [], []
+        for x in arrays_and_dtypes:
+            if isinstance(x, _py_scalars):
+                scalars.append(x)
+            else:
+                others.append(x)
+        if not others:
+            raise ValueError("At least one array or dtype must be provided")
+
+        # combine left-to-right
+        return _reduce(_result_type, others + scalars)
+
+
+def _result_type(x: Array | DType | complex, y: Array | DType | complex) -> DType:
+    if not (isinstance(x, _py_scalars) or isinstance(y, _py_scalars)):
+        xdt = x if isinstance(x, torch.dtype) else x.dtype
+        ydt = y if isinstance(y, torch.dtype) else y.dtype
+
+        try:
+            return _promotion_table[xdt, ydt]
+        except KeyError:
+            pass
+
+    # This doesn't result_type(dtype, dtype) for non-array API dtypes
+    # because torch.result_type only accepts tensors. This does however, allow
+    # cross-kind promotion.
+    x = torch.tensor([], dtype=x) if isinstance(x, torch.dtype) else x
+    y = torch.tensor([], dtype=y) if isinstance(y, torch.dtype) else y
+    return torch.result_type(x, y)
+
+
+def can_cast(from_: DType | Array, to: DType, /) -> bool:
+    if not isinstance(from_, torch.dtype):
+        from_ = from_.dtype
+    return torch.can_cast(from_, to)
+
+# Basic renames
+bitwise_invert = torch.bitwise_not
+newaxis = None
+# torch.conj sets the conjugation bit, which breaks conversion to other
+# libraries. See https://github.com/data-apis/array-api-compat/issues/173
+conj = torch.conj_physical
+
+# Two-arg elementwise functions
+# These require a wrapper to do the correct type promotion on 0-D tensors
+add = _two_arg(torch.add)
+atan2 = _two_arg(torch.atan2)
+bitwise_and = _two_arg(torch.bitwise_and)
+bitwise_left_shift = _two_arg(torch.bitwise_left_shift)
+bitwise_or = _two_arg(torch.bitwise_or)
+bitwise_right_shift = _two_arg(torch.bitwise_right_shift)
+bitwise_xor = _two_arg(torch.bitwise_xor)
+copysign = _two_arg(torch.copysign)
+divide = _two_arg(torch.divide)
+# Also a rename. torch.equal does not broadcast
+equal = _two_arg(torch.eq)
+floor_divide = _two_arg(torch.floor_divide)
+greater = _two_arg(torch.greater)
+greater_equal = _two_arg(torch.greater_equal)
+hypot = _two_arg(torch.hypot)
+less = _two_arg(torch.less)
+less_equal = _two_arg(torch.less_equal)
+logaddexp = _two_arg(torch.logaddexp)
+# logical functions are not included here because they only accept bool in the
+# spec, so type promotion is irrelevant.
+maximum = _two_arg(torch.maximum)
+minimum = _two_arg(torch.minimum)
+multiply = _two_arg(torch.multiply)
+not_equal = _two_arg(torch.not_equal)
+pow = _two_arg(torch.pow)
+remainder = _two_arg(torch.remainder)
+subtract = _two_arg(torch.subtract)
+
+
+def asarray(
+    obj: Array | complex | NestedSequence[complex] | SupportsBufferProtocol,
+    /,
+    *,
+    dtype: DType | None = None,
+    device: Device | None = None,
+    copy: bool | None = None,
+    **kwargs: Any,
+) -> Array:
+    # torch.asarray does not respect input->output device propagation
+    # https://github.com/pytorch/pytorch/issues/150199
+    if device is None and isinstance(obj, torch.Tensor):
+        device = obj.device
+    return torch.asarray(obj, dtype=dtype, device=device, copy=copy, **kwargs)
+
+
+# These wrappers are mostly based on the fact that pytorch uses 'dim' instead
+# of 'axis'.
+
+# torch.min and torch.max return a tuple and don't support multiple axes https://github.com/pytorch/pytorch/issues/58745
+def max(x: Array, /, *, axis: int | tuple[int, ...] | None = None, keepdims: bool = False) -> Array:
+    # https://github.com/pytorch/pytorch/issues/29137
+    if axis == ():
+        return torch.clone(x)
+    return torch.amax(x, axis, keepdims=keepdims)
+
+def min(x: Array, /, *, axis: int | tuple[int, ...] |None = None, keepdims: bool = False) -> Array:
+    # https://github.com/pytorch/pytorch/issues/29137
+    if axis == ():
+        return torch.clone(x)
+    return torch.amin(x, axis, keepdims=keepdims)
+
+clip = get_xp(torch)(_aliases.clip)
+unstack = get_xp(torch)(_aliases.unstack)
+cumulative_sum = get_xp(torch)(_aliases.cumulative_sum)
+cumulative_prod = get_xp(torch)(_aliases.cumulative_prod)
+finfo = get_xp(torch)(_aliases.finfo)
+iinfo = get_xp(torch)(_aliases.iinfo)
+
+
+# torch.sort also returns a tuple
+# https://github.com/pytorch/pytorch/issues/70921
+def sort(
+    x: Array,
+    /,
+    *,
+    axis: int = -1,
+    descending: bool = False,
+    stable: bool = True,
+    **kwargs: object,
+) -> Array:
+    return torch.sort(x, dim=axis, descending=descending, stable=stable, **kwargs).values
+
+
+# Wrap torch.argsort to set stable=True by default
+def argsort(
+    x: Array,
+    /,
+    *,
+    axis: int = -1,
+    descending: bool = False,
+    stable: bool = True,
+    **kwargs: object,
+) -> Array:
+    return torch.argsort(x, dim=axis, descending=descending, stable=stable, **kwargs)
+
+
+def _normalize_axes(axis, ndim):
+    axes = []
+    if ndim == 0 and axis:
+        # Better error message in this case
+        raise IndexError(f"Dimension out of range: {axis[0]}")
+    lower, upper = -ndim, ndim - 1
+    for a in axis:
+        if a < lower or a > upper:
+            # Match torch error message (e.g., from sum())
+            raise IndexError(f"Dimension out of range (expected to be in range of [{lower}, {upper}], but got {a}")
+        if a < 0:
+            a = a + ndim
+        if a in axes:
+            # Use IndexError instead of RuntimeError, and "axis" instead of "dim"
+            raise IndexError(f"Axis {a} appears multiple times in the list of axes")
+        axes.append(a)
+    return sorted(axes)
+
+def _axis_none_keepdims(x, ndim, keepdims):
+    # Apply keepdims when axis=None
+    # (https://github.com/pytorch/pytorch/issues/71209)
+    # Note that this is only valid for the axis=None case.
+    if keepdims:
+        for i in range(ndim):
+            x = torch.unsqueeze(x, 0)
+    return x
+
+def _reduce_multiple_axes(f, x, axis, keepdims=False, **kwargs):
+    # Some reductions don't support multiple axes
+    # (https://github.com/pytorch/pytorch/issues/56586).
+    axes = _normalize_axes(axis, x.ndim)
+    for a in reversed(axes):
+        x = torch.movedim(x, a, -1)
+    x = torch.flatten(x, -len(axes))
+
+    out = f(x, -1, **kwargs)
+
+    if keepdims:
+        for a in axes:
+            out = torch.unsqueeze(out, a)
+    return out
+
+
+def _sum_prod_no_axis(x: Array, dtype: DType | None) -> Array:
+    """
+    Implements `sum(..., axis=())` and `prod(..., axis=())`.
+    
+    Works around https://github.com/pytorch/pytorch/issues/29137
+    """
+    if dtype is not None:
+        return x.clone() if dtype == x.dtype else x.to(dtype)
+
+    # We can't upcast uint8 according to the spec because there is no
+    # torch.uint64, so at least upcast to int64 which is what prod does
+    # when axis=None.
+    if x.dtype in (torch.uint8, torch.int8, torch.int16, torch.int32):
+        return x.to(torch.int64)
+
+    return x.clone()
+
+
+def prod(x: Array,
+         /,
+         *,
+         axis: int | tuple[int, ...] | None = None,
+         dtype: DType | None = None,
+         keepdims: bool = False,
+         **kwargs: object) -> Array:
+
+    if axis == ():
+        return _sum_prod_no_axis(x, dtype)
+    # torch.prod doesn't support multiple axes
+    # (https://github.com/pytorch/pytorch/issues/56586).
+    if isinstance(axis, tuple):
+        return _reduce_multiple_axes(torch.prod, x, axis, keepdims=keepdims, dtype=dtype, **kwargs)
+    if axis is None:
+        # torch doesn't support keepdims with axis=None
+        # (https://github.com/pytorch/pytorch/issues/71209)
+        res = torch.prod(x, dtype=dtype, **kwargs)
+        res = _axis_none_keepdims(res, x.ndim, keepdims)
+        return res
+
+    return torch.prod(x, axis, dtype=dtype, keepdims=keepdims, **kwargs)
+
+
+def sum(x: Array,
+         /,
+         *,
+         axis: int | tuple[int, ...] | None = None,
+         dtype: DType | None = None,
+         keepdims: bool = False,
+         **kwargs: object) -> Array:
+
+    if axis == ():
+        return _sum_prod_no_axis(x, dtype)
+    if axis is None:
+        # torch doesn't support keepdims with axis=None
+        # (https://github.com/pytorch/pytorch/issues/71209)
+        res = torch.sum(x, dtype=dtype, **kwargs)
+        res = _axis_none_keepdims(res, x.ndim, keepdims)
+        return res
+
+    return torch.sum(x, axis, dtype=dtype, keepdims=keepdims, **kwargs)
+
+def any(x: Array,
+        /,
+        *,
+        axis: int | tuple[int, ...] | None = None,
+        keepdims: bool = False,
+        **kwargs: object) -> Array:
+
+    if axis == ():
+        return x.to(torch.bool)
+    # torch.any doesn't support multiple axes
+    # (https://github.com/pytorch/pytorch/issues/56586).
+    if isinstance(axis, tuple):
+        res = _reduce_multiple_axes(torch.any, x, axis, keepdims=keepdims, **kwargs)
+        return res.to(torch.bool)
+    if axis is None:
+        # torch doesn't support keepdims with axis=None
+        # (https://github.com/pytorch/pytorch/issues/71209)
+        res = torch.any(x, **kwargs)
+        res = _axis_none_keepdims(res, x.ndim, keepdims)
+        return res.to(torch.bool)
+
+    # torch.any doesn't return bool for uint8
+    return torch.any(x, axis, keepdims=keepdims).to(torch.bool)
+
+def all(x: Array,
+        /,
+        *,
+        axis: int | tuple[int, ...] | None = None,
+        keepdims: bool = False,
+        **kwargs: object) -> Array:
+
+    if axis == ():
+        return x.to(torch.bool)
+    # torch.all doesn't support multiple axes
+    # (https://github.com/pytorch/pytorch/issues/56586).
+    if isinstance(axis, tuple):
+        res = _reduce_multiple_axes(torch.all, x, axis, keepdims=keepdims, **kwargs)
+        return res.to(torch.bool)
+    if axis is None:
+        # torch doesn't support keepdims with axis=None
+        # (https://github.com/pytorch/pytorch/issues/71209)
+        res = torch.all(x, **kwargs)
+        res = _axis_none_keepdims(res, x.ndim, keepdims)
+        return res.to(torch.bool)
+
+    # torch.all doesn't return bool for uint8
+    return torch.all(x, axis, keepdims=keepdims).to(torch.bool)
+
+def mean(x: Array,
+         /,
+         *,
+         axis: int | tuple[int, ...] | None = None,
+         keepdims: bool = False,
+         **kwargs: object) -> Array:
+    # https://github.com/pytorch/pytorch/issues/29137
+    if axis == ():
+        return torch.clone(x)
+    if axis is None:
+        # torch doesn't support keepdims with axis=None
+        # (https://github.com/pytorch/pytorch/issues/71209)
+        res = torch.mean(x, **kwargs)
+        res = _axis_none_keepdims(res, x.ndim, keepdims)
+        return res
+    return torch.mean(x, axis, keepdims=keepdims, **kwargs)
+
+def std(x: Array,
+        /,
+        *,
+        axis: int | tuple[int, ...] | None = None,
+        correction: float = 0.0,
+        keepdims: bool = False,
+        **kwargs: object) -> Array:
+    # Note, float correction is not supported
+    # https://github.com/pytorch/pytorch/issues/61492. We don't try to
+    # implement it here for now.
+
+    if isinstance(correction, float):
+        _correction = int(correction)
+        if correction != _correction:
+            raise NotImplementedError("float correction in torch std() is not yet supported")
+    else:
+        _correction = correction
+
+    # https://github.com/pytorch/pytorch/issues/29137
+    if axis == ():
+        return torch.zeros_like(x)
+    if isinstance(axis, int):
+        axis = (axis,)
+    if axis is None:
+        # torch doesn't support keepdims with axis=None
+        # (https://github.com/pytorch/pytorch/issues/71209)
+        res = torch.std(x, tuple(range(x.ndim)), correction=_correction, **kwargs)
+        res = _axis_none_keepdims(res, x.ndim, keepdims)
+        return res
+    return torch.std(x, axis, correction=_correction, keepdims=keepdims, **kwargs)
+
+def var(x: Array,
+        /,
+        *,
+        axis: int | tuple[int, ...] | None = None,
+        correction: float = 0.0,
+        keepdims: bool = False,
+        **kwargs: object) -> Array:
+    # Note, float correction is not supported
+    # https://github.com/pytorch/pytorch/issues/61492. We don't try to
+    # implement it here for now.
+
+    # if isinstance(correction, float):
+    #     correction = int(correction)
+
+    # https://github.com/pytorch/pytorch/issues/29137
+    if axis == ():
+        return torch.zeros_like(x)
+    if isinstance(axis, int):
+        axis = (axis,)
+    if axis is None:
+        # torch doesn't support keepdims with axis=None
+        # (https://github.com/pytorch/pytorch/issues/71209)
+        res = torch.var(x, tuple(range(x.ndim)), correction=correction, **kwargs)
+        res = _axis_none_keepdims(res, x.ndim, keepdims)
+        return res
+    return torch.var(x, axis, correction=correction, keepdims=keepdims, **kwargs)
+
+# torch.concat doesn't support dim=None
+# https://github.com/pytorch/pytorch/issues/70925
+def concat(arrays: tuple[Array, ...] | list[Array],
+           /,
+           *,
+           axis: int | None = 0,
+           **kwargs: object) -> Array:
+    if axis is None:
+        arrays = tuple(ar.flatten() for ar in arrays)
+        axis = 0
+    return torch.concat(arrays, axis, **kwargs)
+
+# torch.squeeze only accepts int dim and doesn't require it
+# https://github.com/pytorch/pytorch/issues/70924. Support for tuple dim was
+# added at https://github.com/pytorch/pytorch/pull/89017.
+def squeeze(x: Array, /, axis: int | tuple[int, ...]) -> Array:
+    if isinstance(axis, int):
+        axis = (axis,)
+    for a in axis:
+        if x.shape[a] != 1:
+            raise ValueError("squeezed dimensions must be equal to 1")
+    axes = _normalize_axes(axis, x.ndim)
+    # Remove this once pytorch 1.14 is released with the above PR #89017.
+    sequence = [a - i for i, a in enumerate(axes)]
+    for a in sequence:
+        x = torch.squeeze(x, a)
+    return x
+
+# torch.broadcast_to uses size instead of shape
+def broadcast_to(x: Array, /, shape: tuple[int, ...], **kwargs: object) -> Array:
+    return torch.broadcast_to(x, shape, **kwargs)
+
+# torch.permute uses dims instead of axes
+def permute_dims(x: Array, /, axes: tuple[int, ...]) -> Array:
+    return torch.permute(x, axes)
+
+# The axis parameter doesn't work for flip() and roll()
+# https://github.com/pytorch/pytorch/issues/71210. Also torch.flip() doesn't
+# accept axis=None
+def flip(x: Array, /, *, axis: int | tuple[int, ...] | None = None, **kwargs: object) -> Array:
+    if axis is None:
+        axis = tuple(range(x.ndim))
+    # torch.flip doesn't accept dim as an int but the method does
+    # https://github.com/pytorch/pytorch/issues/18095
+    return x.flip(axis, **kwargs)
+
+def roll(x: Array, /, shift: int | tuple[int, ...], *, axis: int | tuple[int, ...] | None = None, **kwargs: object) -> Array:
+    return torch.roll(x, shift, axis, **kwargs)
+
+def nonzero(x: Array, /, **kwargs: object) -> tuple[Array, ...]:
+    if x.ndim == 0:
+        raise ValueError("nonzero() does not support zero-dimensional arrays")
+    return torch.nonzero(x, as_tuple=True, **kwargs)
+
+
+# torch uses `dim` instead of `axis`
+def diff(
+    x: Array,
+    /,
+    *,
+    axis: int = -1,
+    n: int = 1,
+    prepend: Array | None = None,
+    append: Array | None = None,
+) -> Array:
+    return torch.diff(x, dim=axis, n=n, prepend=prepend, append=append)
+
+
+# torch uses `dim` instead of `axis`, does not have keepdims
+def count_nonzero(
+    x: Array,
+    /,
+    *,
+    axis: int | tuple[int, ...] | None = None,
+    keepdims: bool = False,
+) -> Array:
+    result = torch.count_nonzero(x, dim=axis)
+    if keepdims:
+        if isinstance(axis, int):
+            return result.unsqueeze(axis)
+        elif isinstance(axis, tuple):
+            n_axis = [x.ndim + ax if ax < 0 else ax for ax in axis]
+            sh = [1 if i in n_axis else x.shape[i] for i in range(x.ndim)]
+            return torch.reshape(result, sh)
+        return _axis_none_keepdims(result, x.ndim, keepdims)
+    else:
+        return result
+
+
+# "repeat" is torch.repeat_interleave;  also the dim argument
+def repeat(x: Array, repeats: int | Array, /, *, axis: int | None = None) -> Array:
+    return torch.repeat_interleave(x, repeats, axis)
+
+
+def where(condition: Array, x1: Array | complex, x2: Array | complex, /) -> Array:
+    x1, x2 = _fix_promotion(x1, x2)
+    return torch.where(condition, x1, x2)
+
+
+# torch.reshape doesn't have the copy keyword
+def reshape(x: Array,
+            /,
+            shape: tuple[int, ...],
+            *,
+            copy: bool | None = None,
+            **kwargs: object) -> Array:
+    if copy is not None:
+        raise NotImplementedError("torch.reshape doesn't yet support the copy keyword")
+    return torch.reshape(x, shape, **kwargs)
+
+# torch.arange doesn't support returning empty arrays
+# (https://github.com/pytorch/pytorch/issues/70915), and doesn't support some
+# keyword argument combinations
+# (https://github.com/pytorch/pytorch/issues/70914)
+def arange(start: float,
+           /,
+           stop: float | None = None,
+           step: float = 1,
+           *,
+           dtype: DType | None = None,
+           device: Device | None = None,
+           **kwargs: object) -> Array:
+    if stop is None:
+        start, stop = 0, start
+    if step > 0 and stop <= start or step < 0 and stop >= start:
+        if dtype is None:
+            if _builtin_all(isinstance(i, int) for i in [start, stop, step]):
+                dtype = torch.int64
+            else:
+                dtype = torch.float32
+        return torch.empty(0, dtype=dtype, device=device, **kwargs)
+    return torch.arange(start, stop, step, dtype=dtype, device=device, **kwargs)
+
+# torch.eye does not accept None as a default for the second argument and
+# doesn't support off-diagonals (https://github.com/pytorch/pytorch/issues/70910)
+def eye(n_rows: int,
+        n_cols: int | None = None,
+        /,
+        *,
+        k: int = 0,
+        dtype: DType | None = None,
+        device: Device | None = None,
+        **kwargs: object) -> Array:
+    if n_cols is None:
+        n_cols = n_rows
+    z = torch.zeros(n_rows, n_cols, dtype=dtype, device=device, **kwargs)
+    if abs(k) <= n_rows + n_cols:
+        z.diagonal(k).fill_(1)
+    return z
+
+# torch.linspace doesn't have the endpoint parameter
+def linspace(start: float,
+             stop: float,
+             /,
+             num: int,
+             *,
+             dtype: DType | None = None,
+             device: Device | None = None,
+             endpoint: bool = True,
+             **kwargs: object) -> Array:
+    if not endpoint:
+        return torch.linspace(start, stop, num+1, dtype=dtype, device=device, **kwargs)[:-1]
+    return torch.linspace(start, stop, num, dtype=dtype, device=device, **kwargs)
+
+# torch.full does not accept an int size
+# https://github.com/pytorch/pytorch/issues/70906
+def full(shape: int | tuple[int, ...],
+         fill_value: complex,
+         *,
+         dtype: DType | None = None,
+         device: Device | None = None,
+         **kwargs: object) -> Array:
+    if isinstance(shape, int):
+        shape = (shape,)
+
+    return torch.full(shape, fill_value, dtype=dtype, device=device, **kwargs)
+
+# ones, zeros, and empty do not accept shape as a keyword argument
+def ones(shape: int | tuple[int, ...],
+         *,
+         dtype: DType | None = None,
+         device: Device | None = None,
+         **kwargs: object) -> Array:
+    return torch.ones(shape, dtype=dtype, device=device, **kwargs)
+
+def zeros(shape: int | tuple[int, ...],
+         *,
+         dtype: DType | None = None,
+         device: Device | None = None,
+         **kwargs: object) -> Array:
+    return torch.zeros(shape, dtype=dtype, device=device, **kwargs)
+
+def empty(shape: int | tuple[int, ...],
+         *,
+         dtype: DType | None = None,
+         device: Device | None = None,
+         **kwargs: object) -> Array:
+    return torch.empty(shape, dtype=dtype, device=device, **kwargs)
+
+# tril and triu do not call the keyword argument k
+
+def tril(x: Array, /, *, k: int = 0) -> Array:
+    return torch.tril(x, k)
+
+def triu(x: Array, /, *, k: int = 0) -> Array:
+    return torch.triu(x, k)
+
+# Functions that aren't in torch https://github.com/pytorch/pytorch/issues/58742
+def expand_dims(x: Array, /, *, axis: int = 0) -> Array:
+    return torch.unsqueeze(x, axis)
+
+
+def astype(
+    x: Array,
+    dtype: DType,
+    /,
+    *,
+    copy: bool = True,
+    device: Device | None = None,
+) -> Array:
+    if device is not None:
+        return x.to(device, dtype=dtype, copy=copy)
+    return x.to(dtype=dtype, copy=copy)
+
+
+def broadcast_arrays(*arrays: Array) -> list[Array]:
+    shape = torch.broadcast_shapes(*[a.shape for a in arrays])
+    return [torch.broadcast_to(a, shape) for a in arrays]
+
+# Note that these named tuples aren't actually part of the standard namespace,
+# but I don't see any issue with exporting the names here regardless.
+from ..common._aliases import (UniqueAllResult, UniqueCountsResult,
+                               UniqueInverseResult)
+
+# https://github.com/pytorch/pytorch/issues/70920
+def unique_all(x: Array) -> UniqueAllResult:
+    # torch.unique doesn't support returning indices.
+    # https://github.com/pytorch/pytorch/issues/36748. The workaround
+    # suggested in that issue doesn't actually function correctly (it relies
+    # on non-deterministic behavior of scatter()).
+    raise NotImplementedError("unique_all() not yet implemented for pytorch (see https://github.com/pytorch/pytorch/issues/36748)")
+
+    # values, inverse_indices, counts = torch.unique(x, return_counts=True, return_inverse=True)
+    # # torch.unique incorrectly gives a 0 count for nan values.
+    # # https://github.com/pytorch/pytorch/issues/94106
+    # counts[torch.isnan(values)] = 1
+    # return UniqueAllResult(values, indices, inverse_indices, counts)
+
+def unique_counts(x: Array) -> UniqueCountsResult:
+    values, counts = torch.unique(x, return_counts=True)
+
+    # torch.unique incorrectly gives a 0 count for nan values.
+    # https://github.com/pytorch/pytorch/issues/94106
+    counts[torch.isnan(values)] = 1
+    return UniqueCountsResult(values, counts)
+
+def unique_inverse(x: Array) -> UniqueInverseResult:
+    values, inverse = torch.unique(x, return_inverse=True)
+    return UniqueInverseResult(values, inverse)
+
+def unique_values(x: Array) -> Array:
+    return torch.unique(x)
+
+def matmul(x1: Array, x2: Array, /, **kwargs: object) -> Array:
+    # torch.matmul doesn't type promote (but differently from _fix_promotion)
+    x1, x2 = _fix_promotion(x1, x2, only_scalar=False)
+    return torch.matmul(x1, x2, **kwargs)
+
+matrix_transpose = get_xp(torch)(_aliases.matrix_transpose)
+_vecdot = get_xp(torch)(_aliases.vecdot)
+
+def vecdot(x1: Array, x2: Array, /, *, axis: int = -1) -> Array:
+    x1, x2 = _fix_promotion(x1, x2, only_scalar=False)
+    return _vecdot(x1, x2, axis=axis)
+
+# torch.tensordot uses dims instead of axes
+def tensordot(
+    x1: Array,
+    x2: Array,
+    /,
+    *, 
+    axes: int | tuple[Sequence[int], Sequence[int]] = 2, 
+    **kwargs: object,
+) -> Array:
+    # Note: torch.tensordot fails with integer dtypes when there is only 1
+    # element in the axis (https://github.com/pytorch/pytorch/issues/84530).
+    x1, x2 = _fix_promotion(x1, x2, only_scalar=False)
+    return torch.tensordot(x1, x2, dims=axes, **kwargs)
+
+
+def isdtype(
+    dtype: DType, 
+    kind: DType | str | tuple[DType | str, ...],
+    *,
+    _tuple: bool = True, # Disallow nested tuples
+) -> bool:
+    """
+    Returns a boolean indicating whether a provided dtype is of a specified data type ``kind``.
+
+    Note that outside of this function, this compat library does not yet fully
+    support complex numbers.
+
+    See
+    https://data-apis.org/array-api/latest/API_specification/generated/array_api.isdtype.html
+    for more details
+    """
+    if isinstance(kind, tuple) and _tuple:
+        return _builtin_any(isdtype(dtype, k, _tuple=False) for k in kind)
+    elif isinstance(kind, str):
+        if kind == 'bool':
+            return dtype == torch.bool
+        elif kind == 'signed integer':
+            return dtype in _int_dtypes and dtype.is_signed
+        elif kind == 'unsigned integer':
+            return dtype in _int_dtypes and not dtype.is_signed
+        elif kind == 'integral':
+            return dtype in _int_dtypes
+        elif kind == 'real floating':
+            return dtype.is_floating_point
+        elif kind == 'complex floating':
+            return dtype.is_complex
+        elif kind == 'numeric':
+            return isdtype(dtype, ('integral', 'real floating', 'complex floating'))
+        else:
+            raise ValueError(f"Unrecognized data type kind: {kind!r}")
+    else:
+        return dtype == kind
+
+def take(x: Array, indices: Array, /, *, axis: int | None = None, **kwargs: object) -> Array:
+    if axis is None:
+        if x.ndim != 1:
+            raise ValueError("axis must be specified when ndim > 1")
+        axis = 0
+    # torch does not support negative indices,
+    # see https://github.com/pytorch/pytorch/issues/146211
+    return torch.index_select(
+        x,
+        axis,
+        torch.where(indices < 0, indices + x.shape[axis], indices),
+        **kwargs
+    )
+
+
+def take_along_axis(x: Array, indices: Array, /, *, axis: int = -1) -> Array:
+    # torch does not support negative indices,
+    # see https://github.com/pytorch/pytorch/issues/146211
+    return torch.take_along_dim(
+        x,
+        torch.where(indices < 0, indices + x.shape[axis], indices),
+        dim=axis
+    )
+
+
+def sign(x: Array, /) -> Array:
+    # torch sign() does not support complex numbers and does not propagate
+    # nans. See https://github.com/data-apis/array-api-compat/issues/136
+    if x.dtype.is_complex:
+        out = x/torch.abs(x)
+        # sign(0) = 0 but the above formula would give nan
+        out[x == 0+0j] = 0+0j
+        return out
+    else:
+        out = torch.sign(x)
+        if x.dtype.is_floating_point:
+            out[torch.isnan(x)] = torch.nan
+        return out
+
+
+def meshgrid(*arrays: Array, indexing: Literal['xy', 'ij'] = 'xy') -> list[Array]:
+    # enforce the default of 'xy'
+    # TODO: is the return type a list or a tuple
+    return list(torch.meshgrid(*arrays, indexing=indexing))
+
+
+__all__ = ['asarray', 'result_type', 'can_cast',
+           'permute_dims', 'bitwise_invert', 'newaxis', 'conj', 'add',
+           'atan2', 'bitwise_and', 'bitwise_left_shift', 'bitwise_or',
+           'bitwise_right_shift', 'bitwise_xor', 'copysign', 'count_nonzero',
+           'diff', 'divide',
+           'equal', 'floor_divide', 'greater', 'greater_equal', 'hypot',
+           'less', 'less_equal', 'logaddexp', 'maximum', 'minimum',
+           'multiply', 'not_equal', 'pow', 'remainder', 'subtract', 'max',
+           'min', 'clip', 'unstack', 'cumulative_sum', 'cumulative_prod', 'sort',
+           'argsort', 'prod', 'sum', 'any', 'all', 'mean', 'std', 'var', 'concat',
+           'squeeze', 'broadcast_to', 'flip', 'roll', 'nonzero', 'where', 'reshape',
+           'arange', 'eye', 'linspace', 'full', 'ones', 'zeros', 'empty',
+           'tril', 'triu', 'expand_dims', 'astype', 'broadcast_arrays',
+           'UniqueAllResult', 'UniqueCountsResult', 'UniqueInverseResult',
+           'unique_all', 'unique_counts', 'unique_inverse', 'unique_values',
+           'matmul', 'matrix_transpose', 'vecdot', 'tensordot', 'isdtype',
+           'take', 'take_along_axis', 'sign', 'finfo', 'iinfo', 'repeat', 'meshgrid']
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/torch/_info.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/torch/_info.py
new file mode 100644
index 0000000000000000000000000000000000000000..818e5d3702e38281a607e1d4462a0819c8594a1e
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/torch/_info.py
@@ -0,0 +1,369 @@
+"""
+Array API Inspection namespace
+
+This is the namespace for inspection functions as defined by the array API
+standard. See
+https://data-apis.org/array-api/latest/API_specification/inspection.html for
+more details.
+
+"""
+import torch
+
+from functools import cache
+
+class __array_namespace_info__:
+    """
+    Get the array API inspection namespace for PyTorch.
+
+    The array API inspection namespace defines the following functions:
+
+    - capabilities()
+    - default_device()
+    - default_dtypes()
+    - dtypes()
+    - devices()
+
+    See
+    https://data-apis.org/array-api/latest/API_specification/inspection.html
+    for more details.
+
+    Returns
+    -------
+    info : ModuleType
+        The array API inspection namespace for PyTorch.
+
+    Examples
+    --------
+    >>> info = xp.__array_namespace_info__()
+    >>> info.default_dtypes()
+    {'real floating': numpy.float64,
+     'complex floating': numpy.complex128,
+     'integral': numpy.int64,
+     'indexing': numpy.int64}
+
+    """
+
+    __module__ = 'torch'
+
+    def capabilities(self):
+        """
+        Return a dictionary of array API library capabilities.
+
+        The resulting dictionary has the following keys:
+
+        - **"boolean indexing"**: boolean indicating whether an array library
+          supports boolean indexing. Always ``True`` for PyTorch.
+
+        - **"data-dependent shapes"**: boolean indicating whether an array
+          library supports data-dependent output shapes. Always ``True`` for
+          PyTorch.
+
+        See
+        https://data-apis.org/array-api/latest/API_specification/generated/array_api.info.capabilities.html
+        for more details.
+
+        See Also
+        --------
+        __array_namespace_info__.default_device,
+        __array_namespace_info__.default_dtypes,
+        __array_namespace_info__.dtypes,
+        __array_namespace_info__.devices
+
+        Returns
+        -------
+        capabilities : dict
+            A dictionary of array API library capabilities.
+
+        Examples
+        --------
+        >>> info = xp.__array_namespace_info__()
+        >>> info.capabilities()
+        {'boolean indexing': True,
+         'data-dependent shapes': True,
+         'max dimensions': 64}
+
+        """
+        return {
+            "boolean indexing": True,
+            "data-dependent shapes": True,
+            "max dimensions": 64,
+        }
+
+    def default_device(self):
+        """
+        The default device used for new PyTorch arrays.
+
+        See Also
+        --------
+        __array_namespace_info__.capabilities,
+        __array_namespace_info__.default_dtypes,
+        __array_namespace_info__.dtypes,
+        __array_namespace_info__.devices
+
+        Returns
+        -------
+        device : Device
+            The default device used for new PyTorch arrays.
+
+        Examples
+        --------
+        >>> info = xp.__array_namespace_info__()
+        >>> info.default_device()
+        device(type='cpu')
+
+        Notes
+        -----
+        This method returns the static default device when PyTorch is initialized.
+        However, the *current* device used by creation functions (``empty`` etc.)
+        can be changed at runtime.
+
+        See Also
+        --------
+        https://github.com/data-apis/array-api/issues/835
+        """
+        return torch.device("cpu")
+
+    def default_dtypes(self, *, device=None):
+        """
+        The default data types used for new PyTorch arrays.
+
+        Parameters
+        ----------
+        device : Device, optional
+            The device to get the default data types for.
+            Unused for PyTorch, as all devices use the same default dtypes.
+
+        Returns
+        -------
+        dtypes : dict
+            A dictionary describing the default data types used for new PyTorch
+            arrays.
+
+        See Also
+        --------
+        __array_namespace_info__.capabilities,
+        __array_namespace_info__.default_device,
+        __array_namespace_info__.dtypes,
+        __array_namespace_info__.devices
+
+        Examples
+        --------
+        >>> info = xp.__array_namespace_info__()
+        >>> info.default_dtypes()
+        {'real floating': torch.float32,
+         'complex floating': torch.complex64,
+         'integral': torch.int64,
+         'indexing': torch.int64}
+
+        """
+        # Note: if the default is set to float64, the devices like MPS that
+        # don't support float64 will error. We still return the default_dtype
+        # value here because this error doesn't represent a different default
+        # per-device.
+        default_floating = torch.get_default_dtype()
+        default_complex = torch.complex64 if default_floating == torch.float32 else torch.complex128
+        default_integral = torch.int64
+        return {
+            "real floating": default_floating,
+            "complex floating": default_complex,
+            "integral": default_integral,
+            "indexing": default_integral,
+        }
+
+
+    def _dtypes(self, kind):
+        bool = torch.bool
+        int8 = torch.int8
+        int16 = torch.int16
+        int32 = torch.int32
+        int64 = torch.int64
+        uint8 = torch.uint8
+        # uint16, uint32, and uint64 are present in newer versions of pytorch,
+        # but they aren't generally supported by the array API functions, so
+        # we omit them from this function.
+        float32 = torch.float32
+        float64 = torch.float64
+        complex64 = torch.complex64
+        complex128 = torch.complex128
+
+        if kind is None:
+            return {
+                "bool": bool,
+                "int8": int8,
+                "int16": int16,
+                "int32": int32,
+                "int64": int64,
+                "uint8": uint8,
+                "float32": float32,
+                "float64": float64,
+                "complex64": complex64,
+                "complex128": complex128,
+            }
+        if kind == "bool":
+            return {"bool": bool}
+        if kind == "signed integer":
+            return {
+                "int8": int8,
+                "int16": int16,
+                "int32": int32,
+                "int64": int64,
+            }
+        if kind == "unsigned integer":
+            return {
+                "uint8": uint8,
+            }
+        if kind == "integral":
+            return {
+                "int8": int8,
+                "int16": int16,
+                "int32": int32,
+                "int64": int64,
+                "uint8": uint8,
+            }
+        if kind == "real floating":
+            return {
+                "float32": float32,
+                "float64": float64,
+            }
+        if kind == "complex floating":
+            return {
+                "complex64": complex64,
+                "complex128": complex128,
+            }
+        if kind == "numeric":
+            return {
+                "int8": int8,
+                "int16": int16,
+                "int32": int32,
+                "int64": int64,
+                "uint8": uint8,
+                "float32": float32,
+                "float64": float64,
+                "complex64": complex64,
+                "complex128": complex128,
+            }
+        if isinstance(kind, tuple):
+            res = {}
+            for k in kind:
+                res.update(self.dtypes(kind=k))
+            return res
+        raise ValueError(f"unsupported kind: {kind!r}")
+
+    @cache
+    def dtypes(self, *, device=None, kind=None):
+        """
+        The array API data types supported by PyTorch.
+
+        Note that this function only returns data types that are defined by
+        the array API.
+
+        Parameters
+        ----------
+        device : Device, optional
+            The device to get the data types for.
+            Unused for PyTorch, as all devices use the same dtypes.
+        kind : str or tuple of str, optional
+            The kind of data types to return. If ``None``, all data types are
+            returned. If a string, only data types of that kind are returned.
+            If a tuple, a dictionary containing the union of the given kinds
+            is returned. The following kinds are supported:
+
+            - ``'bool'``: boolean data types (i.e., ``bool``).
+            - ``'signed integer'``: signed integer data types (i.e., ``int8``,
+              ``int16``, ``int32``, ``int64``).
+            - ``'unsigned integer'``: unsigned integer data types (i.e.,
+              ``uint8``, ``uint16``, ``uint32``, ``uint64``).
+            - ``'integral'``: integer data types. Shorthand for ``('signed
+              integer', 'unsigned integer')``.
+            - ``'real floating'``: real-valued floating-point data types
+              (i.e., ``float32``, ``float64``).
+            - ``'complex floating'``: complex floating-point data types (i.e.,
+              ``complex64``, ``complex128``).
+            - ``'numeric'``: numeric data types. Shorthand for ``('integral',
+              'real floating', 'complex floating')``.
+
+        Returns
+        -------
+        dtypes : dict
+            A dictionary mapping the names of data types to the corresponding
+            PyTorch data types.
+
+        See Also
+        --------
+        __array_namespace_info__.capabilities,
+        __array_namespace_info__.default_device,
+        __array_namespace_info__.default_dtypes,
+        __array_namespace_info__.devices
+
+        Examples
+        --------
+        >>> info = xp.__array_namespace_info__()
+        >>> info.dtypes(kind='signed integer')
+        {'int8': numpy.int8,
+         'int16': numpy.int16,
+         'int32': numpy.int32,
+         'int64': numpy.int64}
+
+        """
+        res = self._dtypes(kind)
+        for k, v in res.copy().items():
+            try:
+                torch.empty((0,), dtype=v, device=device)
+            except:
+                del res[k]
+        return res
+
+    @cache
+    def devices(self):
+        """
+        The devices supported by PyTorch.
+
+        Returns
+        -------
+        devices : list[Device]
+            The devices supported by PyTorch.
+
+        See Also
+        --------
+        __array_namespace_info__.capabilities,
+        __array_namespace_info__.default_device,
+        __array_namespace_info__.default_dtypes,
+        __array_namespace_info__.dtypes
+
+        Examples
+        --------
+        >>> info = xp.__array_namespace_info__()
+        >>> info.devices()
+        [device(type='cpu'), device(type='mps', index=0), device(type='meta')]
+
+        """
+        # Torch doesn't have a straightforward way to get the list of all
+        # currently supported devices. To do this, we first parse the error
+        # message of torch.device to get the list of all possible types of
+        # device:
+        try:
+            torch.device('notadevice')
+            raise AssertionError("unreachable")  # pragma: nocover
+        except RuntimeError as e:
+            # The error message is something like:
+            # "Expected one of cpu, cuda, ipu, xpu, mkldnn, opengl, opencl, ideep, hip, ve, fpga, ort, xla, lazy, vulkan, mps, meta, hpu, mtia, privateuseone device type at start of device string: notadevice"
+            devices_names = e.args[0].split('Expected one of ')[1].split(' device type')[0].split(', ')
+
+        # Next we need to check for different indices for different devices.
+        # device(device_name, index=index) doesn't actually check if the
+        # device name or index is valid. We have to try to create a tensor
+        # with it (which is why this function is cached).
+        devices = []
+        for device_name in devices_names:
+            i = 0
+            while True:
+                try:
+                    a = torch.empty((0,), device=torch.device(device_name, index=i))
+                    if a.device in devices:
+                        break
+                    devices.append(a.device)
+                except:
+                    break
+                i += 1
+
+        return devices
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/torch/_typing.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/torch/_typing.py
new file mode 100644
index 0000000000000000000000000000000000000000..52670871563710d326b0a114a7a517bf87098bf0
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/torch/_typing.py
@@ -0,0 +1,3 @@
+__all__ = ["Array", "Device", "DType"]
+
+from torch import device as Device, dtype as DType, Tensor as Array
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/torch/fft.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/torch/fft.py
new file mode 100644
index 0000000000000000000000000000000000000000..f11b3eb5975639150c6add018c7ac36e1205b9e8
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/torch/fft.py
@@ -0,0 +1,82 @@
+from __future__ import annotations
+
+from collections.abc import Sequence
+from typing import Literal
+
+import torch  # noqa: F401
+import torch.fft
+
+from ._typing import Array
+from .._internal import clone_module
+
+__all__ = clone_module("torch.fft", globals())
+
+# Several torch fft functions do not map axes to dim
+
+def fftn(
+    x: Array,
+    /,
+    *,
+    s: Sequence[int] = None,
+    axes: Sequence[int] = None,
+    norm: Literal["backward", "ortho", "forward"] = "backward",
+    **kwargs: object,
+) -> Array:
+    return torch.fft.fftn(x, s=s, dim=axes, norm=norm, **kwargs)
+
+def ifftn(
+    x: Array,
+    /,
+    *,
+    s: Sequence[int] = None,
+    axes: Sequence[int] = None,
+    norm: Literal["backward", "ortho", "forward"] = "backward",
+    **kwargs: object,
+) -> Array:
+    return torch.fft.ifftn(x, s=s, dim=axes, norm=norm, **kwargs)
+
+def rfftn(
+    x: Array,
+    /,
+    *,
+    s: Sequence[int] = None,
+    axes: Sequence[int] = None,
+    norm: Literal["backward", "ortho", "forward"] = "backward",
+    **kwargs: object,
+) -> Array:
+    return torch.fft.rfftn(x, s=s, dim=axes, norm=norm, **kwargs)
+
+def irfftn(
+    x: Array,
+    /,
+    *,
+    s: Sequence[int] = None,
+    axes: Sequence[int] = None,
+    norm: Literal["backward", "ortho", "forward"] = "backward",
+    **kwargs: object,
+) -> Array:
+    return torch.fft.irfftn(x, s=s, dim=axes, norm=norm, **kwargs)
+
+def fftshift(
+    x: Array,
+    /,
+    *,
+    axes: int | Sequence[int] = None,
+    **kwargs: object,
+) -> Array:
+    return torch.fft.fftshift(x, dim=axes, **kwargs)
+
+def ifftshift(
+    x: Array,
+    /,
+    *,
+    axes: int | Sequence[int] = None,
+    **kwargs: object,
+) -> Array:
+    return torch.fft.ifftshift(x, dim=axes, **kwargs)
+
+
+__all__ += ["fftn", "ifftn", "rfftn", "irfftn", "fftshift", "ifftshift"]
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/torch/linalg.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/torch/linalg.py
new file mode 100644
index 0000000000000000000000000000000000000000..08271d226734b64212a8cd687617f9ecb3197b41
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_compat/torch/linalg.py
@@ -0,0 +1,114 @@
+from __future__ import annotations
+
+import torch
+import torch.linalg
+
+from .._internal import clone_module
+
+__all__ = clone_module("torch.linalg", globals())
+
+# outer is implemented in torch but aren't in the linalg namespace
+from torch import outer
+from ._aliases import _fix_promotion, sum
+# These functions are in both the main and linalg namespaces
+from ._aliases import matmul, matrix_transpose, tensordot
+from ._typing import Array, DType
+from ..common._typing import JustInt, JustFloat
+
+# Note: torch.linalg.cross does not default to axis=-1 (it defaults to the
+# first axis with size 3), see https://github.com/pytorch/pytorch/issues/58743
+
+# torch.cross also does not support broadcasting when it would add new
+# dimensions https://github.com/pytorch/pytorch/issues/39656
+def cross(x1: Array, x2: Array, /, *, axis: int = -1) -> Array:
+    x1, x2 = _fix_promotion(x1, x2, only_scalar=False)
+    if not (-min(x1.ndim, x2.ndim) <= axis < max(x1.ndim, x2.ndim)):
+        raise ValueError(f"axis {axis} out of bounds for cross product of arrays with shapes {x1.shape} and {x2.shape}")
+    if not (x1.shape[axis] == x2.shape[axis] == 3):
+        raise ValueError(f"cross product axis must have size 3, got {x1.shape[axis]} and {x2.shape[axis]}")
+    x1, x2 = torch.broadcast_tensors(x1, x2)
+    return torch.linalg.cross(x1, x2, dim=axis)
+
+def vecdot(x1: Array, x2: Array, /, *, axis: int = -1, **kwargs: object) -> Array:
+    from ._aliases import isdtype
+
+    x1, x2 = _fix_promotion(x1, x2, only_scalar=False)
+
+    # torch.linalg.vecdot incorrectly allows broadcasting along the contracted dimension
+    if x1.shape[axis] != x2.shape[axis]:
+        raise ValueError("x1 and x2 must have the same size along the given axis")
+
+    # torch.linalg.vecdot doesn't support integer dtypes
+    if isdtype(x1.dtype, 'integral') or isdtype(x2.dtype, 'integral'):
+        if kwargs:
+            raise RuntimeError("vecdot kwargs not supported for integral dtypes")
+
+        x1_ = torch.moveaxis(x1, axis, -1)
+        x2_ = torch.moveaxis(x2, axis, -1)
+        x1_, x2_ = torch.broadcast_tensors(x1_, x2_)
+
+        res = x1_[..., None, :] @ x2_[..., None]
+        return res[..., 0, 0]
+    return torch.linalg.vecdot(x1, x2, dim=axis, **kwargs)
+
+def solve(x1: Array, x2: Array, /, **kwargs: object) -> Array:
+    x1, x2 = _fix_promotion(x1, x2, only_scalar=False)
+    # Torch tries to emulate NumPy 1 solve behavior by using batched 1-D solve
+    # whenever
+    # 1. x1.ndim - 1 == x2.ndim
+    # 2. x1.shape[:-1] == x2.shape
+    #
+    # See linalg_solve_is_vector_rhs in
+    # aten/src/ATen/native/LinearAlgebraUtils.h and
+    # TORCH_META_FUNC(_linalg_solve_ex) in
+    # aten/src/ATen/native/BatchLinearAlgebra.cpp in the PyTorch source code.
+    #
+    # The easiest way to work around this is to prepend a size 1 dimension to
+    # x2, since x2 is already one dimension less than x1.
+    #
+    # See https://github.com/pytorch/pytorch/issues/52915
+    if x2.ndim != 1 and x1.ndim - 1 == x2.ndim and x1.shape[:-1] == x2.shape:
+        x2 = x2[None]
+    return torch.linalg.solve(x1, x2, **kwargs)
+
+# torch.trace doesn't support the offset argument and doesn't support stacking
+def trace(x: Array, /, *, offset: int = 0, dtype: DType | None = None) -> Array:
+    # Use our wrapped sum to make sure it does upcasting correctly
+    return sum(torch.diagonal(x, offset=offset, dim1=-2, dim2=-1), axis=-1, dtype=dtype)
+
+def vector_norm(
+    x: Array,
+    /,
+    *,
+    axis: int | tuple[int, ...] | None = None,
+    keepdims: bool = False,
+    # JustFloat stands for inf | -inf, which are not valid for Literal
+    ord: JustInt | JustFloat = 2,
+    **kwargs: object,
+) -> Array:
+    # torch.vector_norm incorrectly treats axis=() the same as axis=None
+    if axis == ():
+        out = kwargs.get('out')
+        if out is None:
+            dtype = None
+            if x.dtype == torch.complex64:
+                dtype = torch.float32
+            elif x.dtype == torch.complex128:
+                dtype = torch.float64
+
+            out = torch.zeros_like(x, dtype=dtype)
+
+        # The norm of a single scalar works out to abs(x) in every case except
+        # for ord=0, which is x != 0.
+        if ord == 0:
+            out[:] = (x != 0)
+        else:
+            out[:] = torch.abs(x)
+        return out
+    return torch.linalg.vector_norm(x, ord=ord, axis=axis, keepdim=keepdims, **kwargs)
+
+__all__ += ['outer', 'matmul', 'matrix_transpose', 'tensordot',
+            'cross', 'vecdot', 'solve', 'trace', 'vector_norm']
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/__init__.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..649e304b23d86a85f294108366a312ff544fb4e1
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/__init__.py
@@ -0,0 +1,53 @@
+"""Extra array functions built on top of the array API standard."""
+
+from ._delegation import (
+    argpartition,
+    atleast_nd,
+    cov,
+    create_diagonal,
+    expand_dims,
+    isclose,
+    isin,
+    nan_to_num,
+    one_hot,
+    pad,
+    partition,
+    setdiff1d,
+    sinc,
+)
+from ._lib._at import at
+from ._lib._funcs import (
+    apply_where,
+    broadcast_shapes,
+    default_dtype,
+    kron,
+    nunique,
+)
+from ._lib._lazy import lazy_apply
+
+__version__ = "0.9.1"
+
+# pylint: disable=duplicate-code
+__all__ = [
+    "__version__",
+    "apply_where",
+    "argpartition",
+    "at",
+    "atleast_nd",
+    "broadcast_shapes",
+    "cov",
+    "create_diagonal",
+    "default_dtype",
+    "expand_dims",
+    "isclose",
+    "isin",
+    "kron",
+    "lazy_apply",
+    "nan_to_num",
+    "nunique",
+    "one_hot",
+    "pad",
+    "partition",
+    "setdiff1d",
+    "sinc",
+]
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/__pycache__/__init__.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..8e6b727c037e6931da06f48e7d1ff5417731cf87
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/__pycache__/__init__.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/__pycache__/_delegation.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/__pycache__/_delegation.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..002fce2a9a059b16cfda924616592a01be70d8da
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/__pycache__/_delegation.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/__pycache__/testing.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/__pycache__/testing.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..2ab0166bbece161e1a5cb4385b32c6d302900232
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/__pycache__/testing.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_delegation.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_delegation.py
new file mode 100644
index 0000000000000000000000000000000000000000..cb7f21cbbf91ef7ea58017928286d692a530a61c
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_delegation.py
@@ -0,0 +1,1028 @@
+"""Delegation to existing implementations for Public API Functions."""
+
+from collections.abc import Sequence
+from types import ModuleType
+from typing import Literal
+
+from ._lib import _funcs
+from ._lib._utils._compat import (
+    array_namespace,
+    is_cupy_namespace,
+    is_dask_namespace,
+    is_jax_namespace,
+    is_numpy_namespace,
+    is_pydata_sparse_namespace,
+    is_torch_namespace,
+)
+from ._lib._utils._compat import device as get_device
+from ._lib._utils._helpers import asarrays, eager_shape
+from ._lib._utils._typing import Array, DType
+
+__all__ = [
+    "atleast_nd",
+    "cov",
+    "create_diagonal",
+    "expand_dims",
+    "isclose",
+    "nan_to_num",
+    "one_hot",
+    "pad",
+    "sinc",
+]
+
+
+def atleast_nd(x: Array, /, *, ndim: int, xp: ModuleType | None = None) -> Array:
+    """
+    Recursively expand the dimension of an array to at least `ndim`.
+
+    Parameters
+    ----------
+    x : array
+        Input array.
+    ndim : int
+        The minimum number of dimensions for the result.
+    xp : array_namespace, optional
+        The standard-compatible namespace for `x`. Default: infer.
+
+    Returns
+    -------
+    array
+        An array with ``res.ndim`` >= `ndim`.
+        If ``x.ndim`` >= `ndim`, `x` is returned.
+        If ``x.ndim`` < `ndim`, `x` is expanded by prepending new axes
+        until ``res.ndim`` equals `ndim`.
+
+    Examples
+    --------
+    >>> import array_api_strict as xp
+    >>> import array_api_extra as xpx
+    >>> x = xp.asarray([1])
+    >>> xpx.atleast_nd(x, ndim=3, xp=xp)
+    Array([[[1]]], dtype=array_api_strict.int64)
+
+    >>> x = xp.asarray([[[1, 2],
+    ...                  [3, 4]]])
+    >>> xpx.atleast_nd(x, ndim=1, xp=xp) is x
+    True
+    """
+    if xp is None:
+        xp = array_namespace(x)
+
+    if 1 <= ndim <= 2 and (
+        is_numpy_namespace(xp)
+        or is_jax_namespace(xp)
+        or is_dask_namespace(xp)
+        or is_cupy_namespace(xp)
+        or is_torch_namespace(xp)
+    ):
+        return getattr(xp, f"atleast_{ndim}d")(x)
+
+    return _funcs.atleast_nd(x, ndim=ndim, xp=xp)
+
+
+def cov(m: Array, /, *, xp: ModuleType | None = None) -> Array:
+    """
+    Estimate a covariance matrix (or a stack of covariance matrices).
+
+    Covariance indicates the level to which two variables vary together.
+    If we examine *N*-dimensional samples, :math:`X = [x_1, x_2, ... x_N]^T`,
+    each with *M* observations, then element :math:`C_{ij}` of the
+    :math:`N \times N` covariance matrix is the covariance of
+    :math:`x_i` and :math:`x_j`. The element :math:`C_{ii}` is the variance
+    of :math:`x_i`.
+
+    With the exception of supporting batch input, this provides a subset of
+    the functionality of ``numpy.cov``.
+
+    Parameters
+    ----------
+    m : array
+        An array of shape ``(..., N, M)`` whose innermost two dimensions
+        contain *M* observations of *N* variables. That is,
+        each row of `m` represents a variable, and each column a single
+        observation of all those variables.
+    xp : array_namespace, optional
+        The standard-compatible namespace for `m`. Default: infer.
+
+    Returns
+    -------
+    array
+        An array having shape (..., N, N) whose innermost two dimensions represent
+        the covariance matrix of the variables.
+
+    Examples
+    --------
+    >>> import array_api_strict as xp
+    >>> import array_api_extra as xpx
+
+    Consider two variables, :math:`x_0` and :math:`x_1`, which
+    correlate perfectly, but in opposite directions:
+
+    >>> x = xp.asarray([[0, 2], [1, 1], [2, 0]]).T
+    >>> x
+    Array([[0, 1, 2],
+           [2, 1, 0]], dtype=array_api_strict.int64)
+
+    Note how :math:`x_0` increases while :math:`x_1` decreases. The covariance
+    matrix shows this clearly:
+
+    >>> xpx.cov(x, xp=xp)
+    Array([[ 1., -1.],
+           [-1.,  1.]], dtype=array_api_strict.float64)
+
+    Note that element :math:`C_{0,1}`, which shows the correlation between
+    :math:`x_0` and :math:`x_1`, is negative.
+
+    Further, note how `x` and `y` are combined:
+
+    >>> x = xp.asarray([-2.1, -1,  4.3])
+    >>> y = xp.asarray([3,  1.1,  0.12])
+    >>> X = xp.stack((x, y), axis=0)
+    >>> xpx.cov(X, xp=xp)
+    Array([[11.71      , -4.286     ],
+           [-4.286     ,  2.14413333]], dtype=array_api_strict.float64)
+
+    >>> xpx.cov(x, xp=xp)
+    Array(11.71, dtype=array_api_strict.float64)
+
+    >>> xpx.cov(y, xp=xp)
+    Array(2.14413333, dtype=array_api_strict.float64)
+
+    Input with more than two dimensions is treated as a stack of
+    two-dimensional input.
+
+    >>> stack = xp.stack((X, 2*X))
+    >>> xpx.cov(stack)
+    Array([[[ 11.71      ,  -4.286     ],
+            [ -4.286     ,   2.14413333]],
+
+           [[ 46.84      , -17.144     ],
+            [-17.144     ,   8.57653333]]], dtype=array_api_strict.float64)
+    """
+
+    if xp is None:
+        xp = array_namespace(m)
+
+    if (
+        is_numpy_namespace(xp)
+        or is_cupy_namespace(xp)
+        or is_torch_namespace(xp)
+        or is_dask_namespace(xp)
+        or is_jax_namespace(xp)
+    ) and m.ndim <= 2:
+        return xp.cov(m)
+
+    return _funcs.cov(m, xp=xp)
+
+
+def create_diagonal(
+    x: Array, /, *, offset: int = 0, xp: ModuleType | None = None
+) -> Array:
+    """
+    Construct a diagonal array.
+
+    Parameters
+    ----------
+    x : array
+        An array having shape ``(*batch_dims, k)``.
+    offset : int, optional
+        Offset from the leading diagonal (default is ``0``).
+        Use positive ints for diagonals above the leading diagonal,
+        and negative ints for diagonals below the leading diagonal.
+    xp : array_namespace, optional
+        The standard-compatible namespace for `x`. Default: infer.
+
+    Returns
+    -------
+    array
+        An array having shape ``(*batch_dims, k+abs(offset), k+abs(offset))`` with `x`
+        on the diagonal (offset by `offset`).
+
+    Examples
+    --------
+    >>> import array_api_strict as xp
+    >>> import array_api_extra as xpx
+    >>> x = xp.asarray([2, 4, 8])
+
+    >>> xpx.create_diagonal(x, xp=xp)
+    Array([[2, 0, 0],
+           [0, 4, 0],
+           [0, 0, 8]], dtype=array_api_strict.int64)
+
+    >>> xpx.create_diagonal(x, offset=-2, xp=xp)
+    Array([[0, 0, 0, 0, 0],
+           [0, 0, 0, 0, 0],
+           [2, 0, 0, 0, 0],
+           [0, 4, 0, 0, 0],
+           [0, 0, 8, 0, 0]], dtype=array_api_strict.int64)
+    """
+    if xp is None:
+        xp = array_namespace(x)
+
+    if x.ndim == 0:
+        err_msg = "`x` must be at least 1-dimensional."
+        raise ValueError(err_msg)
+
+    if is_torch_namespace(xp):
+        return xp.diag_embed(x, offset=offset, dim1=-2, dim2=-1)
+
+    if (is_dask_namespace(xp) or is_cupy_namespace(xp)) and x.ndim < 2:
+        return xp.diag(x, k=offset)
+
+    if (is_jax_namespace(xp) or is_numpy_namespace(xp)) and x.ndim < 3:
+        batch_dim, n = eager_shape(x)[:-1], eager_shape(x, -1)[0] + abs(offset)
+        return xp.reshape(xp.diag(x, k=offset), (*batch_dim, n, n))
+
+    return _funcs.create_diagonal(x, offset=offset, xp=xp)
+
+
+def expand_dims(
+    a: Array, /, *, axis: int | tuple[int, ...] = (0,), xp: ModuleType | None = None
+) -> Array:
+    """
+    Expand the shape of an array.
+
+    Insert (a) new axis/axes that will appear at the position(s) specified by
+    `axis` in the expanded array shape.
+
+    This is ``xp.expand_dims`` for `axis` an int *or a tuple of ints*.
+    Roughly equivalent to ``numpy.expand_dims`` for NumPy arrays.
+
+    Parameters
+    ----------
+    a : array
+        Array to have its shape expanded.
+    axis : int or tuple of ints, optional
+        Position(s) in the expanded axes where the new axis (or axes) is/are placed.
+        If multiple positions are provided, they should be unique (note that a position
+        given by a positive index could also be referred to by a negative index -
+        that will also result in an error).
+        Default: ``(0,)``.
+    xp : array_namespace, optional
+        The standard-compatible namespace for `a`. Default: infer.
+
+    Returns
+    -------
+    array
+        `a` with an expanded shape.
+
+    Examples
+    --------
+    >>> import array_api_strict as xp
+    >>> import array_api_extra as xpx
+    >>> x = xp.asarray([1, 2])
+    >>> x.shape
+    (2,)
+
+    The following is equivalent to ``x[xp.newaxis, :]`` or ``x[xp.newaxis]``:
+
+    >>> y = xpx.expand_dims(x, axis=0, xp=xp)
+    >>> y
+    Array([[1, 2]], dtype=array_api_strict.int64)
+    >>> y.shape
+    (1, 2)
+
+    The following is equivalent to ``x[:, xp.newaxis]``:
+
+    >>> y = xpx.expand_dims(x, axis=1, xp=xp)
+    >>> y
+    Array([[1],
+           [2]], dtype=array_api_strict.int64)
+    >>> y.shape
+    (2, 1)
+
+    ``axis`` may also be a tuple:
+
+    >>> y = xpx.expand_dims(x, axis=(0, 1), xp=xp)
+    >>> y
+    Array([[[1, 2]]], dtype=array_api_strict.int64)
+
+    >>> y = xpx.expand_dims(x, axis=(2, 0), xp=xp)
+    >>> y
+    Array([[[1],
+            [2]]], dtype=array_api_strict.int64)
+    """
+    if xp is None:
+        xp = array_namespace(a)
+
+    if not isinstance(axis, tuple):
+        axis = (axis,)
+    ndim = a.ndim + len(axis)
+    if axis != () and (min(axis) < -ndim or max(axis) >= ndim):
+        err_msg = (
+            f"a provided axis position is out of bounds for array of dimension {a.ndim}"
+        )
+        raise IndexError(err_msg)
+    axis = tuple(dim % ndim for dim in axis)
+    if len(set(axis)) != len(axis):
+        err_msg = "Duplicate dimensions specified in `axis`."
+        raise ValueError(err_msg)
+
+    if is_numpy_namespace(xp) or is_dask_namespace(xp) or is_jax_namespace(xp):
+        return xp.expand_dims(a, axis=axis)
+
+    return _funcs.expand_dims(a, axis=axis, xp=xp)
+
+
+def isclose(
+    a: Array | complex,
+    b: Array | complex,
+    *,
+    rtol: float = 1e-05,
+    atol: float = 1e-08,
+    equal_nan: bool = False,
+    xp: ModuleType | None = None,
+) -> Array:
+    """
+    Return a boolean array where two arrays are element-wise equal within a tolerance.
+
+    The tolerance values are positive, typically very small numbers. The relative
+    difference ``(rtol * abs(b))`` and the absolute difference `atol` are added together
+    to compare against the absolute difference between `a` and `b`.
+
+    NaNs are treated as equal if they are in the same place and if ``equal_nan=True``.
+    Infs are treated as equal if they are in the same place and of the same sign in both
+    arrays.
+
+    Parameters
+    ----------
+    a, b : Array | int | float | complex | bool
+        Input objects to compare. At least one must be an array.
+    rtol : array_like, optional
+        The relative tolerance parameter (see Notes).
+    atol : array_like, optional
+        The absolute tolerance parameter (see Notes).
+    equal_nan : bool, optional
+        Whether to compare NaN's as equal. If True, NaN's in `a` will be considered
+        equal to NaN's in `b` in the output array.
+    xp : array_namespace, optional
+        The standard-compatible namespace for `a` and `b`. Default: infer.
+
+    Returns
+    -------
+    Array
+        A boolean array of shape broadcasted from `a` and `b`, containing ``True`` where
+        `a` is close to `b`, and ``False`` otherwise.
+
+    Warnings
+    --------
+    The default `atol` is not appropriate for comparing numbers with magnitudes much
+    smaller than one (see notes).
+
+    See Also
+    --------
+    math.isclose : Similar function in stdlib for Python scalars.
+
+    Notes
+    -----
+    For finite values, `isclose` uses the following equation to test whether two
+    floating point values are equivalent::
+
+        absolute(a - b) <= (atol + rtol * absolute(b))
+
+    Unlike the built-in `math.isclose`,
+    the above equation is not symmetric in `a` and `b`,
+    so that ``isclose(a, b)`` might be different from ``isclose(b, a)`` in some rare
+    cases.
+
+    The default value of `atol` is not appropriate when the reference value `b` has
+    magnitude smaller than one. For example, it is unlikely that ``a = 1e-9`` and
+    ``b = 2e-9`` should be considered "close", yet ``isclose(1e-9, 2e-9)`` is ``True``
+    with default settings. Be sure to select `atol` for the use case at hand, especially
+    for defining the threshold below which a non-zero value in `a` will be considered
+    "close" to a very small or zero value in `b`.
+
+    The comparison of `a` and `b` uses standard broadcasting, which means that `a` and
+    `b` need not have the same shape in order for ``isclose(a, b)`` to evaluate to
+    ``True``.
+
+    `isclose` is not defined for non-numeric data types.
+    ``bool`` is considered a numeric data-type for this purpose.
+    """
+    xp = array_namespace(a, b) if xp is None else xp
+
+    if (
+        is_numpy_namespace(xp)
+        or is_cupy_namespace(xp)
+        or is_dask_namespace(xp)
+        or is_jax_namespace(xp)
+    ):
+        return xp.isclose(a, b, rtol=rtol, atol=atol, equal_nan=equal_nan)
+
+    if is_torch_namespace(xp):
+        a, b = asarrays(a, b, xp=xp)  # Array API 2024.12 support
+        return xp.isclose(a, b, rtol=rtol, atol=atol, equal_nan=equal_nan)
+
+    return _funcs.isclose(a, b, rtol=rtol, atol=atol, equal_nan=equal_nan, xp=xp)
+
+
+def nan_to_num(
+    x: Array | float | complex,
+    /,
+    *,
+    fill_value: int | float = 0.0,
+    xp: ModuleType | None = None,
+) -> Array:
+    """
+    Replace NaN with zero and infinity with large finite numbers (default behaviour).
+
+    If `x` is inexact, NaN is replaced by zero or by the user defined value in the
+    `fill_value` keyword, infinity is replaced by the largest finite floating
+    point value representable by ``x.dtype``, and -infinity is replaced by the
+    most negative finite floating point value representable by ``x.dtype``.
+
+    For complex dtypes, the above is applied to each of the real and
+    imaginary components of `x` separately.
+
+    Parameters
+    ----------
+    x : array | float | complex
+        Input data.
+    fill_value : int | float, optional
+        Value to be used to fill NaN values. If no value is passed
+        then NaN values will be replaced with 0.0.
+    xp : array_namespace, optional
+        The standard-compatible namespace for `x`. Default: infer.
+
+    Returns
+    -------
+    array
+        `x`, with the non-finite values replaced.
+
+    See Also
+    --------
+    array_api.isnan : Shows which elements are Not a Number (NaN).
+
+    Examples
+    --------
+    >>> import array_api_extra as xpx
+    >>> import array_api_strict as xp
+    >>> xpx.nan_to_num(xp.inf)
+    1.7976931348623157e+308
+    >>> xpx.nan_to_num(-xp.inf)
+    -1.7976931348623157e+308
+    >>> xpx.nan_to_num(xp.nan)
+    0.0
+    >>> x = xp.asarray([xp.inf, -xp.inf, xp.nan, -128, 128])
+    >>> xpx.nan_to_num(x)
+    array([ 1.79769313e+308, -1.79769313e+308,  0.00000000e+000, # may vary
+           -1.28000000e+002,  1.28000000e+002])
+    >>> y = xp.asarray([complex(xp.inf, xp.nan), xp.nan, complex(xp.nan, xp.inf)])
+    array([  1.79769313e+308,  -1.79769313e+308,   0.00000000e+000, # may vary
+         -1.28000000e+002,   1.28000000e+002])
+    >>> xpx.nan_to_num(y)
+    array([  1.79769313e+308 +0.00000000e+000j, # may vary
+             0.00000000e+000 +0.00000000e+000j,
+             0.00000000e+000 +1.79769313e+308j])
+    """
+    if isinstance(fill_value, complex):
+        msg = "Complex fill values are not supported."
+        raise TypeError(msg)
+
+    xp = array_namespace(x) if xp is None else xp
+
+    # for scalars we want to output an array
+    y = xp.asarray(x)
+
+    if (
+        is_cupy_namespace(xp)
+        or is_jax_namespace(xp)
+        or is_numpy_namespace(xp)
+        or is_torch_namespace(xp)
+    ):
+        return xp.nan_to_num(y, nan=fill_value)
+
+    return _funcs.nan_to_num(y, fill_value=fill_value, xp=xp)
+
+
+def one_hot(
+    x: Array,
+    /,
+    num_classes: int,
+    *,
+    dtype: DType | None = None,
+    axis: int = -1,
+    xp: ModuleType | None = None,
+) -> Array:
+    """
+    One-hot encode the given indices.
+
+    Each index in the input `x` is encoded as a vector of zeros of length `num_classes`
+    with the element at the given index set to one.
+
+    Parameters
+    ----------
+    x : array
+        An array with integral dtype whose values are between `0` and `num_classes - 1`.
+    num_classes : int
+        Number of classes in the one-hot dimension.
+    dtype : DType, optional
+        The dtype of the return value.  Defaults to the default float dtype (usually
+        float64).
+    axis : int, optional
+        Position in the expanded axes where the new axis is placed. Default: -1.
+    xp : array_namespace, optional
+        The standard-compatible namespace for `x`. Default: infer.
+
+    Returns
+    -------
+    array
+        An array having the same shape as `x` except for a new axis at the position
+        given by `axis` having size `num_classes`.  If `axis` is unspecified, it
+        defaults to -1, which appends a new axis.
+
+        If ``x < 0`` or ``x >= num_classes``, then the result is undefined, may raise
+        an exception, or may even cause a bad state.  `x` is not checked.
+
+    Examples
+    --------
+    >>> import array_api_extra as xpx
+    >>> import array_api_strict as xp
+    >>> xpx.one_hot(xp.asarray([1, 2, 0]), 3)
+    Array([[0., 1., 0.],
+          [0., 0., 1.],
+          [1., 0., 0.]], dtype=array_api_strict.float64)
+    """
+    # Validate inputs.
+    if xp is None:
+        xp = array_namespace(x)
+    if not xp.isdtype(x.dtype, "integral"):
+        msg = "x must have an integral dtype."
+        raise TypeError(msg)
+    if dtype is None:
+        dtype = _funcs.default_dtype(xp, device=get_device(x))
+    # Delegate where possible.
+    if is_jax_namespace(xp):
+        from jax.nn import one_hot as jax_one_hot
+
+        return jax_one_hot(x, num_classes, dtype=dtype, axis=axis)
+    if is_torch_namespace(xp):
+        from torch.nn.functional import one_hot as torch_one_hot
+
+        x = xp.astype(x, xp.int64)  # PyTorch only supports int64 here.
+        try:
+            out = torch_one_hot(x, num_classes)
+        except RuntimeError as e:
+            raise IndexError from e
+    else:
+        out = _funcs.one_hot(x, num_classes, xp=xp)
+    out = xp.astype(out, dtype, copy=False)
+    if axis != -1:
+        out = xp.moveaxis(out, -1, axis)
+    return out
+
+
+def pad(
+    x: Array,
+    pad_width: int | tuple[int, int] | Sequence[tuple[int, int]],
+    mode: Literal["constant"] = "constant",
+    *,
+    constant_values: complex = 0,
+    xp: ModuleType | None = None,
+) -> Array:
+    """
+    Pad the input array.
+
+    Parameters
+    ----------
+    x : array
+        Input array.
+    pad_width : int or tuple of ints or sequence of pairs of ints
+        Pad the input array with this many elements from each side.
+        If a sequence of tuples, ``[(before_0, after_0), ... (before_N, after_N)]``,
+        each pair applies to the corresponding axis of ``x``.
+        A single tuple, ``(before, after)``, is equivalent to a list of ``x.ndim``
+        copies of this tuple.
+    mode : str, optional
+        Only "constant" mode is currently supported, which pads with
+        the value passed to `constant_values`.
+    constant_values : python scalar, optional
+        Use this value to pad the input. Default is zero.
+    xp : array_namespace, optional
+        The standard-compatible namespace for `x`. Default: infer.
+
+    Returns
+    -------
+    array
+        The input array,
+        padded with ``pad_width`` elements equal to ``constant_values``.
+    """
+    xp = array_namespace(x) if xp is None else xp
+
+    if mode != "constant":
+        msg = "Only `'constant'` mode is currently supported"
+        raise NotImplementedError(msg)
+
+    if (
+        is_numpy_namespace(xp)
+        or is_cupy_namespace(xp)
+        or is_jax_namespace(xp)
+        or is_pydata_sparse_namespace(xp)
+    ):
+        return xp.pad(x, pad_width, mode, constant_values=constant_values)
+
+    # https://github.com/pytorch/pytorch/blob/cf76c05b4dc629ac989d1fb8e789d4fac04a095a/torch/_numpy/_funcs_impl.py#L2045-L2056
+    if is_torch_namespace(xp):
+        pad_width = xp.asarray(pad_width)
+        pad_width = xp.broadcast_to(pad_width, (x.ndim, 2))
+        pad_width = xp.flip(pad_width, axis=(0,)).flatten()
+        return xp.nn.functional.pad(x, tuple(pad_width), value=constant_values)  # type: ignore[arg-type]  # pyright: ignore[reportArgumentType]
+
+    return _funcs.pad(x, pad_width, constant_values=constant_values, xp=xp)
+
+
+def setdiff1d(
+    x1: Array | complex,
+    x2: Array | complex,
+    /,
+    *,
+    assume_unique: bool = False,
+    xp: ModuleType | None = None,
+) -> Array:
+    """
+    Find the set difference of two arrays.
+
+    Return the unique values in `x1` that are not in `x2`.
+
+    Parameters
+    ----------
+    x1 : array | int | float | complex | bool
+        Input array.
+    x2 : array
+        Input comparison array.
+    assume_unique : bool
+        If ``True``, the input arrays are both assumed to be unique, which
+        can speed up the calculation. Default is ``False``.
+    xp : array_namespace, optional
+        The standard-compatible namespace for `x1` and `x2`. Default: infer.
+
+    Returns
+    -------
+    array
+        1D array of values in `x1` that are not in `x2`. The result
+        is sorted when `assume_unique` is ``False``, but otherwise only sorted
+        if the input is sorted.
+
+    Examples
+    --------
+    >>> import array_api_strict as xp
+    >>> import array_api_extra as xpx
+
+    >>> x1 = xp.asarray([1, 2, 3, 2, 4, 1])
+    >>> x2 = xp.asarray([3, 4, 5, 6])
+    >>> xpx.setdiff1d(x1, x2, xp=xp)
+    Array([1, 2], dtype=array_api_strict.int64)
+    """
+
+    if xp is None:
+        xp = array_namespace(x1, x2)
+
+    if is_numpy_namespace(xp) or is_cupy_namespace(xp) or is_jax_namespace(xp):
+        x1, x2 = asarrays(x1, x2, xp=xp)
+        return xp.setdiff1d(x1, x2, assume_unique=assume_unique)
+
+    return _funcs.setdiff1d(x1, x2, assume_unique=assume_unique, xp=xp)
+
+
+def sinc(x: Array, /, *, xp: ModuleType | None = None) -> Array:
+    r"""
+    Return the normalized sinc function.
+
+    The sinc function is equal to :math:`\sin(\pi x)/(\pi x)` for any argument
+    :math:`x\ne 0`. ``sinc(0)`` takes the limit value 1, making ``sinc`` not
+    only everywhere continuous but also infinitely differentiable.
+
+    .. note::
+
+        Note the normalization factor of ``pi`` used in the definition.
+        This is the most commonly used definition in signal processing.
+        Use ``sinc(x / xp.pi)`` to obtain the unnormalized sinc function
+        :math:`\sin(x)/x` that is more common in mathematics.
+
+    Parameters
+    ----------
+    x : array
+        Array (possibly multi-dimensional) of values for which to calculate
+        ``sinc(x)``. Must have a real floating point dtype.
+    xp : array_namespace, optional
+        The standard-compatible namespace for `x`. Default: infer.
+
+    Returns
+    -------
+    array
+        ``sinc(x)`` calculated elementwise, which has the same shape as the input.
+
+    Notes
+    -----
+    The name sinc is short for "sine cardinal" or "sinus cardinalis".
+
+    The sinc function is used in various signal processing applications,
+    including in anti-aliasing, in the construction of a Lanczos resampling
+    filter, and in interpolation.
+
+    For bandlimited interpolation of discrete-time signals, the ideal
+    interpolation kernel is proportional to the sinc function.
+
+    References
+    ----------
+    #. Weisstein, Eric W. "Sinc Function." From MathWorld--A Wolfram Web
+       Resource. https://mathworld.wolfram.com/SincFunction.html
+    #. Wikipedia, "Sinc function",
+       https://en.wikipedia.org/wiki/Sinc_function
+
+    Examples
+    --------
+    >>> import array_api_strict as xp
+    >>> import array_api_extra as xpx
+    >>> x = xp.linspace(-4, 4, 41)
+    >>> xpx.sinc(x, xp=xp)
+    Array([-3.89817183e-17, -4.92362781e-02,
+           -8.40918587e-02, -8.90384387e-02,
+           -5.84680802e-02,  3.89817183e-17,
+            6.68206631e-02,  1.16434881e-01,
+            1.26137788e-01,  8.50444803e-02,
+           -3.89817183e-17, -1.03943254e-01,
+           -1.89206682e-01, -2.16236208e-01,
+           -1.55914881e-01,  3.89817183e-17,
+            2.33872321e-01,  5.04551152e-01,
+            7.56826729e-01,  9.35489284e-01,
+            1.00000000e+00,  9.35489284e-01,
+            7.56826729e-01,  5.04551152e-01,
+            2.33872321e-01,  3.89817183e-17,
+           -1.55914881e-01, -2.16236208e-01,
+           -1.89206682e-01, -1.03943254e-01,
+           -3.89817183e-17,  8.50444803e-02,
+            1.26137788e-01,  1.16434881e-01,
+            6.68206631e-02,  3.89817183e-17,
+           -5.84680802e-02, -8.90384387e-02,
+           -8.40918587e-02, -4.92362781e-02,
+           -3.89817183e-17], dtype=array_api_strict.float64)
+    """
+
+    if xp is None:
+        xp = array_namespace(x)
+
+    if not xp.isdtype(x.dtype, "real floating"):
+        err_msg = "`x` must have a real floating data type."
+        raise ValueError(err_msg)
+
+    if (
+        is_numpy_namespace(xp)
+        or is_cupy_namespace(xp)
+        or is_jax_namespace(xp)
+        or is_torch_namespace(xp)
+        or is_dask_namespace(xp)
+    ):
+        return xp.sinc(x)
+
+    return _funcs.sinc(x, xp=xp)
+
+
+def partition(
+    a: Array,
+    kth: int,
+    /,
+    axis: int | None = -1,
+    *,
+    xp: ModuleType | None = None,
+) -> Array:
+    """
+    Return a partitioned copy of an array.
+
+    Creates a copy of the array and partially sorts it in such a way that the value
+    of the element in k-th position is in the position it would be in a sorted array.
+    In the output array, all elements smaller than the k-th element are located to
+    the left of this element and all equal or greater are located to its right.
+    The ordering of the elements in the two partitions on the either side of
+    the k-th element in the output array is undefined.
+
+    Parameters
+    ----------
+    a : Array
+        Input array.
+    kth : int
+        Element index to partition by.
+    axis : int, optional
+        Axis along which to partition. The default is ``-1`` (the last axis).
+        If ``None``, the flattened array is used.
+    xp : array_namespace, optional
+        The standard-compatible namespace for `x`. Default: infer.
+
+    Returns
+    -------
+    partitioned_array
+        Array of the same type and shape as `a`.
+
+    Notes
+    -----
+    If `xp` implements ``partition`` or an equivalent function
+    (e.g. ``topk`` for torch), complexity will likely be O(n).
+    If not, this function simply calls ``xp.sort`` and complexity is O(n log n).
+    """
+    # Validate inputs.
+    if xp is None:
+        xp = array_namespace(a)
+    if a.ndim < 1:
+        msg = "`a` must be at least 1-dimensional"
+        raise TypeError(msg)
+    if axis is None:
+        return partition(xp.reshape(a, (-1,)), kth, axis=0, xp=xp)
+    (size,) = eager_shape(a, axis)
+    if not (0 <= kth < size):
+        msg = f"kth(={kth}) out of bounds [0 {size})"
+        raise ValueError(msg)
+
+    # Delegate where possible.
+    if is_numpy_namespace(xp) or is_cupy_namespace(xp) or is_jax_namespace(xp):
+        return xp.partition(a, kth, axis=axis)
+
+    # Use top-k when possible:
+    if is_torch_namespace(xp):
+        if not (axis == -1 or axis == a.ndim - 1):
+            a = xp.transpose(a, axis, -1)
+
+        out = xp.empty_like(a)
+        ranks = xp.arange(a.shape[-1]).expand_as(a)
+
+        split_value, indices = xp.kthvalue(a, kth + 1, keepdim=True)
+        del indices  # indices won't be used => del ASAP to reduce peak memory usage
+
+        # fill the left-side of the partition
+        mask_src = a < split_value
+        n_left = mask_src.sum(dim=-1, keepdim=True)
+        mask_dest = ranks < n_left
+        out[mask_dest] = a[mask_src]
+
+        # fill the middle of the partition
+        mask_src = a == split_value
+        n_left += mask_src.sum(dim=-1, keepdim=True)
+        mask_dest ^= ranks < n_left
+        out[mask_dest] = a[mask_src]
+
+        # fill the right-side of the partition
+        mask_src = a > split_value
+        mask_dest = ranks >= n_left
+        out[mask_dest] = a[mask_src]
+
+        if not (axis == -1 or axis == a.ndim - 1):
+            out = xp.transpose(out, axis, -1)
+        return out
+
+    # Note: dask topk/argtopk sort the return values, so it's
+    # not much more efficient than sorting everything when
+    # kth is not small compared to x.size
+
+    return _funcs.partition(a, kth, axis=axis, xp=xp)
+
+
+def argpartition(
+    a: Array,
+    kth: int,
+    /,
+    axis: int | None = -1,
+    *,
+    xp: ModuleType | None = None,
+) -> Array:
+    """
+    Perform an indirect partition along the given axis.
+
+    It returns an array of indices of the same shape as `a` that
+    index data along the given axis in partitioned order.
+
+    Parameters
+    ----------
+    a : Array
+        Input array.
+    kth : int
+        Element index to partition by.
+    axis : int, optional
+        Axis along which to partition. The default is ``-1`` (the last axis).
+        If ``None``, the flattened array is used.
+    xp : array_namespace, optional
+        The standard-compatible namespace for `x`. Default: infer.
+
+    Returns
+    -------
+    index_array
+        Array of indices that partition `a` along the specified axis.
+
+    Notes
+    -----
+    If `xp` implements ``argpartition`` or an equivalent function
+    e.g. ``topk`` for torch), complexity will likely be O(n).
+    If not, this function simply calls ``xp.argsort`` and complexity is O(n log n).
+    """
+    # Validate inputs.
+    if xp is None:
+        xp = array_namespace(a)
+    if is_pydata_sparse_namespace(xp):
+        msg = "Not implemented for sparse backend: no argsort"
+        raise NotImplementedError(msg)
+    if a.ndim < 1:
+        msg = "`a` must be at least 1-dimensional"
+        raise TypeError(msg)
+    if axis is None:
+        return argpartition(xp.reshape(a, (-1,)), kth, axis=0, xp=xp)
+    (size,) = eager_shape(a, axis)
+    if not (0 <= kth < size):
+        msg = f"kth(={kth}) out of bounds [0 {size})"
+        raise ValueError(msg)
+
+    # Delegate where possible.
+    if is_numpy_namespace(xp) or is_cupy_namespace(xp) or is_jax_namespace(xp):
+        return xp.argpartition(a, kth, axis=axis)
+
+    # Use top-k when possible:
+    if is_torch_namespace(xp):
+        # see `partition` above for commented details of those steps:
+        if not (axis == -1 or axis == a.ndim - 1):
+            a = xp.transpose(a, axis, -1)
+
+        ranks = xp.arange(a.shape[-1]).expand_as(a)
+        out = xp.empty_like(ranks)
+
+        split_value, indices = xp.kthvalue(a, kth + 1, keepdim=True)
+        del indices  # indices won't be used => del ASAP to reduce peak memory usage
+
+        mask_src = a < split_value
+        n_left = mask_src.sum(dim=-1, keepdim=True)
+        mask_dest = ranks < n_left
+        out[mask_dest] = ranks[mask_src]
+
+        mask_src = a == split_value
+        n_left += mask_src.sum(dim=-1, keepdim=True)
+        mask_dest ^= ranks < n_left
+        out[mask_dest] = ranks[mask_src]
+
+        mask_src = a > split_value
+        mask_dest = ranks >= n_left
+        out[mask_dest] = ranks[mask_src]
+
+        if not (axis == -1 or axis == a.ndim - 1):
+            out = xp.transpose(out, axis, -1)
+        return out
+
+    # Note: dask topk/argtopk sort the return values, so it's
+    # not much more efficient than sorting everything when
+    # kth is not small compared to x.size
+
+    return _funcs.argpartition(a, kth, axis=axis, xp=xp)
+
+
+def isin(
+    a: Array,
+    b: Array,
+    /,
+    *,
+    assume_unique: bool = False,
+    invert: bool = False,
+    kind: str | None = None,
+    xp: ModuleType | None = None,
+) -> Array:
+    """
+    Determine whether each element in `a` is present in `b`.
+
+    Return a boolean array of the same shape as `a` that is True for elements
+    that are in `b` and False otherwise.
+
+    Parameters
+    ----------
+    a : array
+        Input elements.
+    b : array
+        The elements against which to test each element of `a`.
+    assume_unique : bool, optional
+        If True, the input arrays are both assumed to be unique which can speed
+        up the calculation. Default: False.
+    invert : bool, optional
+        If True, the values in the returned array are inverted. Default: False.
+    kind : str | None, optional
+        The algorithm or method to use. This will not affect the final result,
+        but will affect the speed and memory use.
+        For NumPy the options are {None, "sort", "table"}.
+        For Jax the mapped parameter is instead `method` and the options are
+        {"compare_all", "binary_search", "sort", and "auto" (default)}
+        For CuPy, Dask, Torch and the default case this parameter is not present and
+        thus ignored. Default: None.
+    xp : array_namespace, optional
+        The standard-compatible namespace for `a` and `b`. Default: infer.
+
+    Returns
+    -------
+    array
+        An array having the same shape as that of `a` that is True for elements
+        that are in `b` and False otherwise.
+    """
+    if xp is None:
+        xp = array_namespace(a, b)
+
+    if is_numpy_namespace(xp):
+        return xp.isin(a, b, assume_unique=assume_unique, invert=invert, kind=kind)
+    if is_jax_namespace(xp):
+        if kind is None:
+            kind = "auto"
+        return xp.isin(a, b, assume_unique=assume_unique, invert=invert, method=kind)
+    if is_cupy_namespace(xp) or is_torch_namespace(xp) or is_dask_namespace(xp):
+        return xp.isin(a, b, assume_unique=assume_unique, invert=invert)
+
+    return _funcs.isin(a, b, assume_unique=assume_unique, invert=invert, xp=xp)
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/__init__.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..d7b3203346da0006f89216d15670be62452d8571
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/__init__.py
@@ -0,0 +1 @@
+"""Internals of array-api-extra."""
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/__pycache__/__init__.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..795949f6b13b8f98553cb221655fe92a42f0d9fb
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/__pycache__/__init__.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/__pycache__/_at.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/__pycache__/_at.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..3607fa4c2d06fd29ea56f42d38028239ff189c04
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/__pycache__/_at.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/__pycache__/_funcs.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/__pycache__/_funcs.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..f125060b4e3b61d7a46eb2cf0d49cd3d77523a44
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/__pycache__/_funcs.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/__pycache__/_lazy.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/__pycache__/_lazy.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..eecbb5a6d4b373c6d9cfb1c4dfb7b877e47b998e
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/__pycache__/_lazy.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_at.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_at.py
new file mode 100644
index 0000000000000000000000000000000000000000..fb2d6ab7e192d1915cfb25808532e26cbacfd655
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_at.py
@@ -0,0 +1,463 @@
+"""Update operations for read-only arrays."""
+
+from __future__ import annotations
+
+import operator
+from collections.abc import Callable
+from enum import Enum
+from types import ModuleType
+from typing import TYPE_CHECKING, ClassVar, cast
+
+from ._utils import _compat
+from ._utils._compat import (
+    array_namespace,
+    is_dask_array,
+    is_jax_array,
+    is_torch_array,
+    is_writeable_array,
+)
+from ._utils._helpers import meta_namespace
+from ._utils._typing import Array, SetIndex
+
+if TYPE_CHECKING:  # pragma: no cover
+    # TODO import from typing (requires Python >=3.11)
+    from typing_extensions import Self
+
+
+class _AtOp(Enum):
+    """Operations for use in `xpx.at`."""
+
+    SET = "set"
+    ADD = "add"
+    SUBTRACT = "subtract"
+    MULTIPLY = "multiply"
+    DIVIDE = "divide"
+    POWER = "power"
+    MIN = "min"
+    MAX = "max"
+
+    # @override from Python 3.12
+    def __str__(self) -> str:  # pyright: ignore[reportImplicitOverride]
+        """
+        Return string representation (useful for pytest logs).
+
+        Returns
+        -------
+        str
+            The operation's name.
+        """
+        return self.value
+
+
+class Undef(Enum):
+    """Sentinel for undefined values."""
+
+    UNDEF = 0
+
+
+_undef = Undef.UNDEF
+
+
+class at:  # pylint: disable=invalid-name  # numpydoc ignore=PR02
+    """
+    Update operations for read-only arrays.
+
+    This implements ``jax.numpy.ndarray.at`` for all writeable
+    backends (those that support ``__setitem__``) and routes
+    to the ``.at[]`` method for JAX arrays.
+
+    Parameters
+    ----------
+    x : array
+        Input array.
+    idx : index, optional
+        Only `array API standard compliant indices
+        `_
+        are supported.
+
+        You may use two alternate syntaxes::
+
+          >>> import array_api_extra as xpx
+          >>> xpx.at(x, idx).set(value)  # or add(value), etc.
+          >>> xpx.at(x)[idx].set(value)
+
+    copy : bool, optional
+        None (default)
+            The array parameter *may* be modified in place if it is
+            possible and beneficial for performance.
+            You should not reuse it after calling this function.
+        True
+            Ensure that the inputs are not modified.
+        False
+            Ensure that the update operation writes back to the input.
+            Raise ``ValueError`` if a copy cannot be avoided.
+
+    xp : array_namespace, optional
+        The standard-compatible namespace for `x`. Default: infer.
+
+    Returns
+    -------
+    Updated input array.
+
+    Warnings
+    --------
+    (a) When you omit the ``copy`` parameter, you should never reuse the parameter
+    array later on; ideally, you should reassign it immediately::
+
+        >>> import array_api_extra as xpx
+        >>> x = xpx.at(x, 0).set(2)
+
+    The above best practice pattern ensures that the behaviour won't change depending
+    on whether ``x`` is writeable or not, as the original ``x`` object is dereferenced
+    as soon as ``xpx.at`` returns; this way there is no risk to accidentally update it
+    twice.
+
+    On the reverse, the anti-pattern below must be avoided, as it will result in
+    different behaviour on read-only versus writeable arrays::
+
+        >>> x = xp.asarray([0, 0, 0])
+        >>> y = xpx.at(x, 0).set(2)
+        >>> z = xpx.at(x, 1).set(3)
+
+    In the above example, both calls to ``xpx.at`` update ``x`` in place *if possible*.
+    This causes the behaviour to diverge depending on whether ``x`` is writeable or not:
+
+    - If ``x`` is writeable, then after the snippet above you'll have
+      ``x == y == z == [2, 3, 0]``
+    - If ``x`` is read-only, then you'll end up with
+      ``x == [0, 0, 0]``, ``y == [2, 0, 0]`` and ``z == [0, 3, 0]``.
+
+    The correct pattern to use if you want diverging outputs from the same input is
+    to enforce copies::
+
+        >>> x = xp.asarray([0, 0, 0])
+        >>> y = xpx.at(x, 0).set(2, copy=True)  # Never updates x
+        >>> z = xpx.at(x, 1).set(3)  # May or may not update x in place
+        >>> del x  # avoid accidental reuse of x as we don't know its state anymore
+
+    (b) The array API standard does not support integer array indices.
+    The behaviour of update methods when the index is an array of integers is
+    undefined and will vary between backends; this is particularly true when the
+    index contains multiple occurrences of the same index, e.g.::
+
+        >>> import numpy as np
+        >>> import jax.numpy as jnp
+        >>> import array_api_extra as xpx
+        >>> xpx.at(np.asarray([123]), np.asarray([0, 0])).add(1)
+        array([124])
+        >>> xpx.at(jnp.asarray([123]), jnp.asarray([0, 0])).add(1)
+        Array([125], dtype=int32)
+
+    See Also
+    --------
+    jax.numpy.ndarray.at : Equivalent array method in JAX.
+
+    Notes
+    -----
+    `sparse `_, as well as read-only arrays from libraries
+    not explicitly covered by ``array-api-compat``, are not supported by update
+    methods.
+
+    Boolean masks are supported on Dask and jitted JAX arrays exclusively
+    when `idx` has the same shape as `x` and `y` is 0-dimensional.
+    Note that this support is not available in JAX's native
+    ``x.at[mask].set(y)``.
+
+    This pattern::
+
+        >>> mask = m(x)
+        >>> x[mask] = f(x[mask])
+
+    Can't be replaced by `at`, as it won't work on Dask and JAX inside jax.jit::
+
+        >>> mask = m(x)
+        >>> x = xpx.at(x, mask).set(f(x[mask])  # Crash on Dask and jax.jit
+
+    You should instead use::
+
+        >>> x = xp.where(m(x), f(x), x)
+
+    Examples
+    --------
+    Given either of these equivalent expressions::
+
+      >>> import array_api_extra as xpx
+      >>> x = xpx.at(x)[1].add(2)
+      >>> x = xpx.at(x, 1).add(2)
+
+    If x is a JAX array, they are the same as::
+
+      >>> x = x.at[1].add(2)
+
+    If x is a read-only NumPy array, they are the same as::
+
+      >>> x = x.copy()
+      >>> x[1] += 2
+
+    For other known backends, they are the same as::
+
+      >>> x[1] += 2
+    """
+
+    _x: Array
+    _idx: SetIndex | Undef
+    __slots__: ClassVar[tuple[str, ...]] = ("_idx", "_x")
+
+    def __init__(
+        self, x: Array, idx: SetIndex | Undef = _undef, /
+    ) -> None:  # numpydoc ignore=GL08
+        self._x = x
+        self._idx = idx
+
+    def __getitem__(self, idx: SetIndex, /) -> Self:  # numpydoc ignore=PR01,RT01
+        """
+        Allow for the alternate syntax ``at(x)[start:stop:step]``.
+
+        It looks prettier than ``at(x, slice(start, stop, step))``
+        and feels more intuitive coming from the JAX documentation.
+        """
+        if self._idx is not _undef:
+            msg = "Index has already been set"
+            raise ValueError(msg)
+        return type(self)(self._x, idx)
+
+    def _op(
+        self,
+        at_op: _AtOp,
+        in_place_op: Callable[[Array, Array | complex], Array] | None,
+        out_of_place_op: Callable[[Array, Array], Array] | None,
+        y: Array | complex,
+        /,
+        copy: bool | None,
+        xp: ModuleType | None,
+    ) -> Array:
+        """
+        Implement all update operations.
+
+        Parameters
+        ----------
+        at_op : _AtOp
+            Method of JAX's Array.at[].
+        in_place_op : Callable[[Array, Array | complex], Array] | None
+            In-place operation to apply on mutable backends::
+
+                x[idx] = in_place_op(x[idx], y)
+
+            If None::
+
+                x[idx] = y
+
+        out_of_place_op : Callable[[Array, Array], Array] | None
+            Out-of-place operation to apply when idx is a boolean mask and the backend
+            doesn't support in-place updates::
+
+                x = xp.where(idx, out_of_place_op(x, y), x)
+
+            If None::
+
+                x = xp.where(idx, y, x)
+
+        y : array or complex
+            Right-hand side of the operation.
+        copy : bool or None
+            Whether to copy the input array. See the class docstring for details.
+        xp : array_namespace, optional
+            The array namespace for the input array. Default: infer.
+
+        Returns
+        -------
+        Array
+            Updated `x`.
+        """
+        from ._funcs import apply_where  # pylint: disable=cyclic-import
+
+        x, idx = self._x, self._idx
+        xp = array_namespace(x, y) if xp is None else xp
+
+        if isinstance(idx, Undef):
+            msg = (
+                "Index has not been set.\n"
+                "Usage: either\n"
+                "    at(x, idx).set(value)\n"
+                "or\n"
+                "    at(x)[idx].set(value)\n"
+                "(same for all other methods)."
+            )
+            raise ValueError(msg)
+
+        if copy not in (True, False, None):
+            msg = f"copy must be True, False, or None; got {copy!r}"
+            raise ValueError(msg)
+
+        writeable = None if copy else is_writeable_array(x)
+
+        # JAX inside jax.jit doesn't support in-place updates with boolean
+        # masks; Dask exclusively supports __setitem__ but not iops.
+        # We can handle the common special case of 0-dimensional y
+        # with where(idx, y, x) instead.
+        if (
+            (is_dask_array(idx) or is_jax_array(idx))
+            and idx.dtype == xp.bool
+            and idx.shape == x.shape
+        ):
+            y_xp = xp.asarray(y, dtype=x.dtype, device=_compat.device(x))
+            if y_xp.ndim == 0:
+                if out_of_place_op:  # add(), subtract(), ...
+                    # suppress inf warnings on Dask
+                    out = apply_where(
+                        idx, (x, y_xp), out_of_place_op, fill_value=x, xp=xp
+                    )
+                    # Undo int->float promotion on JAX after _AtOp.DIVIDE
+                    out = xp.astype(out, x.dtype, copy=False)
+                else:  # set()
+                    out = xp.where(idx, y_xp, x)
+
+                if copy is False:
+                    x[()] = out
+                    return x
+                return out
+
+            # else: this will work on eager JAX and crash on jax.jit and Dask
+
+        if copy or (copy is None and not writeable):
+            if is_jax_array(x):
+                # Use JAX's at[]
+                func = cast(
+                    Callable[[Array | complex], Array],
+                    getattr(x.at[idx], at_op.value),  # type: ignore[attr-defined]  # pyright: ignore[reportAttributeAccessIssue,reportUnknownArgumentType]
+                )
+                out = func(y)
+                # Undo int->float promotion on JAX after _AtOp.DIVIDE
+                return xp.astype(out, x.dtype, copy=False)
+
+            # Emulate at[] behaviour for non-JAX arrays
+            # with a copy followed by an update
+
+            x = xp.asarray(x, copy=True)
+            # A copy of a read-only numpy array is writeable
+            # Note: this assumes that a copy of a writeable array is writeable
+            assert not writeable
+            writeable = None
+
+        if writeable is None:
+            writeable = is_writeable_array(x)
+        if not writeable:
+            # sparse crashes here
+            msg = f"Can't update read-only array {x}"
+            raise ValueError(msg)
+
+        # Work around bug in PyTorch where __setitem__ doesn't
+        # always support mismatched dtypes
+        # https://github.com/pytorch/pytorch/issues/150017
+        if is_torch_array(y):
+            y = xp.astype(y, x.dtype, copy=False)
+
+        # Backends without boolean indexing (other than JAX) crash here
+        if in_place_op:  # add(), subtract(), ...
+            x[idx] = in_place_op(x[idx], y)
+        else:  # set()
+            x[idx] = y
+        return x
+
+    def set(
+        self,
+        y: Array | complex,
+        /,
+        copy: bool | None = None,
+        xp: ModuleType | None = None,
+    ) -> Array:  # numpydoc ignore=PR01,RT01
+        """Apply ``x[idx] = y`` and return the update array."""
+        return self._op(_AtOp.SET, None, None, y, copy=copy, xp=xp)
+
+    def add(
+        self,
+        y: Array | complex,
+        /,
+        copy: bool | None = None,
+        xp: ModuleType | None = None,
+    ) -> Array:  # numpydoc ignore=PR01,RT01
+        """Apply ``x[idx] += y`` and return the updated array."""
+
+        # Note for this and all other methods based on _iop:
+        # operator.iadd and operator.add subtly differ in behaviour, as
+        # only iadd will trigger exceptions when y has an incompatible dtype.
+        return self._op(_AtOp.ADD, operator.iadd, operator.add, y, copy=copy, xp=xp)
+
+    def subtract(
+        self,
+        y: Array | complex,
+        /,
+        copy: bool | None = None,
+        xp: ModuleType | None = None,
+    ) -> Array:  # numpydoc ignore=PR01,RT01
+        """Apply ``x[idx] -= y`` and return the updated array."""
+        return self._op(
+            _AtOp.SUBTRACT, operator.isub, operator.sub, y, copy=copy, xp=xp
+        )
+
+    def multiply(
+        self,
+        y: Array | complex,
+        /,
+        copy: bool | None = None,
+        xp: ModuleType | None = None,
+    ) -> Array:  # numpydoc ignore=PR01,RT01
+        """Apply ``x[idx] *= y`` and return the updated array."""
+        return self._op(
+            _AtOp.MULTIPLY, operator.imul, operator.mul, y, copy=copy, xp=xp
+        )
+
+    def divide(
+        self,
+        y: Array | complex,
+        /,
+        copy: bool | None = None,
+        xp: ModuleType | None = None,
+    ) -> Array:  # numpydoc ignore=PR01,RT01
+        """Apply ``x[idx] /= y`` and return the updated array."""
+        return self._op(
+            _AtOp.DIVIDE, operator.itruediv, operator.truediv, y, copy=copy, xp=xp
+        )
+
+    def power(
+        self,
+        y: Array | complex,
+        /,
+        copy: bool | None = None,
+        xp: ModuleType | None = None,
+    ) -> Array:  # numpydoc ignore=PR01,RT01
+        """Apply ``x[idx] **= y`` and return the updated array."""
+        return self._op(_AtOp.POWER, operator.ipow, operator.pow, y, copy=copy, xp=xp)
+
+    def min(
+        self,
+        y: Array | complex,
+        /,
+        copy: bool | None = None,
+        xp: ModuleType | None = None,
+    ) -> Array:  # numpydoc ignore=PR01,RT01
+        """Apply ``x[idx] = minimum(x[idx], y)`` and return the updated array."""
+        # On Dask, this function runs on the chunks, so we need to determine the
+        # namespace that Dask is wrapping.
+        # Note that da.minimum _incidentally_ works on NumPy, CuPy, and sparse
+        # thanks to all these meta-namespaces implementing the __array_ufunc__
+        # interface, but there's no guarantee that it will work for other
+        # wrapped libraries in the future.
+        xp = array_namespace(self._x) if xp is None else xp
+        mxp = meta_namespace(self._x, xp=xp)
+        y = xp.asarray(y)
+        return self._op(_AtOp.MIN, mxp.minimum, mxp.minimum, y, copy=copy, xp=xp)
+
+    def max(
+        self,
+        y: Array | complex,
+        /,
+        copy: bool | None = None,
+        xp: ModuleType | None = None,
+    ) -> Array:  # numpydoc ignore=PR01,RT01
+        """Apply ``x[idx] = maximum(x[idx], y)`` and return the updated array."""
+        # See note on min()
+        xp = array_namespace(self._x) if xp is None else xp
+        mxp = meta_namespace(self._x, xp=xp)
+        y = xp.asarray(y)
+        return self._op(_AtOp.MAX, mxp.maximum, mxp.maximum, y, copy=copy, xp=xp)
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_backends.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_backends.py
new file mode 100644
index 0000000000000000000000000000000000000000..936f5dd0a88618099d66e00de7a49d97b65d159e
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_backends.py
@@ -0,0 +1,72 @@
+"""Backends against which array-api-extra runs its tests."""
+
+from __future__ import annotations
+
+from enum import Enum
+from typing import Any
+
+import numpy as np
+import pytest
+
+__all__ = ["NUMPY_VERSION", "Backend"]
+
+NUMPY_VERSION = tuple(int(v) for v in np.__version__.split(".")[:3])  # pyright: ignore[reportUnknownArgumentType]
+
+
+class Backend(Enum):  # numpydoc ignore=PR02
+    """
+    All array library backends explicitly tested by array-api-extra.
+
+    Parameters
+    ----------
+    value : str
+        Tag of the backend's module, in the format ``[:]``.
+    """
+
+    # Use : to prevent Enum from deduplicating items with the same value
+    ARRAY_API_STRICT = "array_api_strict"
+    ARRAY_API_STRICTEST = "array_api_strict:strictest"
+    NUMPY = "numpy"
+    NUMPY_READONLY = "numpy:readonly"
+    CUPY = "cupy"
+    TORCH = "torch"
+    TORCH_GPU = "torch:gpu"
+    DASK = "dask.array"
+    SPARSE = "sparse"
+    JAX = "jax.numpy"
+    JAX_GPU = "jax.numpy:gpu"
+
+    @property
+    def modname(self) -> str:  # numpydoc ignore=RT01
+        """Module name to be imported."""
+        return self.value.split(":")[0]
+
+    def like(self, *others: Backend) -> bool:  # numpydoc ignore=PR01,RT01
+        """Check if this backend uses the same module as others."""
+        return any(self.modname == other.modname for other in others)
+
+    def pytest_param(self) -> Any:
+        """
+        Backend as a pytest parameter
+
+        Returns
+        -------
+        pytest.mark.ParameterSet
+        """
+        id_ = (
+            self.name.lower().replace("_gpu", ":gpu").replace("_readonly", ":readonly")
+        )
+
+        marks = []
+        if self.like(Backend.ARRAY_API_STRICT):
+            marks.append(
+                pytest.mark.skipif(
+                    NUMPY_VERSION < (1, 26),
+                    reason="array_api_strict is untested on NumPy <1.26",
+                )
+            )
+        if self.like(Backend.DASK, Backend.JAX):
+            # Monkey-patched by lazy_xp_function
+            marks.append(pytest.mark.thread_unsafe)
+
+        return pytest.param(self, id=id_, marks=marks)  # pyright: ignore[reportUnknownArgumentType]
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_funcs.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_funcs.py
new file mode 100644
index 0000000000000000000000000000000000000000..6e50ce95c80c83eb61f3d3b91d92e05f06186e9c
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_funcs.py
@@ -0,0 +1,744 @@
+"""Array-agnostic implementations for the public API."""
+
+import math
+import warnings
+from collections.abc import Callable, Sequence
+from types import ModuleType, NoneType
+from typing import Literal, cast, overload
+
+from ._at import at
+from ._utils import _compat, _helpers
+from ._utils._compat import array_namespace, is_dask_namespace, is_jax_array
+from ._utils._helpers import (
+    asarrays,
+    capabilities,
+    eager_shape,
+    meta_namespace,
+    ndindex,
+)
+from ._utils._typing import Array, Device, DType
+
+__all__ = [
+    "apply_where",
+    "atleast_nd",
+    "broadcast_shapes",
+    "cov",
+    "create_diagonal",
+    "expand_dims",
+    "kron",
+    "nunique",
+    "pad",
+    "setdiff1d",
+    "sinc",
+]
+
+
+@overload
+def apply_where(  # numpydoc ignore=GL08
+    cond: Array,
+    args: Array | tuple[Array, ...],
+    f1: Callable[..., Array],
+    f2: Callable[..., Array],
+    /,
+    *,
+    xp: ModuleType | None = None,
+) -> Array: ...
+
+
+@overload
+def apply_where(  # numpydoc ignore=GL08
+    cond: Array,
+    args: Array | tuple[Array, ...],
+    f1: Callable[..., Array],
+    /,
+    *,
+    fill_value: Array | complex,
+    xp: ModuleType | None = None,
+) -> Array: ...
+
+
+def apply_where(  # numpydoc ignore=PR01,PR02
+    cond: Array,
+    args: Array | tuple[Array, ...],
+    f1: Callable[..., Array],
+    f2: Callable[..., Array] | None = None,
+    /,
+    *,
+    fill_value: Array | complex | None = None,
+    xp: ModuleType | None = None,
+) -> Array:
+    """
+    Run one of two elementwise functions depending on a condition.
+
+    Equivalent to ``f1(*args) if cond else fill_value`` performed elementwise
+    when `fill_value` is defined, otherwise to ``f1(*args) if cond else f2(*args)``.
+
+    Parameters
+    ----------
+    cond : array
+        The condition, expressed as a boolean array.
+    args : Array or tuple of Arrays
+        Argument(s) to `f1` (and `f2`). Must be broadcastable with `cond`.
+    f1 : callable
+        Elementwise function of `args`, returning a single array.
+        Where `cond` is True, output will be ``f1(arg0[cond], arg1[cond], ...)``.
+    f2 : callable, optional
+        Elementwise function of `args`, returning a single array.
+        Where `cond` is False, output will be ``f2(arg0[cond], arg1[cond], ...)``.
+        Mutually exclusive with `fill_value`.
+    fill_value : Array or scalar, optional
+        If provided, value with which to fill output array where `cond` is False.
+        It does not need to be scalar; it needs however to be broadcastable with
+        `cond` and `args`.
+        Mutually exclusive with `f2`. You must provide one or the other.
+    xp : array_namespace, optional
+        The standard-compatible namespace for `cond` and `args`. Default: infer.
+
+    Returns
+    -------
+    Array
+        An array with elements from the output of `f1` where `cond` is True and either
+        the output of `f2` or `fill_value` where `cond` is False. The returned array has
+        data type determined by type promotion rules between the output of `f1` and
+        either `fill_value` or the output of `f2`.
+
+    Notes
+    -----
+    ``xp.where(cond, f1(*args), f2(*args))`` requires explicitly evaluating `f1` even
+    when `cond` is False, and `f2` when cond is True. This function evaluates each
+    function only for their matching condition, if the backend allows for it.
+
+    On Dask, `f1` and `f2` are applied to the individual chunks and should use functions
+    from the namespace of the chunks.
+
+    Examples
+    --------
+    >>> import array_api_strict as xp
+    >>> import array_api_extra as xpx
+    >>> a = xp.asarray([5, 4, 3])
+    >>> b = xp.asarray([0, 2, 2])
+    >>> def f(a, b):
+    ...     return a // b
+    >>> xpx.apply_where(b != 0, (a, b), f, fill_value=xp.nan)
+    array([ nan,  2., 1.])
+    """
+    # Parse and normalize arguments
+    if (f2 is None) == (fill_value is None):
+        msg = "Exactly one of `fill_value` or `f2` must be given."
+        raise TypeError(msg)
+    args_ = list(args) if isinstance(args, tuple) else [args]
+    del args
+
+    xp = array_namespace(cond, fill_value, *args_) if xp is None else xp
+
+    if isinstance(fill_value, int | float | complex | NoneType):
+        cond, *args_ = xp.broadcast_arrays(cond, *args_)
+    else:
+        cond, fill_value, *args_ = xp.broadcast_arrays(cond, fill_value, *args_)
+
+    if is_dask_namespace(xp):
+        meta_xp = meta_namespace(cond, fill_value, *args_, xp=xp)
+        # map_blocks doesn't descend into tuples of Arrays
+        return xp.map_blocks(_apply_where, cond, f1, f2, fill_value, *args_, xp=meta_xp)
+    return _apply_where(cond, f1, f2, fill_value, *args_, xp=xp)
+
+
+def _apply_where(  # numpydoc ignore=PR01,RT01
+    cond: Array,
+    f1: Callable[..., Array],
+    f2: Callable[..., Array] | None,
+    fill_value: Array | int | float | complex | bool | None,
+    *args: Array,
+    xp: ModuleType,
+) -> Array:
+    """Helper of `apply_where`. On Dask, this runs on a single chunk."""
+
+    if not capabilities(xp, device=_compat.device(cond))["boolean indexing"]:
+        # jax.jit does not support assignment by boolean mask
+        return xp.where(cond, f1(*args), f2(*args) if f2 is not None else fill_value)
+
+    temp1 = f1(*(arr[cond] for arr in args))
+
+    if f2 is None:
+        dtype = xp.result_type(temp1, fill_value)
+        if isinstance(fill_value, int | float | complex):
+            out = xp.full_like(cond, dtype=dtype, fill_value=fill_value)
+        else:
+            out = xp.astype(fill_value, dtype, copy=True)
+    else:
+        ncond = ~cond
+        temp2 = f2(*(arr[ncond] for arr in args))
+        dtype = xp.result_type(temp1, temp2)
+        out = xp.empty_like(cond, dtype=dtype)
+        out = at(out, ncond).set(temp2)
+
+    return at(out, cond).set(temp1)
+
+
+def atleast_nd(x: Array, /, *, ndim: int, xp: ModuleType) -> Array:
+    # numpydoc ignore=PR01,RT01
+    """See docstring in array_api_extra._delegation."""
+
+    if x.ndim < ndim:
+        x = xp.expand_dims(x, axis=0)
+        x = atleast_nd(x, ndim=ndim, xp=xp)
+    return x
+
+
+# `float` in signature to accept `math.nan` for Dask.
+# `int`s are still accepted as `float` is a superclass of `int` in typing
+def broadcast_shapes(*shapes: tuple[float | None, ...]) -> tuple[int | None, ...]:
+    """
+    Compute the shape of the broadcasted arrays.
+
+    Duplicates :func:`numpy.broadcast_shapes`, with additional support for
+    None and NaN sizes.
+
+    This is equivalent to ``xp.broadcast_arrays(arr1, arr2, ...)[0].shape``
+    without needing to worry about the backend potentially deep copying
+    the arrays.
+
+    Parameters
+    ----------
+    *shapes : tuple[int | None, ...]
+        Shapes of the arrays to broadcast.
+
+    Returns
+    -------
+    tuple[int | None, ...]
+        The shape of the broadcasted arrays.
+
+    See Also
+    --------
+    numpy.broadcast_shapes : Equivalent NumPy function.
+    array_api.broadcast_arrays : Function to broadcast actual arrays.
+
+    Notes
+    -----
+    This function accepts the Array API's ``None`` for unknown sizes,
+    as well as Dask's non-standard ``math.nan``.
+    Regardless of input, the output always contains ``None`` for unknown sizes.
+
+    Examples
+    --------
+    >>> import array_api_extra as xpx
+    >>> xpx.broadcast_shapes((2, 3), (2, 1))
+    (2, 3)
+    >>> xpx.broadcast_shapes((4, 2, 3), (2, 1), (1, 3))
+    (4, 2, 3)
+    """
+    if not shapes:
+        return ()  # Match NumPy output
+
+    ndim = max(len(shape) for shape in shapes)
+    out: list[int | None] = []
+    for axis in range(-ndim, 0):
+        sizes = {shape[axis] for shape in shapes if axis >= -len(shape)}
+        # Dask uses NaN for unknown shape, which predates the Array API spec for None
+        none_size = None in sizes or math.nan in sizes  # noqa: PLW0177
+        sizes -= {1, None, math.nan}
+        if len(sizes) > 1:
+            msg = (
+                "shape mismatch: objects cannot be broadcast to a single shape: "
+                f"{shapes}."
+            )
+            raise ValueError(msg)
+        out.append(None if none_size else cast(int, sizes.pop()) if sizes else 1)
+
+    return tuple(out)
+
+
+def cov(m: Array, /, *, xp: ModuleType) -> Array:  # numpydoc ignore=PR01,RT01
+    """See docstring in array_api_extra._delegation."""
+    m = xp.asarray(m, copy=True)
+    dtype = (
+        xp.float64 if xp.isdtype(m.dtype, "integral") else xp.result_type(m, xp.float64)
+    )
+
+    m = atleast_nd(m, ndim=2, xp=xp)
+    m = xp.astype(m, dtype)
+
+    avg = _helpers.mean(m, axis=-1, keepdims=True, xp=xp)
+
+    m_shape = eager_shape(m)
+    fact = m_shape[-1] - 1
+
+    if fact <= 0:
+        warnings.warn("Degrees of freedom <= 0 for slice", RuntimeWarning, stacklevel=2)
+        fact = 0
+
+    m -= avg
+    m_transpose = xp.matrix_transpose(m)
+    if xp.isdtype(m_transpose.dtype, "complex floating"):
+        m_transpose = xp.conj(m_transpose)
+    c = xp.matmul(m, m_transpose)
+    c /= fact
+    axes = tuple(axis for axis, length in enumerate(c.shape) if length == 1)
+    return xp.squeeze(c, axis=axes)
+
+
+def one_hot(
+    x: Array,
+    /,
+    num_classes: int,
+    *,
+    xp: ModuleType,
+) -> Array:  # numpydoc ignore=PR01,RT01
+    """See docstring in `array_api_extra._delegation.py`."""
+    # TODO: Benchmark whether this is faster on the NumPy backend:
+    # if is_numpy_array(x):
+    #     out = xp.zeros((x.size, num_classes), dtype=dtype)
+    #     out[xp.arange(x.size), xp.reshape(x, (-1,))] = 1
+    #     return xp.reshape(out, (*x.shape, num_classes))
+    range_num_classes = xp.arange(num_classes, dtype=x.dtype, device=_compat.device(x))
+    return x[..., xp.newaxis] == range_num_classes
+
+
+def create_diagonal(
+    x: Array, /, *, offset: int = 0, xp: ModuleType
+) -> Array:  # numpydoc ignore=PR01,RT01
+    """See docstring in array_api_extra._delegation."""
+    x_shape = eager_shape(x)
+    batch_dims = x_shape[:-1]
+    n = x_shape[-1] + abs(offset)
+    diag = xp.zeros((*batch_dims, n**2), dtype=x.dtype, device=_compat.device(x))
+
+    target_slice = slice(
+        offset if offset >= 0 else abs(offset) * n,
+        min(n * (n - offset), diag.shape[-1]),
+        n + 1,
+    )
+    for index in ndindex(*batch_dims):
+        diag = at(diag)[(*index, target_slice)].set(x[(*index, slice(None))])
+    return xp.reshape(diag, (*batch_dims, n, n))
+
+
+def default_dtype(
+    xp: ModuleType,
+    kind: Literal[
+        "real floating", "complex floating", "integral", "indexing"
+    ] = "real floating",
+    *,
+    device: Device | None = None,
+) -> DType:
+    """
+    Return the default dtype for the given namespace and device.
+
+    This is a convenience shorthand for
+    ``xp.__array_namespace_info__().default_dtypes(device=device)[kind]``.
+
+    Parameters
+    ----------
+    xp : array_namespace
+        The standard-compatible namespace for which to get the default dtype.
+    kind : {'real floating', 'complex floating', 'integral', 'indexing'}, optional
+        The kind of dtype to return. Default is 'real floating'.
+    device : Device, optional
+        The device for which to get the default dtype. Default: current device.
+
+    Returns
+    -------
+    dtype
+        The default dtype for the given namespace, kind, and device.
+    """
+    dtypes = xp.__array_namespace_info__().default_dtypes(device=device)
+    try:
+        return dtypes[kind]
+    except KeyError as e:
+        domain = ("real floating", "complex floating", "integral", "indexing")
+        assert set(dtypes) == set(domain), f"Non-compliant namespace: {dtypes}"
+        msg = f"Unknown kind '{kind}'. Expected one of {domain}."
+        raise ValueError(msg) from e
+
+
+def expand_dims(a: Array, /, *, axis: tuple[int, ...] = (0,), xp: ModuleType) -> Array:
+    # numpydoc ignore=PR01,RT01
+    """See docstring in array_api_extra._delegation."""
+    for i in sorted(axis):
+        a = xp.expand_dims(a, axis=i)
+    return a
+
+
+def isclose(
+    a: Array | complex,
+    b: Array | complex,
+    *,
+    rtol: float = 1e-05,
+    atol: float = 1e-08,
+    equal_nan: bool = False,
+    xp: ModuleType,
+) -> Array:  # numpydoc ignore=PR01,RT01
+    """See docstring in array_api_extra._delegation."""
+    a, b = asarrays(a, b, xp=xp)
+
+    a_inexact = xp.isdtype(a.dtype, ("real floating", "complex floating"))
+    b_inexact = xp.isdtype(b.dtype, ("real floating", "complex floating"))
+    if a_inexact or b_inexact:
+        # prevent warnings on NumPy and Dask on inf - inf
+        mxp = meta_namespace(a, b, xp=xp)
+        out = apply_where(
+            xp.isinf(a) | xp.isinf(b),
+            (a, b),
+            lambda a, b: mxp.isinf(a) & mxp.isinf(b) & (mxp.sign(a) == mxp.sign(b)),  # pyright: ignore[reportUnknownArgumentType]
+            # Note: inf <= inf is True!
+            lambda a, b: mxp.abs(a - b) <= (atol + rtol * mxp.abs(b)),  # pyright: ignore[reportUnknownArgumentType]
+            xp=xp,
+        )
+        if equal_nan:
+            out = xp.where(xp.isnan(a) & xp.isnan(b), True, out)
+        return out
+
+    if xp.isdtype(a.dtype, "bool") or xp.isdtype(b.dtype, "bool"):
+        if atol >= 1 or rtol >= 1:
+            return xp.ones_like(a == b)
+        return a == b
+
+    # integer types
+    atol = int(atol)
+    if rtol == 0:
+        return xp.abs(a - b) <= atol
+
+    # Don't rely on OverflowError, as it is not guaranteed by the Array API.
+    nrtol = int(1.0 / rtol)
+    if nrtol > xp.iinfo(b.dtype).max:
+        # rtol * max_int < 1, so it's inconsequential
+        return xp.abs(a - b) <= atol
+    return xp.abs(a - b) <= (atol + xp.abs(b) // nrtol)
+
+
+def kron(
+    a: Array | complex,
+    b: Array | complex,
+    /,
+    *,
+    xp: ModuleType | None = None,
+) -> Array:
+    """
+    Kronecker product of two arrays.
+
+    Computes the Kronecker product, a composite array made of blocks of the
+    second array scaled by the first.
+
+    Equivalent to ``numpy.kron`` for NumPy arrays.
+
+    Parameters
+    ----------
+    a, b : Array | int | float | complex
+        Input arrays or scalars. At least one must be an array.
+    xp : array_namespace, optional
+        The standard-compatible namespace for `a` and `b`. Default: infer.
+
+    Returns
+    -------
+    array
+        The Kronecker product of `a` and `b`.
+
+    Notes
+    -----
+    The function assumes that the number of dimensions of `a` and `b`
+    are the same, if necessary prepending the smallest with ones.
+    If ``a.shape = (r0,r1,..,rN)`` and ``b.shape = (s0,s1,...,sN)``,
+    the Kronecker product has shape ``(r0*s0, r1*s1, ..., rN*SN)``.
+    The elements are products of elements from `a` and `b`, organized
+    explicitly by::
+
+        kron(a,b)[k0,k1,...,kN] = a[i0,i1,...,iN] * b[j0,j1,...,jN]
+
+    where::
+
+        kt = it * st + jt,  t = 0,...,N
+
+    In the common 2-D case (N=1), the block structure can be visualized::
+
+        [[ a[0,0]*b,   a[0,1]*b,  ... , a[0,-1]*b  ],
+         [  ...                              ...   ],
+         [ a[-1,0]*b,  a[-1,1]*b, ... , a[-1,-1]*b ]]
+
+    Examples
+    --------
+    >>> import array_api_strict as xp
+    >>> import array_api_extra as xpx
+    >>> xpx.kron(xp.asarray([1, 10, 100]), xp.asarray([5, 6, 7]), xp=xp)
+    Array([  5,   6,   7,  50,  60,  70, 500,
+           600, 700], dtype=array_api_strict.int64)
+
+    >>> xpx.kron(xp.asarray([5, 6, 7]), xp.asarray([1, 10, 100]), xp=xp)
+    Array([  5,  50, 500,   6,  60, 600,   7,
+            70, 700], dtype=array_api_strict.int64)
+
+    >>> xpx.kron(xp.eye(2), xp.ones((2, 2)), xp=xp)
+    Array([[1., 1., 0., 0.],
+           [1., 1., 0., 0.],
+           [0., 0., 1., 1.],
+           [0., 0., 1., 1.]], dtype=array_api_strict.float64)
+
+    >>> a = xp.reshape(xp.arange(100), (2, 5, 2, 5))
+    >>> b = xp.reshape(xp.arange(24), (2, 3, 4))
+    >>> c = xpx.kron(a, b, xp=xp)
+    >>> c.shape
+    (2, 10, 6, 20)
+    >>> I = (1, 3, 0, 2)
+    >>> J = (0, 2, 1)
+    >>> J1 = (0,) + J             # extend to ndim=4
+    >>> S1 = (1,) + b.shape
+    >>> K = tuple(xp.asarray(I) * xp.asarray(S1) + xp.asarray(J1))
+    >>> c[K] == a[I]*b[J]
+    Array(True, dtype=array_api_strict.bool)
+    """
+    if xp is None:
+        xp = array_namespace(a, b)
+    a, b = asarrays(a, b, xp=xp)
+
+    singletons = (1,) * (b.ndim - a.ndim)
+    a = cast(Array, xp.broadcast_to(a, singletons + a.shape))
+
+    nd_b, nd_a = b.ndim, a.ndim
+    nd_max = max(nd_b, nd_a)
+    if nd_a == 0 or nd_b == 0:
+        return xp.multiply(a, b)
+
+    a_shape = eager_shape(a)
+    b_shape = eager_shape(b)
+
+    # Equalise the shapes by prepending smaller one with 1s
+    a_shape = (1,) * max(0, nd_b - nd_a) + a_shape
+    b_shape = (1,) * max(0, nd_a - nd_b) + b_shape
+
+    # Insert empty dimensions
+    a_arr = expand_dims(a, axis=tuple(range(nd_b - nd_a)), xp=xp)
+    b_arr = expand_dims(b, axis=tuple(range(nd_a - nd_b)), xp=xp)
+
+    # Compute the product
+    a_arr = expand_dims(a_arr, axis=tuple(range(1, nd_max * 2, 2)), xp=xp)
+    b_arr = expand_dims(b_arr, axis=tuple(range(0, nd_max * 2, 2)), xp=xp)
+    result = xp.multiply(a_arr, b_arr)
+
+    # Reshape back and return
+    res_shape = tuple(a_s * b_s for a_s, b_s in zip(a_shape, b_shape, strict=True))
+    return xp.reshape(result, res_shape)
+
+
+def nan_to_num(  # numpydoc ignore=PR01,RT01
+    x: Array,
+    /,
+    fill_value: int | float = 0.0,
+    *,
+    xp: ModuleType,
+) -> Array:
+    """See docstring in `array_api_extra._delegation.py`."""
+
+    def perform_replacements(  # numpydoc ignore=PR01,RT01
+        x: Array,
+        fill_value: int | float,
+        xp: ModuleType,
+    ) -> Array:
+        """Internal function to perform the replacements."""
+        x = xp.where(xp.isnan(x), fill_value, x)
+
+        # convert infinities to finite values
+        finfo = xp.finfo(x.dtype)
+        idx_posinf = xp.isinf(x) & ~xp.signbit(x)
+        idx_neginf = xp.isinf(x) & xp.signbit(x)
+        x = xp.where(idx_posinf, finfo.max, x)
+        return xp.where(idx_neginf, finfo.min, x)
+
+    if xp.isdtype(x.dtype, "complex floating"):
+        return perform_replacements(
+            xp.real(x),
+            fill_value,
+            xp,
+        ) + 1j * perform_replacements(
+            xp.imag(x),
+            fill_value,
+            xp,
+        )
+
+    if xp.isdtype(x.dtype, "numeric"):
+        return perform_replacements(x, fill_value, xp)
+
+    return x
+
+
+def nunique(x: Array, /, *, xp: ModuleType | None = None) -> Array:
+    """
+    Count the number of unique elements in an array.
+
+    Compatible with JAX and Dask, whose laziness would be otherwise
+    problematic.
+
+    Parameters
+    ----------
+    x : Array
+        Input array.
+    xp : array_namespace, optional
+        The standard-compatible namespace for `x`. Default: infer.
+
+    Returns
+    -------
+    array: 0-dimensional integer array
+        The number of unique elements in `x`. It can be lazy.
+    """
+    if xp is None:
+        xp = array_namespace(x)
+
+    if is_jax_array(x):
+        # size= is JAX-specific
+        # https://github.com/data-apis/array-api/issues/883
+        _, counts = xp.unique_counts(x, size=_compat.size(x))
+        return (counts > 0).sum()
+
+    # There are 3 general use cases:
+    # 1. backend has unique_counts and it returns an array with known shape
+    # 2. backend has unique_counts and it returns a None-sized array;
+    #    e.g. Dask, ndonnx
+    # 3. backend does not have unique_counts; e.g. wrapped JAX
+    if capabilities(xp, device=_compat.device(x))["data-dependent shapes"]:
+        # xp has unique_counts; O(n) complexity
+        _, counts = xp.unique_counts(x)
+        n = _compat.size(counts)
+        if n is None:
+            return xp.sum(xp.ones_like(counts))
+        return xp.asarray(n, device=_compat.device(x))
+
+    # xp does not have unique_counts; O(n*logn) complexity
+    x = xp.reshape(x, (-1,))
+    x = xp.sort(x, stable=False)
+    mask = x != xp.roll(x, -1)
+    default_int = default_dtype(xp, "integral", device=_compat.device(x))
+    return xp.maximum(
+        # Special cases:
+        # - array is size 0
+        # - array has all elements equal to each other
+        xp.astype(xp.any(~mask), default_int),
+        xp.sum(xp.astype(mask, default_int)),
+    )
+
+
+def pad(
+    x: Array,
+    pad_width: int | tuple[int, int] | Sequence[tuple[int, int]],
+    *,
+    constant_values: complex = 0,
+    xp: ModuleType,
+) -> Array:  # numpydoc ignore=PR01,RT01
+    """See docstring in `array_api_extra._delegation.py`."""
+    # make pad_width a list of length-2 tuples of ints
+    if isinstance(pad_width, int):
+        pad_width_seq = [(pad_width, pad_width)] * x.ndim
+    elif (
+        isinstance(pad_width, tuple)
+        and len(pad_width) == 2
+        and all(isinstance(i, int) for i in pad_width)
+    ):
+        pad_width_seq = [cast(tuple[int, int], pad_width)] * x.ndim
+    else:
+        pad_width_seq = cast(list[tuple[int, int]], list(pad_width))
+
+    slices: list[slice] = []
+    newshape: list[int] = []
+    for ax, w_tpl in enumerate(pad_width_seq):
+        if len(w_tpl) != 2:
+            msg = f"expect a 2-tuple (before, after), got {w_tpl}."
+            raise ValueError(msg)
+
+        sh = eager_shape(x)[ax]
+
+        if w_tpl[0] == 0 and w_tpl[1] == 0:
+            sl = slice(None, None, None)
+        else:
+            stop: int | None
+            start, stop = w_tpl
+            stop = None if stop == 0 else -stop
+
+            sl = slice(start, stop, None)
+            sh += w_tpl[0] + w_tpl[1]
+
+        newshape.append(sh)
+        slices.append(sl)
+
+    padded = xp.full(
+        tuple(newshape),
+        fill_value=constant_values,
+        dtype=x.dtype,
+        device=_compat.device(x),
+    )
+    return at(padded, tuple(slices)).set(x)
+
+
+def setdiff1d(
+    x1: Array | complex,
+    x2: Array | complex,
+    /,
+    *,
+    assume_unique: bool = False,
+    xp: ModuleType,
+) -> Array:  # numpydoc ignore=PR01,RT01
+    """See docstring in `array_api_extra._delegation.py`."""
+
+    # https://github.com/microsoft/pyright/issues/10103
+    x1_, x2_ = asarrays(x1, x2, xp=xp)
+
+    if assume_unique:
+        x1_ = xp.reshape(x1_, (-1,))
+        x2_ = xp.reshape(x2_, (-1,))
+    else:
+        x1_ = xp.unique_values(x1_)
+        x2_ = xp.unique_values(x2_)
+
+    return x1_[_helpers.in1d(x1_, x2_, assume_unique=True, invert=True, xp=xp)]
+
+
+def sinc(x: Array, /, *, xp: ModuleType) -> Array:
+    # numpydoc ignore=PR01,RT01
+    """See docstring in `array_api_extra._delegation.py`."""
+
+    # no scalars in `where` - array-api#807
+    y = xp.pi * xp.where(
+        xp.astype(x, xp.bool),
+        x,
+        xp.asarray(xp.finfo(x.dtype).eps, dtype=x.dtype, device=_compat.device(x)),
+    )
+    return xp.sin(y) / y
+
+
+def partition(  # numpydoc ignore=PR01,RT01
+    x: Array,
+    kth: int,  # noqa: ARG001
+    /,
+    axis: int = -1,
+    *,
+    xp: ModuleType,
+) -> Array:
+    """See docstring in `array_api_extra._delegation.py`."""
+    return xp.sort(x, axis=axis, stable=False)
+
+
+def argpartition(  # numpydoc ignore=PR01,RT01
+    x: Array,
+    kth: int,  # noqa: ARG001
+    /,
+    axis: int = -1,
+    *,
+    xp: ModuleType,
+) -> Array:
+    """See docstring in `array_api_extra._delegation.py`."""
+    return xp.argsort(x, axis=axis, stable=False)
+
+
+def isin(  # numpydoc ignore=PR01,RT01
+    a: Array,
+    b: Array,
+    /,
+    *,
+    assume_unique: bool = False,
+    invert: bool = False,
+    xp: ModuleType,
+) -> Array:
+    """See docstring in `array_api_extra._delegation.py`."""
+    original_a_shape = a.shape
+    a = xp.reshape(a, (-1,))
+    b = xp.reshape(b, (-1,))
+    return xp.reshape(
+        _helpers.in1d(a, b, assume_unique=assume_unique, invert=invert, xp=xp),
+        original_a_shape,
+    )
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_lazy.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_lazy.py
new file mode 100644
index 0000000000000000000000000000000000000000..d509500132a4b8886f5ccca93d7d6bd2aa25b4dc
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_lazy.py
@@ -0,0 +1,357 @@
+"""Public API Functions."""
+
+from __future__ import annotations
+
+import math
+from collections.abc import Callable, Sequence
+from functools import partial, wraps
+from types import ModuleType
+from typing import TYPE_CHECKING, Any, ParamSpec, TypeAlias, cast, overload
+
+from ._funcs import broadcast_shapes
+from ._utils import _compat
+from ._utils._compat import (
+    array_namespace,
+    is_dask_namespace,
+    is_jax_namespace,
+)
+from ._utils._helpers import is_python_scalar
+from ._utils._typing import Array, DType
+
+if TYPE_CHECKING:  # pragma: no cover
+    import numpy as np
+    from numpy.typing import ArrayLike
+
+    NumPyObject: TypeAlias = np.ndarray[Any, Any] | np.generic
+else:
+    # Sphinx hack
+    NumPyObject = Any
+
+P = ParamSpec("P")
+
+
+@overload
+def lazy_apply(  # type: ignore[valid-type]
+    func: Callable[P, Array | ArrayLike],
+    *args: Array | complex | None,
+    shape: tuple[int | None, ...] | None = None,
+    dtype: DType | None = None,
+    as_numpy: bool = False,
+    xp: ModuleType | None = None,
+    **kwargs: P.kwargs,  # pyright: ignore[reportGeneralTypeIssues]
+) -> Array: ...  # numpydoc ignore=GL08
+
+
+@overload
+def lazy_apply(  # type: ignore[valid-type]
+    func: Callable[P, Sequence[Array | ArrayLike]],
+    *args: Array | complex | None,
+    shape: Sequence[tuple[int | None, ...]],
+    dtype: Sequence[DType] | None = None,
+    as_numpy: bool = False,
+    xp: ModuleType | None = None,
+    **kwargs: P.kwargs,  # pyright: ignore[reportGeneralTypeIssues]
+) -> tuple[Array, ...]: ...  # numpydoc ignore=GL08
+
+
+def lazy_apply(  # type: ignore[valid-type]  # numpydoc ignore=GL07,SA04
+    func: Callable[P, Array | ArrayLike | Sequence[Array | ArrayLike]],
+    *args: Array | complex | None,
+    shape: tuple[int | None, ...] | Sequence[tuple[int | None, ...]] | None = None,
+    dtype: DType | Sequence[DType] | None = None,
+    as_numpy: bool = False,
+    xp: ModuleType | None = None,
+    **kwargs: P.kwargs,  # pyright: ignore[reportGeneralTypeIssues]
+) -> Array | tuple[Array, ...]:
+    """
+    Lazily apply an eager function.
+
+    If the backend of the input arrays is lazy, e.g. Dask or jitted JAX, the execution
+    of the function is delayed until the graph is materialized; if it's eager, the
+    function is executed immediately.
+
+    Parameters
+    ----------
+    func : callable
+        The function to apply.
+
+        It must accept one or more array API compliant arrays as positional arguments.
+        If `as_numpy=True`, inputs are converted to NumPy before they are passed to
+        `func`.
+        It must return either a single array-like or a sequence of array-likes.
+
+        `func` must be a pure function, i.e. without side effects, as depending on the
+        backend it may be executed more than once or never.
+    *args : Array | int | float | complex | bool | None
+        One or more Array API compliant arrays, Python scalars, or None's.
+
+        If `as_numpy=True`, you need to be able to apply :func:`numpy.asarray` to
+        non-None args to convert them to NumPy; read notes below about specific
+        backends.
+    shape : tuple[int | None, ...] | Sequence[tuple[int | None, ...]], optional
+        Output shape or sequence of output shapes, one for each output of `func`.
+        Default: assume single output and broadcast shapes of the input arrays.
+    dtype : DType | Sequence[DType], optional
+        Output dtype or sequence of output dtypes, one for each output of `func`.
+        dtype(s) must belong to the same array namespace as the input arrays.
+        Default: infer the result type(s) from the input arrays.
+    as_numpy : bool, optional
+        If True, convert the input arrays to NumPy before passing them to `func`.
+        This is particularly useful to make NumPy-only functions, e.g. written in Cython
+       or Numba, work transparently with array API-compliant arrays.
+        Default: False.
+    xp : array_namespace, optional
+        The standard-compatible namespace for `args`. Default: infer.
+    **kwargs : Any, optional
+        Additional keyword arguments to pass verbatim to `func`.
+        They cannot contain Array objects.
+
+    Returns
+    -------
+    Array | tuple[Array, ...]
+        The result(s) of `func` applied to the input arrays, wrapped in the same
+        array namespace as the inputs.
+        If shape is omitted or a single `tuple[int | None, ...]`, return a single array.
+        Otherwise, return a tuple of arrays.
+
+    Notes
+    -----
+    JAX
+        This allows applying eager functions to jitted JAX arrays, which are lazy.
+        The function won't be applied until the JAX array is materialized.
+        When running inside ``jax.jit``, `shape` must be fully known, i.e. it cannot
+        contain any `None` elements.
+
+        .. warning::
+
+            `func` must never raise inside ``jax.jit``, as the resulting behavior is
+            undefined.
+
+        Using this with `as_numpy=False` is particularly useful to apply non-jittable
+        JAX functions to arrays on GPU devices.
+        If ``as_numpy=True``, the :doc:`jax:transfer_guard` may prevent arrays on a GPU
+        device from being transferred back to CPU. This is treated as an implicit
+        transfer.
+
+    PyTorch, CuPy
+        If ``as_numpy=True``, these backends raise by default if you attempt to convert
+        arrays on a GPU device to NumPy.
+
+    Sparse
+        If ``as_numpy=True``, by default sparse prevents implicit densification through
+        :func:`numpy.asarray`. `This safety mechanism can be disabled
+        `_.
+
+    Dask
+        This allows applying eager functions to Dask arrays.
+        The Dask graph won't be computed until the user calls ``compute()`` or
+        ``persist()`` down the line.
+
+        The function name will be prominently visible on the user-facing Dask
+        dashboard and on Prometheus metrics, so it is recommended for it to be
+        meaningful.
+
+        `lazy_apply` doesn't know if `func` reduces along any axes; also, shape
+        changes are non-trivial in chunked Dask arrays. For these reasons, all inputs
+        will be rechunked into a single chunk.
+
+        .. warning::
+
+           The whole operation needs to fit in memory all at once on a single worker.
+
+        The outputs will also be returned as a single chunk and you should consider
+        rechunking them into smaller chunks afterwards.
+
+        If you want to distribute the calculation across multiple workers, you
+        should use :func:`dask.array.map_blocks`, :func:`dask.array.map_overlap`,
+        :func:`dask.array.blockwise`, or a native Dask wrapper instead of
+        `lazy_apply`.
+
+    Dask wrapping around other backends
+        If ``as_numpy=False``, `func` will receive in input eager arrays of the meta
+        namespace, as defined by the ``._meta`` attribute of the input Dask arrays.
+        The outputs of `func` will be wrapped by the meta namespace, and then wrapped
+        again by Dask.
+
+    Raises
+    ------
+    ValueError
+        When ``xp=jax.numpy``, the output `shape` is unknown (it contains ``None`` on
+        one or more axes) and this function was called inside ``jax.jit``.
+    RuntimeError
+        When ``xp=sparse`` and auto-densification is disabled.
+    Exception (backend-specific)
+        When the backend disallows implicit device to host transfers and the input
+        arrays are on a non-CPU device, e.g. on GPU.
+
+    See Also
+    --------
+    jax.transfer_guard
+    jax.pure_callback
+    dask.array.map_blocks
+    dask.array.map_overlap
+    dask.array.blockwise
+    """
+    args_not_none = [arg for arg in args if arg is not None]
+    array_args = [arg for arg in args_not_none if not is_python_scalar(arg)]
+    if not array_args:
+        msg = "Must have at least one argument array"
+        raise ValueError(msg)
+    if xp is None:
+        xp = array_namespace(*args)
+
+    # Normalize and validate shape and dtype
+    shapes: list[tuple[int | None, ...]]
+    dtypes: list[DType]
+    multi_output = False
+
+    if shape is None:
+        shapes = [broadcast_shapes(*(arg.shape for arg in array_args))]
+    elif all(isinstance(s, int | None) for s in shape):
+        # Do not test for shape to be a tuple
+        # https://github.com/data-apis/array-api/issues/891#issuecomment-2637430522
+        shapes = [cast(tuple[int | None, ...], shape)]
+    else:
+        shapes = list(shape)  # type: ignore[arg-type]  # pyright: ignore[reportAssignmentType]
+        multi_output = True
+
+    if dtype is None:
+        dtypes = [xp.result_type(*args_not_none)] * len(shapes)
+    elif multi_output:
+        if not isinstance(dtype, Sequence):
+            msg = "Got multiple shapes but only one dtype"
+            raise ValueError(msg)
+        dtypes = list(dtype)  # pyright: ignore[reportUnknownArgumentType]
+    else:
+        if isinstance(dtype, Sequence):
+            msg = "Got single shape but multiple dtypes"
+            raise ValueError(msg)
+
+        dtypes = [dtype]
+
+    if len(shapes) != len(dtypes):
+        msg = f"Got {len(shapes)} shapes and {len(dtypes)} dtypes"
+        raise ValueError(msg)
+    del shape
+    del dtype
+    # End of shape and dtype parsing
+
+    # Backend-specific branches
+    if is_dask_namespace(xp):
+        import dask
+
+        metas: list[Array] = [arg._meta for arg in array_args]  # pylint: disable=protected-access    # pyright: ignore[reportAttributeAccessIssue]
+        meta_xp = array_namespace(*metas)
+
+        wrapped = dask.delayed(  # type: ignore[attr-defined]  # pyright: ignore[reportPrivateImportUsage]
+            _lazy_apply_wrapper(func, as_numpy, multi_output, meta_xp),
+            pure=True,
+        )
+        # This finalizes each arg, which is the same as arg.rechunk(-1).
+        # Please read docstring above for why we're not using
+        # dask.array.map_blocks or dask.array.blockwise!
+        delayed_out = wrapped(*args, **kwargs)
+
+        out = tuple(
+            xp.from_delayed(
+                delayed_out[i],  # pyright: ignore[reportIndexIssue]
+                # Dask's unknown shapes diverge from the Array API specification
+                shape=tuple(math.nan if s is None else s for s in shape),
+                dtype=dtype,
+                meta=metas[0],
+            )
+            for i, (shape, dtype) in enumerate(zip(shapes, dtypes, strict=True))
+        )
+
+    elif is_jax_namespace(xp) and _is_jax_jit_enabled(xp):
+        # Delay calling func with jax.pure_callback, which will forward to func eager
+        # JAX arrays. Do not use jax.pure_callback when running outside of the JIT,
+        # as it does not support raising exceptions:
+        # https://github.com/jax-ml/jax/issues/26102
+        import jax
+
+        if any(None in shape for shape in shapes):
+            msg = "Output shape must be fully known when running inside jax.jit"
+            raise ValueError(msg)
+
+        # Shield kwargs from being coerced into JAX arrays.
+        # jax.pure_callback calls jax.jit under the hood, but without the chance of
+        # passing static_argnames / static_argnums.
+        wrapped = _lazy_apply_wrapper(
+            partial(func, **kwargs), as_numpy, multi_output, xp
+        )
+
+        # suppress unused-ignore to run mypy in -e lint as well as -e dev
+        out = cast(  # type: ignore[bad-cast,unused-ignore]
+            tuple[Array, ...],
+            jax.pure_callback(
+                wrapped,
+                tuple(
+                    jax.ShapeDtypeStruct(shape, dtype)  # pyright: ignore[reportUnknownArgumentType]
+                    for shape, dtype in zip(shapes, dtypes, strict=True)
+                ),
+                *args,
+            ),
+        )
+
+    else:
+        # Eager backends, including non-jitted JAX
+        wrapped = _lazy_apply_wrapper(func, as_numpy, multi_output, xp)
+        out = wrapped(*args, **kwargs)
+
+    return out if multi_output else out[0]
+
+
+def _is_jax_jit_enabled(xp: ModuleType) -> bool:  # numpydoc ignore=PR01,RT01
+    """Return True if this function is being called inside ``jax.jit``."""
+    import jax  # pylint: disable=import-outside-toplevel
+
+    x = xp.asarray(False)
+    try:
+        return bool(x)
+    except jax.errors.TracerBoolConversionError:
+        return True
+
+
+def _lazy_apply_wrapper(  # numpydoc ignore=PR01,RT01
+    func: Callable[..., Array | ArrayLike | Sequence[Array | ArrayLike]],
+    as_numpy: bool,
+    multi_output: bool,
+    xp: ModuleType,
+) -> Callable[..., tuple[Array, ...]]:
+    """
+    Helper of `lazy_apply`.
+
+    Given a function that accepts one or more arrays as positional arguments and returns
+    a single array-like or a sequence of array-likes, return a function that accepts the
+    same number of Array API arrays and always returns a tuple of Array API array.
+
+    Any keyword arguments are passed through verbatim to the wrapped function.
+    """
+
+    # On Dask, @wraps causes the graph key to contain the wrapped function's name
+    @wraps(func)
+    def wrapper(
+        *args: Array | complex | None, **kwargs: Any
+    ) -> tuple[Array, ...]:  # numpydoc ignore=GL08
+        args_list = []
+        device = None
+        for arg in args:
+            if arg is not None and not is_python_scalar(arg):
+                if device is None:
+                    device = _compat.device(arg)
+                if as_numpy:
+                    import numpy as np
+
+                    arg = cast(Array, np.asarray(arg))  # pyright: ignore[reportInvalidCast] # noqa: PLW2901
+            args_list.append(arg)
+        assert device is not None
+
+        out = func(*args_list, **kwargs)
+
+        if multi_output:
+            assert isinstance(out, Sequence)
+            return tuple(xp.asarray(o, device=device) for o in out)
+        return (xp.asarray(out, device=device),)
+
+    return wrapper
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_testing.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_testing.py
new file mode 100644
index 0000000000000000000000000000000000000000..30e2f1efb7b0e017673c871acddd2bb6a74522cf
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_testing.py
@@ -0,0 +1,304 @@
+"""
+Testing utilities.
+
+Note that this is private API; don't expect it to be stable.
+See also ..testing for public testing utilities.
+"""
+
+from __future__ import annotations
+
+import math
+from types import ModuleType
+from typing import Any, cast
+
+import numpy as np
+import pytest
+
+from ._utils._compat import (
+    array_namespace,
+    is_array_api_strict_namespace,
+    is_cupy_namespace,
+    is_dask_namespace,
+    is_jax_namespace,
+    is_numpy_namespace,
+    is_pydata_sparse_namespace,
+    is_torch_array,
+    is_torch_namespace,
+    to_device,
+)
+from ._utils._typing import Array, Device
+
+__all__ = ["as_numpy_array", "xp_assert_close", "xp_assert_equal", "xp_assert_less"]
+
+
+def _check_ns_shape_dtype(
+    actual: Array,
+    desired: Array,
+    check_dtype: bool,
+    check_shape: bool,
+    check_scalar: bool,
+) -> ModuleType:  # numpydoc ignore=RT03
+    """
+    Assert that namespace, shape and dtype of the two arrays match.
+
+    Parameters
+    ----------
+    actual : Array
+        The array produced by the tested function.
+    desired : Array
+        The expected array (typically hardcoded).
+    check_dtype, check_shape : bool, default: True
+        Whether to check agreement between actual and desired dtypes and shapes
+    check_scalar : bool, default: False
+        NumPy only: whether to check agreement between actual and desired types -
+        0d array vs scalar.
+
+    Returns
+    -------
+    Arrays namespace.
+    """
+    actual_xp = array_namespace(actual)  # Raises on scalars and lists
+    desired_xp = array_namespace(desired)
+
+    msg = f"namespaces do not match: {actual_xp} != f{desired_xp}"
+    assert actual_xp == desired_xp, msg
+
+    # Dask uses nan instead of None for unknown shapes
+    actual_shape = cast(tuple[float, ...], actual.shape)
+    desired_shape = cast(tuple[float, ...], desired.shape)
+    assert None not in actual_shape  # Requires explicit support
+    assert None not in desired_shape
+    if is_dask_namespace(desired_xp):
+        if any(math.isnan(i) for i in actual_shape):
+            actual_shape = actual.compute().shape  # type: ignore[attr-defined]  # pyright: ignore[reportAttributeAccessIssue]
+        if any(math.isnan(i) for i in desired_shape):
+            desired_shape = desired.compute().shape  # type: ignore[attr-defined]  # pyright: ignore[reportAttributeAccessIssue]
+
+    if check_shape:
+        msg = f"shapes do not match: {actual_shape} != f{desired_shape}"
+        assert actual_shape == desired_shape, msg
+    else:
+        # Ignore shape, but check flattened size. This is normally done by
+        # np.testing.assert_array_equal etc even when strict=False, but not for
+        # non-materializable arrays.
+        actual_size = math.prod(actual_shape)  # pyright: ignore[reportUnknownArgumentType]
+        desired_size = math.prod(desired_shape)  # pyright: ignore[reportUnknownArgumentType]
+        msg = f"sizes do not match: {actual_size} != f{desired_size}"
+        assert actual_size == desired_size, msg
+
+    if check_dtype:
+        msg = f"dtypes do not match: {actual.dtype} != {desired.dtype}"
+        assert actual.dtype == desired.dtype, msg
+
+    if is_numpy_namespace(actual_xp) and check_scalar:
+        # only NumPy distinguishes between scalars and arrays; we do if check_scalar.
+        _msg = (
+            "array-ness does not match:\n Actual: "
+            f"{type(actual)}\n Desired: {type(desired)}"
+        )
+        assert np.isscalar(actual) == np.isscalar(desired), _msg
+
+    return desired_xp
+
+
+def _is_materializable(x: Array) -> bool:
+    """
+    Return True if you can call `as_numpy_array(x)`; False otherwise.
+    """
+    # Important: here we assume that we're not tracing -
+    # e.g. we're not inside `jax.jit`` nor `cupy.cuda.Stream.begin_capture`.
+    return not is_torch_array(x) or x.device.type != "meta"  # type: ignore[attr-defined]  # pyright: ignore[reportAttributeAccessIssue]
+
+
+def as_numpy_array(array: Array, *, xp: ModuleType) -> np.typing.NDArray[Any]:
+    """
+    Convert array to NumPy, bypassing GPU-CPU transfer guards and densification guards.
+    """
+    if is_cupy_namespace(xp):
+        return xp.asnumpy(array)
+    if is_pydata_sparse_namespace(xp):
+        return array.todense()  # type: ignore[attr-defined]  # pyright: ignore[reportAttributeAccessIssue]
+
+    if is_torch_namespace(xp):
+        array = to_device(array, "cpu")
+    if is_array_api_strict_namespace(xp):
+        cpu: Device = xp.Device("CPU_DEVICE")
+        array = to_device(array, cpu)
+    if is_jax_namespace(xp):
+        import jax
+
+        # Note: only needed if the transfer guard is enabled
+        cpu = cast(Device, jax.devices("cpu")[0])
+        array = to_device(array, cpu)
+
+    return np.asarray(array)
+
+
+def xp_assert_equal(
+    actual: Array,
+    desired: Array,
+    *,
+    err_msg: str = "",
+    check_dtype: bool = True,
+    check_shape: bool = True,
+    check_scalar: bool = False,
+) -> None:
+    """
+    Array-API compatible version of `np.testing.assert_array_equal`.
+
+    Parameters
+    ----------
+    actual : Array
+        The array produced by the tested function.
+    desired : Array
+        The expected array (typically hardcoded).
+    err_msg : str, optional
+        Error message to display on failure.
+    check_dtype, check_shape : bool, default: True
+        Whether to check agreement between actual and desired dtypes and shapes
+    check_scalar : bool, default: False
+        NumPy only: whether to check agreement between actual and desired types -
+        0d array vs scalar.
+
+    See Also
+    --------
+    xp_assert_close : Similar function for inexact equality checks.
+    numpy.testing.assert_array_equal : Similar function for NumPy arrays.
+    """
+    xp = _check_ns_shape_dtype(actual, desired, check_dtype, check_shape, check_scalar)
+    if not _is_materializable(actual):
+        return
+    actual_np = as_numpy_array(actual, xp=xp)
+    desired_np = as_numpy_array(desired, xp=xp)
+    np.testing.assert_array_equal(actual_np, desired_np, err_msg=err_msg)
+
+
+def xp_assert_less(
+    x: Array,
+    y: Array,
+    *,
+    err_msg: str = "",
+    check_dtype: bool = True,
+    check_shape: bool = True,
+    check_scalar: bool = False,
+) -> None:
+    """
+    Array-API compatible version of `np.testing.assert_array_less`.
+
+    Parameters
+    ----------
+    x, y : Array
+        The arrays to compare according to ``x < y`` (elementwise).
+    err_msg : str, optional
+        Error message to display on failure.
+    check_dtype, check_shape : bool, default: True
+        Whether to check agreement between actual and desired dtypes and shapes
+    check_scalar : bool, default: False
+        NumPy only: whether to check agreement between actual and desired types -
+        0d array vs scalar.
+
+    See Also
+    --------
+    xp_assert_close : Similar function for inexact equality checks.
+    numpy.testing.assert_array_equal : Similar function for NumPy arrays.
+    """
+    xp = _check_ns_shape_dtype(x, y, check_dtype, check_shape, check_scalar)
+    if not _is_materializable(x):
+        return
+    x_np = as_numpy_array(x, xp=xp)
+    y_np = as_numpy_array(y, xp=xp)
+    np.testing.assert_array_less(x_np, y_np, err_msg=err_msg)
+
+
+def xp_assert_close(
+    actual: Array,
+    desired: Array,
+    *,
+    rtol: float | None = None,
+    atol: float = 0,
+    err_msg: str = "",
+    check_dtype: bool = True,
+    check_shape: bool = True,
+    check_scalar: bool = False,
+) -> None:
+    """
+    Array-API compatible version of `np.testing.assert_allclose`.
+
+    Parameters
+    ----------
+    actual : Array
+        The array produced by the tested function.
+    desired : Array
+        The expected array (typically hardcoded).
+    rtol : float, optional
+        Relative tolerance. Default: dtype-dependent.
+    atol : float, optional
+        Absolute tolerance. Default: 0.
+    err_msg : str, optional
+        Error message to display on failure.
+    check_dtype, check_shape : bool, default: True
+        Whether to check agreement between actual and desired dtypes and shapes
+    check_scalar : bool, default: False
+        NumPy only: whether to check agreement between actual and desired types -
+        0d array vs scalar.
+
+    See Also
+    --------
+    xp_assert_equal : Similar function for exact equality checks.
+    isclose : Public function for checking closeness.
+    numpy.testing.assert_allclose : Similar function for NumPy arrays.
+
+    Notes
+    -----
+    The default `atol` and `rtol` differ from `xp.all(xpx.isclose(a, b))`.
+    """
+    xp = _check_ns_shape_dtype(actual, desired, check_dtype, check_shape, check_scalar)
+    if not _is_materializable(actual):
+        return
+
+    if rtol is None:
+        if xp.isdtype(actual.dtype, ("real floating", "complex 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
+        else:
+            rtol = 1e-7
+
+    actual_np = as_numpy_array(actual, xp=xp)
+    desired_np = as_numpy_array(desired, xp=xp)
+    np.testing.assert_allclose(  # pyright: ignore[reportCallIssue]
+        actual_np,
+        desired_np,
+        rtol=rtol,  # pyright: ignore[reportArgumentType]
+        atol=atol,
+        err_msg=err_msg,
+    )
+
+
+def xfail(
+    request: pytest.FixtureRequest, *, reason: str, strict: bool | None = None
+) -> None:
+    """
+    XFAIL the currently running test.
+
+    Unlike ``pytest.xfail``, allow rest of test to execute instead of immediately
+    halting it, so that it may result in a XPASS.
+    xref https://github.com/pandas-dev/pandas/issues/38902
+
+    Parameters
+    ----------
+    request : pytest.FixtureRequest
+        ``request`` argument of the test function.
+    reason : str
+        Reason for the expected failure.
+    strict: bool, optional
+        If True, the test will be marked as failed if it passes.
+        If False, the test will be marked as passed if it fails.
+        Default: ``xfail_strict`` value in ``pyproject.toml``, or False if absent.
+    """
+    if strict is not None:
+        marker = pytest.mark.xfail(reason=reason, strict=strict)
+    else:
+        marker = pytest.mark.xfail(reason=reason)
+    request.node.add_marker(marker)
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/__init__.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3628c45f0e0a48ba403abd0ba4a6bec76dc8a9f1
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/__init__.py
@@ -0,0 +1 @@
+"""Modules housing private utility functions."""
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/__pycache__/__init__.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..45548dbec1d2bfc84c2b87c5c1f1fd9f64724aa2
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/__pycache__/__init__.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/__pycache__/_compat.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/__pycache__/_compat.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..91fd4d5fb45f1a3d17fe60b1940cd06976ab6ef9
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/__pycache__/_compat.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/__pycache__/_helpers.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/__pycache__/_helpers.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..f666969f11bfebeb3eface7b516ae8c9df09e783
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/__pycache__/_helpers.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/__pycache__/_typing.cpython-311.pyc b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/__pycache__/_typing.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..09259db075dae4ed6e41dac9abe723af437ac385
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/__pycache__/_typing.cpython-311.pyc differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/_compat.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/_compat.py
new file mode 100644
index 0000000000000000000000000000000000000000..82ce76b8ecbcd506c639beb2c9b5b3934d94b67d
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/_compat.py
@@ -0,0 +1,74 @@
+"""Acquire helpers from array-api-compat."""
+# Allow packages that vendor both `array-api-extra` and
+# `array-api-compat` to override the import location
+
+# pylint: disable=duplicate-code
+try:
+    from ...._array_api_compat_vendor import (
+        array_namespace,
+        device,
+        is_array_api_obj,
+        is_array_api_strict_namespace,
+        is_cupy_array,
+        is_cupy_namespace,
+        is_dask_array,
+        is_dask_namespace,
+        is_jax_array,
+        is_jax_namespace,
+        is_lazy_array,
+        is_numpy_array,
+        is_numpy_namespace,
+        is_pydata_sparse_array,
+        is_pydata_sparse_namespace,
+        is_torch_array,
+        is_torch_namespace,
+        is_writeable_array,
+        size,
+        to_device,
+    )
+except ImportError:
+    from array_api_compat import (
+        array_namespace,
+        device,
+        is_array_api_obj,
+        is_array_api_strict_namespace,
+        is_cupy_array,
+        is_cupy_namespace,
+        is_dask_array,
+        is_dask_namespace,
+        is_jax_array,
+        is_jax_namespace,
+        is_lazy_array,
+        is_numpy_array,
+        is_numpy_namespace,
+        is_pydata_sparse_array,
+        is_pydata_sparse_namespace,
+        is_torch_array,
+        is_torch_namespace,
+        is_writeable_array,
+        size,
+        to_device,
+    )
+
+__all__ = [
+    "array_namespace",
+    "device",
+    "is_array_api_obj",
+    "is_array_api_strict_namespace",
+    "is_cupy_array",
+    "is_cupy_namespace",
+    "is_dask_array",
+    "is_dask_namespace",
+    "is_jax_array",
+    "is_jax_namespace",
+    "is_lazy_array",
+    "is_numpy_array",
+    "is_numpy_namespace",
+    "is_pydata_sparse_array",
+    "is_pydata_sparse_namespace",
+    "is_torch_array",
+    "is_torch_namespace",
+    "is_writeable_array",
+    "size",
+    "to_device",
+]
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/_compat.pyi b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/_compat.pyi
new file mode 100644
index 0000000000000000000000000000000000000000..95c6bc8a1baed1f083d907994e5c939d6cc325f2
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/_compat.pyi
@@ -0,0 +1,45 @@
+"""Static type stubs for `_compat.py`."""
+
+# https://github.com/scikit-learn/scikit-learn/pull/27910#issuecomment-2568023972
+from __future__ import annotations
+
+from types import ModuleType
+from typing import Any, TypeGuard
+
+# TODO import from typing (requires Python >=3.13)
+from typing_extensions import TypeIs
+
+from ._typing import Array, Device
+
+# pylint: disable=missing-class-docstring,unused-argument
+
+def array_namespace(
+    *xs: Array | complex | None,
+    api_version: str | None = None,
+    use_compat: bool | None = None,
+) -> ModuleType: ...
+def device(x: Array, /) -> Device: ...
+def is_array_api_obj(x: object, /) -> TypeIs[Array]: ...
+def is_array_api_strict_namespace(xp: ModuleType, /) -> bool: ...
+def is_cupy_namespace(xp: ModuleType, /) -> bool: ...
+def is_dask_namespace(xp: ModuleType, /) -> bool: ...
+def is_jax_namespace(xp: ModuleType, /) -> bool: ...
+def is_numpy_namespace(xp: ModuleType, /) -> bool: ...
+def is_pydata_sparse_namespace(xp: ModuleType, /) -> bool: ...
+def is_torch_namespace(xp: ModuleType, /) -> bool: ...
+def is_cupy_array(x: object, /) -> TypeGuard[Array]: ...
+def is_dask_array(x: object, /) -> TypeGuard[Array]: ...
+def is_jax_array(x: object, /) -> TypeGuard[Array]: ...
+def is_numpy_array(x: object, /) -> TypeGuard[Array]: ...
+def is_pydata_sparse_array(x: object, /) -> TypeGuard[Array]: ...
+def is_torch_array(x: object, /) -> TypeGuard[Array]: ...
+def is_lazy_array(x: object, /) -> TypeGuard[Array]: ...
+def is_writeable_array(x: object, /) -> TypeGuard[Array]: ...
+def size(x: Array, /) -> int | None: ...
+def to_device(
+    x: Array,
+    device: Device,  # pylint: disable=redefined-outer-name
+    /,
+    *,
+    stream: int | Any | None = None,
+) -> Array: ...
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/_helpers.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/_helpers.py
new file mode 100644
index 0000000000000000000000000000000000000000..22da8259eff73d6d0cecfba6d25514de70c23d3b
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/_helpers.py
@@ -0,0 +1,619 @@
+"""Helper functions used by `array_api_extra/_funcs.py`."""
+
+from __future__ import annotations
+
+import io
+import math
+import pickle
+import types
+from collections.abc import Callable, Generator, Iterable, Iterator
+from functools import wraps
+from types import ModuleType
+from typing import (
+    TYPE_CHECKING,
+    Any,
+    ClassVar,
+    Generic,
+    Literal,
+    ParamSpec,
+    TypeAlias,
+    TypeVar,
+    cast,
+)
+
+from . import _compat
+from ._compat import (
+    array_namespace,
+    is_array_api_obj,
+    is_dask_namespace,
+    is_jax_namespace,
+    is_numpy_array,
+    is_pydata_sparse_namespace,
+    is_torch_namespace,
+)
+from ._typing import Array, Device
+
+if TYPE_CHECKING:  # pragma: no cover
+    # TODO import from typing (requires Python >=3.12 and >=3.13)
+    from typing_extensions import TypeIs, override
+else:
+
+    def override(func):
+        return func
+
+
+P = ParamSpec("P")
+T = TypeVar("T")
+
+
+__all__ = [
+    "asarrays",
+    "capabilities",
+    "eager_shape",
+    "in1d",
+    "is_python_scalar",
+    "jax_autojit",
+    "mean",
+    "meta_namespace",
+    "pickle_flatten",
+    "pickle_unflatten",
+]
+
+
+def in1d(
+    x1: Array,
+    x2: Array,
+    /,
+    *,
+    assume_unique: bool = False,
+    invert: bool = False,
+    xp: ModuleType | None = None,
+) -> Array:  # numpydoc ignore=PR01,RT01
+    """
+    Check whether each element of an array is also present in a second array.
+
+    Returns a boolean array the same length as `x1` that is True
+    where an element of `x1` is in `x2` and False otherwise.
+
+    This function has been adapted using the original implementation
+    present in numpy:
+    https://github.com/numpy/numpy/blob/v1.26.0/numpy/lib/arraysetops.py#L524-L758
+    """
+    if xp is None:
+        xp = array_namespace(x1, x2)
+
+    x1_shape = eager_shape(x1)
+    x2_shape = eager_shape(x2)
+
+    # This code is run to make the code significantly faster
+    if x2_shape[0] < 10 * x1_shape[0] ** 0.145 and isinstance(x2, Iterable):
+        if invert:
+            mask = xp.ones(x1_shape[0], dtype=xp.bool, device=_compat.device(x1))
+            for a in x2:
+                mask &= x1 != a
+        else:
+            mask = xp.zeros(x1_shape[0], dtype=xp.bool, device=_compat.device(x1))
+            for a in x2:
+                mask |= x1 == a
+        return mask
+
+    rev_idx = xp.empty(0)  # placeholder
+    if not assume_unique:
+        x1, rev_idx = xp.unique_inverse(x1)
+        x2 = xp.unique_values(x2)
+
+    ar = xp.concat((x1, x2))
+    device_ = _compat.device(ar)
+    # We need this to be a stable sort.
+    order = xp.argsort(ar, stable=True)
+    reverse_order = xp.argsort(order, stable=True)
+    sar = xp.take(ar, order, axis=0)
+    ar_size = _compat.size(sar)
+    assert ar_size is not None, "xp.unique*() on lazy backends raises"
+    if ar_size >= 1:
+        bool_ar = sar[1:] != sar[:-1] if invert else sar[1:] == sar[:-1]
+    else:
+        bool_ar = xp.asarray([False]) if invert else xp.asarray([True])
+    flag = xp.concat((bool_ar, xp.asarray([invert], device=device_)))
+    ret = xp.take(flag, reverse_order, axis=0)
+
+    if assume_unique:
+        return ret[: x1.shape[0]]
+    return xp.take(ret, rev_idx, axis=0)
+
+
+def mean(
+    x: Array,
+    /,
+    *,
+    axis: int | tuple[int, ...] | None = None,
+    keepdims: bool = False,
+    xp: ModuleType | None = None,
+) -> Array:  # numpydoc ignore=PR01,RT01
+    """
+    Complex mean, https://github.com/data-apis/array-api/issues/846.
+    """
+    if xp is None:
+        xp = array_namespace(x)
+
+    if xp.isdtype(x.dtype, "complex floating"):
+        x_real = xp.real(x)
+        x_imag = xp.imag(x)
+        mean_real = xp.mean(x_real, axis=axis, keepdims=keepdims)
+        mean_imag = xp.mean(x_imag, axis=axis, keepdims=keepdims)
+        return mean_real + (mean_imag * xp.asarray(1j))
+    return xp.mean(x, axis=axis, keepdims=keepdims)
+
+
+def is_python_scalar(x: object) -> TypeIs[complex]:  # numpydoc ignore=PR01,RT01
+    """Return True if `x` is a Python scalar, False otherwise."""
+    # isinstance(x, float) returns True for np.float64
+    # isinstance(x, complex) returns True for np.complex128
+    # bool is a subclass of int
+    return isinstance(x, int | float | complex) and not is_numpy_array(x)
+
+
+def asarrays(
+    a: Array | complex,
+    b: Array | complex,
+    xp: ModuleType,
+) -> tuple[Array, Array]:
+    """
+    Ensure both `a` and `b` are arrays.
+
+    If `b` is a python scalar, it is converted to the same dtype as `a`, and vice versa.
+
+    Behavior is not specified when mixing a Python ``float`` and an array with an
+    integer data type; this may give ``float32``, ``float64``, or raise an exception.
+    Behavior is implementation-specific.
+
+    Similarly, behavior is not specified when mixing a Python ``complex`` and an array
+    with a real-valued data type; this may give ``complex64``, ``complex128``, or raise
+    an exception. Behavior is implementation-specific.
+
+    Parameters
+    ----------
+    a, b : Array | int | float | complex | bool
+        Input arrays or scalars. At least one must be an array.
+    xp : array_namespace, optional
+        The standard-compatible namespace for `x`. Default: infer.
+
+    Returns
+    -------
+    Array, Array
+        The input arrays, possibly converted to arrays if they were scalars.
+
+    See Also
+    --------
+    mixing-arrays-with-python-scalars : Array API specification for the behavior.
+    """
+    a_scalar = is_python_scalar(a)
+    b_scalar = is_python_scalar(b)
+    if not a_scalar and not b_scalar:
+        # This includes misc. malformed input e.g. str
+        return a, b  # type: ignore[return-value]
+
+    swap = False
+    if a_scalar:
+        swap = True
+        b, a = a, b
+
+    if is_array_api_obj(a):
+        # a is an Array API object
+        # b is a int | float | complex | bool
+        xa = a
+
+        # https://data-apis.org/array-api/draft/API_specification/type_promotion.html#mixing-arrays-with-python-scalars
+        same_dtype = {
+            bool: "bool",
+            int: ("integral", "real floating", "complex floating"),
+            float: ("real floating", "complex floating"),
+            complex: "complex floating",
+        }
+        kind = same_dtype[type(cast(complex, b))]
+        if xp.isdtype(a.dtype, kind):
+            xb = xp.asarray(b, dtype=a.dtype, device=_compat.device(a))
+        else:
+            # Undefined behaviour. Let the function deal with it, if it can.
+            xb = xp.asarray(b, device=_compat.device(a))
+
+    else:
+        # Neither a nor b are Array API objects.
+        # Note: we can only reach this point when one explicitly passes
+        # xp=xp to the calling function; otherwise we fail earlier on
+        # array_namespace(a, b).
+        xa, xb = xp.asarray(a), xp.asarray(b)
+
+    return (xb, xa) if swap else (xa, xb)
+
+
+def ndindex(*x: int) -> Generator[tuple[int, ...]]:
+    """
+    Generate all N-dimensional indices for a given array shape.
+
+    Given the shape of an array, an ndindex instance iterates over the N-dimensional
+    index of the array. At each iteration a tuple of indices is returned, the last
+    dimension is iterated over first.
+
+    This has an identical API to numpy.ndindex.
+
+    Parameters
+    ----------
+    *x : int
+        The shape of the array.
+    """
+    if not x:
+        yield ()
+        return
+    for i in ndindex(*x[:-1]):
+        for j in range(x[-1]):
+            yield *i, j
+
+
+def eager_shape(x: Array, /, axis: int | None = None) -> tuple[int, ...]:
+    """
+    Return shape of an array. Raise if shape is not fully defined.
+
+    Parameters
+    ----------
+    x : Array
+        Input array.
+    axis : int, optional
+        If provided, only returns the tuple (shape[axis],).
+
+    Returns
+    -------
+    tuple[int, ...]
+        Shape of the array.
+    """
+    shape = x.shape
+    if axis is not None:
+        s = shape[axis]
+        # Dask arrays uses non-standard NaN instead of None
+        if s is None or math.isnan(s):
+            msg = f"Unsupported lazy shape for axis {axis}"
+            raise TypeError(msg)
+        return (s,)
+
+    if any(s is None or math.isnan(s) for s in shape):
+        msg = "Unsupported lazy shape"
+        raise TypeError(msg)
+    return cast(tuple[int, ...], shape)
+
+
+def meta_namespace(
+    *arrays: Array | complex | None, xp: ModuleType | None = None
+) -> ModuleType:
+    """
+    Get the namespace of Dask chunks.
+
+    On all other backends, just return the namespace of the arrays.
+
+    Parameters
+    ----------
+    *arrays : Array | int | float | complex | bool | None
+        Input arrays.
+    xp : array_namespace, optional
+        The standard-compatible namespace for the input arrays. Default: infer.
+
+    Returns
+    -------
+    array_namespace
+        If xp is Dask, the namespace of the Dask chunks;
+        otherwise, the namespace of the arrays.
+    """
+    xp = array_namespace(*arrays) if xp is None else xp
+    if not is_dask_namespace(xp):
+        return xp
+    # Quietly skip scalars and None's
+    metas = [cast(Array | None, getattr(a, "_meta", None)) for a in arrays]
+    return array_namespace(*metas)
+
+
+def capabilities(
+    xp: ModuleType, *, device: Device | None = None
+) -> dict[str, int | None]:
+    """
+    Return patched ``xp.__array_namespace_info__().capabilities()``.
+
+    TODO this helper should be eventually removed once all the special cases
+    it handles are fixed in the respective backends.
+
+    Parameters
+    ----------
+    xp : array_namespace
+        The standard-compatible namespace.
+    device : Device, optional
+        The device to use.
+
+    Returns
+    -------
+    dict
+        Capabilities of the namespace.
+    """
+    out = xp.__array_namespace_info__().capabilities()
+    if is_pydata_sparse_namespace(xp):
+        if out["boolean indexing"]:
+            # FIXME https://github.com/pydata/sparse/issues/876
+            # boolean indexing is supported, but not when the index is a sparse array.
+            # boolean indexing by list or numpy array is not part of the Array API.
+            out = out.copy()
+            out["boolean indexing"] = False
+    elif is_jax_namespace(xp):
+        if out["boolean indexing"]:  # pragma: no cover
+            # Backwards compatibility with jax <0.6.0
+            # https://github.com/jax-ml/jax/issues/27418
+            out = out.copy()
+            out["boolean indexing"] = False
+    elif is_torch_namespace(xp):
+        # FIXME https://github.com/data-apis/array-api/issues/945
+        device = xp.get_default_device() if device is None else xp.device(device)
+        if device.type == "meta":  # type: ignore[union-attr]  # pyright: ignore[reportAttributeAccessIssue,reportOptionalMemberAccess]
+            out = out.copy()
+            out["boolean indexing"] = False
+            out["data-dependent shapes"] = False
+
+    return out
+
+
+_BASIC_PICKLED_TYPES = frozenset((
+    bool, int, float, complex, str, bytes, bytearray,
+    list, tuple, dict, set, frozenset, range, slice,
+    types.NoneType, types.EllipsisType,
+))  # fmt: skip
+_BASIC_REST_TYPES = frozenset((
+    type, types.BuiltinFunctionType, types.FunctionType, types.ModuleType
+))  # fmt: skip
+
+FlattenRest: TypeAlias = tuple[object, ...]
+
+
+def pickle_flatten(
+    obj: object, cls: type[T] | tuple[type[T], ...]
+) -> tuple[list[T], FlattenRest]:
+    """
+    Use the pickle machinery to extract objects out of an arbitrary container.
+
+    Unlike regular ``pickle.dumps``, this function always succeeds.
+
+    Parameters
+    ----------
+    obj : object
+        The object to pickle.
+    cls : type | tuple[type, ...]
+        One or multiple classes to extract from the object.
+        The instances of these classes inside ``obj`` will not be pickled.
+
+    Returns
+    -------
+    instances : list[cls]
+        All instances of ``cls`` found inside ``obj`` (not pickled).
+    rest
+        Opaque object containing the pickled bytes plus all other objects where
+        ``__reduce__`` / ``__reduce_ex__`` is either not implemented or raised.
+        These are unpickleable objects, types, modules, and functions.
+
+        This object is *typically* hashable save for fairly exotic objects
+        that are neither pickleable nor hashable.
+
+        This object is pickleable if everything except ``instances`` was pickleable
+        in the input object.
+
+    See Also
+    --------
+    pickle_unflatten : Reverse function.
+
+    Examples
+    --------
+    >>> class A:
+    ...     def __repr__(self):
+    ...         return ""
+    >>> class NS:
+    ...     def __repr__(self):
+    ...         return ""
+    ...     def __reduce__(self):
+    ...         assert False, "not serializable"
+    >>> obj = {1: A(), 2: [A(), NS(), A()]}
+    >>> instances, rest = pickle_flatten(obj, A)
+    >>> instances
+    [, , ]
+    >>> pickle_unflatten(instances, rest)
+    {1: , 2: [, , ]}
+
+    This can be also used to swap inner objects; the only constraint is that
+    the number of objects in and out must be the same:
+
+    >>> pickle_unflatten(["foo", "bar", "baz"], rest)
+    {1: "foo", 2: ["bar", , "baz"]}
+    """
+    instances: list[T] = []
+    rest: list[object] = []
+
+    class Pickler(pickle.Pickler):  # numpydoc ignore=GL08
+        """
+        Use the `pickle.Pickler.persistent_id` hook to extract objects.
+        """
+
+        @override
+        def persistent_id(
+            self, obj: object
+        ) -> Literal[0, 1, None]:  # numpydoc ignore=GL08
+            if isinstance(obj, cls):
+                instances.append(obj)  # type: ignore[arg-type]
+                return 0
+
+            typ_ = type(obj)
+            if typ_ in _BASIC_PICKLED_TYPES:  # No subclasses!
+                # If obj is a collection, recursively descend inside it
+                return None
+            if typ_ in _BASIC_REST_TYPES:
+                rest.append(obj)
+                return 1
+
+            try:
+                # Note: a class that defines __slots__ without defining __getstate__
+                # cannot be pickled with __reduce__(), but can with __reduce_ex__(5)
+                _ = obj.__reduce_ex__(pickle.HIGHEST_PROTOCOL)
+            except Exception:  # pylint: disable=broad-exception-caught
+                rest.append(obj)
+                return 1
+
+            # Object can be pickled. Let the Pickler recursively descend inside it.
+            return None
+
+    f = io.BytesIO()
+    p = Pickler(f, protocol=pickle.HIGHEST_PROTOCOL)
+    p.dump(obj)
+    return instances, (f.getvalue(), *rest)
+
+
+def pickle_unflatten(instances: Iterable[object], rest: FlattenRest) -> Any:
+    """
+    Reverse of ``pickle_flatten``.
+
+    Parameters
+    ----------
+    instances : Iterable
+        Inner objects to be reinserted into the flattened container.
+    rest : FlattenRest
+        Extra bits, as returned by ``pickle_flatten``.
+
+    Returns
+    -------
+    object
+        The outer object originally passed to ``pickle_flatten`` after a
+        pickle->unpickle round-trip.
+
+    See Also
+    --------
+    pickle_flatten : Serializing function.
+    pickle.loads : Standard unpickle function.
+
+    Notes
+    -----
+    The `instances` iterable must yield at least the same number of elements as the ones
+    returned by ``pickle_flatten``, but the elements do not need to be the same objects
+    or even the same types of objects. Excess elements, if any, will be left untouched.
+    """
+    iters = iter(instances), iter(rest)
+    pik = cast(bytes, next(iters[1]))
+
+    class Unpickler(pickle.Unpickler):  # numpydoc ignore=GL08
+        """Mirror of the overridden Pickler in pickle_flatten."""
+
+        @override
+        def persistent_load(self, pid: Literal[0, 1]) -> object:  # numpydoc ignore=GL08
+            try:
+                return next(iters[pid])
+            except StopIteration as e:
+                msg = "Not enough objects to unpickle"
+                raise ValueError(msg) from e
+
+    f = io.BytesIO(pik)
+    return Unpickler(f).load()
+
+
+class _AutoJITWrapper(Generic[T]):  # numpydoc ignore=PR01
+    """
+    Helper of :func:`jax_autojit`.
+
+    Wrap arbitrary inputs and outputs of the jitted function and
+    convert them to/from PyTrees.
+    """
+
+    _obj: Any
+    _is_iter: bool
+    _registered: ClassVar[bool] = False
+    __slots__: tuple[str, ...] = ("_is_iter", "_obj")
+
+    def __init__(self, obj: T) -> None:  # numpydoc ignore=GL08
+        self._register()
+        if isinstance(obj, Iterator):
+            self._obj = list(obj)
+            self._is_iter = True
+        else:
+            self._obj = obj
+            self._is_iter = False
+
+    @property
+    def obj(self) -> T:  # numpydoc ignore=RT01
+        """Return wrapped object."""
+        return iter(self._obj) if self._is_iter else self._obj
+
+    @classmethod
+    def _register(cls) -> None:  # numpydoc ignore=SS06
+        """
+        Register upon first use instead of at import time, to avoid
+        globally importing JAX.
+        """
+        if not cls._registered:
+            import jax
+
+            jax.tree_util.register_pytree_node(
+                cls,
+                lambda instance: pickle_flatten(instance, jax.Array),  # pyright: ignore[reportUnknownArgumentType]
+                lambda aux_data, children: pickle_unflatten(children, aux_data),  # pyright: ignore[reportUnknownArgumentType]
+            )
+            cls._registered = True
+
+
+def jax_autojit(
+    func: Callable[P, T],
+) -> Callable[P, T]:  # numpydoc ignore=PR01,RT01,SS03
+    """
+    Wrap `func` with ``jax.jit``, with the following differences:
+
+    - Python scalar arguments and return values are not automatically converted to
+      ``jax.Array`` objects.
+    - All non-array arguments are automatically treated as static.
+      Unlike ``jax.jit``, static arguments must be either hashable or serializable with
+      ``pickle``.
+    - Unlike ``jax.jit``, non-array arguments and return values are not limited to
+      tuple/list/dict, but can be any object serializable with ``pickle``.
+    - Automatically descend into non-array arguments and find ``jax.Array`` objects
+      inside them, then rebuild the arguments when entering `func`, swapping the JAX
+      concrete arrays with tracer objects.
+    - Automatically descend into non-array return values and find ``jax.Array`` objects
+      inside them, then rebuild them downstream of exiting the JIT, swapping the JAX
+      tracer objects with concrete arrays.
+    - Returned iterators are immediately completely consumed.
+
+    See Also
+    --------
+    jax.jit : JAX JIT compilation function.
+
+    Notes
+    -----
+    These are useful choices *for testing purposes only*, which is how this function is
+    intended to be used. The output of ``jax.jit`` is a C++ level callable, that
+    directly dispatches to the compiled kernel after the initial call. In comparison,
+    ``jax_autojit`` incurs a much higher dispatch time.
+
+    Additionally, consider::
+
+        def f(x: Array, y: float, plus: bool) -> Array:
+            return x + y if plus else x - y
+
+        j1 = jax.jit(f, static_argnames="plus")
+        j2 = jax_autojit(f)
+
+    In the above example, ``j2`` requires a lot less setup to be tested effectively than
+    ``j1``, but on the flip side it means that it will be re-traced for every different
+    value of ``y``, which likely makes it not fit for purpose in production.
+    """
+    import jax
+
+    @jax.jit  # type: ignore[misc]  # pyright: ignore[reportUntypedFunctionDecorator]
+    def inner(  # numpydoc ignore=GL08
+        wargs: _AutoJITWrapper[Any],
+    ) -> _AutoJITWrapper[T]:
+        args, kwargs = wargs.obj
+        res = func(*args, **kwargs)  # pyright: ignore[reportCallIssue]
+        return _AutoJITWrapper(res)
+
+    @wraps(func)
+    def outer(*args: P.args, **kwargs: P.kwargs) -> T:  # numpydoc ignore=GL08
+        wargs = _AutoJITWrapper((args, kwargs))
+        return inner(wargs).obj
+
+    return outer
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/_typing.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/_typing.py
new file mode 100644
index 0000000000000000000000000000000000000000..8204be4759610b1ddb33b3579ccaa5f3a1f0866a
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/_typing.py
@@ -0,0 +1,10 @@
+# numpydoc ignore=GL08
+# pylint: disable=missing-module-docstring,duplicate-code
+
+Array = object
+DType = object
+Device = object
+GetIndex = object
+SetIndex = object
+
+__all__ = ["Array", "DType", "Device", "GetIndex", "SetIndex"]
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/_typing.pyi b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/_typing.pyi
new file mode 100644
index 0000000000000000000000000000000000000000..35c255fc9ad5c6c93e1b988dc9fe1fc6543ee0a6
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/_lib/_utils/_typing.pyi
@@ -0,0 +1,105 @@
+"""Static typing helpers."""
+
+from __future__ import annotations
+
+from types import EllipsisType
+from typing import Protocol, TypeAlias
+
+# TODO import from typing (requires Python >=3.12)
+from typing_extensions import override
+
+# TODO: use array-api-typing once it is available
+
+class Array(Protocol):  # pylint: disable=missing-class-docstring
+    # Unary operations
+    def __abs__(self) -> Array: ...
+    def __pos__(self) -> Array: ...
+    def __neg__(self) -> Array: ...
+    def __invert__(self) -> Array: ...
+    # Binary operations
+    def __add__(self, other: Array | complex, /) -> Array: ...
+    def __sub__(self, other: Array | complex, /) -> Array: ...
+    def __mul__(self, other: Array | complex, /) -> Array: ...
+    def __truediv__(self, other: Array | complex, /) -> Array: ...
+    def __floordiv__(self, other: Array | complex, /) -> Array: ...
+    def __mod__(self, other: Array | complex, /) -> Array: ...
+    def __pow__(self, other: Array | complex, /) -> Array: ...
+    def __matmul__(self, other: Array, /) -> Array: ...
+    def __and__(self, other: Array | int, /) -> Array: ...
+    def __or__(self, other: Array | int, /) -> Array: ...
+    def __xor__(self, other: Array | int, /) -> Array: ...
+    def __lshift__(self, other: Array | int, /) -> Array: ...
+    def __rshift__(self, other: Array | int, /) -> Array: ...
+    def __lt__(self, other: Array | complex, /) -> Array: ...
+    def __le__(self, other: Array | complex, /) -> Array: ...
+    def __gt__(self, other: Array | complex, /) -> Array: ...
+    def __ge__(self, other: Array | complex, /) -> Array: ...
+    @override
+    def __eq__(self, other: Array | complex, /) -> Array: ...  # type: ignore[override]  # pyright: ignore[reportIncompatibleMethodOverride]
+    @override
+    def __ne__(self, other: Array | complex, /) -> Array: ...  # type: ignore[override]  # pyright: ignore[reportIncompatibleMethodOverride]
+    # Reflected operations
+    def __radd__(self, other: Array | complex, /) -> Array: ...
+    def __rsub__(self, other: Array | complex, /) -> Array: ...
+    def __rmul__(self, other: Array | complex, /) -> Array: ...
+    def __rtruediv__(self, other: Array | complex, /) -> Array: ...
+    def __rfloordiv__(self, other: Array | complex, /) -> Array: ...
+    def __rmod__(self, other: Array | complex, /) -> Array: ...
+    def __rpow__(self, other: Array | complex, /) -> Array: ...
+    def __rmatmul__(self, other: Array, /) -> Array: ...
+    def __rand__(self, other: Array | int, /) -> Array: ...
+    def __ror__(self, other: Array | int, /) -> Array: ...
+    def __rxor__(self, other: Array | int, /) -> Array: ...
+    def __rlshift__(self, other: Array | int, /) -> Array: ...
+    def __rrshift__(self, other: Array | int, /) -> Array: ...
+    # Attributes
+    @property
+    def dtype(self) -> DType: ...
+    @property
+    def device(self) -> Device: ...
+    @property
+    def mT(self) -> Array: ...  # pylint: disable=invalid-name
+    @property
+    def ndim(self) -> int: ...
+    @property
+    def shape(self) -> tuple[int | None, ...]: ...
+    @property
+    def size(self) -> int | None: ...
+    @property
+    def T(self) -> Array: ...  # pylint: disable=invalid-name
+    # Collection operations (note: an Array does not have to be Sized or Iterable)
+    def __getitem__(self, key: GetIndex, /) -> Array: ...
+    def __setitem__(self, key: SetIndex, value: Array | complex, /) -> None: ...
+    # Materialization methods (may raise on lazy arrays)
+    def __bool__(self) -> bool: ...
+    def __complex__(self) -> complex: ...
+    def __float__(self) -> float: ...
+    def __index__(self) -> int: ...
+    def __int__(self) -> int: ...
+
+    # Misc methods (frequently not implemented in Arrays wrapped by array-api-compat)
+    # def __array_namespace__(*, api_version: str | None) -> ModuleType: ...
+    # def __dlpack__(
+    #     *,
+    #     stream: int | Any | None = None,
+    #     max_version: tuple[int, int] | None = None,
+    #     dl_device: tuple[int, int] | None = None,  # tuple[Enum, int]
+    #     copy: bool | None = None,
+    # ) -> Any: ...
+    # def __dlpack_device__() -> tuple[int, int]: ...  # tuple[Enum, int]
+    # def to_device(device: Device, /, *, stream: int | Any | None = None) -> Array: ...
+
+class DType(Protocol):  # pylint: disable=missing-class-docstring
+    pass
+
+class Device(Protocol):  # pylint: disable=missing-class-docstring
+    pass
+
+SetIndex: TypeAlias = (
+    int | slice | EllipsisType | Array | tuple[int | slice | EllipsisType | Array, ...]
+)
+GetIndex: TypeAlias = (
+    SetIndex | None | tuple[int | slice | EllipsisType | None | Array, ...]
+)
+
+__all__ = ["Array", "DType", "Device", "GetIndex", "SetIndex"]
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/testing.py b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/testing.py
new file mode 100644
index 0000000000000000000000000000000000000000..d40fea1a085319c82a7d89e21f2182b5be5ad3c9
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/array_api_extra/testing.py
@@ -0,0 +1,415 @@
+"""
+Public testing utilities.
+
+See also _lib._testing for additional private testing utilities.
+"""
+
+from __future__ import annotations
+
+import contextlib
+import enum
+import warnings
+from collections.abc import Callable, Generator, Iterator, Sequence
+from functools import wraps
+from types import ModuleType
+from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, cast
+
+from ._lib._utils._compat import is_dask_namespace, is_jax_namespace
+from ._lib._utils._helpers import jax_autojit, pickle_flatten, pickle_unflatten
+
+__all__ = ["lazy_xp_function", "patch_lazy_xp_functions"]
+
+if TYPE_CHECKING:  # pragma: no cover
+    # TODO import override from typing (requires Python >=3.12)
+    import pytest
+    from dask.typing import Graph, Key, SchedulerGetCallable
+    from typing_extensions import override
+
+else:
+    # Sphinx hacks
+    SchedulerGetCallable = object
+
+    def override(func):
+        return func
+
+
+P = ParamSpec("P")
+T = TypeVar("T")
+
+_ufuncs_tags: dict[object, dict[str, Any]] = {}
+
+
+class Deprecated(enum.Enum):
+    """Unique type for deprecated parameters."""
+
+    DEPRECATED = 1
+
+
+DEPRECATED = Deprecated.DEPRECATED
+
+
+def lazy_xp_function(
+    func: Callable[..., Any],
+    *,
+    allow_dask_compute: bool | int = False,
+    jax_jit: bool = True,
+    static_argnums: Deprecated = DEPRECATED,
+    static_argnames: Deprecated = DEPRECATED,
+) -> None:  # numpydoc ignore=GL07
+    """
+    Tag a function to be tested on lazy backends.
+
+    Tag a function so that when any tests are executed with ``xp=jax.numpy`` the
+    function is replaced with a jitted version of itself, and when it is executed with
+    ``xp=dask.array`` the function will raise if it attempts to materialize the graph.
+    This will be later expanded to provide test coverage for other lazy backends.
+
+    In order for the tag to be effective, the test or a fixture must call
+    :func:`patch_lazy_xp_functions`.
+
+    Parameters
+    ----------
+    func : callable
+        Function to be tested.
+    allow_dask_compute : bool | int, optional
+        Whether `func` is allowed to internally materialize the Dask graph, or maximum
+        number of times it is allowed to do so. This is typically triggered by
+        ``bool()``, ``float()``, or ``np.asarray()``.
+
+        Set to 1 if you are aware that `func` converts the input parameters to NumPy and
+        want to let it do so at least for the time being, knowing that it is going to be
+        extremely detrimental for performance.
+
+        If a test needs values higher than 1 to pass, it is a canary that the conversion
+        to NumPy/bool/float is happening multiple times, which translates to multiple
+        computations of the whole graph. Short of making the function fully lazy, you
+        should at least add explicit calls to ``np.asarray()`` early in the function.
+        *Note:* the counter of `allow_dask_compute` resets after each call to `func`, so
+        a test function that invokes `func` multiple times should still work with this
+        parameter set to 1.
+
+        Set to True to allow `func` to materialize the graph an unlimited number
+        of times.
+
+        Default: False, meaning that `func` must be fully lazy and never materialize the
+        graph.
+    jax_jit : bool, optional
+        Set to True to replace `func` with a smart variant of ``jax.jit(func)`` after
+        calling the :func:`patch_lazy_xp_functions` test helper with ``xp=jax.numpy``.
+        This is the default behaviour.
+        Set to False if `func` is only compatible with eager (non-jitted) JAX.
+
+        Unlike with vanilla ``jax.jit``, all arguments and return types that are not JAX
+        arrays are treated as static; the function can accept and return arbitrary
+        wrappers around JAX arrays. This difference is because, in real life, most users
+        won't wrap the function directly with ``jax.jit`` but rather they will use it
+        within their own code, which is itself then wrapped by ``jax.jit``, and
+        internally consume the function's outputs.
+
+        In other words, the pattern that is being tested is::
+
+            >>> @jax.jit
+            ... def user_func(x):
+            ...     y = user_prepares_inputs(x)
+            ...     z = func(y, some_static_arg=True)
+            ...     return user_consumes(z)
+
+        Default: True.
+    static_argnums :
+        Deprecated; ignored
+    static_argnames :
+        Deprecated; ignored
+
+    See Also
+    --------
+    patch_lazy_xp_functions : Companion function to call from the test or fixture.
+    jax.jit : JAX function to compile a function for performance.
+
+    Examples
+    --------
+    In ``test_mymodule.py``::
+
+      from array_api_extra.testing import lazy_xp_function from mymodule import myfunc
+
+      lazy_xp_function(myfunc)
+
+      def test_myfunc(xp):
+          a = xp.asarray([1, 2])
+          # When xp=jax.numpy, this is similar to `b = jax.jit(myfunc)(a)`
+          # When xp=dask.array, crash on compute() or persist()
+          b = myfunc(a)
+
+    Notes
+    -----
+    In order for this tag to be effective, the test function must be imported into the
+    test module globals without its namespace; alternatively its namespace must be
+    declared in a ``lazy_xp_modules`` list in the test module globals.
+
+    Example 1::
+
+      from mymodule import myfunc
+
+      lazy_xp_function(myfunc)
+
+      def test_myfunc(xp):
+          x = myfunc(xp.asarray([1, 2]))
+
+    Example 2::
+
+      import mymodule
+
+      lazy_xp_modules = [mymodule]
+      lazy_xp_function(mymodule.myfunc)
+
+      def test_myfunc(xp):
+          x = mymodule.myfunc(xp.asarray([1, 2]))
+
+    A test function can circumvent this monkey-patching system by using a namespace
+    outside of the two above patterns. You need to sanitize your code to make sure this
+    only happens intentionally.
+
+    Example 1::
+
+      import mymodule
+      from mymodule import myfunc
+
+      lazy_xp_function(myfunc)
+
+      def test_myfunc(xp):
+          a = xp.asarray([1, 2])
+          b = myfunc(a)  # This is wrapped when xp=jax.numpy or xp=dask.array
+          c = mymodule.myfunc(a)  # This is not
+
+    Example 2::
+
+      import mymodule
+
+      class naked:
+          myfunc = mymodule.myfunc
+
+      lazy_xp_modules = [mymodule]
+      lazy_xp_function(mymodule.myfunc)
+
+      def test_myfunc(xp):
+          a = xp.asarray([1, 2])
+          b = mymodule.myfunc(a)  # This is wrapped when xp=jax.numpy or xp=dask.array
+          c = naked.myfunc(a)  # This is not
+    """
+    if static_argnums is not DEPRECATED or static_argnames is not DEPRECATED:
+        warnings.warn(
+            (
+                "The `static_argnums` and `static_argnames` parameters are deprecated "
+                "and ignored. They will be removed in a future version."
+            ),
+            DeprecationWarning,
+            stacklevel=2,
+        )
+    tags = {
+        "allow_dask_compute": allow_dask_compute,
+        "jax_jit": jax_jit,
+    }
+
+    try:
+        func._lazy_xp_function = tags  # type: ignore[attr-defined]  # pylint: disable=protected-access  # pyright: ignore[reportFunctionMemberAccess]
+    except AttributeError:  # @cython.vectorize
+        _ufuncs_tags[func] = tags
+
+
+def patch_lazy_xp_functions(
+    request: pytest.FixtureRequest,
+    monkeypatch: pytest.MonkeyPatch | None = None,
+    *,
+    xp: ModuleType,
+) -> contextlib.AbstractContextManager[None]:
+    """
+    Test lazy execution of functions tagged with :func:`lazy_xp_function`.
+
+    If ``xp==jax.numpy``, search for all functions which have been tagged with
+    :func:`lazy_xp_function` in the globals of the module that defines the current test,
+    as well as in the ``lazy_xp_modules`` list in the globals of the same module,
+    and wrap them with :func:`jax.jit`. Unwrap them at the end of the test.
+
+    If ``xp==dask.array``, wrap the functions with a decorator that disables
+    ``compute()`` and ``persist()`` and ensures that exceptions and warnings are raised
+    eagerly.
+
+    This function should be typically called by your library's `xp` fixture that runs
+    tests on multiple backends::
+
+        @pytest.fixture(params=[
+            numpy,
+            array_api_strict,
+            pytest.param(jax.numpy, marks=pytest.mark.thread_unsafe),
+            pytest.param(dask.array, marks=pytest.mark.thread_unsafe),
+        ])
+        def xp(request):
+            with patch_lazy_xp_functions(request, xp=request.param):
+                yield request.param
+
+    but it can be otherwise be called by the test itself too.
+
+    Parameters
+    ----------
+    request : pytest.FixtureRequest
+        Pytest fixture, as acquired by the test itself or by one of its fixtures.
+    monkeypatch : pytest.MonkeyPatch
+        Deprecated
+    xp : array_namespace
+        Array namespace to be tested.
+
+    See Also
+    --------
+    lazy_xp_function : Tag a function to be tested on lazy backends.
+    pytest.FixtureRequest : `request` test function parameter.
+
+    Notes
+    -----
+    This context manager monkey-patches modules and as such is thread unsafe
+    on Dask and JAX. If you run your test suite with
+    `pytest-run-parallel `_,
+    you should mark these backends with ``@pytest.mark.thread_unsafe``, as shown in
+    the example above.
+    """
+    mod = cast(ModuleType, request.module)
+    mods = [mod, *cast(list[ModuleType], getattr(mod, "lazy_xp_modules", []))]
+
+    to_revert: list[tuple[ModuleType, str, object]] = []
+
+    def temp_setattr(mod: ModuleType, name: str, func: object) -> None:
+        """
+        Variant of monkeypatch.setattr, which allows monkey-patching only selected
+        parameters of a test so that pytest-run-parallel can run on the remainder.
+        """
+        assert hasattr(mod, name)
+        to_revert.append((mod, name, getattr(mod, name)))
+        setattr(mod, name, func)
+
+    if monkeypatch is not None:
+        warnings.warn(
+            (
+                "The `monkeypatch` parameter is deprecated and will be removed in a "
+                "future version. "
+                "Use `patch_lazy_xp_function` as a context manager instead."
+            ),
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        # Enable using patch_lazy_xp_function not as a context manager
+        temp_setattr = monkeypatch.setattr  # type: ignore[assignment]  # pyright: ignore[reportAssignmentType]
+
+    def iter_tagged() -> Iterator[
+        tuple[ModuleType, str, Callable[..., Any], dict[str, Any]]
+    ]:
+        for mod in mods:
+            for name, func in mod.__dict__.items():
+                tags: dict[str, Any] | None = None
+                with contextlib.suppress(AttributeError):
+                    tags = func._lazy_xp_function  # pylint: disable=protected-access
+                if tags is None:
+                    with contextlib.suppress(KeyError, TypeError):
+                        tags = _ufuncs_tags[func]
+                if tags is not None:
+                    yield mod, name, func, tags
+
+    if is_dask_namespace(xp):
+        for mod, name, func, tags in iter_tagged():
+            n = tags["allow_dask_compute"]
+            if n is True:
+                n = 1_000_000
+            elif n is False:
+                n = 0
+            wrapped = _dask_wrap(func, n)
+            temp_setattr(mod, name, wrapped)
+
+    elif is_jax_namespace(xp):
+        for mod, name, func, tags in iter_tagged():
+            if tags["jax_jit"]:
+                wrapped = jax_autojit(func)
+                temp_setattr(mod, name, wrapped)
+
+    # We can't just decorate patch_lazy_xp_functions with
+    # @contextlib.contextmanager because it would not work with the
+    # deprecated monkeypatch when not used as a context manager.
+    @contextlib.contextmanager
+    def revert_on_exit() -> Generator[None]:
+        try:
+            yield
+        finally:
+            for mod, name, orig_func in to_revert:
+                setattr(mod, name, orig_func)
+
+    return revert_on_exit()
+
+
+class CountingDaskScheduler(SchedulerGetCallable):
+    """
+    Dask scheduler that counts how many times `dask.compute` is called.
+
+    If the number of times exceeds 'max_count', it raises an error.
+    This is a wrapper around Dask's own 'synchronous' scheduler.
+
+    Parameters
+    ----------
+    max_count : int
+        Maximum number of allowed calls to `dask.compute`.
+    msg : str
+        Assertion to raise when the count exceeds `max_count`.
+    """
+
+    count: int
+    max_count: int
+    msg: str
+
+    def __init__(self, max_count: int, msg: str):  # numpydoc ignore=GL08
+        self.count = 0
+        self.max_count = max_count
+        self.msg = msg
+
+    @override
+    def __call__(
+        self, dsk: Graph, keys: Sequence[Key] | Key, **kwargs: Any
+    ) -> Any:  # numpydoc ignore=GL08
+        import dask
+
+        self.count += 1
+        # This should yield a nice traceback to the
+        # offending line in the user's code
+        assert self.count <= self.max_count, self.msg
+
+        return dask.get(dsk, keys, **kwargs)  # type: ignore[attr-defined]  # pyright: ignore[reportPrivateImportUsage]
+
+
+def _dask_wrap(
+    func: Callable[P, T], n: int
+) -> Callable[P, T]:  # numpydoc ignore=PR01,RT01
+    """
+    Wrap `func` to raise if it attempts to call `dask.compute` more than `n` times.
+
+    After the function returns, materialize the graph in order to re-raise exceptions.
+    """
+    import dask
+    import dask.array as da
+
+    func_name = getattr(func, "__name__", str(func))
+    n_str = f"only up to {n}" if n else "no"
+    msg = (
+        f"Called `dask.compute()` or `dask.persist()` {n + 1} times, "
+        f"but {n_str} calls are allowed. Set "
+        f"`lazy_xp_function({func_name}, allow_dask_compute={n + 1})` "
+        "to allow for more (but note that this will harm performance). "
+    )
+
+    @wraps(func)
+    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:  # numpydoc ignore=GL08
+        scheduler = CountingDaskScheduler(n, msg)
+        with dask.config.set({"scheduler": scheduler}):  # pyright: ignore[reportPrivateImportUsage]
+            out = func(*args, **kwargs)
+
+        # Block until the graph materializes and reraise exceptions. This allows
+        # `pytest.raises` and `pytest.warns` to work as expected. Note that this would
+        # not work on scheduler='distributed', as it would not block.
+        arrays, rest = pickle_flatten(out, da.Array)
+        arrays = dask.persist(arrays, scheduler="threads")[0]  # type: ignore[attr-defined,no-untyped-call]  # pyright: ignore[reportPrivateImportUsage]
+        return pickle_unflatten(arrays, rest)  # pyright: ignore[reportUnknownArgumentType]
+
+    return wrapper
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/__init__.py b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..2654c8a3d21ed3db207f6204816d852e6b4c0efc
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/__init__.py
@@ -0,0 +1,20 @@
+from .main import minimize
+from .utils import show_versions
+
+# PEP0440 compatible formatted version, see:
+# https://www.python.org/dev/peps/pep-0440/
+#
+# Final release markers:
+#   X.Y.0   # For first release after an increment in Y
+#   X.Y.Z   # For bugfix releases
+#
+# Admissible pre-release markers:
+#   X.YaN   # Alpha release
+#   X.YbN   # Beta release
+#   X.YrcN  # Release Candidate
+#
+# Dev branch marker is: 'X.Y.dev' or 'X.Y.devN' where N is an integer.
+# 'X.Y.dev0' is the canonical version of 'X.Y.dev'.
+__version__ = "1.1.3"
+
+__all__ = ["minimize", "show_versions"]
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/framework.py b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/framework.py
new file mode 100644
index 0000000000000000000000000000000000000000..9afea66281067e27a486ff317a4bffa03ec3e68b
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/framework.py
@@ -0,0 +1,1240 @@
+import warnings
+
+import numpy as np
+from scipy.optimize import lsq_linear
+
+from .models import Models, Quadratic
+from .settings import Options, Constants
+from .subsolvers import (
+    cauchy_geometry,
+    spider_geometry,
+    normal_byrd_omojokun,
+    tangential_byrd_omojokun,
+    constrained_tangential_byrd_omojokun,
+)
+from .subsolvers.optim import qr_tangential_byrd_omojokun
+from .utils import get_arrays_tol
+
+
+TINY = np.finfo(float).tiny
+EPS = np.finfo(float).eps
+
+
+class TrustRegion:
+    """
+    Trust-region framework.
+    """
+
+    def __init__(self, pb, options, constants):
+        """
+        Initialize the trust-region framework.
+
+        Parameters
+        ----------
+        pb : `cobyqa.problem.Problem`
+            Problem to solve.
+        options : dict
+            Options of the solver.
+        constants : dict
+            Constants of the solver.
+
+        Raises
+        ------
+        `cobyqa.utils.MaxEvalError`
+            If the maximum number of evaluations is reached.
+        `cobyqa.utils.TargetSuccess`
+            If a nearly feasible point has been found with an objective
+            function value below the target.
+        `cobyqa.utils.FeasibleSuccess`
+            If a feasible point has been found for a feasibility problem.
+        `numpy.linalg.LinAlgError`
+            If the initial interpolation system is ill-defined.
+        """
+        # Set the initial penalty parameter.
+        self._penalty = 0.0
+
+        # Initialize the models.
+        self._pb = pb
+        self._models = Models(self._pb, options, self.penalty)
+        self._constants = constants
+
+        # Set the index of the best interpolation point.
+        self._best_index = 0
+        self.set_best_index()
+
+        # Set the initial Lagrange multipliers.
+        self._lm_linear_ub = np.zeros(self.m_linear_ub)
+        self._lm_linear_eq = np.zeros(self.m_linear_eq)
+        self._lm_nonlinear_ub = np.zeros(self.m_nonlinear_ub)
+        self._lm_nonlinear_eq = np.zeros(self.m_nonlinear_eq)
+        self.set_multipliers(self.x_best)
+
+        # Set the initial trust-region radius and the resolution.
+        self._resolution = options[Options.RHOBEG]
+        self._radius = self.resolution
+
+    @property
+    def n(self):
+        """
+        Number of variables.
+
+        Returns
+        -------
+        int
+            Number of variables.
+        """
+        return self._pb.n
+
+    @property
+    def m_linear_ub(self):
+        """
+        Number of linear inequality constraints.
+
+        Returns
+        -------
+        int
+            Number of linear inequality constraints.
+        """
+        return self._pb.m_linear_ub
+
+    @property
+    def m_linear_eq(self):
+        """
+        Number of linear equality constraints.
+
+        Returns
+        -------
+        int
+            Number of linear equality constraints.
+        """
+        return self._pb.m_linear_eq
+
+    @property
+    def m_nonlinear_ub(self):
+        """
+        Number of nonlinear inequality constraints.
+
+        Returns
+        -------
+        int
+            Number of nonlinear inequality constraints.
+        """
+        return self._pb.m_nonlinear_ub
+
+    @property
+    def m_nonlinear_eq(self):
+        """
+        Number of nonlinear equality constraints.
+
+        Returns
+        -------
+        int
+            Number of nonlinear equality constraints.
+        """
+        return self._pb.m_nonlinear_eq
+
+    @property
+    def radius(self):
+        """
+        Trust-region radius.
+
+        Returns
+        -------
+        float
+            Trust-region radius.
+        """
+        return self._radius
+
+    @radius.setter
+    def radius(self, radius):
+        """
+        Set the trust-region radius.
+
+        Parameters
+        ----------
+        radius : float
+            New trust-region radius.
+        """
+        self._radius = radius
+        if (
+            self.radius
+            <= self._constants[Constants.DECREASE_RADIUS_THRESHOLD]
+            * self.resolution
+        ):
+            self._radius = self.resolution
+
+    @property
+    def resolution(self):
+        """
+        Resolution of the trust-region framework.
+
+        The resolution is a lower bound on the trust-region radius.
+
+        Returns
+        -------
+        float
+            Resolution of the trust-region framework.
+        """
+        return self._resolution
+
+    @resolution.setter
+    def resolution(self, resolution):
+        """
+        Set the resolution of the trust-region framework.
+
+        Parameters
+        ----------
+        resolution : float
+            New resolution of the trust-region framework.
+        """
+        self._resolution = resolution
+
+    @property
+    def penalty(self):
+        """
+        Penalty parameter.
+
+        Returns
+        -------
+        float
+            Penalty parameter.
+        """
+        return self._penalty
+
+    @property
+    def models(self):
+        """
+        Models of the objective function and constraints.
+
+        Returns
+        -------
+        `cobyqa.models.Models`
+            Models of the objective function and constraints.
+        """
+        return self._models
+
+    @property
+    def best_index(self):
+        """
+        Index of the best interpolation point.
+
+        Returns
+        -------
+        int
+            Index of the best interpolation point.
+        """
+        return self._best_index
+
+    @property
+    def x_best(self):
+        """
+        Best interpolation point.
+
+        Its value is interpreted as relative to the origin, not the base point.
+
+        Returns
+        -------
+        `numpy.ndarray`
+            Best interpolation point.
+        """
+        return self.models.interpolation.point(self.best_index)
+
+    @property
+    def fun_best(self):
+        """
+        Value of the objective function at `x_best`.
+
+        Returns
+        -------
+        float
+            Value of the objective function at `x_best`.
+        """
+        return self.models.fun_val[self.best_index]
+
+    @property
+    def cub_best(self):
+        """
+        Values of the nonlinear inequality constraints at `x_best`.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (m_nonlinear_ub,)
+            Values of the nonlinear inequality constraints at `x_best`.
+        """
+        return self.models.cub_val[self.best_index, :]
+
+    @property
+    def ceq_best(self):
+        """
+        Values of the nonlinear equality constraints at `x_best`.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (m_nonlinear_eq,)
+            Values of the nonlinear equality constraints at `x_best`.
+        """
+        return self.models.ceq_val[self.best_index, :]
+
+    def lag_model(self, x):
+        """
+        Evaluate the Lagrangian model at a given point.
+
+        Parameters
+        ----------
+        x : `numpy.ndarray`, shape (n,)
+            Point at which the Lagrangian model is evaluated.
+
+        Returns
+        -------
+        float
+            Value of the Lagrangian model at `x`.
+        """
+        return (
+            self.models.fun(x)
+            + self._lm_linear_ub
+            @ (self._pb.linear.a_ub @ x - self._pb.linear.b_ub)
+            + self._lm_linear_eq
+            @ (self._pb.linear.a_eq @ x - self._pb.linear.b_eq)
+            + self._lm_nonlinear_ub @ self.models.cub(x)
+            + self._lm_nonlinear_eq @ self.models.ceq(x)
+        )
+
+    def lag_model_grad(self, x):
+        """
+        Evaluate the gradient of the Lagrangian model at a given point.
+
+        Parameters
+        ----------
+        x : `numpy.ndarray`, shape (n,)
+            Point at which the gradient of the Lagrangian model is evaluated.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n,)
+            Gradient of the Lagrangian model at `x`.
+        """
+        return (
+            self.models.fun_grad(x)
+            + self._lm_linear_ub @ self._pb.linear.a_ub
+            + self._lm_linear_eq @ self._pb.linear.a_eq
+            + self._lm_nonlinear_ub @ self.models.cub_grad(x)
+            + self._lm_nonlinear_eq @ self.models.ceq_grad(x)
+        )
+
+    def lag_model_hess(self):
+        """
+        Evaluate the Hessian matrix of the Lagrangian model at a given point.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n, n)
+            Hessian matrix of the Lagrangian model at `x`.
+        """
+        hess = self.models.fun_hess()
+        if self.m_nonlinear_ub > 0:
+            hess += self._lm_nonlinear_ub @ self.models.cub_hess()
+        if self.m_nonlinear_eq > 0:
+            hess += self._lm_nonlinear_eq @ self.models.ceq_hess()
+        return hess
+
+    def lag_model_hess_prod(self, v):
+        """
+        Evaluate the right product of the Hessian matrix of the Lagrangian
+        model with a given vector.
+
+        Parameters
+        ----------
+        v : `numpy.ndarray`, shape (n,)
+            Vector with which the Hessian matrix of the Lagrangian model is
+            multiplied from the right.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n,)
+            Right product of the Hessian matrix of the Lagrangian model with
+            `v`.
+        """
+        return (
+            self.models.fun_hess_prod(v)
+            + self._lm_nonlinear_ub @ self.models.cub_hess_prod(v)
+            + self._lm_nonlinear_eq @ self.models.ceq_hess_prod(v)
+        )
+
+    def lag_model_curv(self, v):
+        """
+        Evaluate the curvature of the Lagrangian model along a given direction.
+
+        Parameters
+        ----------
+        v : `numpy.ndarray`, shape (n,)
+            Direction along which the curvature of the Lagrangian model is
+            evaluated.
+
+        Returns
+        -------
+        float
+            Curvature of the Lagrangian model along `v`.
+        """
+        return (
+            self.models.fun_curv(v)
+            + self._lm_nonlinear_ub @ self.models.cub_curv(v)
+            + self._lm_nonlinear_eq @ self.models.ceq_curv(v)
+        )
+
+    def sqp_fun(self, step):
+        """
+        Evaluate the objective function of the SQP subproblem.
+
+        Parameters
+        ----------
+        step : `numpy.ndarray`, shape (n,)
+            Step along which the objective function of the SQP subproblem is
+            evaluated.
+
+        Returns
+        -------
+        float
+            Value of the objective function of the SQP subproblem along `step`.
+        """
+        return step @ (
+            self.models.fun_grad(self.x_best)
+            + 0.5 * self.lag_model_hess_prod(step)
+        )
+
+    def sqp_cub(self, step):
+        """
+        Evaluate the linearization of the nonlinear inequality constraints.
+
+        Parameters
+        ----------
+        step : `numpy.ndarray`, shape (n,)
+            Step along which the linearization of the nonlinear inequality
+            constraints is evaluated.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (m_nonlinear_ub,)
+            Value of the linearization of the nonlinear inequality constraints
+            along `step`.
+        """
+        return (
+            self.models.cub(self.x_best)
+            + self.models.cub_grad(self.x_best) @ step
+        )
+
+    def sqp_ceq(self, step):
+        """
+        Evaluate the linearization of the nonlinear equality constraints.
+
+        Parameters
+        ----------
+        step : `numpy.ndarray`, shape (n,)
+            Step along which the linearization of the nonlinear equality
+            constraints is evaluated.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (m_nonlinear_ub,)
+            Value of the linearization of the nonlinear equality constraints
+            along `step`.
+        """
+        return (
+            self.models.ceq(self.x_best)
+            + self.models.ceq_grad(self.x_best) @ step
+        )
+
+    def merit(self, x, fun_val=None, cub_val=None, ceq_val=None):
+        """
+        Evaluate the merit function at a given point.
+
+        Parameters
+        ----------
+        x : `numpy.ndarray`, shape (n,)
+            Point at which the merit function is evaluated.
+        fun_val : float, optional
+            Value of the objective function at `x`. If not provided, the
+            objective function is evaluated at `x`.
+        cub_val : `numpy.ndarray`, shape (m_nonlinear_ub,), optional
+            Values of the nonlinear inequality constraints. If not provided,
+            the nonlinear inequality constraints are evaluated at `x`.
+        ceq_val : `numpy.ndarray`, shape (m_nonlinear_eq,), optional
+            Values of the nonlinear equality constraints. If not provided,
+            the nonlinear equality constraints are evaluated at `x`.
+
+        Returns
+        -------
+        float
+            Value of the merit function at `x`.
+        """
+        if fun_val is None or cub_val is None or ceq_val is None:
+            fun_val, cub_val, ceq_val = self._pb(x, self.penalty)
+        m_val = fun_val
+        if self._penalty > 0.0:
+            c_val = self._pb.violation(x, cub_val=cub_val, ceq_val=ceq_val)
+            if np.count_nonzero(c_val):
+                m_val += self._penalty * np.linalg.norm(c_val)
+        return m_val
+
+    def get_constraint_linearizations(self, x):
+        """
+        Get the linearizations of the constraints at a given point.
+
+        Parameters
+        ----------
+        x : `numpy.ndarray`, shape (n,)
+            Point at which the linearizations of the constraints are evaluated.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (m_linear_ub + m_nonlinear_ub, n)
+            Left-hand side matrix of the linearized inequality constraints.
+        `numpy.ndarray`, shape (m_linear_ub + m_nonlinear_ub,)
+            Right-hand side vector of the linearized inequality constraints.
+        `numpy.ndarray`, shape (m_linear_eq + m_nonlinear_eq, n)
+            Left-hand side matrix of the linearized equality constraints.
+        `numpy.ndarray`, shape (m_linear_eq + m_nonlinear_eq,)
+            Right-hand side vector of the linearized equality constraints.
+        """
+        aub = np.block(
+            [
+                [self._pb.linear.a_ub],
+                [self.models.cub_grad(x)],
+            ]
+        )
+        bub = np.block(
+            [
+                self._pb.linear.b_ub - self._pb.linear.a_ub @ x,
+                -self.models.cub(x),
+            ]
+        )
+        aeq = np.block(
+            [
+                [self._pb.linear.a_eq],
+                [self.models.ceq_grad(x)],
+            ]
+        )
+        beq = np.block(
+            [
+                self._pb.linear.b_eq - self._pb.linear.a_eq @ x,
+                -self.models.ceq(x),
+            ]
+        )
+        return aub, bub, aeq, beq
+
+    def get_trust_region_step(self, options):
+        """
+        Get the trust-region step.
+
+        The trust-region step is computed by solving the derivative-free
+        trust-region SQP subproblem using a Byrd-Omojokun composite-step
+        approach. For more details, see Section 5.2.3 of [1]_.
+
+        Parameters
+        ----------
+        options : dict
+            Options of the solver.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n,)
+            Normal step.
+        `numpy.ndarray`, shape (n,)
+            Tangential step.
+
+        References
+        ----------
+        .. [1] T. M. Ragonneau. *Model-Based Derivative-Free Optimization
+           Methods and Software*. PhD thesis, Department of Applied
+           Mathematics, The Hong Kong Polytechnic University, Hong Kong, China,
+           2022. URL: https://theses.lib.polyu.edu.hk/handle/200/12294.
+        """
+        # Evaluate the linearizations of the constraints.
+        aub, bub, aeq, beq = self.get_constraint_linearizations(self.x_best)
+        xl = self._pb.bounds.xl - self.x_best
+        xu = self._pb.bounds.xu - self.x_best
+
+        # Evaluate the normal step.
+        radius = self._constants[Constants.BYRD_OMOJOKUN_FACTOR] * self.radius
+        normal_step = normal_byrd_omojokun(
+            aub,
+            bub,
+            aeq,
+            beq,
+            xl,
+            xu,
+            radius,
+            options[Options.DEBUG],
+            **self._constants,
+        )
+        if options[Options.DEBUG]:
+            tol = get_arrays_tol(xl, xu)
+            if (np.any(normal_step + tol < xl)
+                    or np.any(xu < normal_step - tol)):
+                warnings.warn(
+                    "the normal step does not respect the bound constraint.",
+                    RuntimeWarning,
+                    2,
+                )
+            if np.linalg.norm(normal_step) > 1.1 * radius:
+                warnings.warn(
+                    "the normal step does not respect the trust-region "
+                    "constraint.",
+                    RuntimeWarning,
+                    2,
+                )
+
+        # Evaluate the tangential step.
+        radius = np.sqrt(self.radius**2.0 - normal_step @ normal_step)
+        xl -= normal_step
+        xu -= normal_step
+        bub = np.maximum(bub - aub @ normal_step, 0.0)
+        g_best = self.models.fun_grad(self.x_best) + self.lag_model_hess_prod(
+            normal_step
+        )
+        if self._pb.type in ["unconstrained", "bound-constrained"]:
+            tangential_step = tangential_byrd_omojokun(
+                g_best,
+                self.lag_model_hess_prod,
+                xl,
+                xu,
+                radius,
+                options[Options.DEBUG],
+                **self._constants,
+            )
+        else:
+            tangential_step = constrained_tangential_byrd_omojokun(
+                g_best,
+                self.lag_model_hess_prod,
+                xl,
+                xu,
+                aub,
+                bub,
+                aeq,
+                radius,
+                options["debug"],
+                **self._constants,
+            )
+        if options[Options.DEBUG]:
+            tol = get_arrays_tol(xl, xu)
+            if np.any(tangential_step + tol < xl) or np.any(
+                xu < tangential_step - tol
+            ):
+                warnings.warn(
+                    "The tangential step does not respect the bound "
+                    "constraints.",
+                    RuntimeWarning,
+                    2,
+                )
+            if (
+                np.linalg.norm(normal_step + tangential_step)
+                > 1.1 * np.sqrt(2.0) * self.radius
+            ):
+                warnings.warn(
+                    "The trial step does not respect the trust-region "
+                    "constraint.",
+                    RuntimeWarning,
+                    2,
+                )
+        return normal_step, tangential_step
+
+    def get_geometry_step(self, k_new, options):
+        """
+        Get the geometry-improving step.
+
+        Three different geometry-improving steps are computed and the best one
+        is returned. For more details, see Section 5.2.7 of [1]_.
+
+        Parameters
+        ----------
+        k_new : int
+            Index of the interpolation point to be modified.
+        options : dict
+            Options of the solver.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n,)
+            Geometry-improving step.
+
+        Raises
+        ------
+        `numpy.linalg.LinAlgError`
+            If the computation of a determinant fails.
+
+        References
+        ----------
+        .. [1] T. M. Ragonneau. *Model-Based Derivative-Free Optimization
+           Methods and Software*. PhD thesis, Department of Applied
+           Mathematics, The Hong Kong Polytechnic University, Hong Kong, China,
+           2022. URL: https://theses.lib.polyu.edu.hk/handle/200/12294.
+        """
+        if options[Options.DEBUG]:
+            assert (
+                k_new != self.best_index
+            ), "The index `k_new` must be different from the best index."
+
+        # Build the k_new-th Lagrange polynomial.
+        coord_vec = np.squeeze(np.eye(1, self.models.npt, k_new))
+        lag = Quadratic(
+            self.models.interpolation,
+            coord_vec,
+            options[Options.DEBUG],
+        )
+        g_lag = lag.grad(self.x_best, self.models.interpolation)
+
+        # Compute a simple constrained Cauchy step.
+        xl = self._pb.bounds.xl - self.x_best
+        xu = self._pb.bounds.xu - self.x_best
+        step = cauchy_geometry(
+            0.0,
+            g_lag,
+            lambda v: lag.curv(v, self.models.interpolation),
+            xl,
+            xu,
+            self.radius,
+            options[Options.DEBUG],
+        )
+        sigma = self.models.determinants(self.x_best + step, k_new)
+
+        # Compute the solution on the straight lines joining the interpolation
+        # points to the k-th one, and choose it if it provides a larger value
+        # of the determinant of the interpolation system in absolute value.
+        xpt = (
+            self.models.interpolation.xpt
+            - self.models.interpolation.xpt[:, self.best_index, np.newaxis]
+        )
+        xpt[:, [0, self.best_index]] = xpt[:, [self.best_index, 0]]
+        step_alt = spider_geometry(
+            0.0,
+            g_lag,
+            lambda v: lag.curv(v, self.models.interpolation),
+            xpt[:, 1:],
+            xl,
+            xu,
+            self.radius,
+            options[Options.DEBUG],
+        )
+        sigma_alt = self.models.determinants(self.x_best + step_alt, k_new)
+        if abs(sigma_alt) > abs(sigma):
+            step = step_alt
+            sigma = sigma_alt
+
+        # Compute a Cauchy step on the tangent space of the active constraints.
+        if self._pb.type in [
+            "linearly constrained",
+            "nonlinearly constrained",
+        ]:
+            aub, bub, aeq, beq = (
+                self.get_constraint_linearizations(self.x_best))
+            tol_bd = get_arrays_tol(xl, xu)
+            tol_ub = get_arrays_tol(bub)
+            free_xl = xl <= -tol_bd
+            free_xu = xu >= tol_bd
+            free_ub = bub >= tol_ub
+
+            # Compute the Cauchy step.
+            n_act, q = qr_tangential_byrd_omojokun(
+                aub,
+                aeq,
+                free_xl,
+                free_xu,
+                free_ub,
+            )
+            g_lag_proj = q[:, n_act:] @ (q[:, n_act:].T @ g_lag)
+            norm_g_lag_proj = np.linalg.norm(g_lag_proj)
+            if 0 < n_act < self._pb.n and norm_g_lag_proj > TINY * self.radius:
+                step_alt = (self.radius / norm_g_lag_proj) * g_lag_proj
+                if lag.curv(step_alt, self.models.interpolation) < 0.0:
+                    step_alt = -step_alt
+
+                # Evaluate the constraint violation at the Cauchy step.
+                cbd = np.block([xl - step_alt, step_alt - xu])
+                cub = aub @ step_alt - bub
+                ceq = aeq @ step_alt - beq
+                maxcv_val = max(
+                    np.max(array, initial=0.0)
+                    for array in [cbd, cub, np.abs(ceq)]
+                )
+
+                # Accept the new step if it is nearly feasible and do not
+                # drastically worsen the determinant of the interpolation
+                # system in absolute value.
+                tol = np.max(np.abs(step_alt[~free_xl]), initial=0.0)
+                tol = np.max(np.abs(step_alt[~free_xu]), initial=tol)
+                tol = np.max(np.abs(aub[~free_ub, :] @ step_alt), initial=tol)
+                tol = min(10.0 * tol, 1e-2 * np.linalg.norm(step_alt))
+                if maxcv_val <= tol:
+                    sigma_alt = self.models.determinants(
+                        self.x_best + step_alt, k_new
+                    )
+                    if abs(sigma_alt) >= 0.1 * abs(sigma):
+                        step = np.clip(step_alt, xl, xu)
+
+        if options[Options.DEBUG]:
+            tol = get_arrays_tol(xl, xu)
+            if np.any(step + tol < xl) or np.any(xu < step - tol):
+                warnings.warn(
+                    "The geometry step does not respect the bound "
+                    "constraints.",
+                    RuntimeWarning,
+                    2,
+                )
+            if np.linalg.norm(step) > 1.1 * self.radius:
+                warnings.warn(
+                    "The geometry step does not respect the "
+                    "trust-region constraint.",
+                    RuntimeWarning,
+                    2,
+                )
+        return step
+
+    def get_second_order_correction_step(self, step, options):
+        """
+        Get the second-order correction step.
+
+        Parameters
+        ----------
+        step : `numpy.ndarray`, shape (n,)
+            Trust-region step.
+        options : dict
+            Options of the solver.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n,)
+            Second-order correction step.
+        """
+        # Evaluate the linearizations of the constraints.
+        aub, bub, aeq, beq = self.get_constraint_linearizations(self.x_best)
+        xl = self._pb.bounds.xl - self.x_best
+        xu = self._pb.bounds.xu - self.x_best
+        radius = np.linalg.norm(step)
+        soc_step = normal_byrd_omojokun(
+            aub,
+            bub,
+            aeq,
+            beq,
+            xl,
+            xu,
+            radius,
+            options[Options.DEBUG],
+            **self._constants,
+        )
+        if options[Options.DEBUG]:
+            tol = get_arrays_tol(xl, xu)
+            if np.any(soc_step + tol < xl) or np.any(xu < soc_step - tol):
+                warnings.warn(
+                    "The second-order correction step does not "
+                    "respect the bound constraints.",
+                    RuntimeWarning,
+                    2,
+                )
+            if np.linalg.norm(soc_step) > 1.1 * radius:
+                warnings.warn(
+                    "The second-order correction step does not "
+                    "respect the trust-region constraint.",
+                    RuntimeWarning,
+                    2,
+                )
+        return soc_step
+
+    def get_reduction_ratio(self, step, fun_val, cub_val, ceq_val):
+        """
+        Get the reduction ratio.
+
+        Parameters
+        ----------
+        step : `numpy.ndarray`, shape (n,)
+            Trust-region step.
+        fun_val : float
+            Objective function value at the trial point.
+        cub_val : `numpy.ndarray`, shape (m_nonlinear_ub,)
+            Nonlinear inequality constraint values at the trial point.
+        ceq_val : `numpy.ndarray`, shape (m_nonlinear_eq,)
+            Nonlinear equality constraint values at the trial point.
+
+        Returns
+        -------
+        float
+            Reduction ratio.
+        """
+        merit_old = self.merit(
+            self.x_best,
+            self.fun_best,
+            self.cub_best,
+            self.ceq_best,
+        )
+        merit_new = self.merit(self.x_best + step, fun_val, cub_val, ceq_val)
+        merit_model_old = self.merit(
+            self.x_best,
+            0.0,
+            self.models.cub(self.x_best),
+            self.models.ceq(self.x_best),
+        )
+        merit_model_new = self.merit(
+            self.x_best + step,
+            self.sqp_fun(step),
+            self.sqp_cub(step),
+            self.sqp_ceq(step),
+        )
+        if abs(merit_model_old - merit_model_new) > TINY * abs(
+            merit_old - merit_new
+        ):
+            return (merit_old - merit_new) / abs(
+                merit_model_old - merit_model_new
+            )
+        else:
+            return -1.0
+
+    def increase_penalty(self, step):
+        """
+        Increase the penalty parameter.
+
+        Parameters
+        ----------
+        step : `numpy.ndarray`, shape (n,)
+            Trust-region step.
+        """
+        aub, bub, aeq, beq = self.get_constraint_linearizations(self.x_best)
+        viol_diff = max(
+            np.linalg.norm(
+                np.block(
+                    [
+                        np.maximum(0.0, -bub),
+                        beq,
+                    ]
+                )
+            )
+            - np.linalg.norm(
+                np.block(
+                    [
+                        np.maximum(0.0, aub @ step - bub),
+                        aeq @ step - beq,
+                    ]
+                )
+            ),
+            0.0,
+        )
+        sqp_val = self.sqp_fun(step)
+
+        threshold = np.linalg.norm(
+            np.block(
+                [
+                    self._lm_linear_ub,
+                    self._lm_linear_eq,
+                    self._lm_nonlinear_ub,
+                    self._lm_nonlinear_eq,
+                ]
+            )
+        )
+        if abs(viol_diff) > TINY * abs(sqp_val):
+            threshold = max(threshold, sqp_val / viol_diff)
+        best_index_save = self.best_index
+        if (
+            self._penalty
+            <= self._constants[Constants.PENALTY_INCREASE_THRESHOLD]
+                * threshold
+        ):
+            self._penalty = max(
+                self._constants[Constants.PENALTY_INCREASE_FACTOR] * threshold,
+                1.0,
+            )
+            self.set_best_index()
+        return best_index_save == self.best_index
+
+    def decrease_penalty(self):
+        """
+        Decrease the penalty parameter.
+        """
+        self._penalty = min(self._penalty, self._get_low_penalty())
+        self.set_best_index()
+
+    def set_best_index(self):
+        """
+        Set the index of the best point.
+        """
+        best_index = self.best_index
+        m_best = self.merit(
+            self.x_best,
+            self.models.fun_val[best_index],
+            self.models.cub_val[best_index, :],
+            self.models.ceq_val[best_index, :],
+        )
+        r_best = self._pb.maxcv(
+            self.x_best,
+            self.models.cub_val[best_index, :],
+            self.models.ceq_val[best_index, :],
+        )
+        tol = (
+            10.0
+            * EPS
+            * max(self.models.n, self.models.npt)
+            * max(abs(m_best), 1.0)
+        )
+        for k in range(self.models.npt):
+            if k != self.best_index:
+                x_val = self.models.interpolation.point(k)
+                m_val = self.merit(
+                    x_val,
+                    self.models.fun_val[k],
+                    self.models.cub_val[k, :],
+                    self.models.ceq_val[k, :],
+                )
+                r_val = self._pb.maxcv(
+                    x_val,
+                    self.models.cub_val[k, :],
+                    self.models.ceq_val[k, :],
+                )
+                if m_val < m_best or (m_val < m_best + tol and r_val < r_best):
+                    best_index = k
+                    m_best = m_val
+                    r_best = r_val
+        self._best_index = best_index
+
+    def get_index_to_remove(self, x_new=None):
+        """
+        Get the index of the interpolation point to remove.
+
+        If `x_new` is not provided, the index returned should be used during
+        the geometry-improvement phase. Otherwise, the index returned is the
+        best index for included `x_new` in the interpolation set.
+
+        Parameters
+        ----------
+        x_new : `numpy.ndarray`, shape (n,), optional
+            New point to be included in the interpolation set.
+
+        Returns
+        -------
+        int
+            Index of the interpolation point to remove.
+        float
+            Distance between `x_best` and the removed point.
+
+        Raises
+        ------
+        `numpy.linalg.LinAlgError`
+            If the computation of a determinant fails.
+        """
+        dist_sq = np.sum(
+            (
+                self.models.interpolation.xpt
+                - self.models.interpolation.xpt[:, self.best_index, np.newaxis]
+            )
+            ** 2.0,
+            axis=0,
+        )
+        if x_new is None:
+            sigma = 1.0
+            weights = dist_sq
+        else:
+            sigma = self.models.determinants(x_new)
+            weights = (
+                np.maximum(
+                    1.0,
+                    dist_sq
+                    / max(
+                        self._constants[Constants.LOW_RADIUS_FACTOR]
+                        * self.radius,
+                        self.resolution,
+                    )
+                    ** 2.0,
+                )
+                ** 3.0
+            )
+            weights[self.best_index] = -1.0  # do not remove the best point
+        k_max = np.argmax(weights * np.abs(sigma))
+        return k_max, np.sqrt(dist_sq[k_max])
+
+    def update_radius(self, step, ratio):
+        """
+        Update the trust-region radius.
+
+        Parameters
+        ----------
+        step : `numpy.ndarray`, shape (n,)
+            Trust-region step.
+        ratio : float
+            Reduction ratio.
+        """
+        s_norm = np.linalg.norm(step)
+        if ratio <= self._constants[Constants.LOW_RATIO]:
+            self.radius *= self._constants[Constants.DECREASE_RADIUS_FACTOR]
+        elif ratio <= self._constants[Constants.HIGH_RATIO]:
+            self.radius = max(
+                self._constants[Constants.DECREASE_RADIUS_FACTOR]
+                * self.radius,
+                s_norm,
+            )
+        else:
+            self.radius = min(
+                self._constants[Constants.INCREASE_RADIUS_FACTOR]
+                * self.radius,
+                max(
+                    self._constants[Constants.DECREASE_RADIUS_FACTOR]
+                    * self.radius,
+                    self._constants[Constants.INCREASE_RADIUS_THRESHOLD]
+                    * s_norm,
+                ),
+            )
+
+    def enhance_resolution(self, options):
+        """
+        Enhance the resolution of the trust-region framework.
+
+        Parameters
+        ----------
+        options : dict
+            Options of the solver.
+        """
+        if (
+            self._constants[Constants.LARGE_RESOLUTION_THRESHOLD]
+            * options[Options.RHOEND]
+            < self.resolution
+        ):
+            self.resolution *= self._constants[
+                Constants.DECREASE_RESOLUTION_FACTOR
+            ]
+        elif (
+            self._constants[Constants.MODERATE_RESOLUTION_THRESHOLD]
+            * options[Options.RHOEND]
+            < self.resolution
+        ):
+            self.resolution = np.sqrt(self.resolution
+                                      * options[Options.RHOEND])
+        else:
+            self.resolution = options[Options.RHOEND]
+
+        # Reduce the trust-region radius.
+        self._radius = max(
+            self._constants[Constants.DECREASE_RADIUS_FACTOR] * self._radius,
+            self.resolution,
+        )
+
+    def shift_x_base(self, options):
+        """
+        Shift the base point to `x_best`.
+
+        Parameters
+        ----------
+        options : dict
+            Options of the solver.
+        """
+        self.models.shift_x_base(np.copy(self.x_best), options)
+
+    def set_multipliers(self, x):
+        """
+        Set the Lagrange multipliers.
+
+        This method computes and set the Lagrange multipliers of the linear and
+        nonlinear constraints to be the QP multipliers.
+
+        Parameters
+        ----------
+        x : `numpy.ndarray`, shape (n,)
+            Point at which the Lagrange multipliers are computed.
+        """
+        # Build the constraints of the least-squares problem.
+        incl_linear_ub = self._pb.linear.a_ub @ x >= self._pb.linear.b_ub
+        incl_nonlinear_ub = self.cub_best >= 0.0
+        incl_xl = self._pb.bounds.xl >= x
+        incl_xu = self._pb.bounds.xu <= x
+        m_linear_ub = np.count_nonzero(incl_linear_ub)
+        m_nonlinear_ub = np.count_nonzero(incl_nonlinear_ub)
+        m_xl = np.count_nonzero(incl_xl)
+        m_xu = np.count_nonzero(incl_xu)
+
+        if (
+            m_linear_ub + m_nonlinear_ub + self.m_linear_eq
+                + self.m_nonlinear_eq > 0
+        ):
+            identity = np.eye(self._pb.n)
+            c_jac = np.r_[
+                -identity[incl_xl, :],
+                identity[incl_xu, :],
+                self._pb.linear.a_ub[incl_linear_ub, :],
+                self.models.cub_grad(x, incl_nonlinear_ub),
+                self._pb.linear.a_eq,
+                self.models.ceq_grad(x),
+            ]
+
+            # Solve the least-squares problem.
+            g_best = self.models.fun_grad(x)
+            xl_lm = np.full(c_jac.shape[0], -np.inf)
+            xl_lm[: m_xl + m_xu + m_linear_ub + m_nonlinear_ub] = 0.0
+            res = lsq_linear(
+                c_jac.T,
+                -g_best,
+                bounds=(xl_lm, np.inf),
+                method="bvls",
+            )
+
+            # Extract the Lagrange multipliers.
+            self._lm_linear_ub[incl_linear_ub] = res.x[
+                m_xl + m_xu:m_xl + m_xu + m_linear_ub
+            ]
+            self._lm_linear_ub[~incl_linear_ub] = 0.0
+            self._lm_nonlinear_ub[incl_nonlinear_ub] = res.x[
+                m_xl
+                + m_xu
+                + m_linear_ub:m_xl
+                + m_xu
+                + m_linear_ub
+                + m_nonlinear_ub
+            ]
+            self._lm_nonlinear_ub[~incl_nonlinear_ub] = 0.0
+            self._lm_linear_eq[:] = res.x[
+                m_xl
+                + m_xu
+                + m_linear_ub
+                + m_nonlinear_ub:m_xl
+                + m_xu
+                + m_linear_ub
+                + m_nonlinear_ub
+                + self.m_linear_eq
+            ]
+            self._lm_nonlinear_eq[:] = res.x[
+                m_xl + m_xu + m_linear_ub + m_nonlinear_ub + self.m_linear_eq:
+            ]
+
+    def _get_low_penalty(self):
+        r_val_ub = np.c_[
+            (
+                self.models.interpolation.x_base[np.newaxis, :]
+                + self.models.interpolation.xpt.T
+            )
+            @ self._pb.linear.a_ub.T
+            - self._pb.linear.b_ub[np.newaxis, :],
+            self.models.cub_val,
+        ]
+        r_val_eq = (
+            self.models.interpolation.x_base[np.newaxis, :]
+            + self.models.interpolation.xpt.T
+        ) @ self._pb.linear.a_eq.T - self._pb.linear.b_eq[np.newaxis, :]
+        r_val_eq = np.block(
+            [
+                r_val_eq,
+                -r_val_eq,
+                self.models.ceq_val,
+                -self.models.ceq_val,
+            ]
+        )
+        r_val = np.block([r_val_ub, r_val_eq])
+        c_min = np.nanmin(r_val, axis=0)
+        c_max = np.nanmax(r_val, axis=0)
+        indices = (
+            c_min
+            < self._constants[Constants.THRESHOLD_RATIO_CONSTRAINTS] * c_max
+        )
+        if np.any(indices):
+            f_min = np.nanmin(self.models.fun_val)
+            f_max = np.nanmax(self.models.fun_val)
+            c_min_neg = np.minimum(0.0, c_min[indices])
+            c_diff = np.min(c_max[indices] - c_min_neg)
+            if c_diff > TINY * (f_max - f_min):
+                penalty = (f_max - f_min) / c_diff
+            else:
+                penalty = np.inf
+        else:
+            penalty = 0.0
+        return penalty
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/main.py b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/main.py
new file mode 100644
index 0000000000000000000000000000000000000000..01e5159e0dfebed9a78c6948cb99bfb1d744b6c7
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/main.py
@@ -0,0 +1,1506 @@
+import warnings
+
+import numpy as np
+from scipy.optimize import (
+    Bounds,
+    LinearConstraint,
+    NonlinearConstraint,
+    OptimizeResult,
+)
+
+from .framework import TrustRegion
+from .problem import (
+    ObjectiveFunction,
+    BoundConstraints,
+    LinearConstraints,
+    NonlinearConstraints,
+    Problem,
+)
+from .utils import (
+    MaxEvalError,
+    TargetSuccess,
+    CallbackSuccess,
+    FeasibleSuccess,
+    exact_1d_array,
+)
+from .settings import (
+    ExitStatus,
+    Options,
+    Constants,
+    DEFAULT_OPTIONS,
+    DEFAULT_CONSTANTS,
+    PRINT_OPTIONS,
+)
+
+
+def minimize(
+    fun,
+    x0,
+    args=(),
+    bounds=None,
+    constraints=(),
+    callback=None,
+    options=None,
+    **kwargs,
+):
+    r"""
+    Minimize a scalar function using the COBYQA method.
+
+    The Constrained Optimization BY Quadratic Approximations (COBYQA) method is
+    a derivative-free optimization method designed to solve general nonlinear
+    optimization problems. A complete description of COBYQA is given in [3]_.
+
+    Parameters
+    ----------
+    fun : {callable, None}
+        Objective function to be minimized.
+
+            ``fun(x, *args) -> float``
+
+        where ``x`` is an array with shape (n,) and `args` is a tuple. If `fun`
+        is ``None``, the objective function is assumed to be the zero function,
+        resulting in a feasibility problem.
+    x0 : array_like, shape (n,)
+        Initial guess.
+    args : tuple, optional
+        Extra arguments passed to the objective function.
+    bounds : {`scipy.optimize.Bounds`, array_like, shape (n, 2)}, optional
+        Bound constraints of the problem. It can be one of the cases below.
+
+        #. An instance of `scipy.optimize.Bounds`. For the time being, the
+           argument ``keep_feasible`` is disregarded, and all the constraints
+           are considered unrelaxable and will be enforced.
+        #. An array with shape (n, 2). The bound constraints for ``x[i]`` are
+           ``bounds[i][0] <= x[i] <= bounds[i][1]``. Set ``bounds[i][0]`` to
+           :math:`-\infty` if there is no lower bound, and set ``bounds[i][1]``
+           to :math:`\infty` if there is no upper bound.
+
+        The COBYQA method always respect the bound constraints.
+    constraints : {Constraint, list}, optional
+        General constraints of the problem. It can be one of the cases below.
+
+        #. An instance of `scipy.optimize.LinearConstraint`. The argument
+           ``keep_feasible`` is disregarded.
+        #. An instance of `scipy.optimize.NonlinearConstraint`. The arguments
+           ``jac``, ``hess``, ``keep_feasible``, ``finite_diff_rel_step``, and
+           ``finite_diff_jac_sparsity`` are disregarded.
+
+        #. A list, each of whose elements are described in the cases above.
+
+    callback : callable, optional
+        A callback executed at each objective function evaluation. The method
+        terminates if a ``StopIteration`` exception is raised by the callback
+        function. Its signature can be one of the following:
+
+            ``callback(intermediate_result)``
+
+        where ``intermediate_result`` is a keyword parameter that contains an
+        instance of `scipy.optimize.OptimizeResult`, with attributes ``x``
+        and ``fun``, being the point at which the objective function is
+        evaluated and the value of the objective function, respectively. The
+        name of the parameter must be ``intermediate_result`` for the callback
+        to be passed an instance of `scipy.optimize.OptimizeResult`.
+
+        Alternatively, the callback function can have the signature:
+
+            ``callback(xk)``
+
+        where ``xk`` is the point at which the objective function is evaluated.
+        Introspection is used to determine which of the signatures to invoke.
+    options : dict, optional
+        Options passed to the solver. Accepted keys are:
+
+            disp : bool, optional
+                Whether to print information about the optimization procedure.
+                Default is ``False``.
+            maxfev : int, optional
+                Maximum number of function evaluations. Default is ``500 * n``.
+            maxiter : int, optional
+                Maximum number of iterations. Default is ``1000 * n``.
+            target : float, optional
+                Target on the objective function value. The optimization
+                procedure is terminated when the objective function value of a
+                feasible point is less than or equal to this target. Default is
+                ``-numpy.inf``.
+            feasibility_tol : float, optional
+                Tolerance on the constraint violation. If the maximum
+                constraint violation at a point is less than or equal to this
+                tolerance, the point is considered feasible. Default is
+                ``numpy.sqrt(numpy.finfo(float).eps)``.
+            radius_init : float, optional
+                Initial trust-region radius. Typically, this value should be in
+                the order of one tenth of the greatest expected change to `x0`.
+                Default is ``1.0``.
+            radius_final : float, optional
+                Final trust-region radius. It should indicate the accuracy
+                required in the final values of the variables. Default is
+                ``1e-6``.
+            nb_points : int, optional
+                Number of interpolation points used to build the quadratic
+                models of the objective and constraint functions. Default is
+                ``2 * n + 1``.
+            scale : bool, optional
+                Whether to scale the variables according to the bounds. Default
+                is ``False``.
+            filter_size : int, optional
+                Maximum number of points in the filter. The filter is used to
+                select the best point returned by the optimization procedure.
+                Default is ``sys.maxsize``.
+            store_history : bool, optional
+                Whether to store the history of the function evaluations.
+                Default is ``False``.
+            history_size : int, optional
+                Maximum number of function evaluations to store in the history.
+                Default is ``sys.maxsize``.
+            debug : bool, optional
+                Whether to perform additional checks during the optimization
+                procedure. This option should be used only for debugging
+                purposes and is highly discouraged to general users. Default is
+                ``False``.
+
+        Other constants (from the keyword arguments) are described below. They
+        are not intended to be changed by general users. They should only be
+        changed by users with a deep understanding of the algorithm, who want
+        to experiment with different settings.
+
+    Returns
+    -------
+    `scipy.optimize.OptimizeResult`
+        Result of the optimization procedure, with the following fields:
+
+            message : str
+                Description of the cause of the termination.
+            success : bool
+                Whether the optimization procedure terminated successfully.
+            status : int
+                Termination status of the optimization procedure.
+            x : `numpy.ndarray`, shape (n,)
+                Solution point.
+            fun : float
+                Objective function value at the solution point.
+            maxcv : float
+                Maximum constraint violation at the solution point.
+            nfev : int
+                Number of function evaluations.
+            nit : int
+                Number of iterations.
+
+        If ``store_history`` is True, the result also has the following fields:
+
+            fun_history : `numpy.ndarray`, shape (nfev,)
+                History of the objective function values.
+            maxcv_history : `numpy.ndarray`, shape (nfev,)
+                History of the maximum constraint violations.
+
+        A description of the termination statuses is given below.
+
+        .. list-table::
+            :widths: 25 75
+            :header-rows: 1
+
+            * - Exit status
+              - Description
+            * - 0
+              - The lower bound for the trust-region radius has been reached.
+            * - 1
+              - The target objective function value has been reached.
+            * - 2
+              - All variables are fixed by the bound constraints.
+            * - 3
+              - The callback requested to stop the optimization procedure.
+            * - 4
+              - The feasibility problem received has been solved successfully.
+            * - 5
+              - The maximum number of function evaluations has been exceeded.
+            * - 6
+              - The maximum number of iterations has been exceeded.
+            * - -1
+              - The bound constraints are infeasible.
+            * - -2
+              - A linear algebra error occurred.
+
+    Other Parameters
+    ----------------
+    decrease_radius_factor : float, optional
+        Factor by which the trust-region radius is reduced when the reduction
+        ratio is low or negative. Default is ``0.5``.
+    increase_radius_factor : float, optional
+        Factor by which the trust-region radius is increased when the reduction
+        ratio is large. Default is ``numpy.sqrt(2.0)``.
+    increase_radius_threshold : float, optional
+        Threshold that controls the increase of the trust-region radius when
+        the reduction ratio is large. Default is ``2.0``.
+    decrease_radius_threshold : float, optional
+        Threshold used to determine whether the trust-region radius should be
+        reduced to the resolution. Default is ``1.4``.
+    decrease_resolution_factor : float, optional
+        Factor by which the resolution is reduced when the current value is far
+        from its final value. Default is ``0.1``.
+    large_resolution_threshold : float, optional
+        Threshold used to determine whether the resolution is far from its
+        final value. Default is ``250.0``.
+    moderate_resolution_threshold : float, optional
+        Threshold used to determine whether the resolution is close to its
+        final value. Default is ``16.0``.
+    low_ratio : float, optional
+        Threshold used to determine whether the reduction ratio is low. Default
+        is ``0.1``.
+    high_ratio : float, optional
+        Threshold used to determine whether the reduction ratio is high.
+        Default is ``0.7``.
+    very_low_ratio : float, optional
+        Threshold used to determine whether the reduction ratio is very low.
+        This is used to determine whether the models should be reset. Default
+        is ``0.01``.
+    penalty_increase_threshold : float, optional
+        Threshold used to determine whether the penalty parameter should be
+        increased. Default is ``1.5``.
+    penalty_increase_factor : float, optional
+        Factor by which the penalty parameter is increased. Default is ``2.0``.
+    short_step_threshold : float, optional
+        Factor used to determine whether the trial step is too short. Default
+        is ``0.5``.
+    low_radius_factor : float, optional
+        Factor used to determine which interpolation point should be removed
+        from the interpolation set at each iteration. Default is ``0.1``.
+    byrd_omojokun_factor : float, optional
+        Factor by which the trust-region radius is reduced for the computations
+        of the normal step in the Byrd-Omojokun composite-step approach.
+        Default is ``0.8``.
+    threshold_ratio_constraints : float, optional
+        Threshold used to determine which constraints should be taken into
+        account when decreasing the penalty parameter. Default is ``2.0``.
+    large_shift_factor : float, optional
+        Factor used to determine whether the point around which the quadratic
+        models are built should be updated. Default is ``10.0``.
+    large_gradient_factor : float, optional
+        Factor used to determine whether the models should be reset. Default is
+        ``10.0``.
+    resolution_factor : float, optional
+        Factor by which the resolution is decreased. Default is ``2.0``.
+    improve_tcg : bool, optional
+        Whether to improve the steps computed by the truncated conjugate
+        gradient method when the trust-region boundary is reached. Default is
+        ``True``.
+
+    References
+    ----------
+    .. [1] J. Nocedal and S. J. Wright. *Numerical Optimization*. Springer Ser.
+       Oper. Res. Financ. Eng. Springer, New York, NY, USA, second edition,
+       2006. `doi:10.1007/978-0-387-40065-5
+       `_.
+    .. [2] M. J. D. Powell. A direct search optimization method that models the
+       objective and constraint functions by linear interpolation. In S. Gomez
+       and J.-P. Hennart, editors, *Advances in Optimization and Numerical
+       Analysis*, volume 275 of Math. Appl., pages 51--67. Springer, Dordrecht,
+       Netherlands, 1994. `doi:10.1007/978-94-015-8330-5_4
+       `_.
+    .. [3] T. M. Ragonneau. *Model-Based Derivative-Free Optimization Methods
+       and Software*. PhD thesis, Department of Applied Mathematics, The Hong
+       Kong Polytechnic University, Hong Kong, China, 2022. URL:
+       https://theses.lib.polyu.edu.hk/handle/200/12294.
+
+    Examples
+    --------
+    To demonstrate how to use `minimize`, we first minimize the Rosenbrock
+    function implemented in `scipy.optimize` in an unconstrained setting.
+
+    .. testsetup::
+
+        import numpy as np
+        np.set_printoptions(precision=3, suppress=True)
+
+    >>> from cobyqa import minimize
+    >>> from scipy.optimize import rosen
+
+    To solve the problem using COBYQA, run:
+
+    >>> x0 = [1.3, 0.7, 0.8, 1.9, 1.2]
+    >>> res = minimize(rosen, x0)
+    >>> res.x
+    array([1., 1., 1., 1., 1.])
+
+    To see how bound and constraints are handled using `minimize`, we solve
+    Example 16.4 of [1]_, defined as
+
+    .. math::
+
+        \begin{aligned}
+            \min_{x \in \mathbb{R}^2}   & \quad (x_1 - 1)^2 + (x_2 - 2.5)^2\\
+            \text{s.t.}                 & \quad -x_1 + 2x_2 \le 2,\\
+                                        & \quad x_1 + 2x_2 \le 6,\\
+                                        & \quad x_1 - 2x_2 \le 2,\\
+                                        & \quad x_1 \ge 0,\\
+                                        & \quad x_2 \ge 0.
+        \end{aligned}
+
+    >>> import numpy as np
+    >>> from scipy.optimize import Bounds, LinearConstraint
+
+    Its objective function can be implemented as:
+
+    >>> def fun(x):
+    ...     return (x[0] - 1.0)**2 + (x[1] - 2.5)**2
+
+    This problem can be solved using `minimize` as:
+
+    >>> x0 = [2.0, 0.0]
+    >>> bounds = Bounds([0.0, 0.0], np.inf)
+    >>> constraints = LinearConstraint([
+    ...     [-1.0, 2.0],
+    ...     [1.0, 2.0],
+    ...     [1.0, -2.0],
+    ... ], -np.inf, [2.0, 6.0, 2.0])
+    >>> res = minimize(fun, x0, bounds=bounds, constraints=constraints)
+    >>> res.x
+    array([1.4, 1.7])
+
+    To see how nonlinear constraints are handled, we solve Problem (F) of [2]_,
+    defined as
+
+    .. math::
+
+        \begin{aligned}
+            \min_{x \in \mathbb{R}^2}   & \quad -x_1 - x_2\\
+            \text{s.t.}                 & \quad x_1^2 - x_2 \le 0,\\
+                                        & \quad x_1^2 + x_2^2 \le 1.
+        \end{aligned}
+
+    >>> from scipy.optimize import NonlinearConstraint
+
+    Its objective and constraint functions can be implemented as:
+
+    >>> def fun(x):
+    ...     return -x[0] - x[1]
+    >>>
+    >>> def cub(x):
+    ...     return [x[0]**2 - x[1], x[0]**2 + x[1]**2]
+
+    This problem can be solved using `minimize` as:
+
+    >>> x0 = [1.0, 1.0]
+    >>> constraints = NonlinearConstraint(cub, -np.inf, [0.0, 1.0])
+    >>> res = minimize(fun, x0, constraints=constraints)
+    >>> res.x
+    array([0.707, 0.707])
+
+    Finally, to see how to supply linear and nonlinear constraints
+    simultaneously, we solve Problem (G) of [2]_, defined as
+
+    .. math::
+
+        \begin{aligned}
+            \min_{x \in \mathbb{R}^3}   & \quad x_3\\
+            \text{s.t.}                 & \quad 5x_1 - x_2 + x_3 \ge 0,\\
+                                        & \quad -5x_1 - x_2 + x_3 \ge 0,\\
+                                        & \quad x_1^2 + x_2^2 + 4x_2 \le x_3.
+        \end{aligned}
+
+    Its objective and nonlinear constraint functions can be implemented as:
+
+    >>> def fun(x):
+    ...     return x[2]
+    >>>
+    >>> def cub(x):
+    ...     return x[0]**2 + x[1]**2 + 4.0*x[1] - x[2]
+
+    This problem can be solved using `minimize` as:
+
+    >>> x0 = [1.0, 1.0, 1.0]
+    >>> constraints = [
+    ...     LinearConstraint(
+    ...         [[5.0, -1.0, 1.0], [-5.0, -1.0, 1.0]],
+    ...         [0.0, 0.0],
+    ...         np.inf,
+    ...     ),
+    ...     NonlinearConstraint(cub, -np.inf, 0.0),
+    ... ]
+    >>> res = minimize(fun, x0, constraints=constraints)
+    >>> res.x
+    array([ 0., -3., -3.])
+    """
+    # Get basic options that are needed for the initialization.
+    if options is None:
+        options = {}
+    else:
+        options = dict(options)
+    verbose = options.get(Options.VERBOSE, DEFAULT_OPTIONS[Options.VERBOSE])
+    verbose = bool(verbose)
+    feasibility_tol = options.get(
+        Options.FEASIBILITY_TOL,
+        DEFAULT_OPTIONS[Options.FEASIBILITY_TOL],
+    )
+    feasibility_tol = float(feasibility_tol)
+    scale = options.get(Options.SCALE, DEFAULT_OPTIONS[Options.SCALE])
+    scale = bool(scale)
+    store_history = options.get(
+        Options.STORE_HISTORY,
+        DEFAULT_OPTIONS[Options.STORE_HISTORY],
+    )
+    store_history = bool(store_history)
+    if Options.HISTORY_SIZE in options and options[Options.HISTORY_SIZE] <= 0:
+        raise ValueError("The size of the history must be positive.")
+    history_size = options.get(
+        Options.HISTORY_SIZE,
+        DEFAULT_OPTIONS[Options.HISTORY_SIZE],
+    )
+    history_size = int(history_size)
+    if Options.FILTER_SIZE in options and options[Options.FILTER_SIZE] <= 0:
+        raise ValueError("The size of the filter must be positive.")
+    filter_size = options.get(
+        Options.FILTER_SIZE,
+        DEFAULT_OPTIONS[Options.FILTER_SIZE],
+    )
+    filter_size = int(filter_size)
+    debug = options.get(Options.DEBUG, DEFAULT_OPTIONS[Options.DEBUG])
+    debug = bool(debug)
+
+    # Initialize the objective function.
+    if not isinstance(args, tuple):
+        args = (args,)
+    obj = ObjectiveFunction(fun, verbose, debug, *args)
+
+    # Initialize the bound constraints.
+    if not hasattr(x0, "__len__"):
+        x0 = [x0]
+    n_orig = len(x0)
+    bounds = BoundConstraints(_get_bounds(bounds, n_orig))
+
+    # Initialize the constraints.
+    linear_constraints, nonlinear_constraints = _get_constraints(constraints)
+    linear = LinearConstraints(linear_constraints, n_orig, debug)
+    nonlinear = NonlinearConstraints(nonlinear_constraints, verbose, debug)
+
+    # Initialize the problem (and remove the fixed variables).
+    pb = Problem(
+        obj,
+        x0,
+        bounds,
+        linear,
+        nonlinear,
+        callback,
+        feasibility_tol,
+        scale,
+        store_history,
+        history_size,
+        filter_size,
+        debug,
+    )
+
+    # Set the default options.
+    _set_default_options(options, pb.n)
+    constants = _set_default_constants(**kwargs)
+
+    # Initialize the models and skip the computations whenever possible.
+    if not pb.bounds.is_feasible:
+        # The bound constraints are infeasible.
+        return _build_result(
+            pb,
+            0.0,
+            False,
+            ExitStatus.INFEASIBLE_ERROR,
+            0,
+            options,
+        )
+    elif pb.n == 0:
+        # All variables are fixed by the bound constraints.
+        return _build_result(
+            pb,
+            0.0,
+            True,
+            ExitStatus.FIXED_SUCCESS,
+            0,
+            options,
+        )
+    if verbose:
+        print("Starting the optimization procedure.")
+        print(f"Initial trust-region radius: {options[Options.RHOBEG]}.")
+        print(f"Final trust-region radius: {options[Options.RHOEND]}.")
+        print(
+            f"Maximum number of function evaluations: "
+            f"{options[Options.MAX_EVAL]}."
+        )
+        print(f"Maximum number of iterations: {options[Options.MAX_ITER]}.")
+        print()
+    try:
+        framework = TrustRegion(pb, options, constants)
+    except TargetSuccess:
+        # The target on the objective function value has been reached
+        return _build_result(
+            pb,
+            0.0,
+            True,
+            ExitStatus.TARGET_SUCCESS,
+            0,
+            options,
+        )
+    except CallbackSuccess:
+        # The callback raised a StopIteration exception.
+        return _build_result(
+            pb,
+            0.0,
+            True,
+            ExitStatus.CALLBACK_SUCCESS,
+            0,
+            options,
+        )
+    except FeasibleSuccess:
+        # The feasibility problem has been solved successfully.
+        return _build_result(
+            pb,
+            0.0,
+            True,
+            ExitStatus.FEASIBLE_SUCCESS,
+            0,
+            options,
+        )
+    except MaxEvalError:
+        # The maximum number of function evaluations has been exceeded.
+        return _build_result(
+            pb,
+            0.0,
+            False,
+            ExitStatus.MAX_ITER_WARNING,
+            0,
+            options,
+        )
+    except np.linalg.LinAlgError:
+        # The construction of the initial interpolation set failed.
+        return _build_result(
+            pb,
+            0.0,
+            False,
+            ExitStatus.LINALG_ERROR,
+            0,
+            options,
+        )
+
+    # Start the optimization procedure.
+    success = False
+    n_iter = 0
+    k_new = None
+    n_short_steps = 0
+    n_very_short_steps = 0
+    n_alt_models = 0
+    while True:
+        # Stop the optimization procedure if the maximum number of iterations
+        # has been exceeded. We do not write the main loop as a for loop
+        # because we want to access the number of iterations outside the loop.
+        if n_iter >= options[Options.MAX_ITER]:
+            status = ExitStatus.MAX_ITER_WARNING
+            break
+        n_iter += 1
+
+        # Update the point around which the quadratic models are built.
+        if (
+            np.linalg.norm(
+                framework.x_best - framework.models.interpolation.x_base
+            )
+            >= constants[Constants.LARGE_SHIFT_FACTOR] * framework.radius
+        ):
+            framework.shift_x_base(options)
+
+        # Evaluate the trial step.
+        radius_save = framework.radius
+        normal_step, tangential_step = framework.get_trust_region_step(options)
+        step = normal_step + tangential_step
+        s_norm = np.linalg.norm(step)
+
+        # If the trial step is too short, we do not attempt to evaluate the
+        # objective and constraint functions. Instead, we reduce the
+        # trust-region radius and check whether the resolution should be
+        # enhanced and whether the geometry of the interpolation set should be
+        # improved. Otherwise, we entertain a classical iteration. The
+        # criterion for performing an exceptional jump is taken from NEWUOA.
+        if (
+            s_norm
+            <= constants[Constants.SHORT_STEP_THRESHOLD] * framework.resolution
+        ):
+            framework.radius *= constants[Constants.DECREASE_RESOLUTION_FACTOR]
+            if radius_save > framework.resolution:
+                n_short_steps = 0
+                n_very_short_steps = 0
+            else:
+                n_short_steps += 1
+                n_very_short_steps += 1
+                if s_norm > 0.1 * framework.resolution:
+                    n_very_short_steps = 0
+            enhance_resolution = n_short_steps >= 5 or n_very_short_steps >= 3
+            if enhance_resolution:
+                n_short_steps = 0
+                n_very_short_steps = 0
+                improve_geometry = False
+            else:
+                try:
+                    k_new, dist_new = framework.get_index_to_remove()
+                except np.linalg.LinAlgError:
+                    status = ExitStatus.LINALG_ERROR
+                    break
+                improve_geometry = dist_new > max(
+                    framework.radius,
+                    constants[Constants.RESOLUTION_FACTOR]
+                    * framework.resolution,
+                )
+        else:
+            # Increase the penalty parameter if necessary.
+            same_best_point = framework.increase_penalty(step)
+            if same_best_point:
+                # Evaluate the objective and constraint functions.
+                try:
+                    fun_val, cub_val, ceq_val = _eval(
+                        pb,
+                        framework,
+                        step,
+                        options,
+                    )
+                except TargetSuccess:
+                    status = ExitStatus.TARGET_SUCCESS
+                    success = True
+                    break
+                except FeasibleSuccess:
+                    status = ExitStatus.FEASIBLE_SUCCESS
+                    success = True
+                    break
+                except CallbackSuccess:
+                    status = ExitStatus.CALLBACK_SUCCESS
+                    success = True
+                    break
+                except MaxEvalError:
+                    status = ExitStatus.MAX_EVAL_WARNING
+                    break
+
+                # Perform a second-order correction step if necessary.
+                merit_old = framework.merit(
+                    framework.x_best,
+                    framework.fun_best,
+                    framework.cub_best,
+                    framework.ceq_best,
+                )
+                merit_new = framework.merit(
+                    framework.x_best + step, fun_val, cub_val, ceq_val
+                )
+                if (
+                    pb.type == "nonlinearly constrained"
+                    and merit_new > merit_old
+                    and np.linalg.norm(normal_step)
+                    > constants[Constants.BYRD_OMOJOKUN_FACTOR] ** 2.0
+                    * framework.radius
+                ):
+                    soc_step = framework.get_second_order_correction_step(
+                        step, options
+                    )
+                    if np.linalg.norm(soc_step) > 0.0:
+                        step += soc_step
+
+                        # Evaluate the objective and constraint functions.
+                        try:
+                            fun_val, cub_val, ceq_val = _eval(
+                                pb,
+                                framework,
+                                step,
+                                options,
+                            )
+                        except TargetSuccess:
+                            status = ExitStatus.TARGET_SUCCESS
+                            success = True
+                            break
+                        except FeasibleSuccess:
+                            status = ExitStatus.FEASIBLE_SUCCESS
+                            success = True
+                            break
+                        except CallbackSuccess:
+                            status = ExitStatus.CALLBACK_SUCCESS
+                            success = True
+                            break
+                        except MaxEvalError:
+                            status = ExitStatus.MAX_EVAL_WARNING
+                            break
+
+                # Calculate the reduction ratio.
+                ratio = framework.get_reduction_ratio(
+                    step,
+                    fun_val,
+                    cub_val,
+                    ceq_val,
+                )
+
+                # Choose an interpolation point to remove.
+                try:
+                    k_new = framework.get_index_to_remove(
+                        framework.x_best + step
+                    )[0]
+                except np.linalg.LinAlgError:
+                    status = ExitStatus.LINALG_ERROR
+                    break
+
+                # Update the interpolation set.
+                try:
+                    ill_conditioned = framework.models.update_interpolation(
+                        k_new, framework.x_best + step, fun_val, cub_val,
+                        ceq_val
+                    )
+                except np.linalg.LinAlgError:
+                    status = ExitStatus.LINALG_ERROR
+                    break
+                framework.set_best_index()
+
+                # Update the trust-region radius.
+                framework.update_radius(step, ratio)
+
+                # Attempt to replace the models by the alternative ones.
+                if framework.radius <= framework.resolution:
+                    if ratio >= constants[Constants.VERY_LOW_RATIO]:
+                        n_alt_models = 0
+                    else:
+                        n_alt_models += 1
+                        grad = framework.models.fun_grad(framework.x_best)
+                        try:
+                            grad_alt = framework.models.fun_alt_grad(
+                                framework.x_best
+                            )
+                        except np.linalg.LinAlgError:
+                            status = ExitStatus.LINALG_ERROR
+                            break
+                        if np.linalg.norm(grad) < constants[
+                            Constants.LARGE_GRADIENT_FACTOR
+                        ] * np.linalg.norm(grad_alt):
+                            n_alt_models = 0
+                        if n_alt_models >= 3:
+                            try:
+                                framework.models.reset_models()
+                            except np.linalg.LinAlgError:
+                                status = ExitStatus.LINALG_ERROR
+                                break
+                            n_alt_models = 0
+
+                # Update the Lagrange multipliers.
+                framework.set_multipliers(framework.x_best + step)
+
+                # Check whether the resolution should be enhanced.
+                try:
+                    k_new, dist_new = framework.get_index_to_remove()
+                except np.linalg.LinAlgError:
+                    status = ExitStatus.LINALG_ERROR
+                    break
+                improve_geometry = (
+                    ill_conditioned
+                    or ratio <= constants[Constants.LOW_RATIO]
+                    and dist_new
+                    > max(
+                        framework.radius,
+                        constants[Constants.RESOLUTION_FACTOR]
+                        * framework.resolution,
+                    )
+                )
+                enhance_resolution = (
+                    radius_save <= framework.resolution
+                    and ratio <= constants[Constants.LOW_RATIO]
+                    and not improve_geometry
+                )
+            else:
+                # When increasing the penalty parameter, the best point so far
+                # may change. In this case, we restart the iteration.
+                enhance_resolution = False
+                improve_geometry = False
+
+        # Reduce the resolution if necessary.
+        if enhance_resolution:
+            if framework.resolution <= options[Options.RHOEND]:
+                success = True
+                status = ExitStatus.RADIUS_SUCCESS
+                break
+            framework.enhance_resolution(options)
+            framework.decrease_penalty()
+
+            if verbose:
+                maxcv_val = pb.maxcv(
+                    framework.x_best, framework.cub_best, framework.ceq_best
+                )
+                _print_step(
+                    f"New trust-region radius: {framework.resolution}",
+                    pb,
+                    pb.build_x(framework.x_best),
+                    framework.fun_best,
+                    maxcv_val,
+                    pb.n_eval,
+                    n_iter,
+                )
+                print()
+
+        # Improve the geometry of the interpolation set if necessary.
+        if improve_geometry:
+            try:
+                step = framework.get_geometry_step(k_new, options)
+            except np.linalg.LinAlgError:
+                status = ExitStatus.LINALG_ERROR
+                break
+
+            # Evaluate the objective and constraint functions.
+            try:
+                fun_val, cub_val, ceq_val = _eval(pb, framework, step, options)
+            except TargetSuccess:
+                status = ExitStatus.TARGET_SUCCESS
+                success = True
+                break
+            except FeasibleSuccess:
+                status = ExitStatus.FEASIBLE_SUCCESS
+                success = True
+                break
+            except CallbackSuccess:
+                status = ExitStatus.CALLBACK_SUCCESS
+                success = True
+                break
+            except MaxEvalError:
+                status = ExitStatus.MAX_EVAL_WARNING
+                break
+
+            # Update the interpolation set.
+            try:
+                framework.models.update_interpolation(
+                    k_new,
+                    framework.x_best + step,
+                    fun_val,
+                    cub_val,
+                    ceq_val,
+                )
+            except np.linalg.LinAlgError:
+                status = ExitStatus.LINALG_ERROR
+                break
+            framework.set_best_index()
+
+    return _build_result(
+        pb,
+        framework.penalty,
+        success,
+        status,
+        n_iter,
+        options,
+    )
+
+
+def _get_bounds(bounds, n):
+    """
+    Uniformize the bounds.
+    """
+    if bounds is None:
+        return Bounds(np.full(n, -np.inf), np.full(n, np.inf))
+    elif isinstance(bounds, Bounds):
+        if bounds.lb.shape != (n,) or bounds.ub.shape != (n,):
+            raise ValueError(f"The bounds must have {n} elements.")
+        return Bounds(bounds.lb, bounds.ub)
+    elif hasattr(bounds, "__len__"):
+        bounds = np.asarray(bounds)
+        if bounds.shape != (n, 2):
+            raise ValueError(
+                "The shape of the bounds is not compatible with "
+                "the number of variables."
+            )
+        return Bounds(bounds[:, 0], bounds[:, 1])
+    else:
+        raise TypeError(
+            "The bounds must be an instance of "
+            "scipy.optimize.Bounds or an array-like object."
+        )
+
+
+def _get_constraints(constraints):
+    """
+    Extract the linear and nonlinear constraints.
+    """
+    if isinstance(constraints, dict) or not hasattr(constraints, "__len__"):
+        constraints = (constraints,)
+
+    # Extract the linear and nonlinear constraints.
+    linear_constraints = []
+    nonlinear_constraints = []
+    for constraint in constraints:
+        if isinstance(constraint, LinearConstraint):
+            lb = exact_1d_array(
+                constraint.lb,
+                "The lower bound of the linear constraints must be a vector.",
+            )
+            ub = exact_1d_array(
+                constraint.ub,
+                "The upper bound of the linear constraints must be a vector.",
+            )
+            linear_constraints.append(
+                LinearConstraint(
+                    constraint.A,
+                    *np.broadcast_arrays(lb, ub),
+                )
+            )
+        elif isinstance(constraint, NonlinearConstraint):
+            lb = exact_1d_array(
+                constraint.lb,
+                "The lower bound of the "
+                "nonlinear constraints must be a "
+                "vector.",
+            )
+            ub = exact_1d_array(
+                constraint.ub,
+                "The upper bound of the "
+                "nonlinear constraints must be a "
+                "vector.",
+            )
+            nonlinear_constraints.append(
+                NonlinearConstraint(
+                    constraint.fun,
+                    *np.broadcast_arrays(lb, ub),
+                )
+            )
+        elif isinstance(constraint, dict):
+            if "type" not in constraint or constraint["type"] not in (
+                "eq",
+                "ineq",
+            ):
+                raise ValueError('The constraint type must be "eq" or "ineq".')
+            if "fun" not in constraint or not callable(constraint["fun"]):
+                raise ValueError("The constraint function must be callable.")
+            nonlinear_constraints.append(
+                {
+                    "fun": constraint["fun"],
+                    "type": constraint["type"],
+                    "args": constraint.get("args", ()),
+                }
+            )
+        else:
+            raise TypeError(
+                "The constraints must be instances of "
+                "scipy.optimize.LinearConstraint, "
+                "scipy.optimize.NonlinearConstraint, or dict."
+            )
+    return linear_constraints, nonlinear_constraints
+
+
+def _set_default_options(options, n):
+    """
+    Set the default options.
+    """
+    if Options.RHOBEG in options and options[Options.RHOBEG] <= 0.0:
+        raise ValueError("The initial trust-region radius must be positive.")
+    if Options.RHOEND in options and options[Options.RHOEND] < 0.0:
+        raise ValueError("The final trust-region radius must be nonnegative.")
+    if Options.RHOBEG in options and Options.RHOEND in options:
+        if options[Options.RHOBEG] < options[Options.RHOEND]:
+            raise ValueError(
+                "The initial trust-region radius must be greater "
+                "than or equal to the final trust-region radius."
+            )
+    elif Options.RHOBEG in options:
+        options[Options.RHOEND.value] = np.min(
+            [
+                DEFAULT_OPTIONS[Options.RHOEND],
+                options[Options.RHOBEG],
+            ]
+        )
+    elif Options.RHOEND in options:
+        options[Options.RHOBEG.value] = np.max(
+            [
+                DEFAULT_OPTIONS[Options.RHOBEG],
+                options[Options.RHOEND],
+            ]
+        )
+    else:
+        options[Options.RHOBEG.value] = DEFAULT_OPTIONS[Options.RHOBEG]
+        options[Options.RHOEND.value] = DEFAULT_OPTIONS[Options.RHOEND]
+    options[Options.RHOBEG.value] = float(options[Options.RHOBEG])
+    options[Options.RHOEND.value] = float(options[Options.RHOEND])
+    if Options.NPT in options and options[Options.NPT] <= 0:
+        raise ValueError("The number of interpolation points must be "
+                         "positive.")
+    if (
+        Options.NPT in options
+        and options[Options.NPT] > ((n + 1) * (n + 2)) // 2
+    ):
+        raise ValueError(
+            f"The number of interpolation points must be at most "
+            f"{((n + 1) * (n + 2)) // 2}."
+        )
+    options.setdefault(Options.NPT.value, DEFAULT_OPTIONS[Options.NPT](n))
+    options[Options.NPT.value] = int(options[Options.NPT])
+    if Options.MAX_EVAL in options and options[Options.MAX_EVAL] <= 0:
+        raise ValueError(
+            "The maximum number of function evaluations must be positive."
+        )
+    options.setdefault(
+        Options.MAX_EVAL.value,
+        np.max(
+            [
+                DEFAULT_OPTIONS[Options.MAX_EVAL](n),
+                options[Options.NPT] + 1,
+            ]
+        ),
+    )
+    options[Options.MAX_EVAL.value] = int(options[Options.MAX_EVAL])
+    if Options.MAX_ITER in options and options[Options.MAX_ITER] <= 0:
+        raise ValueError("The maximum number of iterations must be positive.")
+    options.setdefault(
+        Options.MAX_ITER.value,
+        DEFAULT_OPTIONS[Options.MAX_ITER](n),
+    )
+    options[Options.MAX_ITER.value] = int(options[Options.MAX_ITER])
+    options.setdefault(Options.TARGET.value, DEFAULT_OPTIONS[Options.TARGET])
+    options[Options.TARGET.value] = float(options[Options.TARGET])
+    options.setdefault(
+        Options.FEASIBILITY_TOL.value,
+        DEFAULT_OPTIONS[Options.FEASIBILITY_TOL],
+    )
+    options[Options.FEASIBILITY_TOL.value] = float(
+        options[Options.FEASIBILITY_TOL]
+    )
+    options.setdefault(Options.VERBOSE.value, DEFAULT_OPTIONS[Options.VERBOSE])
+    options[Options.VERBOSE.value] = bool(options[Options.VERBOSE])
+    options.setdefault(Options.SCALE.value, DEFAULT_OPTIONS[Options.SCALE])
+    options[Options.SCALE.value] = bool(options[Options.SCALE])
+    options.setdefault(
+        Options.FILTER_SIZE.value,
+        DEFAULT_OPTIONS[Options.FILTER_SIZE],
+    )
+    options[Options.FILTER_SIZE.value] = int(options[Options.FILTER_SIZE])
+    options.setdefault(
+        Options.STORE_HISTORY.value,
+        DEFAULT_OPTIONS[Options.STORE_HISTORY],
+    )
+    options[Options.STORE_HISTORY.value] = bool(options[Options.STORE_HISTORY])
+    options.setdefault(
+        Options.HISTORY_SIZE.value,
+        DEFAULT_OPTIONS[Options.HISTORY_SIZE],
+    )
+    options[Options.HISTORY_SIZE.value] = int(options[Options.HISTORY_SIZE])
+    options.setdefault(Options.DEBUG.value, DEFAULT_OPTIONS[Options.DEBUG])
+    options[Options.DEBUG.value] = bool(options[Options.DEBUG])
+
+    # Check whether they are any unknown options.
+    for key in options:
+        if key not in Options.__members__.values():
+            warnings.warn(f"Unknown option: {key}.", RuntimeWarning, 3)
+
+
+def _set_default_constants(**kwargs):
+    """
+    Set the default constants.
+    """
+    constants = dict(kwargs)
+    constants.setdefault(
+        Constants.DECREASE_RADIUS_FACTOR.value,
+        DEFAULT_CONSTANTS[Constants.DECREASE_RADIUS_FACTOR],
+    )
+    constants[Constants.DECREASE_RADIUS_FACTOR.value] = float(
+        constants[Constants.DECREASE_RADIUS_FACTOR]
+    )
+    if (
+        constants[Constants.DECREASE_RADIUS_FACTOR] <= 0.0
+        or constants[Constants.DECREASE_RADIUS_FACTOR] >= 1.0
+    ):
+        raise ValueError(
+            "The constant decrease_radius_factor must be in the interval "
+            "(0, 1)."
+        )
+    constants.setdefault(
+        Constants.INCREASE_RADIUS_THRESHOLD.value,
+        DEFAULT_CONSTANTS[Constants.INCREASE_RADIUS_THRESHOLD],
+    )
+    constants[Constants.INCREASE_RADIUS_THRESHOLD.value] = float(
+        constants[Constants.INCREASE_RADIUS_THRESHOLD]
+    )
+    if constants[Constants.INCREASE_RADIUS_THRESHOLD] <= 1.0:
+        raise ValueError(
+            "The constant increase_radius_threshold must be greater than 1."
+        )
+    if (
+        Constants.INCREASE_RADIUS_FACTOR in constants
+        and constants[Constants.INCREASE_RADIUS_FACTOR] <= 1.0
+    ):
+        raise ValueError(
+            "The constant increase_radius_factor must be greater than 1."
+        )
+    if (
+        Constants.DECREASE_RADIUS_THRESHOLD in constants
+        and constants[Constants.DECREASE_RADIUS_THRESHOLD] <= 1.0
+    ):
+        raise ValueError(
+            "The constant decrease_radius_threshold must be greater than 1."
+        )
+    if (
+        Constants.INCREASE_RADIUS_FACTOR in constants
+        and Constants.DECREASE_RADIUS_THRESHOLD in constants
+    ):
+        if (
+            constants[Constants.DECREASE_RADIUS_THRESHOLD]
+            >= constants[Constants.INCREASE_RADIUS_FACTOR]
+        ):
+            raise ValueError(
+                "The constant decrease_radius_threshold must be "
+                "less than increase_radius_factor."
+            )
+    elif Constants.INCREASE_RADIUS_FACTOR in constants:
+        constants[Constants.DECREASE_RADIUS_THRESHOLD.value] = np.min(
+            [
+                DEFAULT_CONSTANTS[Constants.DECREASE_RADIUS_THRESHOLD],
+                0.5 * (1.0 + constants[Constants.INCREASE_RADIUS_FACTOR]),
+            ]
+        )
+    elif Constants.DECREASE_RADIUS_THRESHOLD in constants:
+        constants[Constants.INCREASE_RADIUS_FACTOR.value] = np.max(
+            [
+                DEFAULT_CONSTANTS[Constants.INCREASE_RADIUS_FACTOR],
+                2.0 * constants[Constants.DECREASE_RADIUS_THRESHOLD],
+            ]
+        )
+    else:
+        constants[Constants.INCREASE_RADIUS_FACTOR.value] = DEFAULT_CONSTANTS[
+            Constants.INCREASE_RADIUS_FACTOR
+        ]
+        constants[Constants.DECREASE_RADIUS_THRESHOLD.value] = (
+            DEFAULT_CONSTANTS[Constants.DECREASE_RADIUS_THRESHOLD])
+    constants.setdefault(
+        Constants.DECREASE_RESOLUTION_FACTOR.value,
+        DEFAULT_CONSTANTS[Constants.DECREASE_RESOLUTION_FACTOR],
+    )
+    constants[Constants.DECREASE_RESOLUTION_FACTOR.value] = float(
+        constants[Constants.DECREASE_RESOLUTION_FACTOR]
+    )
+    if (
+        constants[Constants.DECREASE_RESOLUTION_FACTOR] <= 0.0
+        or constants[Constants.DECREASE_RESOLUTION_FACTOR] >= 1.0
+    ):
+        raise ValueError(
+            "The constant decrease_resolution_factor must be in the interval "
+            "(0, 1)."
+        )
+    if (
+        Constants.LARGE_RESOLUTION_THRESHOLD in constants
+        and constants[Constants.LARGE_RESOLUTION_THRESHOLD] <= 1.0
+    ):
+        raise ValueError(
+            "The constant large_resolution_threshold must be greater than 1."
+        )
+    if (
+        Constants.MODERATE_RESOLUTION_THRESHOLD in constants
+        and constants[Constants.MODERATE_RESOLUTION_THRESHOLD] <= 1.0
+    ):
+        raise ValueError(
+            "The constant moderate_resolution_threshold must be greater than "
+            "1."
+        )
+    if (
+        Constants.LARGE_RESOLUTION_THRESHOLD in constants
+        and Constants.MODERATE_RESOLUTION_THRESHOLD in constants
+    ):
+        if (
+            constants[Constants.MODERATE_RESOLUTION_THRESHOLD]
+            > constants[Constants.LARGE_RESOLUTION_THRESHOLD]
+        ):
+            raise ValueError(
+                "The constant moderate_resolution_threshold "
+                "must be at most large_resolution_threshold."
+            )
+    elif Constants.LARGE_RESOLUTION_THRESHOLD in constants:
+        constants[Constants.MODERATE_RESOLUTION_THRESHOLD.value] = np.min(
+            [
+                DEFAULT_CONSTANTS[Constants.MODERATE_RESOLUTION_THRESHOLD],
+                constants[Constants.LARGE_RESOLUTION_THRESHOLD],
+            ]
+        )
+    elif Constants.MODERATE_RESOLUTION_THRESHOLD in constants:
+        constants[Constants.LARGE_RESOLUTION_THRESHOLD.value] = np.max(
+            [
+                DEFAULT_CONSTANTS[Constants.LARGE_RESOLUTION_THRESHOLD],
+                constants[Constants.MODERATE_RESOLUTION_THRESHOLD],
+            ]
+        )
+    else:
+        constants[Constants.LARGE_RESOLUTION_THRESHOLD.value] = (
+            DEFAULT_CONSTANTS[Constants.LARGE_RESOLUTION_THRESHOLD]
+        )
+        constants[Constants.MODERATE_RESOLUTION_THRESHOLD.value] = (
+            DEFAULT_CONSTANTS[Constants.MODERATE_RESOLUTION_THRESHOLD]
+        )
+    if Constants.LOW_RATIO in constants and (
+        constants[Constants.LOW_RATIO] <= 0.0
+        or constants[Constants.LOW_RATIO] >= 1.0
+    ):
+        raise ValueError(
+            "The constant low_ratio must be in the interval (0, 1)."
+        )
+    if Constants.HIGH_RATIO in constants and (
+        constants[Constants.HIGH_RATIO] <= 0.0
+        or constants[Constants.HIGH_RATIO] >= 1.0
+    ):
+        raise ValueError(
+            "The constant high_ratio must be in the interval (0, 1)."
+        )
+    if Constants.LOW_RATIO in constants and Constants.HIGH_RATIO in constants:
+        if constants[Constants.LOW_RATIO] > constants[Constants.HIGH_RATIO]:
+            raise ValueError(
+                "The constant low_ratio must be at most high_ratio."
+            )
+    elif Constants.LOW_RATIO in constants:
+        constants[Constants.HIGH_RATIO.value] = np.max(
+            [
+                DEFAULT_CONSTANTS[Constants.HIGH_RATIO],
+                constants[Constants.LOW_RATIO],
+            ]
+        )
+    elif Constants.HIGH_RATIO in constants:
+        constants[Constants.LOW_RATIO.value] = np.min(
+            [
+                DEFAULT_CONSTANTS[Constants.LOW_RATIO],
+                constants[Constants.HIGH_RATIO],
+            ]
+        )
+    else:
+        constants[Constants.LOW_RATIO.value] = DEFAULT_CONSTANTS[
+            Constants.LOW_RATIO
+        ]
+        constants[Constants.HIGH_RATIO.value] = DEFAULT_CONSTANTS[
+            Constants.HIGH_RATIO
+        ]
+    constants.setdefault(
+        Constants.VERY_LOW_RATIO.value,
+        DEFAULT_CONSTANTS[Constants.VERY_LOW_RATIO],
+    )
+    constants[Constants.VERY_LOW_RATIO.value] = float(
+        constants[Constants.VERY_LOW_RATIO]
+    )
+    if (
+        constants[Constants.VERY_LOW_RATIO] <= 0.0
+        or constants[Constants.VERY_LOW_RATIO] >= 1.0
+    ):
+        raise ValueError(
+            "The constant very_low_ratio must be in the interval (0, 1)."
+        )
+    if (
+        Constants.PENALTY_INCREASE_THRESHOLD in constants
+        and constants[Constants.PENALTY_INCREASE_THRESHOLD] < 1.0
+    ):
+        raise ValueError(
+            "The constant penalty_increase_threshold must be "
+            "greater than or equal to 1."
+        )
+    if (
+        Constants.PENALTY_INCREASE_FACTOR in constants
+        and constants[Constants.PENALTY_INCREASE_FACTOR] <= 1.0
+    ):
+        raise ValueError(
+            "The constant penalty_increase_factor must be greater than 1."
+        )
+    if (
+        Constants.PENALTY_INCREASE_THRESHOLD in constants
+        and Constants.PENALTY_INCREASE_FACTOR in constants
+    ):
+        if (
+            constants[Constants.PENALTY_INCREASE_FACTOR]
+            < constants[Constants.PENALTY_INCREASE_THRESHOLD]
+        ):
+            raise ValueError(
+                "The constant penalty_increase_factor must be "
+                "greater than or equal to "
+                "penalty_increase_threshold."
+            )
+    elif Constants.PENALTY_INCREASE_THRESHOLD in constants:
+        constants[Constants.PENALTY_INCREASE_FACTOR.value] = np.max(
+            [
+                DEFAULT_CONSTANTS[Constants.PENALTY_INCREASE_FACTOR],
+                constants[Constants.PENALTY_INCREASE_THRESHOLD],
+            ]
+        )
+    elif Constants.PENALTY_INCREASE_FACTOR in constants:
+        constants[Constants.PENALTY_INCREASE_THRESHOLD.value] = np.min(
+            [
+                DEFAULT_CONSTANTS[Constants.PENALTY_INCREASE_THRESHOLD],
+                constants[Constants.PENALTY_INCREASE_FACTOR],
+            ]
+        )
+    else:
+        constants[Constants.PENALTY_INCREASE_THRESHOLD.value] = (
+            DEFAULT_CONSTANTS[Constants.PENALTY_INCREASE_THRESHOLD]
+        )
+        constants[Constants.PENALTY_INCREASE_FACTOR.value] = DEFAULT_CONSTANTS[
+            Constants.PENALTY_INCREASE_FACTOR
+        ]
+    constants.setdefault(
+        Constants.SHORT_STEP_THRESHOLD.value,
+        DEFAULT_CONSTANTS[Constants.SHORT_STEP_THRESHOLD],
+    )
+    constants[Constants.SHORT_STEP_THRESHOLD.value] = float(
+        constants[Constants.SHORT_STEP_THRESHOLD]
+    )
+    if (
+        constants[Constants.SHORT_STEP_THRESHOLD] <= 0.0
+        or constants[Constants.SHORT_STEP_THRESHOLD] >= 1.0
+    ):
+        raise ValueError(
+            "The constant short_step_threshold must be in the interval (0, 1)."
+        )
+    constants.setdefault(
+        Constants.LOW_RADIUS_FACTOR.value,
+        DEFAULT_CONSTANTS[Constants.LOW_RADIUS_FACTOR],
+    )
+    constants[Constants.LOW_RADIUS_FACTOR.value] = float(
+        constants[Constants.LOW_RADIUS_FACTOR]
+    )
+    if (
+        constants[Constants.LOW_RADIUS_FACTOR] <= 0.0
+        or constants[Constants.LOW_RADIUS_FACTOR] >= 1.0
+    ):
+        raise ValueError(
+            "The constant low_radius_factor must be in the interval (0, 1)."
+        )
+    constants.setdefault(
+        Constants.BYRD_OMOJOKUN_FACTOR.value,
+        DEFAULT_CONSTANTS[Constants.BYRD_OMOJOKUN_FACTOR],
+    )
+    constants[Constants.BYRD_OMOJOKUN_FACTOR.value] = float(
+        constants[Constants.BYRD_OMOJOKUN_FACTOR]
+    )
+    if (
+        constants[Constants.BYRD_OMOJOKUN_FACTOR] <= 0.0
+        or constants[Constants.BYRD_OMOJOKUN_FACTOR] >= 1.0
+    ):
+        raise ValueError(
+            "The constant byrd_omojokun_factor must be in the interval (0, 1)."
+        )
+    constants.setdefault(
+        Constants.THRESHOLD_RATIO_CONSTRAINTS.value,
+        DEFAULT_CONSTANTS[Constants.THRESHOLD_RATIO_CONSTRAINTS],
+    )
+    constants[Constants.THRESHOLD_RATIO_CONSTRAINTS.value] = float(
+        constants[Constants.THRESHOLD_RATIO_CONSTRAINTS]
+    )
+    if constants[Constants.THRESHOLD_RATIO_CONSTRAINTS] <= 1.0:
+        raise ValueError(
+            "The constant threshold_ratio_constraints must be greater than 1."
+        )
+    constants.setdefault(
+        Constants.LARGE_SHIFT_FACTOR.value,
+        DEFAULT_CONSTANTS[Constants.LARGE_SHIFT_FACTOR],
+    )
+    constants[Constants.LARGE_SHIFT_FACTOR.value] = float(
+        constants[Constants.LARGE_SHIFT_FACTOR]
+    )
+    if constants[Constants.LARGE_SHIFT_FACTOR] < 0.0:
+        raise ValueError("The constant large_shift_factor must be "
+                         "nonnegative.")
+    constants.setdefault(
+        Constants.LARGE_GRADIENT_FACTOR.value,
+        DEFAULT_CONSTANTS[Constants.LARGE_GRADIENT_FACTOR],
+    )
+    constants[Constants.LARGE_GRADIENT_FACTOR.value] = float(
+        constants[Constants.LARGE_GRADIENT_FACTOR]
+    )
+    if constants[Constants.LARGE_GRADIENT_FACTOR] <= 1.0:
+        raise ValueError(
+            "The constant large_gradient_factor must be greater than 1."
+        )
+    constants.setdefault(
+        Constants.RESOLUTION_FACTOR.value,
+        DEFAULT_CONSTANTS[Constants.RESOLUTION_FACTOR],
+    )
+    constants[Constants.RESOLUTION_FACTOR.value] = float(
+        constants[Constants.RESOLUTION_FACTOR]
+    )
+    if constants[Constants.RESOLUTION_FACTOR] <= 1.0:
+        raise ValueError(
+            "The constant resolution_factor must be greater than 1."
+        )
+    constants.setdefault(
+        Constants.IMPROVE_TCG.value,
+        DEFAULT_CONSTANTS[Constants.IMPROVE_TCG],
+    )
+    constants[Constants.IMPROVE_TCG.value] = bool(
+        constants[Constants.IMPROVE_TCG]
+    )
+
+    # Check whether they are any unknown options.
+    for key in kwargs:
+        if key not in Constants.__members__.values():
+            warnings.warn(f"Unknown constant: {key}.", RuntimeWarning, 3)
+    return constants
+
+
+def _eval(pb, framework, step, options):
+    """
+    Evaluate the objective and constraint functions.
+    """
+    if pb.n_eval >= options[Options.MAX_EVAL]:
+        raise MaxEvalError
+    x_eval = framework.x_best + step
+    fun_val, cub_val, ceq_val = pb(x_eval, framework.penalty)
+    r_val = pb.maxcv(x_eval, cub_val, ceq_val)
+    if (
+        fun_val <= options[Options.TARGET]
+        and r_val <= options[Options.FEASIBILITY_TOL]
+    ):
+        raise TargetSuccess
+    if pb.is_feasibility and r_val <= options[Options.FEASIBILITY_TOL]:
+        raise FeasibleSuccess
+    return fun_val, cub_val, ceq_val
+
+
+def _build_result(pb, penalty, success, status, n_iter, options):
+    """
+    Build the result of the optimization process.
+    """
+    # Build the result.
+    x, fun, maxcv = pb.best_eval(penalty)
+    success = success and np.isfinite(fun) and np.isfinite(maxcv)
+    if status not in [ExitStatus.TARGET_SUCCESS, ExitStatus.FEASIBLE_SUCCESS]:
+        success = success and maxcv <= options[Options.FEASIBILITY_TOL]
+    result = OptimizeResult()
+    result.message = {
+        ExitStatus.RADIUS_SUCCESS: "The lower bound for the trust-region "
+                                   "radius has been reached",
+        ExitStatus.TARGET_SUCCESS: "The target objective function value has "
+                                   "been reached",
+        ExitStatus.FIXED_SUCCESS: "All variables are fixed by the bound "
+                                  "constraints",
+        ExitStatus.CALLBACK_SUCCESS: "The callback requested to stop the "
+                                     "optimization procedure",
+        ExitStatus.FEASIBLE_SUCCESS: "The feasibility problem received has "
+                                     "been solved successfully",
+        ExitStatus.MAX_EVAL_WARNING: "The maximum number of function "
+                                     "evaluations has been exceeded",
+        ExitStatus.MAX_ITER_WARNING: "The maximum number of iterations has "
+                                     "been exceeded",
+        ExitStatus.INFEASIBLE_ERROR: "The bound constraints are infeasible",
+        ExitStatus.LINALG_ERROR: "A linear algebra error occurred",
+    }.get(status, "Unknown exit status")
+    result.success = success
+    result.status = status.value
+    result.x = pb.build_x(x)
+    result.fun = fun
+    result.maxcv = maxcv
+    result.nfev = pb.n_eval
+    result.nit = n_iter
+    if options[Options.STORE_HISTORY]:
+        result.fun_history = pb.fun_history
+        result.maxcv_history = pb.maxcv_history
+
+    # Print the result if requested.
+    if options[Options.VERBOSE]:
+        _print_step(
+            result.message,
+            pb,
+            result.x,
+            result.fun,
+            result.maxcv,
+            result.nfev,
+            result.nit,
+        )
+    return result
+
+
+def _print_step(message, pb, x, fun_val, r_val, n_eval, n_iter):
+    """
+    Print information about the current state of the optimization process.
+    """
+    print()
+    print(f"{message}.")
+    print(f"Number of function evaluations: {n_eval}.")
+    print(f"Number of iterations: {n_iter}.")
+    if not pb.is_feasibility:
+        print(f"Least value of {pb.fun_name}: {fun_val}.")
+    print(f"Maximum constraint violation: {r_val}.")
+    with np.printoptions(**PRINT_OPTIONS):
+        print(f"Corresponding point: {x}.")
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/models.py b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..44da70bf03a77e63af9ee45974050d008eed0147
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/models.py
@@ -0,0 +1,1531 @@
+import warnings
+
+import numpy as np
+from scipy.linalg import eigh
+
+from .settings import Options
+from .utils import MaxEvalError, TargetSuccess, FeasibleSuccess
+
+
+EPS = np.finfo(float).eps
+
+
+class Interpolation:
+    """
+    Interpolation set.
+
+    This class stores a base point around which the models are expanded and the
+    interpolation points. The coordinates of the interpolation points are
+    relative to the base point.
+    """
+
+    def __init__(self, pb, options):
+        """
+        Initialize the interpolation set.
+
+        Parameters
+        ----------
+        pb : `cobyqa.problem.Problem`
+            Problem to be solved.
+        options : dict
+            Options of the solver.
+        """
+        # Reduce the initial trust-region radius if necessary.
+        self._debug = options[Options.DEBUG]
+        max_radius = 0.5 * np.min(pb.bounds.xu - pb.bounds.xl)
+        if options[Options.RHOBEG] > max_radius:
+            options[Options.RHOBEG.value] = max_radius
+            options[Options.RHOEND.value] = np.min(
+                [
+                    options[Options.RHOEND],
+                    max_radius,
+                ]
+            )
+
+        # Set the initial point around which the models are expanded.
+        self._x_base = np.copy(pb.x0)
+        very_close_xl_idx = (
+            self.x_base <= pb.bounds.xl + 0.5 * options[Options.RHOBEG]
+        )
+        self.x_base[very_close_xl_idx] = pb.bounds.xl[very_close_xl_idx]
+        close_xl_idx = (
+            pb.bounds.xl + 0.5 * options[Options.RHOBEG] < self.x_base
+        ) & (self.x_base <= pb.bounds.xl + options[Options.RHOBEG])
+        self.x_base[close_xl_idx] = np.minimum(
+            pb.bounds.xl[close_xl_idx] + options[Options.RHOBEG],
+            pb.bounds.xu[close_xl_idx],
+        )
+        very_close_xu_idx = (
+            self.x_base >= pb.bounds.xu - 0.5 * options[Options.RHOBEG]
+        )
+        self.x_base[very_close_xu_idx] = pb.bounds.xu[very_close_xu_idx]
+        close_xu_idx = (
+            self.x_base < pb.bounds.xu - 0.5 * options[Options.RHOBEG]
+        ) & (pb.bounds.xu - options[Options.RHOBEG] <= self.x_base)
+        self.x_base[close_xu_idx] = np.maximum(
+            pb.bounds.xu[close_xu_idx] - options[Options.RHOBEG],
+            pb.bounds.xl[close_xu_idx],
+        )
+
+        # Set the initial interpolation set.
+        self._xpt = np.zeros((pb.n, options[Options.NPT]))
+        for k in range(1, options[Options.NPT]):
+            if k <= pb.n:
+                if very_close_xu_idx[k - 1]:
+                    self.xpt[k - 1, k] = -options[Options.RHOBEG]
+                else:
+                    self.xpt[k - 1, k] = options[Options.RHOBEG]
+            elif k <= 2 * pb.n:
+                if very_close_xl_idx[k - pb.n - 1]:
+                    self.xpt[k - pb.n - 1, k] = 2.0 * options[Options.RHOBEG]
+                elif very_close_xu_idx[k - pb.n - 1]:
+                    self.xpt[k - pb.n - 1, k] = -2.0 * options[Options.RHOBEG]
+                else:
+                    self.xpt[k - pb.n - 1, k] = -options[Options.RHOBEG]
+            else:
+                spread = (k - pb.n - 1) // pb.n
+                k1 = k - (1 + spread) * pb.n - 1
+                k2 = (k1 + spread) % pb.n
+                self.xpt[k1, k] = self.xpt[k1, k1 + 1]
+                self.xpt[k2, k] = self.xpt[k2, k2 + 1]
+        self._lhs_cache = None
+
+    @property
+    def n(self):
+        """
+        Number of variables.
+
+        Returns
+        -------
+        int
+            Number of variables.
+        """
+        return self.xpt.shape[0]
+
+    @property
+    def npt(self):
+        """
+        Number of interpolation points.
+
+        Returns
+        -------
+        int
+            Number of interpolation points.
+        """
+        return self.xpt.shape[1]
+
+    @property
+    def xpt(self):
+        """
+        Interpolation points.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n, npt)
+            Interpolation points.
+        """
+        return self._xpt
+
+    @xpt.setter
+    def xpt(self, xpt):
+        """
+        Set the interpolation points.
+
+        Parameters
+        ----------
+        xpt : `numpy.ndarray`, shape (n, npt)
+            New interpolation points.
+        """
+        if self._debug:
+            assert xpt.shape == (
+                self.n,
+                self.npt,
+            ), "The shape of `xpt` is not valid."
+        self._xpt = xpt
+
+    @property
+    def x_base(self):
+        """
+        Base point around which the models are expanded.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n,)
+            Base point around which the models are expanded.
+        """
+        return self._x_base
+
+    @x_base.setter
+    def x_base(self, x_base):
+        """
+        Set the base point around which the models are expanded.
+
+        Parameters
+        ----------
+        x_base : `numpy.ndarray`, shape (n,)
+            New base point around which the models are expanded.
+        """
+        if self._debug:
+            assert x_base.shape == (
+                self.n,
+            ), "The shape of `x_base` is not valid."
+        self._x_base = x_base
+
+    def point(self, k):
+        """
+        Get the `k`-th interpolation point.
+
+        The return point is relative to the origin.
+
+        Parameters
+        ----------
+        k : int
+            Index of the interpolation point.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n,)
+            `k`-th interpolation point.
+        """
+        if self._debug:
+            assert 0 <= k < self.npt, "The index `k` is not valid."
+        return self.x_base + self.xpt[:, k]
+
+
+def build_system(interpolation):
+    """
+    Build the left-hand side matrix of the interpolation system. The
+    matrix below stores W * diag(right_scaling),
+    where W is the theoretical matrix of the interpolation system. The
+    right scaling matrices is chosen to keep the elements in
+    the matrix well-balanced.
+
+    Parameters
+    ----------
+    interpolation : `cobyqa.models.Interpolation`
+        Interpolation set.
+    """
+
+    _cache = interpolation._lhs_cache
+    # Compute the scaled directions from the base point to the
+    # interpolation points. We scale the directions to avoid numerical
+    # difficulties.
+    if _cache is not None and np.array_equal(
+        interpolation.xpt, _cache["xpt"]
+    ):
+        return _cache["a"], _cache["right_scaling"], _cache["eigh"]
+
+    scale = np.max(np.linalg.norm(interpolation.xpt, axis=0), initial=EPS)
+    xpt_scale = interpolation.xpt / scale
+
+    n, npt = xpt_scale.shape
+    a = np.zeros((npt + n + 1, npt + n + 1))
+    a[:npt, :npt] = 0.5 * (xpt_scale.T @ xpt_scale) ** 2.0
+    a[:npt, npt] = 1.0
+    a[:npt, npt + 1:] = xpt_scale.T
+    a[npt, :npt] = 1.0
+    a[npt + 1:, :npt] = xpt_scale
+
+    # Build the left and right scaling diagonal matrices.
+    right_scaling = np.empty(npt + n + 1)
+    right_scaling[:npt] = 1.0 / scale**2.0
+    right_scaling[npt] = scale**2.0
+    right_scaling[npt + 1:] = scale
+
+    eig_values, eig_vectors = eigh(a, check_finite=False)
+
+    new_cache = {
+        "xpt": np.copy(interpolation.xpt),
+        "a": np.copy(a),
+        "right_scaling": np.copy(right_scaling),
+        "eigh": (eig_values, eig_vectors),
+    }
+    interpolation._lhs_cache = new_cache
+
+    return a, right_scaling, (eig_values, eig_vectors)
+
+
+class Quadratic:
+    """
+    Quadratic model.
+
+    This class stores the Hessian matrix of the quadratic model using the
+    implicit/explicit representation designed by Powell for NEWUOA [1]_.
+
+    References
+    ----------
+    .. [1] M. J. D. Powell. The NEWUOA software for unconstrained optimization
+       without derivatives. In G. Di Pillo and M. Roma, editors, *Large-Scale
+       Nonlinear Optimization*, volume 83 of Nonconvex Optim. Appl., pages
+       255--297. Springer, Boston, MA, USA, 2006. `doi:10.1007/0-387-30065-1_16
+       `_.
+    """
+
+    def __init__(self, interpolation, values, debug):
+        """
+        Initialize the quadratic model.
+
+        Parameters
+        ----------
+        interpolation : `cobyqa.models.Interpolation`
+            Interpolation set.
+        values : `numpy.ndarray`, shape (npt,)
+            Values of the interpolated function at the interpolation points.
+        debug : bool
+            Whether to make debugging tests during the execution.
+
+        Raises
+        ------
+        `numpy.linalg.LinAlgError`
+            If the interpolation system is ill-defined.
+        """
+        self._debug = debug
+        if self._debug:
+            assert values.shape == (
+                interpolation.npt,
+            ), "The shape of `values` is not valid."
+        if interpolation.npt < interpolation.n + 1:
+            raise ValueError(
+                f"The number of interpolation points must be at least "
+                f"{interpolation.n + 1}."
+            )
+        self._const, self._grad, self._i_hess, _ = self._get_model(
+            interpolation,
+            values,
+        )
+        self._e_hess = np.zeros((self.n, self.n))
+
+    def __call__(self, x, interpolation):
+        """
+        Evaluate the quadratic model at a given point.
+
+        Parameters
+        ----------
+        x : `numpy.ndarray`, shape (n,)
+            Point at which the quadratic model is evaluated.
+        interpolation : `cobyqa.models.Interpolation`
+            Interpolation set.
+
+        Returns
+        -------
+        float
+            Value of the quadratic model at `x`.
+        """
+        if self._debug:
+            assert x.shape == (self.n,), "The shape of `x` is not valid."
+        x_diff = x - interpolation.x_base
+        return (
+            self._const
+            + self._grad @ x_diff
+            + 0.5
+            * (
+                self._i_hess @ (interpolation.xpt.T @ x_diff) ** 2.0
+                + x_diff @ self._e_hess @ x_diff
+            )
+        )
+
+    @property
+    def n(self):
+        """
+        Number of variables.
+
+        Returns
+        -------
+        int
+            Number of variables.
+        """
+        return self._grad.size
+
+    @property
+    def npt(self):
+        """
+        Number of interpolation points used to define the quadratic model.
+
+        Returns
+        -------
+        int
+            Number of interpolation points used to define the quadratic model.
+        """
+        return self._i_hess.size
+
+    def grad(self, x, interpolation):
+        """
+        Evaluate the gradient of the quadratic model at a given point.
+
+        Parameters
+        ----------
+        x : `numpy.ndarray`, shape (n,)
+            Point at which the gradient of the quadratic model is evaluated.
+        interpolation : `cobyqa.models.Interpolation`
+            Interpolation set.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n,)
+            Gradient of the quadratic model at `x`.
+        """
+        if self._debug:
+            assert x.shape == (self.n,), "The shape of `x` is not valid."
+        x_diff = x - interpolation.x_base
+        return self._grad + self.hess_prod(x_diff, interpolation)
+
+    def hess(self, interpolation):
+        """
+        Evaluate the Hessian matrix of the quadratic model.
+
+        Parameters
+        ----------
+        interpolation : `cobyqa.models.Interpolation`
+            Interpolation set.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n, n)
+            Hessian matrix of the quadratic model.
+        """
+        return self._e_hess + interpolation.xpt @ (
+            self._i_hess[:, np.newaxis] * interpolation.xpt.T
+        )
+
+    def hess_prod(self, v, interpolation):
+        """
+        Evaluate the right product of the Hessian matrix of the quadratic model
+        with a given vector.
+
+        Parameters
+        ----------
+        v : `numpy.ndarray`, shape (n,)
+            Vector with which the Hessian matrix of the quadratic model is
+            multiplied from the right.
+        interpolation : `cobyqa.models.Interpolation`
+            Interpolation set.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n,)
+            Right product of the Hessian matrix of the quadratic model with
+            `v`.
+        """
+        if self._debug:
+            assert v.shape == (self.n,), "The shape of `v` is not valid."
+        return self._e_hess @ v + interpolation.xpt @ (
+            self._i_hess * (interpolation.xpt.T @ v)
+        )
+
+    def curv(self, v, interpolation):
+        """
+        Evaluate the curvature of the quadratic model along a given direction.
+
+        Parameters
+        ----------
+        v : `numpy.ndarray`, shape (n,)
+            Direction along which the curvature of the quadratic model is
+            evaluated.
+        interpolation : `cobyqa.models.Interpolation`
+            Interpolation set.
+
+        Returns
+        -------
+        float
+            Curvature of the quadratic model along `v`.
+        """
+        if self._debug:
+            assert v.shape == (self.n,), "The shape of `v` is not valid."
+        return (
+            v @ self._e_hess @ v
+            + self._i_hess @ (interpolation.xpt.T @ v) ** 2.0
+        )
+
+    def update(self, interpolation, k_new, dir_old, values_diff):
+        """
+        Update the quadratic model.
+
+        This method applies the derivative-free symmetric Broyden update to the
+        quadratic model. The `knew`-th interpolation point must be updated
+        before calling this method.
+
+        Parameters
+        ----------
+        interpolation : `cobyqa.models.Interpolation`
+            Updated interpolation set.
+        k_new : int
+            Index of the updated interpolation point.
+        dir_old : `numpy.ndarray`, shape (n,)
+            Value of ``interpolation.xpt[:, k_new]`` before the update.
+        values_diff : `numpy.ndarray`, shape (npt,)
+            Differences between the values of the interpolated nonlinear
+            function and the previous quadratic model at the updated
+            interpolation points.
+
+        Raises
+        ------
+        `numpy.linalg.LinAlgError`
+            If the interpolation system is ill-defined.
+        """
+        if self._debug:
+            assert 0 <= k_new < self.npt, "The index `k_new` is not valid."
+            assert dir_old.shape == (
+                self.n,
+            ), "The shape of `dir_old` is not valid."
+            assert values_diff.shape == (
+                self.npt,
+            ), "The shape of `values_diff` is not valid."
+
+        # Forward the k_new-th element of the implicit Hessian matrix to the
+        # explicit Hessian matrix. This must be done because the implicit
+        # Hessian matrix is related to the interpolation points, and the
+        # k_new-th interpolation point is modified.
+        self._e_hess += self._i_hess[k_new] * np.outer(dir_old, dir_old)
+        self._i_hess[k_new] = 0.0
+
+        # Update the quadratic model.
+        const, grad, i_hess, ill_conditioned = self._get_model(
+            interpolation,
+            values_diff,
+        )
+        self._const += const
+        self._grad += grad
+        self._i_hess += i_hess
+        return ill_conditioned
+
+    def shift_x_base(self, interpolation, new_x_base):
+        """
+        Shift the point around which the quadratic model is defined.
+
+        Parameters
+        ----------
+        interpolation : `cobyqa.models.Interpolation`
+            Previous interpolation set.
+        new_x_base : `numpy.ndarray`, shape (n,)
+            Point that will replace ``interpolation.x_base``.
+        """
+        if self._debug:
+            assert new_x_base.shape == (
+                self.n,
+            ), "The shape of `new_x_base` is not valid."
+        self._const = self(new_x_base, interpolation)
+        self._grad = self.grad(new_x_base, interpolation)
+        shift = new_x_base - interpolation.x_base
+        update = np.outer(
+            shift,
+            (interpolation.xpt - 0.5 * shift[:, np.newaxis]) @ self._i_hess,
+        )
+        self._e_hess += update + update.T
+
+    @staticmethod
+    def solve_systems(interpolation, rhs):
+        """
+        Solve the interpolation systems.
+
+        Parameters
+        ----------
+        interpolation : `cobyqa.models.Interpolation`
+            Interpolation set.
+        rhs : `numpy.ndarray`, shape (npt + n + 1, m)
+            Right-hand side vectors of the ``m`` interpolation systems.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (npt + n + 1, m)
+            Solutions of the interpolation systems.
+        `numpy.ndarray`, shape (m, )
+            Whether the interpolation systems are ill-conditioned.
+
+        Raises
+        ------
+        `numpy.linalg.LinAlgError`
+            If the interpolation systems are ill-defined.
+        """
+        n, npt = interpolation.xpt.shape
+        assert (
+            rhs.ndim == 2 and rhs.shape[0] == npt + n + 1
+        ), "The shape of `rhs` is not valid."
+
+        # Build the left-hand side matrix of the interpolation system. The
+        # matrix below stores diag(left_scaling) * W * diag(right_scaling),
+        # where W is the theoretical matrix of the interpolation system. The
+        # left and right scaling matrices are chosen to keep the elements in
+        # the matrix well-balanced.
+        a, right_scaling, eig = build_system(interpolation)
+
+        # Build the solution. After a discussion with Mike Saunders and Alexis
+        # Montoison during their visit to the Hong Kong Polytechnic University
+        # in 2024, we decided to use the eigendecomposition of the symmetric
+        # matrix a. This is more stable than the previously employed LBL
+        # decomposition, and allows us to directly detect ill-conditioning of
+        # the system and to build the least-squares solution if necessary.
+        # Numerical experiments have shown that this strategy improves the
+        # performance of the solver.
+        rhs_scaled = rhs * right_scaling[:, np.newaxis]
+        if not (np.all(np.isfinite(a)) and np.all(np.isfinite(rhs_scaled))):
+            raise np.linalg.LinAlgError(
+                "The interpolation system is ill-defined."
+            )
+
+        # calculated in build_system
+        eig_values, eig_vectors = eig
+
+        large_eig_values = np.abs(eig_values) > EPS
+        eig_vectors = eig_vectors[:, large_eig_values]
+        inv_eig_values = 1.0 / eig_values[large_eig_values]
+        ill_conditioned = ~np.all(large_eig_values, 0)
+        left_scaled_solutions = eig_vectors @ (
+            (eig_vectors.T @ rhs_scaled) * inv_eig_values[:, np.newaxis]
+        )
+        return (
+            left_scaled_solutions * right_scaling[:, np.newaxis],
+            ill_conditioned,
+        )
+
+    @staticmethod
+    def _get_model(interpolation, values):
+        """
+        Solve the interpolation system.
+
+        Parameters
+        ----------
+        interpolation : `cobyqa.models.Interpolation`
+            Interpolation set.
+        values : `numpy.ndarray`, shape (npt,)
+            Values of the interpolated function at the interpolation points.
+
+        Returns
+        -------
+        float
+            Constant term of the quadratic model.
+        `numpy.ndarray`, shape (n,)
+            Gradient of the quadratic model at ``interpolation.x_base``.
+        `numpy.ndarray`, shape (npt,)
+            Implicit Hessian matrix of the quadratic model.
+
+        Raises
+        ------
+        `numpy.linalg.LinAlgError`
+            If the interpolation system is ill-defined.
+        """
+        assert values.shape == (
+            interpolation.npt,
+        ), "The shape of `values` is not valid."
+        n, npt = interpolation.xpt.shape
+        x, ill_conditioned = Quadratic.solve_systems(
+            interpolation,
+            np.block(
+                [
+                    [
+                        values,
+                        np.zeros(n + 1),
+                    ]
+                ]
+            ).T,
+        )
+        return x[npt, 0], x[npt + 1:, 0], x[:npt, 0], ill_conditioned
+
+
+class Models:
+    """
+    Models for a nonlinear optimization problem.
+    """
+
+    def __init__(self, pb, options, penalty):
+        """
+        Initialize the models.
+
+        Parameters
+        ----------
+        pb : `cobyqa.problem.Problem`
+            Problem to be solved.
+        options : dict
+            Options of the solver.
+        penalty : float
+            Penalty parameter used to select the point in the filter to forward
+            to the callback function.
+
+        Raises
+        ------
+        `cobyqa.utils.MaxEvalError`
+            If the maximum number of evaluations is reached.
+        `cobyqa.utils.TargetSuccess`
+            If a nearly feasible point has been found with an objective
+            function value below the target.
+        `cobyqa.utils.FeasibleSuccess`
+            If a feasible point has been found for a feasibility problem.
+        `numpy.linalg.LinAlgError`
+            If the interpolation system is ill-defined.
+        """
+        # Set the initial interpolation set.
+        self._debug = options[Options.DEBUG]
+        self._interpolation = Interpolation(pb, options)
+
+        # Evaluate the nonlinear functions at the initial interpolation points.
+        x_eval = self.interpolation.point(0)
+        fun_init, cub_init, ceq_init = pb(x_eval, penalty)
+        self._fun_val = np.full(options[Options.NPT], np.nan)
+        self._cub_val = np.full((options[Options.NPT], cub_init.size), np.nan)
+        self._ceq_val = np.full((options[Options.NPT], ceq_init.size), np.nan)
+        for k in range(options[Options.NPT]):
+            if k >= options[Options.MAX_EVAL]:
+                raise MaxEvalError
+            if k == 0:
+                self.fun_val[k] = fun_init
+                self.cub_val[k, :] = cub_init
+                self.ceq_val[k, :] = ceq_init
+            else:
+                x_eval = self.interpolation.point(k)
+                self.fun_val[k], self.cub_val[k, :], self.ceq_val[k, :] = pb(
+                    x_eval,
+                    penalty,
+                )
+
+            # Stop the iterations if the problem is a feasibility problem and
+            # the current interpolation point is feasible.
+            if (
+                pb.is_feasibility
+                and pb.maxcv(
+                    self.interpolation.point(k),
+                    self.cub_val[k, :],
+                    self.ceq_val[k, :],
+                )
+                <= options[Options.FEASIBILITY_TOL]
+            ):
+                raise FeasibleSuccess
+
+            # Stop the iterations if the current interpolation point is nearly
+            # feasible and has an objective function value below the target.
+            if (
+                self._fun_val[k] <= options[Options.TARGET]
+                and pb.maxcv(
+                    self.interpolation.point(k),
+                    self.cub_val[k, :],
+                    self.ceq_val[k, :],
+                )
+                <= options[Options.FEASIBILITY_TOL]
+            ):
+                raise TargetSuccess
+
+        # Build the initial quadratic models.
+        self._fun = Quadratic(
+            self.interpolation,
+            self._fun_val,
+            options[Options.DEBUG],
+        )
+        self._cub = np.empty(self.m_nonlinear_ub, dtype=Quadratic)
+        self._ceq = np.empty(self.m_nonlinear_eq, dtype=Quadratic)
+        for i in range(self.m_nonlinear_ub):
+            self._cub[i] = Quadratic(
+                self.interpolation,
+                self.cub_val[:, i],
+                options[Options.DEBUG],
+            )
+        for i in range(self.m_nonlinear_eq):
+            self._ceq[i] = Quadratic(
+                self.interpolation,
+                self.ceq_val[:, i],
+                options[Options.DEBUG],
+            )
+        if self._debug:
+            self._check_interpolation_conditions()
+
+    @property
+    def n(self):
+        """
+        Dimension of the problem.
+
+        Returns
+        -------
+        int
+            Dimension of the problem.
+        """
+        return self.interpolation.n
+
+    @property
+    def npt(self):
+        """
+        Number of interpolation points.
+
+        Returns
+        -------
+        int
+            Number of interpolation points.
+        """
+        return self.interpolation.npt
+
+    @property
+    def m_nonlinear_ub(self):
+        """
+        Number of nonlinear inequality constraints.
+
+        Returns
+        -------
+        int
+            Number of nonlinear inequality constraints.
+        """
+        return self.cub_val.shape[1]
+
+    @property
+    def m_nonlinear_eq(self):
+        """
+        Number of nonlinear equality constraints.
+
+        Returns
+        -------
+        int
+            Number of nonlinear equality constraints.
+        """
+        return self.ceq_val.shape[1]
+
+    @property
+    def interpolation(self):
+        """
+        Interpolation set.
+
+        Returns
+        -------
+        `cobyqa.models.Interpolation`
+            Interpolation set.
+        """
+        return self._interpolation
+
+    @property
+    def fun_val(self):
+        """
+        Values of the objective function at the interpolation points.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (npt,)
+            Values of the objective function at the interpolation points.
+        """
+        return self._fun_val
+
+    @property
+    def cub_val(self):
+        """
+        Values of the nonlinear inequality constraint functions at the
+        interpolation points.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (npt, m_nonlinear_ub)
+            Values of the nonlinear inequality constraint functions at the
+            interpolation points.
+        """
+        return self._cub_val
+
+    @property
+    def ceq_val(self):
+        """
+        Values of the nonlinear equality constraint functions at the
+        interpolation points.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (npt, m_nonlinear_eq)
+            Values of the nonlinear equality constraint functions at the
+            interpolation points.
+        """
+        return self._ceq_val
+
+    def fun(self, x):
+        """
+        Evaluate the quadratic model of the objective function at a given
+        point.
+
+        Parameters
+        ----------
+        x : `numpy.ndarray`, shape (n,)
+            Point at which to evaluate the quadratic model of the objective
+            function.
+
+        Returns
+        -------
+        float
+            Value of the quadratic model of the objective function at `x`.
+        """
+        if self._debug:
+            assert x.shape == (self.n,), "The shape of `x` is not valid."
+        return self._fun(x, self.interpolation)
+
+    def fun_grad(self, x):
+        """
+        Evaluate the gradient of the quadratic model of the objective function
+        at a given point.
+
+        Parameters
+        ----------
+        x : `numpy.ndarray`, shape (n,)
+            Point at which to evaluate the gradient of the quadratic model of
+            the objective function.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n,)
+            Gradient of the quadratic model of the objective function at `x`.
+        """
+        if self._debug:
+            assert x.shape == (self.n,), "The shape of `x` is not valid."
+        return self._fun.grad(x, self.interpolation)
+
+    def fun_hess(self):
+        """
+        Evaluate the Hessian matrix of the quadratic model of the objective
+        function.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n, n)
+            Hessian matrix of the quadratic model of the objective function.
+        """
+        return self._fun.hess(self.interpolation)
+
+    def fun_hess_prod(self, v):
+        """
+        Evaluate the right product of the Hessian matrix of the quadratic model
+        of the objective function with a given vector.
+
+        Parameters
+        ----------
+        v : `numpy.ndarray`, shape (n,)
+            Vector with which the Hessian matrix of the quadratic model of the
+            objective function is multiplied from the right.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n,)
+            Right product of the Hessian matrix of the quadratic model of the
+            objective function with `v`.
+        """
+        if self._debug:
+            assert v.shape == (self.n,), "The shape of `v` is not valid."
+        return self._fun.hess_prod(v, self.interpolation)
+
+    def fun_curv(self, v):
+        """
+        Evaluate the curvature of the quadratic model of the objective function
+        along a given direction.
+
+        Parameters
+        ----------
+        v : `numpy.ndarray`, shape (n,)
+            Direction along which the curvature of the quadratic model of the
+            objective function is evaluated.
+
+        Returns
+        -------
+        float
+            Curvature of the quadratic model of the objective function along
+            `v`.
+        """
+        if self._debug:
+            assert v.shape == (self.n,), "The shape of `v` is not valid."
+        return self._fun.curv(v, self.interpolation)
+
+    def fun_alt_grad(self, x):
+        """
+        Evaluate the gradient of the alternative quadratic model of the
+        objective function at a given point.
+
+        Parameters
+        ----------
+        x : `numpy.ndarray`, shape (n,)
+            Point at which to evaluate the gradient of the alternative
+            quadratic model of the objective function.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n,)
+            Gradient of the alternative quadratic model of the objective
+            function at `x`.
+
+        Raises
+        ------
+        `numpy.linalg.LinAlgError`
+            If the interpolation system is ill-defined.
+        """
+        if self._debug:
+            assert x.shape == (self.n,), "The shape of `x` is not valid."
+        model = Quadratic(self.interpolation, self.fun_val, self._debug)
+        return model.grad(x, self.interpolation)
+
+    def cub(self, x, mask=None):
+        """
+        Evaluate the quadratic models of the nonlinear inequality functions at
+        a given point.
+
+        Parameters
+        ----------
+        x : `numpy.ndarray`, shape (n,)
+            Point at which to evaluate the quadratic models of the nonlinear
+            inequality functions.
+        mask : `numpy.ndarray`, shape (m_nonlinear_ub,), optional
+            Mask of the quadratic models to consider.
+
+        Returns
+        -------
+        `numpy.ndarray`
+            Values of the quadratic model of the nonlinear inequality
+            functions.
+        """
+        if self._debug:
+            assert x.shape == (self.n,), "The shape of `x` is not valid."
+            assert mask is None or mask.shape == (
+                self.m_nonlinear_ub,
+            ), "The shape of `mask` is not valid."
+        return np.array(
+            [model(x, self.interpolation) for model in self._get_cub(mask)]
+        )
+
+    def cub_grad(self, x, mask=None):
+        """
+        Evaluate the gradients of the quadratic models of the nonlinear
+        inequality functions at a given point.
+
+        Parameters
+        ----------
+        x : `numpy.ndarray`, shape (n,)
+            Point at which to evaluate the gradients of the quadratic models of
+            the nonlinear inequality functions.
+        mask : `numpy.ndarray`, shape (m_nonlinear_eq,), optional
+            Mask of the quadratic models to consider.
+
+        Returns
+        -------
+        `numpy.ndarray`
+            Gradients of the quadratic model of the nonlinear inequality
+            functions.
+        """
+        if self._debug:
+            assert x.shape == (self.n,), "The shape of `x` is not valid."
+            assert mask is None or mask.shape == (
+                self.m_nonlinear_ub,
+            ), "The shape of `mask` is not valid."
+        return np.reshape(
+            [model.grad(x, self.interpolation)
+             for model in self._get_cub(mask)],
+            (-1, self.n),
+        )
+
+    def cub_hess(self, mask=None):
+        """
+        Evaluate the Hessian matrices of the quadratic models of the nonlinear
+        inequality functions.
+
+        Parameters
+        ----------
+        mask : `numpy.ndarray`, shape (m_nonlinear_ub,), optional
+            Mask of the quadratic models to consider.
+
+        Returns
+        -------
+        `numpy.ndarray`
+            Hessian matrices of the quadratic models of the nonlinear
+            inequality functions.
+        """
+        if self._debug:
+            assert mask is None or mask.shape == (
+                self.m_nonlinear_ub,
+            ), "The shape of `mask` is not valid."
+        return np.reshape(
+            [model.hess(self.interpolation) for model in self._get_cub(mask)],
+            (-1, self.n, self.n),
+        )
+
+    def cub_hess_prod(self, v, mask=None):
+        """
+        Evaluate the right product of the Hessian matrices of the quadratic
+        models of the nonlinear inequality functions with a given vector.
+
+        Parameters
+        ----------
+        v : `numpy.ndarray`, shape (n,)
+            Vector with which the Hessian matrices of the quadratic models of
+            the nonlinear inequality functions are multiplied from the right.
+        mask : `numpy.ndarray`, shape (m_nonlinear_ub,), optional
+            Mask of the quadratic models to consider.
+
+        Returns
+        -------
+        `numpy.ndarray`
+            Right products of the Hessian matrices of the quadratic models of
+            the nonlinear inequality functions with `v`.
+        """
+        if self._debug:
+            assert v.shape == (self.n,), "The shape of `v` is not valid."
+            assert mask is None or mask.shape == (
+                self.m_nonlinear_ub,
+            ), "The shape of `mask` is not valid."
+        return np.reshape(
+            [
+                model.hess_prod(v, self.interpolation)
+                for model in self._get_cub(mask)
+            ],
+            (-1, self.n),
+        )
+
+    def cub_curv(self, v, mask=None):
+        """
+        Evaluate the curvature of the quadratic models of the nonlinear
+        inequality functions along a given direction.
+
+        Parameters
+        ----------
+        v : `numpy.ndarray`, shape (n,)
+            Direction along which the curvature of the quadratic models of the
+            nonlinear inequality functions is evaluated.
+        mask : `numpy.ndarray`, shape (m_nonlinear_ub,), optional
+            Mask of the quadratic models to consider.
+
+        Returns
+        -------
+        `numpy.ndarray`
+            Curvature of the quadratic models of the nonlinear inequality
+            functions along `v`.
+        """
+        if self._debug:
+            assert v.shape == (self.n,), "The shape of `v` is not valid."
+            assert mask is None or mask.shape == (
+                self.m_nonlinear_ub,
+            ), "The shape of `mask` is not valid."
+        return np.array(
+            [model.curv(v, self.interpolation)
+             for model in self._get_cub(mask)]
+        )
+
+    def ceq(self, x, mask=None):
+        """
+        Evaluate the quadratic models of the nonlinear equality functions at a
+        given point.
+
+        Parameters
+        ----------
+        x : `numpy.ndarray`, shape (n,)
+            Point at which to evaluate the quadratic models of the nonlinear
+            equality functions.
+        mask : `numpy.ndarray`, shape (m_nonlinear_eq,), optional
+            Mask of the quadratic models to consider.
+
+        Returns
+        -------
+        `numpy.ndarray`
+            Values of the quadratic model of the nonlinear equality functions.
+        """
+        if self._debug:
+            assert x.shape == (self.n,), "The shape of `x` is not valid."
+            assert mask is None or mask.shape == (
+                self.m_nonlinear_eq,
+            ), "The shape of `mask` is not valid."
+        return np.array(
+            [model(x, self.interpolation) for model in self._get_ceq(mask)]
+        )
+
+    def ceq_grad(self, x, mask=None):
+        """
+        Evaluate the gradients of the quadratic models of the nonlinear
+        equality functions at a given point.
+
+        Parameters
+        ----------
+        x : `numpy.ndarray`, shape (n,)
+            Point at which to evaluate the gradients of the quadratic models of
+            the nonlinear equality functions.
+        mask : `numpy.ndarray`, shape (m_nonlinear_eq,), optional
+            Mask of the quadratic models to consider.
+
+        Returns
+        -------
+        `numpy.ndarray`
+            Gradients of the quadratic model of the nonlinear equality
+            functions.
+        """
+        if self._debug:
+            assert x.shape == (self.n,), "The shape of `x` is not valid."
+            assert mask is None or mask.shape == (
+                self.m_nonlinear_eq,
+            ), "The shape of `mask` is not valid."
+        return np.reshape(
+            [model.grad(x, self.interpolation)
+             for model in self._get_ceq(mask)],
+            (-1, self.n),
+        )
+
+    def ceq_hess(self, mask=None):
+        """
+        Evaluate the Hessian matrices of the quadratic models of the nonlinear
+        equality functions.
+
+        Parameters
+        ----------
+        mask : `numpy.ndarray`, shape (m_nonlinear_eq,), optional
+            Mask of the quadratic models to consider.
+
+        Returns
+        -------
+        `numpy.ndarray`
+            Hessian matrices of the quadratic models of the nonlinear equality
+            functions.
+        """
+        if self._debug:
+            assert mask is None or mask.shape == (
+                self.m_nonlinear_eq,
+            ), "The shape of `mask` is not valid."
+        return np.reshape(
+            [model.hess(self.interpolation) for model in self._get_ceq(mask)],
+            (-1, self.n, self.n),
+        )
+
+    def ceq_hess_prod(self, v, mask=None):
+        """
+        Evaluate the right product of the Hessian matrices of the quadratic
+        models of the nonlinear equality functions with a given vector.
+
+        Parameters
+        ----------
+        v : `numpy.ndarray`, shape (n,)
+            Vector with which the Hessian matrices of the quadratic models of
+            the nonlinear equality functions are multiplied from the right.
+        mask : `numpy.ndarray`, shape (m_nonlinear_eq,), optional
+            Mask of the quadratic models to consider.
+
+        Returns
+        -------
+        `numpy.ndarray`
+            Right products of the Hessian matrices of the quadratic models of
+            the nonlinear equality functions with `v`.
+        """
+        if self._debug:
+            assert v.shape == (self.n,), "The shape of `v` is not valid."
+            assert mask is None or mask.shape == (
+                self.m_nonlinear_eq,
+            ), "The shape of `mask` is not valid."
+        return np.reshape(
+            [
+                model.hess_prod(v, self.interpolation)
+                for model in self._get_ceq(mask)
+            ],
+            (-1, self.n),
+        )
+
+    def ceq_curv(self, v, mask=None):
+        """
+        Evaluate the curvature of the quadratic models of the nonlinear
+        equality functions along a given direction.
+
+        Parameters
+        ----------
+        v : `numpy.ndarray`, shape (n,)
+            Direction along which the curvature of the quadratic models of the
+            nonlinear equality functions is evaluated.
+        mask : `numpy.ndarray`, shape (m_nonlinear_eq,), optional
+            Mask of the quadratic models to consider.
+
+        Returns
+        -------
+        `numpy.ndarray`
+            Curvature of the quadratic models of the nonlinear equality
+            functions along `v`.
+        """
+        if self._debug:
+            assert v.shape == (self.n,), "The shape of `v` is not valid."
+            assert mask is None or mask.shape == (
+                self.m_nonlinear_eq,
+            ), "The shape of `mask` is not valid."
+        return np.array(
+            [model.curv(v, self.interpolation)
+             for model in self._get_ceq(mask)]
+        )
+
+    def reset_models(self):
+        """
+        Set the quadratic models of the objective function, nonlinear
+        inequality constraints, and nonlinear equality constraints to the
+        alternative quadratic models.
+
+        Raises
+        ------
+        `numpy.linalg.LinAlgError`
+            If the interpolation system is ill-defined.
+        """
+        self._fun = Quadratic(self.interpolation, self.fun_val, self._debug)
+        for i in range(self.m_nonlinear_ub):
+            self._cub[i] = Quadratic(
+                self.interpolation,
+                self.cub_val[:, i],
+                self._debug,
+            )
+        for i in range(self.m_nonlinear_eq):
+            self._ceq[i] = Quadratic(
+                self.interpolation,
+                self.ceq_val[:, i],
+                self._debug,
+            )
+        if self._debug:
+            self._check_interpolation_conditions()
+
+    def update_interpolation(self, k_new, x_new, fun_val, cub_val, ceq_val):
+        """
+        Update the interpolation set.
+
+        This method updates the interpolation set by replacing the `knew`-th
+        interpolation point with `xnew`. It also updates the function values
+        and the quadratic models.
+
+        Parameters
+        ----------
+        k_new : int
+            Index of the updated interpolation point.
+        x_new : `numpy.ndarray`, shape (n,)
+            New interpolation point. Its value is interpreted as relative to
+            the origin, not the base point.
+        fun_val : float
+            Value of the objective function at `x_new`.
+            Objective function value at `x_new`.
+        cub_val : `numpy.ndarray`, shape (m_nonlinear_ub,)
+            Values of the nonlinear inequality constraints at `x_new`.
+        ceq_val : `numpy.ndarray`, shape (m_nonlinear_eq,)
+            Values of the nonlinear equality constraints at `x_new`.
+
+        Raises
+        ------
+        `numpy.linalg.LinAlgError`
+            If the interpolation system is ill-defined.
+        """
+        if self._debug:
+            assert 0 <= k_new < self.npt, "The index `k_new` is not valid."
+            assert x_new.shape == (self.n,), \
+                "The shape of `x_new` is not valid."
+            assert isinstance(fun_val, float), \
+                "The function value is not valid."
+            assert cub_val.shape == (
+                self.m_nonlinear_ub,
+            ), "The shape of `cub_val` is not valid."
+            assert ceq_val.shape == (
+                self.m_nonlinear_eq,
+            ), "The shape of `ceq_val` is not valid."
+
+        # Compute the updates in the interpolation conditions.
+        fun_diff = np.zeros(self.npt)
+        cub_diff = np.zeros(self.cub_val.shape)
+        ceq_diff = np.zeros(self.ceq_val.shape)
+        fun_diff[k_new] = fun_val - self.fun(x_new)
+        cub_diff[k_new, :] = cub_val - self.cub(x_new)
+        ceq_diff[k_new, :] = ceq_val - self.ceq(x_new)
+
+        # Update the function values.
+        self.fun_val[k_new] = fun_val
+        self.cub_val[k_new, :] = cub_val
+        self.ceq_val[k_new, :] = ceq_val
+
+        # Update the interpolation set.
+        dir_old = np.copy(self.interpolation.xpt[:, k_new])
+        self.interpolation.xpt[:, k_new] = x_new - self.interpolation.x_base
+
+        # Update the quadratic models.
+        ill_conditioned = self._fun.update(
+            self.interpolation,
+            k_new,
+            dir_old,
+            fun_diff,
+        )
+        for i in range(self.m_nonlinear_ub):
+            ill_conditioned = ill_conditioned or self._cub[i].update(
+                self.interpolation,
+                k_new,
+                dir_old,
+                cub_diff[:, i],
+            )
+        for i in range(self.m_nonlinear_eq):
+            ill_conditioned = ill_conditioned or self._ceq[i].update(
+                self.interpolation,
+                k_new,
+                dir_old,
+                ceq_diff[:, i],
+            )
+        if self._debug:
+            self._check_interpolation_conditions()
+        return ill_conditioned
+
+    def determinants(self, x_new, k_new=None):
+        """
+        Compute the normalized determinants of the new interpolation systems.
+
+        Parameters
+        ----------
+        x_new : `numpy.ndarray`, shape (n,)
+            New interpolation point. Its value is interpreted as relative to
+            the origin, not the base point.
+        k_new : int, optional
+            Index of the updated interpolation point. If `k_new` is not
+            specified, all the possible determinants are computed.
+
+        Returns
+        -------
+        {float, `numpy.ndarray`, shape (npt,)}
+            Determinant(s) of the new interpolation system.
+
+        Raises
+        ------
+        `numpy.linalg.LinAlgError`
+            If the interpolation system is ill-defined.
+
+        Notes
+        -----
+        The determinants are normalized by the determinant of the current
+        interpolation system. For stability reasons, the calculations are done
+        using the formula (2.12) in [1]_.
+
+        References
+        ----------
+        .. [1] M. J. D. Powell. On updating the inverse of a KKT matrix.
+           Technical Report DAMTP 2004/NA01, Department of Applied Mathematics
+           and Theoretical Physics, University of Cambridge, Cambridge, UK,
+           2004.
+        """
+        if self._debug:
+            assert x_new.shape == (self.n,), \
+                "The shape of `x_new` is not valid."
+            assert (
+                k_new is None or 0 <= k_new < self.npt
+            ), "The index `k_new` is not valid."
+
+        # Compute the values independent of k_new.
+        shift = x_new - self.interpolation.x_base
+        new_col = np.empty((self.npt + self.n + 1, 1))
+        new_col[: self.npt, 0] = (
+                0.5 * (self.interpolation.xpt.T @ shift) ** 2.0)
+        new_col[self.npt, 0] = 1.0
+        new_col[self.npt + 1:, 0] = shift
+        inv_new_col = Quadratic.solve_systems(self.interpolation, new_col)[0]
+        beta = 0.5 * (shift @ shift) ** 2.0 - new_col[:, 0] @ inv_new_col[:, 0]
+
+        # Compute the values that depend on k.
+        if k_new is None:
+            coord_vec = np.eye(self.npt + self.n + 1, self.npt)
+            alpha = np.diag(
+                Quadratic.solve_systems(
+                    self.interpolation,
+                    coord_vec,
+                )[0]
+            )
+            tau = inv_new_col[: self.npt, 0]
+        else:
+            coord_vec = np.eye(self.npt + self.n + 1, 1, -k_new)
+            alpha = Quadratic.solve_systems(
+                self.interpolation,
+                coord_vec,
+            )[
+                0
+            ][k_new, 0]
+            tau = inv_new_col[k_new, 0]
+        return alpha * beta + tau**2.0
+
+    def shift_x_base(self, new_x_base, options):
+        """
+        Shift the base point without changing the interpolation set.
+
+        Parameters
+        ----------
+        new_x_base : `numpy.ndarray`, shape (n,)
+            New base point.
+        options : dict
+            Options of the solver.
+        """
+        if self._debug:
+            assert new_x_base.shape == (
+                self.n,
+            ), "The shape of `new_x_base` is not valid."
+
+        # Update the models.
+        self._fun.shift_x_base(self.interpolation, new_x_base)
+        for model in self._cub:
+            model.shift_x_base(self.interpolation, new_x_base)
+        for model in self._ceq:
+            model.shift_x_base(self.interpolation, new_x_base)
+
+        # Update the base point and the interpolation points.
+        shift = new_x_base - self.interpolation.x_base
+        self.interpolation.x_base += shift
+        self.interpolation.xpt -= shift[:, np.newaxis]
+        if options[Options.DEBUG]:
+            self._check_interpolation_conditions()
+
+    def _get_cub(self, mask=None):
+        """
+        Get the quadratic models of the nonlinear inequality constraints.
+
+        Parameters
+        ----------
+        mask : `numpy.ndarray`, shape (m_nonlinear_ub,), optional
+            Mask of the quadratic models to return.
+
+        Returns
+        -------
+        `numpy.ndarray`
+            Quadratic models of the nonlinear inequality constraints.
+        """
+        return self._cub if mask is None else self._cub[mask]
+
+    def _get_ceq(self, mask=None):
+        """
+        Get the quadratic models of the nonlinear equality constraints.
+
+        Parameters
+        ----------
+        mask : `numpy.ndarray`, shape (m_nonlinear_eq,), optional
+            Mask of the quadratic models to return.
+
+        Returns
+        -------
+        `numpy.ndarray`
+            Quadratic models of the nonlinear equality constraints.
+        """
+        return self._ceq if mask is None else self._ceq[mask]
+
+    def _check_interpolation_conditions(self):
+        """
+        Check the interpolation conditions of all quadratic models.
+        """
+        error_fun = 0.0
+        error_cub = 0.0
+        error_ceq = 0.0
+        for k in range(self.npt):
+            error_fun = np.max(
+                [
+                    error_fun,
+                    np.abs(
+                        self.fun(self.interpolation.point(k)) - self.fun_val[k]
+                    ),
+                ]
+            )
+            error_cub = np.max(
+                np.abs(
+                    self.cub(self.interpolation.point(k)) - self.cub_val[k, :]
+                ),
+                initial=error_cub,
+            )
+            error_ceq = np.max(
+                np.abs(
+                    self.ceq(self.interpolation.point(k)) - self.ceq_val[k, :]
+                ),
+                initial=error_ceq,
+            )
+        tol = 10.0 * np.sqrt(EPS) * max(self.n, self.npt)
+        if error_fun > tol * np.max(np.abs(self.fun_val), initial=1.0):
+            warnings.warn(
+                "The interpolation conditions for the objective function are "
+                "not satisfied.",
+                RuntimeWarning,
+                2,
+            )
+        if error_cub > tol * np.max(np.abs(self.cub_val), initial=1.0):
+            warnings.warn(
+                "The interpolation conditions for the inequality constraint "
+                "function are not satisfied.",
+                RuntimeWarning,
+                2,
+            )
+        if error_ceq > tol * np.max(np.abs(self.ceq_val), initial=1.0):
+            warnings.warn(
+                "The interpolation conditions for the equality constraint "
+                "function are not satisfied.",
+                RuntimeWarning,
+                2,
+            )
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/problem.py b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/problem.py
new file mode 100644
index 0000000000000000000000000000000000000000..2dbebce3a48067e97da2b75bd2cdd609e01029b2
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/problem.py
@@ -0,0 +1,1296 @@
+from contextlib import suppress
+from inspect import signature
+import copy
+
+import numpy as np
+from scipy.optimize import (
+    Bounds,
+    LinearConstraint,
+    NonlinearConstraint,
+    OptimizeResult,
+)
+from scipy.optimize._constraints import PreparedConstraint
+
+
+from .settings import PRINT_OPTIONS, BARRIER
+from .utils import CallbackSuccess, get_arrays_tol
+from .utils import exact_1d_array
+
+
+class ObjectiveFunction:
+    """
+    Real-valued objective function.
+    """
+
+    def __init__(self, fun, verbose, debug, *args):
+        """
+        Initialize the objective function.
+
+        Parameters
+        ----------
+        fun : {callable, None}
+            Function to evaluate, or None.
+
+                ``fun(x, *args) -> float``
+
+            where ``x`` is an array with shape (n,) and `args` is a tuple.
+        verbose : bool
+            Whether to print the function evaluations.
+        debug : bool
+            Whether to make debugging tests during the execution.
+        *args : tuple
+            Additional arguments to be passed to the function.
+        """
+        if debug:
+            assert fun is None or callable(fun)
+            assert isinstance(verbose, bool)
+            assert isinstance(debug, bool)
+
+        self._fun = fun
+        self._verbose = verbose
+        self._args = args
+        self._n_eval = 0
+
+    def __call__(self, x):
+        """
+        Evaluate the objective function.
+
+        Parameters
+        ----------
+        x : array_like, shape (n,)
+            Point at which the objective function is evaluated.
+
+        Returns
+        -------
+        float
+            Function value at `x`.
+        """
+        x = np.array(x, dtype=float)
+        if self._fun is None:
+            f = 0.0
+        else:
+            f = float(np.squeeze(self._fun(x, *self._args)))
+            self._n_eval += 1
+            if self._verbose:
+                with np.printoptions(**PRINT_OPTIONS):
+                    print(f"{self.name}({x}) = {f}")
+        return f
+
+    @property
+    def n_eval(self):
+        """
+        Number of function evaluations.
+
+        Returns
+        -------
+        int
+            Number of function evaluations.
+        """
+        return self._n_eval
+
+    @property
+    def name(self):
+        """
+        Name of the objective function.
+
+        Returns
+        -------
+        str
+            Name of the objective function.
+        """
+        name = ""
+        if self._fun is not None:
+            try:
+                name = self._fun.__name__
+            except AttributeError:
+                name = "fun"
+        return name
+
+
+class BoundConstraints:
+    """
+    Bound constraints ``xl <= x <= xu``.
+    """
+
+    def __init__(self, bounds):
+        """
+        Initialize the bound constraints.
+
+        Parameters
+        ----------
+        bounds : scipy.optimize.Bounds
+            Bound constraints.
+        """
+        self._xl = np.array(bounds.lb, float)
+        self._xu = np.array(bounds.ub, float)
+
+        # Remove the ill-defined bounds.
+        self.xl[np.isnan(self.xl)] = -np.inf
+        self.xu[np.isnan(self.xu)] = np.inf
+
+        self.is_feasible = (
+            np.all(self.xl <= self.xu)
+            and np.all(self.xl < np.inf)
+            and np.all(self.xu > -np.inf)
+        )
+        self.m = np.count_nonzero(self.xl > -np.inf) + np.count_nonzero(
+            self.xu < np.inf
+        )
+        self.pcs = PreparedConstraint(bounds, np.ones(bounds.lb.size))
+
+    @property
+    def xl(self):
+        """
+        Lower bound.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n,)
+            Lower bound.
+        """
+        return self._xl
+
+    @property
+    def xu(self):
+        """
+        Upper bound.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n,)
+            Upper bound.
+        """
+        return self._xu
+
+    def maxcv(self, x):
+        """
+        Evaluate the maximum constraint violation.
+
+        Parameters
+        ----------
+        x : array_like, shape (n,)
+            Point at which the maximum constraint violation is evaluated.
+
+        Returns
+        -------
+        float
+            Maximum constraint violation at `x`.
+        """
+        x = np.asarray(x, dtype=float)
+        return self.violation(x)
+
+    def violation(self, x):
+        # shortcut for no bounds
+        if self.is_feasible:
+            return np.array([0])
+        else:
+            return self.pcs.violation(x)
+
+    def project(self, x):
+        """
+        Project a point onto the feasible set.
+
+        Parameters
+        ----------
+        x : array_like, shape (n,)
+            Point to be projected.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n,)
+            Projection of `x` onto the feasible set.
+        """
+        return np.clip(x, self.xl, self.xu) if self.is_feasible else x
+
+
+class LinearConstraints:
+    """
+    Linear constraints ``a_ub @ x <= b_ub`` and ``a_eq @ x == b_eq``.
+    """
+
+    def __init__(self, constraints, n, debug):
+        """
+        Initialize the linear constraints.
+
+        Parameters
+        ----------
+        constraints : list of LinearConstraint
+            Linear constraints.
+        n : int
+            Number of variables.
+        debug : bool
+            Whether to make debugging tests during the execution.
+        """
+        if debug:
+            assert isinstance(constraints, list)
+            for constraint in constraints:
+                assert isinstance(constraint, LinearConstraint)
+            assert isinstance(debug, bool)
+
+        self._a_ub = np.empty((0, n))
+        self._b_ub = np.empty(0)
+        self._a_eq = np.empty((0, n))
+        self._b_eq = np.empty(0)
+        for constraint in constraints:
+            is_equality = np.abs(
+                constraint.ub - constraint.lb
+            ) <= get_arrays_tol(constraint.lb, constraint.ub)
+            if np.any(is_equality):
+                self._a_eq = np.vstack((self.a_eq, constraint.A[is_equality]))
+                self._b_eq = np.concatenate(
+                    (
+                        self.b_eq,
+                        0.5
+                        * (
+                            constraint.lb[is_equality]
+                            + constraint.ub[is_equality]
+                        ),
+                    )
+                )
+            if not np.all(is_equality):
+                self._a_ub = np.vstack(
+                    (
+                        self.a_ub,
+                        constraint.A[~is_equality],
+                        -constraint.A[~is_equality],
+                    )
+                )
+                self._b_ub = np.concatenate(
+                    (
+                        self.b_ub,
+                        constraint.ub[~is_equality],
+                        -constraint.lb[~is_equality],
+                    )
+                )
+
+        # Remove the ill-defined constraints.
+        self.a_ub[np.isnan(self.a_ub)] = 0.0
+        self.a_eq[np.isnan(self.a_eq)] = 0.0
+        undef_ub = np.isnan(self.b_ub) | np.isinf(self.b_ub)
+        undef_eq = np.isnan(self.b_eq)
+        self._a_ub = self.a_ub[~undef_ub, :]
+        self._b_ub = self.b_ub[~undef_ub]
+        self._a_eq = self.a_eq[~undef_eq, :]
+        self._b_eq = self.b_eq[~undef_eq]
+        self.pcs = [
+            PreparedConstraint(c, np.ones(n)) for c in constraints if c.A.size
+        ]
+
+    @property
+    def a_ub(self):
+        """
+        Left-hand side matrix of the linear inequality constraints.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (m, n)
+            Left-hand side matrix of the linear inequality constraints.
+        """
+        return self._a_ub
+
+    @property
+    def b_ub(self):
+        """
+        Right-hand side vector of the linear inequality constraints.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (m, n)
+            Right-hand side vector of the linear inequality constraints.
+        """
+        return self._b_ub
+
+    @property
+    def a_eq(self):
+        """
+        Left-hand side matrix of the linear equality constraints.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (m, n)
+            Left-hand side matrix of the linear equality constraints.
+        """
+        return self._a_eq
+
+    @property
+    def b_eq(self):
+        """
+        Right-hand side vector of the linear equality constraints.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (m, n)
+            Right-hand side vector of the linear equality constraints.
+        """
+        return self._b_eq
+
+    @property
+    def m_ub(self):
+        """
+        Number of linear inequality constraints.
+
+        Returns
+        -------
+        int
+            Number of linear inequality constraints.
+        """
+        return self.b_ub.size
+
+    @property
+    def m_eq(self):
+        """
+        Number of linear equality constraints.
+
+        Returns
+        -------
+        int
+            Number of linear equality constraints.
+        """
+        return self.b_eq.size
+
+    def maxcv(self, x):
+        """
+        Evaluate the maximum constraint violation.
+
+        Parameters
+        ----------
+        x : array_like, shape (n,)
+            Point at which the maximum constraint violation is evaluated.
+
+        Returns
+        -------
+        float
+            Maximum constraint violation at `x`.
+        """
+        return np.max(self.violation(x), initial=0.0)
+
+    def violation(self, x):
+        if len(self.pcs):
+            return np.concatenate([pc.violation(x) for pc in self.pcs])
+        return np.array([])
+
+
+class NonlinearConstraints:
+    """
+    Nonlinear constraints ``c_ub(x) <= 0`` and ``c_eq(x) == b_eq``.
+    """
+
+    def __init__(self, constraints, verbose, debug):
+        """
+        Initialize the nonlinear constraints.
+
+        Parameters
+        ----------
+        constraints : list
+            Nonlinear constraints.
+        verbose : bool
+            Whether to print the function evaluations.
+        debug : bool
+            Whether to make debugging tests during the execution.
+        """
+        if debug:
+            assert isinstance(constraints, list)
+            for constraint in constraints:
+                assert isinstance(constraint, NonlinearConstraint)
+            assert isinstance(verbose, bool)
+            assert isinstance(debug, bool)
+
+        self._constraints = constraints
+        self.pcs = []
+        self._verbose = verbose
+
+        # map of indexes for equality and inequality constraints
+        self._map_ub = None
+        self._map_eq = None
+        self._m_ub = self._m_eq = None
+
+    def __call__(self, x):
+        """
+        Calculates the residual (slack) for the constraints.
+
+        Parameters
+        ----------
+        x : array_like, shape (n,)
+            Point at which the constraints are evaluated.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (m_nonlinear_ub,)
+            Nonlinear inequality constraint slack values.
+        `numpy.ndarray`, shape (m_nonlinear_eq,)
+            Nonlinear equality constraint slack values.
+        """
+        if not len(self._constraints):
+            self._m_eq = self._m_ub = 0
+            return np.array([]), np.array([])
+
+        x = np.array(x, dtype=float)
+        # first time around the constraints haven't been prepared
+        if not len(self.pcs):
+            self._map_ub = []
+            self._map_eq = []
+            self._m_eq = 0
+            self._m_ub = 0
+
+            for constraint in self._constraints:
+                if not callable(constraint.jac):
+                    # having a callable constraint function prevents
+                    # constraint.fun from being evaluated when preparing
+                    # constraint
+                    c = copy.copy(constraint)
+                    c.jac = lambda x0: x0
+                    c.hess = lambda x0, v: 0.0
+                    pc = PreparedConstraint(c, x)
+                else:
+                    pc = PreparedConstraint(constraint, x)
+                # we're going to be using the same x value again immediately
+                # after this initialisation
+                pc.fun.f_updated = True
+
+                self.pcs.append(pc)
+                idx = np.arange(pc.fun.m)
+
+                # figure out equality and inequality maps
+                lb, ub = pc.bounds[0], pc.bounds[1]
+                arr_tol = get_arrays_tol(lb, ub)
+                is_equality = np.abs(ub - lb) <= arr_tol
+                self._map_eq.append(idx[is_equality])
+                self._map_ub.append(idx[~is_equality])
+
+                # these values will be corrected to their proper values later
+                self._m_eq += np.count_nonzero(is_equality)
+                self._m_ub += np.count_nonzero(~is_equality)
+
+        c_ub = []
+        c_eq = []
+        for i, pc in enumerate(self.pcs):
+            val = pc.fun.fun(x)
+            if self._verbose:
+                with np.printoptions(**PRINT_OPTIONS):
+                    with suppress(AttributeError):
+                        fun_name = self._constraints[i].fun.__name__
+                        print(f"{fun_name}({x}) = {val}")
+
+            # separate violations into c_eq and c_ub
+            eq_idx = self._map_eq[i]
+            ub_idx = self._map_ub[i]
+
+            ub_val = val[ub_idx]
+            if len(ub_idx):
+                xl = pc.bounds[0][ub_idx]
+                xu = pc.bounds[1][ub_idx]
+
+                # calculate slack within lower bound
+                finite_xl = xl > -np.inf
+                _v = xl[finite_xl] - ub_val[finite_xl]
+                c_ub.append(_v)
+
+                # calculate slack within lower bound
+                finite_xu = xu < np.inf
+                _v = ub_val[finite_xu] - xu[finite_xu]
+                c_ub.append(_v)
+
+            # equality constraints taken from midpoint between lb and ub
+            eq_val = val[eq_idx]
+            if len(eq_idx):
+                midpoint = 0.5 * (pc.bounds[1][eq_idx] + pc.bounds[0][eq_idx])
+                eq_val -= midpoint
+            c_eq.append(eq_val)
+
+        if self._m_eq:
+            c_eq = np.concatenate(c_eq)
+        else:
+            c_eq = np.array([])
+
+        if self._m_ub:
+            c_ub = np.concatenate(c_ub)
+        else:
+            c_ub = np.array([])
+
+        self._m_ub = c_ub.size
+        self._m_eq = c_eq.size
+
+        return c_ub, c_eq
+
+    @property
+    def m_ub(self):
+        """
+        Number of nonlinear inequality constraints.
+
+        Returns
+        -------
+        int
+            Number of nonlinear inequality constraints.
+
+        Raises
+        ------
+        ValueError
+            If the number of nonlinear inequality constraints is unknown.
+        """
+        if self._m_ub is None:
+            raise ValueError(
+                "The number of nonlinear inequality constraints is unknown."
+            )
+        else:
+            return self._m_ub
+
+    @property
+    def m_eq(self):
+        """
+        Number of nonlinear equality constraints.
+
+        Returns
+        -------
+        int
+            Number of nonlinear equality constraints.
+
+        Raises
+        ------
+        ValueError
+            If the number of nonlinear equality constraints is unknown.
+        """
+        if self._m_eq is None:
+            raise ValueError(
+                "The number of nonlinear equality constraints is unknown."
+            )
+        else:
+            return self._m_eq
+
+    @property
+    def n_eval(self):
+        """
+        Number of function evaluations.
+
+        Returns
+        -------
+        int
+            Number of function evaluations.
+        """
+        if len(self.pcs):
+            return self.pcs[0].fun.nfev
+        else:
+            return 0
+
+    def maxcv(self, x, cub_val=None, ceq_val=None):
+        """
+        Evaluate the maximum constraint violation.
+
+        Parameters
+        ----------
+        x : array_like, shape (n,)
+            Point at which the maximum constraint violation is evaluated.
+        cub_val : array_like, shape (m_nonlinear_ub,), optional
+            Values of the nonlinear inequality constraints. If not provided,
+            the nonlinear inequality constraints are evaluated at `x`.
+        ceq_val : array_like, shape (m_nonlinear_eq,), optional
+            Values of the nonlinear equality constraints. If not provided,
+            the nonlinear equality constraints are evaluated at `x`.
+
+        Returns
+        -------
+        float
+            Maximum constraint violation at `x`.
+        """
+        return np.max(
+            self.violation(x, cub_val=cub_val, ceq_val=ceq_val), initial=0.0
+        )
+
+    def violation(self, x, cub_val=None, ceq_val=None):
+        return np.concatenate([pc.violation(x) for pc in self.pcs])
+
+
+class Problem:
+    """
+    Optimization problem.
+    """
+
+    def __init__(
+        self,
+        obj,
+        x0,
+        bounds,
+        linear,
+        nonlinear,
+        callback,
+        feasibility_tol,
+        scale,
+        store_history,
+        history_size,
+        filter_size,
+        debug,
+    ):
+        """
+        Initialize the nonlinear problem.
+
+        The problem is preprocessed to remove all the variables that are fixed
+        by the bound constraints.
+
+        Parameters
+        ----------
+        obj : ObjectiveFunction
+            Objective function.
+        x0 : array_like, shape (n,)
+            Initial guess.
+        bounds : BoundConstraints
+            Bound constraints.
+        linear : LinearConstraints
+            Linear constraints.
+        nonlinear : NonlinearConstraints
+            Nonlinear constraints.
+        callback : {callable, None}
+            Callback function.
+        feasibility_tol : float
+            Tolerance on the constraint violation.
+        scale : bool
+            Whether to scale the problem according to the bounds.
+        store_history : bool
+            Whether to store the function evaluations.
+        history_size : int
+            Maximum number of function evaluations to store.
+        filter_size : int
+            Maximum number of points in the filter.
+        debug : bool
+            Whether to make debugging tests during the execution.
+        """
+        if debug:
+            assert isinstance(obj, ObjectiveFunction)
+            assert isinstance(bounds, BoundConstraints)
+            assert isinstance(linear, LinearConstraints)
+            assert isinstance(nonlinear, NonlinearConstraints)
+            assert isinstance(feasibility_tol, float)
+            assert isinstance(scale, bool)
+            assert isinstance(store_history, bool)
+            assert isinstance(history_size, int)
+            if store_history:
+                assert history_size > 0
+            assert isinstance(filter_size, int)
+            assert filter_size > 0
+            assert isinstance(debug, bool)
+
+        self._obj = obj
+        self._linear = linear
+        self._nonlinear = nonlinear
+        if callback is not None:
+            if not callable(callback):
+                raise TypeError("The callback must be a callable function.")
+        self._callback = callback
+
+        # Check the consistency of the problem.
+        x0 = exact_1d_array(x0, "The initial guess must be a vector.")
+        n = x0.size
+        if bounds.xl.size != n:
+            raise ValueError(f"The bounds must have {n} elements.")
+        if linear.a_ub.shape[1] != n:
+            raise ValueError(
+                f"The left-hand side matrices of the linear constraints must "
+                f"have {n} columns."
+            )
+
+        # Check which variables are fixed.
+        tol = get_arrays_tol(bounds.xl, bounds.xu)
+        self._fixed_idx = (bounds.xl <= bounds.xu) & (
+            np.abs(bounds.xl - bounds.xu) < tol
+        )
+        self._fixed_val = 0.5 * (
+            bounds.xl[self._fixed_idx] + bounds.xu[self._fixed_idx]
+        )
+        self._fixed_val = np.clip(
+            self._fixed_val,
+            bounds.xl[self._fixed_idx],
+            bounds.xu[self._fixed_idx],
+        )
+
+        # Set the bound constraints.
+        self._orig_bounds = bounds
+        self._bounds = BoundConstraints(
+            Bounds(bounds.xl[~self._fixed_idx], bounds.xu[~self._fixed_idx])
+        )
+
+        # Set the initial guess.
+        self._x0 = self._bounds.project(x0[~self._fixed_idx])
+
+        # Set the linear constraints.
+        b_eq = linear.b_eq - linear.a_eq[:, self._fixed_idx] @ self._fixed_val
+        self._linear = LinearConstraints(
+            [
+                LinearConstraint(
+                    linear.a_ub[:, ~self._fixed_idx],
+                    -np.inf,
+                    linear.b_ub
+                    - linear.a_ub[:, self._fixed_idx] @ self._fixed_val,
+                ),
+                LinearConstraint(linear.a_eq[:, ~self._fixed_idx], b_eq, b_eq),
+            ],
+            self.n,
+            debug,
+        )
+
+        # Scale the problem if necessary.
+        scale = (
+            scale
+            and self._bounds.is_feasible
+            and np.all(np.isfinite(self._bounds.xl))
+            and np.all(np.isfinite(self._bounds.xu))
+        )
+        if scale:
+            self._scaling_factor = 0.5 * (self._bounds.xu - self._bounds.xl)
+            self._scaling_shift = 0.5 * (self._bounds.xu + self._bounds.xl)
+            self._bounds = BoundConstraints(
+                Bounds(-np.ones(self.n), np.ones(self.n))
+            )
+            b_eq = self._linear.b_eq - self._linear.a_eq @ self._scaling_shift
+            self._linear = LinearConstraints(
+                [
+                    LinearConstraint(
+                        self._linear.a_ub @ np.diag(self._scaling_factor),
+                        -np.inf,
+                        self._linear.b_ub
+                        - self._linear.a_ub @ self._scaling_shift,
+                    ),
+                    LinearConstraint(
+                        self._linear.a_eq @ np.diag(self._scaling_factor),
+                        b_eq,
+                        b_eq,
+                    ),
+                ],
+                self.n,
+                debug,
+            )
+            self._x0 = (self._x0 - self._scaling_shift) / self._scaling_factor
+        else:
+            self._scaling_factor = np.ones(self.n)
+            self._scaling_shift = np.zeros(self.n)
+
+        # Set the initial filter.
+        self._feasibility_tol = feasibility_tol
+        self._filter_size = filter_size
+        self._fun_filter = []
+        self._maxcv_filter = []
+        self._x_filter = []
+
+        # Set the initial history.
+        self._store_history = store_history
+        self._history_size = history_size
+        self._fun_history = []
+        self._maxcv_history = []
+        self._x_history = []
+
+    def __call__(self, x, penalty=0.0):
+        """
+        Evaluate the objective and nonlinear constraint functions.
+
+        Parameters
+        ----------
+        x : array_like, shape (n,)
+            Point at which the functions are evaluated.
+        penalty : float, optional
+            Penalty parameter used to select the point in the filter to forward
+            to the callback function.
+
+        Returns
+        -------
+        float
+            Objective function value.
+        `numpy.ndarray`, shape (m_nonlinear_ub,)
+            Nonlinear inequality constraint function values.
+        `numpy.ndarray`, shape (m_nonlinear_eq,)
+            Nonlinear equality constraint function values.
+
+        Raises
+        ------
+        `cobyqa.utils.CallbackSuccess`
+            If the callback function raises a ``StopIteration``.
+        """
+        # Evaluate the objective and nonlinear constraint functions.
+        x = np.asarray(x, dtype=float)
+        x_full = self.build_x(x)
+        fun_val = self._obj(x_full)
+        cub_val, ceq_val = self._nonlinear(x_full)
+        maxcv_val = self.maxcv(x, cub_val, ceq_val)
+        if self._store_history:
+            self._fun_history.append(fun_val)
+            self._maxcv_history.append(maxcv_val)
+            self._x_history.append(x)
+            if len(self._fun_history) > self._history_size:
+                self._fun_history.pop(0)
+                self._maxcv_history.pop(0)
+                self._x_history.pop(0)
+
+        # Add the point to the filter if it is not dominated by any point.
+        if np.isnan(fun_val) and np.isnan(maxcv_val):
+            include_point = len(self._fun_filter) == 0
+        elif np.isnan(fun_val):
+            include_point = all(
+                np.isnan(fun_filter)
+                and maxcv_val < maxcv_filter
+                or np.isnan(maxcv_filter)
+                for fun_filter, maxcv_filter in zip(
+                    self._fun_filter,
+                    self._maxcv_filter,
+                )
+            )
+        elif np.isnan(maxcv_val):
+            include_point = all(
+                np.isnan(maxcv_filter)
+                and fun_val < fun_filter
+                or np.isnan(fun_filter)
+                for fun_filter, maxcv_filter in zip(
+                    self._fun_filter,
+                    self._maxcv_filter,
+                )
+            )
+        else:
+            include_point = all(
+                fun_val < fun_filter or maxcv_val < maxcv_filter
+                for fun_filter, maxcv_filter in zip(
+                    self._fun_filter,
+                    self._maxcv_filter,
+                )
+            )
+        if include_point:
+            self._fun_filter.append(fun_val)
+            self._maxcv_filter.append(maxcv_val)
+            self._x_filter.append(x)
+
+            # Remove the points in the filter that are dominated by the new
+            # point. We must iterate in reverse order to avoid problems when
+            # removing elements from the list.
+            for k in range(len(self._fun_filter) - 2, -1, -1):
+                if np.isnan(fun_val):
+                    remove_point = np.isnan(self._fun_filter[k])
+                elif np.isnan(maxcv_val):
+                    remove_point = np.isnan(self._maxcv_filter[k])
+                else:
+                    remove_point = (
+                        np.isnan(self._fun_filter[k])
+                        or np.isnan(self._maxcv_filter[k])
+                        or fun_val <= self._fun_filter[k]
+                        and maxcv_val <= self._maxcv_filter[k]
+                    )
+                if remove_point:
+                    self._fun_filter.pop(k)
+                    self._maxcv_filter.pop(k)
+                    self._x_filter.pop(k)
+
+            # Keep only the most recent points in the filter.
+            if len(self._fun_filter) > self._filter_size:
+                self._fun_filter.pop(0)
+                self._maxcv_filter.pop(0)
+                self._x_filter.pop(0)
+
+        # Evaluate the callback function after updating the filter to ensure
+        # that the current point can be returned by the method.
+        if self._callback is not None:
+            sig = signature(self._callback)
+            try:
+                x_best, fun_best, _ = self.best_eval(penalty)
+                x_best = self.build_x(x_best)
+                if set(sig.parameters) == {"intermediate_result"}:
+                    intermediate_result = OptimizeResult(
+                        x=x_best,
+                        fun=fun_best,
+                        # maxcv=maxcv_best,
+                    )
+                    self._callback(intermediate_result=intermediate_result)
+                else:
+                    self._callback(x_best)
+            except StopIteration as exc:
+                raise CallbackSuccess from exc
+
+        # Apply the extreme barriers and return.
+        if np.isnan(fun_val):
+            fun_val = BARRIER
+        cub_val[np.isnan(cub_val)] = BARRIER
+        ceq_val[np.isnan(ceq_val)] = BARRIER
+        fun_val = max(min(fun_val, BARRIER), -BARRIER)
+        cub_val = np.maximum(np.minimum(cub_val, BARRIER), -BARRIER)
+        ceq_val = np.maximum(np.minimum(ceq_val, BARRIER), -BARRIER)
+        return fun_val, cub_val, ceq_val
+
+    @property
+    def n(self):
+        """
+        Number of variables.
+
+        Returns
+        -------
+        int
+            Number of variables.
+        """
+        return self.x0.size
+
+    @property
+    def n_orig(self):
+        """
+        Number of variables in the original problem (with fixed variables).
+
+        Returns
+        -------
+        int
+            Number of variables in the original problem (with fixed variables).
+        """
+        return self._fixed_idx.size
+
+    @property
+    def x0(self):
+        """
+        Initial guess.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n,)
+            Initial guess.
+        """
+        return self._x0
+
+    @property
+    def n_eval(self):
+        """
+        Number of function evaluations.
+
+        Returns
+        -------
+        int
+            Number of function evaluations.
+        """
+        return self._obj.n_eval
+
+    @property
+    def fun_name(self):
+        """
+        Name of the objective function.
+
+        Returns
+        -------
+        str
+            Name of the objective function.
+        """
+        return self._obj.name
+
+    @property
+    def bounds(self):
+        """
+        Bound constraints.
+
+        Returns
+        -------
+        BoundConstraints
+            Bound constraints.
+        """
+        return self._bounds
+
+    @property
+    def linear(self):
+        """
+        Linear constraints.
+
+        Returns
+        -------
+        LinearConstraints
+            Linear constraints.
+        """
+        return self._linear
+
+    @property
+    def m_bounds(self):
+        """
+        Number of bound constraints.
+
+        Returns
+        -------
+        int
+            Number of bound constraints.
+        """
+        return self.bounds.m
+
+    @property
+    def m_linear_ub(self):
+        """
+        Number of linear inequality constraints.
+
+        Returns
+        -------
+        int
+            Number of linear inequality constraints.
+        """
+        return self.linear.m_ub
+
+    @property
+    def m_linear_eq(self):
+        """
+        Number of linear equality constraints.
+
+        Returns
+        -------
+        int
+            Number of linear equality constraints.
+        """
+        return self.linear.m_eq
+
+    @property
+    def m_nonlinear_ub(self):
+        """
+        Number of nonlinear inequality constraints.
+
+        Returns
+        -------
+        int
+            Number of nonlinear inequality constraints.
+
+        Raises
+        ------
+        ValueError
+            If the number of nonlinear inequality constraints is not known.
+        """
+        return self._nonlinear.m_ub
+
+    @property
+    def m_nonlinear_eq(self):
+        """
+        Number of nonlinear equality constraints.
+
+        Returns
+        -------
+        int
+            Number of nonlinear equality constraints.
+
+        Raises
+        ------
+        ValueError
+            If the number of nonlinear equality constraints is not known.
+        """
+        return self._nonlinear.m_eq
+
+    @property
+    def fun_history(self):
+        """
+        History of objective function evaluations.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n_eval,)
+            History of objective function evaluations.
+        """
+        return np.array(self._fun_history, dtype=float)
+
+    @property
+    def maxcv_history(self):
+        """
+        History of maximum constraint violations.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n_eval,)
+            History of maximum constraint violations.
+        """
+        return np.array(self._maxcv_history, dtype=float)
+
+    @property
+    def type(self):
+        """
+        Type of the problem.
+
+        The problem can be either 'unconstrained', 'bound-constrained',
+        'linearly constrained', or 'nonlinearly constrained'.
+
+        Returns
+        -------
+        str
+            Type of the problem.
+        """
+        try:
+            if self.m_nonlinear_ub > 0 or self.m_nonlinear_eq > 0:
+                return "nonlinearly constrained"
+            elif self.m_linear_ub > 0 or self.m_linear_eq > 0:
+                return "linearly constrained"
+            elif self.m_bounds > 0:
+                return "bound-constrained"
+            else:
+                return "unconstrained"
+        except ValueError:
+            # The number of nonlinear constraints is not known. It may be zero
+            # if the user provided a nonlinear inequality and/or equality
+            # constraint function that returns an empty array. However, as this
+            # is not known before the first call to the function, we assume
+            # that the problem is nonlinearly constrained.
+            return "nonlinearly constrained"
+
+    @property
+    def is_feasibility(self):
+        """
+        Whether the problem is a feasibility problem.
+
+        Returns
+        -------
+        bool
+            Whether the problem is a feasibility problem.
+        """
+        return self.fun_name == ""
+
+    def build_x(self, x):
+        """
+        Build the full vector of variables from the reduced vector.
+
+        Parameters
+        ----------
+        x : array_like, shape (n,)
+            Reduced vector of variables.
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n_orig,)
+            Full vector of variables.
+        """
+        x_full = np.empty(self.n_orig)
+        x_full[self._fixed_idx] = self._fixed_val
+        x_full[~self._fixed_idx] = (x * self._scaling_factor
+                                    + self._scaling_shift)
+        return self._orig_bounds.project(x_full)
+
+    def maxcv(self, x, cub_val=None, ceq_val=None):
+        """
+        Evaluate the maximum constraint violation.
+
+        Parameters
+        ----------
+        x : array_like, shape (n,)
+            Point at which the maximum constraint violation is evaluated.
+        cub_val : array_like, shape (m_nonlinear_ub,), optional
+            Values of the nonlinear inequality constraints. If not provided,
+            the nonlinear inequality constraints are evaluated at `x`.
+        ceq_val : array_like, shape (m_nonlinear_eq,), optional
+            Values of the nonlinear equality constraints. If not provided,
+            the nonlinear equality constraints are evaluated at `x`.
+
+        Returns
+        -------
+        float
+            Maximum constraint violation at `x`.
+        """
+        violation = self.violation(x, cub_val=cub_val, ceq_val=ceq_val)
+        if np.count_nonzero(violation):
+            return np.max(violation, initial=0.0)
+        else:
+            return 0.0
+
+    def violation(self, x, cub_val=None, ceq_val=None):
+        violation = []
+        if not self.bounds.is_feasible:
+            b = self.bounds.violation(x)
+            violation.append(b)
+
+        if len(self.linear.pcs):
+            lc = self.linear.violation(x)
+            violation.append(lc)
+        if len(self._nonlinear.pcs):
+            nlc = self._nonlinear.violation(x, cub_val, ceq_val)
+            violation.append(nlc)
+
+        if len(violation):
+            return np.concatenate(violation)
+
+    def best_eval(self, penalty):
+        """
+        Return the best point in the filter and the corresponding objective and
+        nonlinear constraint function evaluations.
+
+        Parameters
+        ----------
+        penalty : float
+            Penalty parameter
+
+        Returns
+        -------
+        `numpy.ndarray`, shape (n,)
+            Best point.
+        float
+            Corresponding objective function value.
+        float
+            Corresponding maximum constraint violation.
+        """
+        # If the filter is empty, i.e., if no function evaluation has been
+        # performed, we evaluate the objective and nonlinear constraint
+        # functions at the initial guess.
+        if len(self._fun_filter) == 0:
+            self(self.x0)
+
+        # Find the best point in the filter.
+        fun_filter = np.array(self._fun_filter)
+        maxcv_filter = np.array(self._maxcv_filter)
+        x_filter = np.array(self._x_filter)
+        finite_idx = np.isfinite(maxcv_filter)
+        if np.any(finite_idx):
+            # At least one point has a finite maximum constraint violation.
+            feasible_idx = maxcv_filter <= self._feasibility_tol
+            if np.any(feasible_idx) and not np.all(
+                np.isnan(fun_filter[feasible_idx])
+            ):
+                # At least one point is feasible and has a well-defined
+                # objective function value. We select the point with the least
+                # objective function value. If there is a tie, we select the
+                # point with the least maximum constraint violation. If there
+                # is still a tie, we select the most recent point.
+                fun_min_idx = feasible_idx & (
+                    fun_filter <= np.nanmin(fun_filter[feasible_idx])
+                )
+                if np.count_nonzero(fun_min_idx) > 1:
+                    fun_min_idx &= maxcv_filter <= np.min(
+                        maxcv_filter[fun_min_idx]
+                    )
+                i = np.flatnonzero(fun_min_idx)[-1]
+            elif np.any(feasible_idx):
+                # At least one point is feasible but no feasible point has a
+                # well-defined objective function value. We select the most
+                # recent feasible point.
+                i = np.flatnonzero(feasible_idx)[-1]
+            else:
+                # No point is feasible. We first compute the merit function
+                # value for each point.
+                merit_filter = np.full_like(fun_filter, np.nan)
+                merit_filter[finite_idx] = (
+                    fun_filter[finite_idx] + penalty * maxcv_filter[finite_idx]
+                )
+                if np.all(np.isnan(merit_filter)):
+                    # No point has a well-defined merit function value. In
+                    # other words, among the points with a well-defined maximum
+                    # constraint violation, none has a well-defined objective
+                    # function value. We select the point with the least
+                    # maximum constraint violation. If there is a tie, we
+                    # select the most recent point.
+                    min_maxcv_idx = maxcv_filter <= np.nanmin(maxcv_filter)
+                    i = np.flatnonzero(min_maxcv_idx)[-1]
+                else:
+                    # At least one point has a well-defined merit function
+                    # value. We select the point with the least merit function
+                    # value. If there is a tie, we select the point with the
+                    # least maximum constraint violation. If there is still a
+                    # tie, we select the point with the least objective
+                    # function value. If there is still a tie, we select the
+                    # most recent point.
+                    merit_min_idx = merit_filter <= np.nanmin(merit_filter)
+                    if np.count_nonzero(merit_min_idx) > 1:
+                        merit_min_idx &= maxcv_filter <= np.min(
+                            maxcv_filter[merit_min_idx]
+                        )
+
+                    if np.count_nonzero(merit_min_idx) > 1:
+                        merit_min_idx &= fun_filter <= np.min(
+                            fun_filter[merit_min_idx]
+                        )
+                    i = np.flatnonzero(merit_min_idx)[-1]
+        elif not np.all(np.isnan(fun_filter)):
+            # No maximum constraint violation is well-defined but at least one
+            # point has a well-defined objective function value. We select the
+            # point with the least objective function value. If there is a tie,
+            # we select the most recent point.
+            fun_min_idx = fun_filter <= np.nanmin(fun_filter)
+            i = np.flatnonzero(fun_min_idx)[-1]
+        else:
+            # No point has a well-defined maximum constraint violation or
+            # objective function value. We select the most recent point.
+            i = len(fun_filter) - 1
+        return (
+            self.bounds.project(x_filter[i, :]),
+            fun_filter[i],
+            maxcv_filter[i],
+        )
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/settings.py b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/settings.py
new file mode 100644
index 0000000000000000000000000000000000000000..6394822826e094a803a485556a298e342bf260ac
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/settings.py
@@ -0,0 +1,132 @@
+import sys
+from enum import Enum
+
+import numpy as np
+
+
+# Exit status.
+class ExitStatus(Enum):
+    """
+    Exit statuses.
+    """
+
+    RADIUS_SUCCESS = 0
+    TARGET_SUCCESS = 1
+    FIXED_SUCCESS = 2
+    CALLBACK_SUCCESS = 3
+    FEASIBLE_SUCCESS = 4
+    MAX_EVAL_WARNING = 5
+    MAX_ITER_WARNING = 6
+    INFEASIBLE_ERROR = -1
+    LINALG_ERROR = -2
+
+
+class Options(str, Enum):
+    """
+    Options.
+    """
+
+    DEBUG = "debug"
+    FEASIBILITY_TOL = "feasibility_tol"
+    FILTER_SIZE = "filter_size"
+    HISTORY_SIZE = "history_size"
+    MAX_EVAL = "maxfev"
+    MAX_ITER = "maxiter"
+    NPT = "nb_points"
+    RHOBEG = "radius_init"
+    RHOEND = "radius_final"
+    SCALE = "scale"
+    STORE_HISTORY = "store_history"
+    TARGET = "target"
+    VERBOSE = "disp"
+
+
+class Constants(str, Enum):
+    """
+    Constants.
+    """
+
+    DECREASE_RADIUS_FACTOR = "decrease_radius_factor"
+    INCREASE_RADIUS_FACTOR = "increase_radius_factor"
+    INCREASE_RADIUS_THRESHOLD = "increase_radius_threshold"
+    DECREASE_RADIUS_THRESHOLD = "decrease_radius_threshold"
+    DECREASE_RESOLUTION_FACTOR = "decrease_resolution_factor"
+    LARGE_RESOLUTION_THRESHOLD = "large_resolution_threshold"
+    MODERATE_RESOLUTION_THRESHOLD = "moderate_resolution_threshold"
+    LOW_RATIO = "low_ratio"
+    HIGH_RATIO = "high_ratio"
+    VERY_LOW_RATIO = "very_low_ratio"
+    PENALTY_INCREASE_THRESHOLD = "penalty_increase_threshold"
+    PENALTY_INCREASE_FACTOR = "penalty_increase_factor"
+    SHORT_STEP_THRESHOLD = "short_step_threshold"
+    LOW_RADIUS_FACTOR = "low_radius_factor"
+    BYRD_OMOJOKUN_FACTOR = "byrd_omojokun_factor"
+    THRESHOLD_RATIO_CONSTRAINTS = "threshold_ratio_constraints"
+    LARGE_SHIFT_FACTOR = "large_shift_factor"
+    LARGE_GRADIENT_FACTOR = "large_gradient_factor"
+    RESOLUTION_FACTOR = "resolution_factor"
+    IMPROVE_TCG = "improve_tcg"
+
+
+# Default options.
+DEFAULT_OPTIONS = {
+    Options.DEBUG.value: False,
+    Options.FEASIBILITY_TOL.value: np.sqrt(np.finfo(float).eps),
+    Options.FILTER_SIZE.value: sys.maxsize,
+    Options.HISTORY_SIZE.value: sys.maxsize,
+    Options.MAX_EVAL.value: lambda n: 500 * n,
+    Options.MAX_ITER.value: lambda n: 1000 * n,
+    Options.NPT.value: lambda n: 2 * n + 1,
+    Options.RHOBEG.value: 1.0,
+    Options.RHOEND.value: 1e-6,
+    Options.SCALE.value: False,
+    Options.STORE_HISTORY.value: False,
+    Options.TARGET.value: -np.inf,
+    Options.VERBOSE.value: False,
+}
+
+# Default constants.
+DEFAULT_CONSTANTS = {
+    Constants.DECREASE_RADIUS_FACTOR.value: 0.5,
+    Constants.INCREASE_RADIUS_FACTOR.value: np.sqrt(2.0),
+    Constants.INCREASE_RADIUS_THRESHOLD.value: 2.0,
+    Constants.DECREASE_RADIUS_THRESHOLD.value: 1.4,
+    Constants.DECREASE_RESOLUTION_FACTOR.value: 0.1,
+    Constants.LARGE_RESOLUTION_THRESHOLD.value: 250.0,
+    Constants.MODERATE_RESOLUTION_THRESHOLD.value: 16.0,
+    Constants.LOW_RATIO.value: 0.1,
+    Constants.HIGH_RATIO.value: 0.7,
+    Constants.VERY_LOW_RATIO.value: 0.01,
+    Constants.PENALTY_INCREASE_THRESHOLD.value: 1.5,
+    Constants.PENALTY_INCREASE_FACTOR.value: 2.0,
+    Constants.SHORT_STEP_THRESHOLD.value: 0.5,
+    Constants.LOW_RADIUS_FACTOR.value: 0.1,
+    Constants.BYRD_OMOJOKUN_FACTOR.value: 0.8,
+    Constants.THRESHOLD_RATIO_CONSTRAINTS.value: 2.0,
+    Constants.LARGE_SHIFT_FACTOR.value: 10.0,
+    Constants.LARGE_GRADIENT_FACTOR.value: 10.0,
+    Constants.RESOLUTION_FACTOR.value: 2.0,
+    Constants.IMPROVE_TCG.value: True,
+}
+
+# Printing options.
+PRINT_OPTIONS = {
+    "threshold": 6,
+    "edgeitems": 2,
+    "linewidth": sys.maxsize,
+    "formatter": {
+        "float_kind": lambda x: np.format_float_scientific(
+            x,
+            precision=3,
+            unique=False,
+            pad_left=2,
+        )
+    },
+}
+
+# Constants.
+BARRIER = 2.0 ** min(
+    100,
+    np.finfo(float).maxexp // 2,
+    -np.finfo(float).minexp // 2,
+)
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/subsolvers/__init__.py b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/subsolvers/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..01a1ad3c6f4cb5c0c9b99d1ce35fea92e7618ff5
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/subsolvers/__init__.py
@@ -0,0 +1,14 @@
+from .geometry import cauchy_geometry, spider_geometry
+from .optim import (
+    tangential_byrd_omojokun,
+    constrained_tangential_byrd_omojokun,
+    normal_byrd_omojokun,
+)
+
+__all__ = [
+    "cauchy_geometry",
+    "spider_geometry",
+    "tangential_byrd_omojokun",
+    "constrained_tangential_byrd_omojokun",
+    "normal_byrd_omojokun",
+]
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/subsolvers/geometry.py b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/subsolvers/geometry.py
new file mode 100644
index 0000000000000000000000000000000000000000..7b67fd7c813ee493b18720d1daf71324d72330b6
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/subsolvers/geometry.py
@@ -0,0 +1,387 @@
+import inspect
+
+import numpy as np
+
+from ..utils import get_arrays_tol
+
+
+TINY = np.finfo(float).tiny
+
+
+def cauchy_geometry(const, grad, curv, xl, xu, delta, debug):
+    r"""
+    Maximize approximately the absolute value of a quadratic function subject
+    to bound constraints in a trust region.
+
+    This function solves approximately
+
+    .. math::
+
+        \max_{s \in \mathbb{R}^n} \quad \bigg\lvert c + g^{\mathsf{T}} s +
+        \frac{1}{2} s^{\mathsf{T}} H s \bigg\rvert \quad \text{s.t.} \quad
+        \left\{ \begin{array}{l}
+            l \le s \le u,\\
+            \lVert s \rVert \le \Delta,
+        \end{array} \right.
+
+    by maximizing the objective function along the constrained Cauchy
+    direction.
+
+    Parameters
+    ----------
+    const : float
+        Constant :math:`c` as shown above.
+    grad : `numpy.ndarray`, shape (n,)
+        Gradient :math:`g` as shown above.
+    curv : callable
+        Curvature of :math:`H` along any vector.
+
+            ``curv(s) -> float``
+
+        returns :math:`s^{\mathsf{T}} H s`.
+    xl : `numpy.ndarray`, shape (n,)
+        Lower bounds :math:`l` as shown above.
+    xu : `numpy.ndarray`, shape (n,)
+        Upper bounds :math:`u` as shown above.
+    delta : float
+        Trust-region radius :math:`\Delta` as shown above.
+    debug : bool
+        Whether to make debugging tests during the execution.
+
+    Returns
+    -------
+    `numpy.ndarray`, shape (n,)
+        Approximate solution :math:`s`.
+
+    Notes
+    -----
+    This function is described as the first alternative in Section 6.5 of [1]_.
+    It is assumed that the origin is feasible with respect to the bound
+    constraints and that `delta` is finite and positive.
+
+    References
+    ----------
+    .. [1] T. M. Ragonneau. *Model-Based Derivative-Free Optimization Methods
+       and Software*. PhD thesis, Department of Applied Mathematics, The Hong
+       Kong Polytechnic University, Hong Kong, China, 2022. URL:
+       https://theses.lib.polyu.edu.hk/handle/200/12294.
+    """
+    if debug:
+        assert isinstance(const, float)
+        assert isinstance(grad, np.ndarray) and grad.ndim == 1
+        assert inspect.signature(curv).bind(grad)
+        assert isinstance(xl, np.ndarray) and xl.shape == grad.shape
+        assert isinstance(xu, np.ndarray) and xu.shape == grad.shape
+        assert isinstance(delta, float)
+        assert isinstance(debug, bool)
+        tol = get_arrays_tol(xl, xu)
+        assert np.all(xl <= tol)
+        assert np.all(xu >= -tol)
+        assert np.isfinite(delta) and delta > 0.0
+    xl = np.minimum(xl, 0.0)
+    xu = np.maximum(xu, 0.0)
+
+    # To maximize the absolute value of a quadratic function, we maximize the
+    # function itself or its negative, and we choose the solution that provides
+    # the largest function value.
+    step1, q_val1 = _cauchy_geom(const, grad, curv, xl, xu, delta, debug)
+    step2, q_val2 = _cauchy_geom(
+        -const,
+        -grad,
+        lambda x: -curv(x),
+        xl,
+        xu,
+        delta,
+        debug,
+    )
+    step = step1 if abs(q_val1) >= abs(q_val2) else step2
+
+    if debug:
+        assert np.all(xl <= step)
+        assert np.all(step <= xu)
+        assert np.linalg.norm(step) < 1.1 * delta
+    return step
+
+
+def spider_geometry(const, grad, curv, xpt, xl, xu, delta, debug):
+    r"""
+    Maximize approximately the absolute value of a quadratic function subject
+    to bound constraints in a trust region.
+
+    This function solves approximately
+
+    .. math::
+
+        \max_{s \in \mathbb{R}^n} \quad \bigg\lvert c + g^{\mathsf{T}} s +
+        \frac{1}{2} s^{\mathsf{T}} H s \bigg\rvert \quad \text{s.t.} \quad
+        \left\{ \begin{array}{l}
+            l \le s \le u,\\
+            \lVert s \rVert \le \Delta,
+        \end{array} \right.
+
+    by maximizing the objective function along given straight lines.
+
+    Parameters
+    ----------
+    const : float
+        Constant :math:`c` as shown above.
+    grad : `numpy.ndarray`, shape (n,)
+        Gradient :math:`g` as shown above.
+    curv : callable
+        Curvature of :math:`H` along any vector.
+
+            ``curv(s) -> float``
+
+        returns :math:`s^{\mathsf{T}} H s`.
+    xpt : `numpy.ndarray`, shape (n, npt)
+        Points defining the straight lines. The straight lines considered are
+        the ones passing through the origin and the points in `xpt`.
+    xl : `numpy.ndarray`, shape (n,)
+        Lower bounds :math:`l` as shown above.
+    xu : `numpy.ndarray`, shape (n,)
+        Upper bounds :math:`u` as shown above.
+    delta : float
+        Trust-region radius :math:`\Delta` as shown above.
+    debug : bool
+        Whether to make debugging tests during the execution.
+
+    Returns
+    -------
+    `numpy.ndarray`, shape (n,)
+        Approximate solution :math:`s`.
+
+    Notes
+    -----
+    This function is described as the second alternative in Section 6.5 of
+    [1]_. It is assumed that the origin is feasible with respect to the bound
+    constraints and that `delta` is finite and positive.
+
+    References
+    ----------
+    .. [1] T. M. Ragonneau. *Model-Based Derivative-Free Optimization Methods
+       and Software*. PhD thesis, Department of Applied Mathematics, The Hong
+       Kong Polytechnic University, Hong Kong, China, 2022. URL:
+       https://theses.lib.polyu.edu.hk/handle/200/12294.
+    """
+    if debug:
+        assert isinstance(const, float)
+        assert isinstance(grad, np.ndarray) and grad.ndim == 1
+        assert inspect.signature(curv).bind(grad)
+        assert (
+            isinstance(xpt, np.ndarray)
+            and xpt.ndim == 2
+            and xpt.shape[0] == grad.size
+        )
+        assert isinstance(xl, np.ndarray) and xl.shape == grad.shape
+        assert isinstance(xu, np.ndarray) and xu.shape == grad.shape
+        assert isinstance(delta, float)
+        assert isinstance(debug, bool)
+        tol = get_arrays_tol(xl, xu)
+        assert np.all(xl <= tol)
+        assert np.all(xu >= -tol)
+        assert np.isfinite(delta) and delta > 0.0
+    xl = np.minimum(xl, 0.0)
+    xu = np.maximum(xu, 0.0)
+
+    # Iterate through the straight lines.
+    step = np.zeros_like(grad)
+    q_val = const
+    s_norm = np.linalg.norm(xpt, axis=0)
+
+    # Set alpha_xl to the step size for the lower-bound constraint and
+    # alpha_xu to the step size for the upper-bound constraint.
+
+    # xl.shape = (N,)
+    # xpt.shape = (N, M)
+    # i_xl_pos.shape = (M, N)
+    i_xl_pos = (xl > -np.inf) & (xpt.T > -TINY * xl)
+    i_xl_neg = (xl > -np.inf) & (xpt.T < TINY * xl)
+    i_xu_pos = (xu < np.inf) & (xpt.T > TINY * xu)
+    i_xu_neg = (xu < np.inf) & (xpt.T < -TINY * xu)
+
+    # (M, N)
+    alpha_xl_pos = np.atleast_2d(
+        np.broadcast_to(xl, i_xl_pos.shape)[i_xl_pos] / xpt.T[i_xl_pos]
+    )
+    # (M,)
+    alpha_xl_pos = np.max(alpha_xl_pos, axis=1, initial=-np.inf)
+    # make sure it's (M,)
+    alpha_xl_pos = np.broadcast_to(np.atleast_1d(alpha_xl_pos), xpt.shape[1])
+
+    alpha_xl_neg = np.atleast_2d(
+        np.broadcast_to(xl, i_xl_neg.shape)[i_xl_neg] / xpt.T[i_xl_neg]
+    )
+    alpha_xl_neg = np.max(alpha_xl_neg, axis=1, initial=np.inf)
+    alpha_xl_neg = np.broadcast_to(np.atleast_1d(alpha_xl_neg), xpt.shape[1])
+
+    alpha_xu_neg = np.atleast_2d(
+        np.broadcast_to(xu, i_xu_neg.shape)[i_xu_neg] / xpt.T[i_xu_neg]
+    )
+    alpha_xu_neg = np.max(alpha_xu_neg, axis=1, initial=-np.inf)
+    alpha_xu_neg = np.broadcast_to(np.atleast_1d(alpha_xu_neg), xpt.shape[1])
+
+    alpha_xu_pos = np.atleast_2d(
+        np.broadcast_to(xu, i_xu_pos.shape)[i_xu_pos] / xpt.T[i_xu_pos]
+    )
+    alpha_xu_pos = np.max(alpha_xu_pos, axis=1, initial=np.inf)
+    alpha_xu_pos = np.broadcast_to(np.atleast_1d(alpha_xu_pos), xpt.shape[1])
+
+    for k in range(xpt.shape[1]):
+        # Set alpha_tr to the step size for the trust-region constraint.
+        if s_norm[k] > TINY * delta:
+            alpha_tr = max(delta / s_norm[k], 0.0)
+        else:
+            # The current straight line is basically zero.
+            continue
+
+        alpha_bd_pos = max(min(alpha_xu_pos[k], alpha_xl_neg[k]), 0.0)
+        alpha_bd_neg = min(max(alpha_xl_pos[k], alpha_xu_neg[k]), 0.0)
+
+        # Set alpha_quad_pos and alpha_quad_neg to the step size to the extrema
+        # of the quadratic function along the positive and negative directions.
+        grad_step = grad @ xpt[:, k]
+        curv_step = curv(xpt[:, k])
+        if (
+            grad_step >= 0.0
+            and curv_step < -TINY * grad_step
+            or grad_step <= 0.0
+            and curv_step > -TINY * grad_step
+        ):
+            alpha_quad_pos = max(-grad_step / curv_step, 0.0)
+        else:
+            alpha_quad_pos = np.inf
+        if (
+            grad_step >= 0.0
+            and curv_step > TINY * grad_step
+            or grad_step <= 0.0
+            and curv_step < TINY * grad_step
+        ):
+            alpha_quad_neg = min(-grad_step / curv_step, 0.0)
+        else:
+            alpha_quad_neg = -np.inf
+
+        # Select the step that provides the largest value of the objective
+        # function if it improves the current best. The best positive step is
+        # either the one that reaches the constraints or the one that reaches
+        # the extremum of the objective function along the current direction
+        # (only possible if the resulting step is feasible). We test both, and
+        # we perform similar calculations along the negative step.
+        # N.B.: we select the largest possible step among all the ones that
+        # maximize the objective function. This is to avoid returning the zero
+        # step in some extreme cases.
+        alpha_pos = min(alpha_tr, alpha_bd_pos)
+        alpha_neg = max(-alpha_tr, alpha_bd_neg)
+        q_val_pos = (
+            const + alpha_pos * grad_step + 0.5 * alpha_pos**2.0 * curv_step
+        )
+        q_val_neg = (
+            const + alpha_neg * grad_step + 0.5 * alpha_neg**2.0 * curv_step
+        )
+        if alpha_quad_pos < alpha_pos:
+            q_val_quad_pos = (
+                const
+                + alpha_quad_pos * grad_step
+                + 0.5 * alpha_quad_pos**2.0 * curv_step
+            )
+            if abs(q_val_quad_pos) > abs(q_val_pos):
+                alpha_pos = alpha_quad_pos
+                q_val_pos = q_val_quad_pos
+        if alpha_quad_neg > alpha_neg:
+            q_val_quad_neg = (
+                const
+                + alpha_quad_neg * grad_step
+                + 0.5 * alpha_quad_neg**2.0 * curv_step
+            )
+            if abs(q_val_quad_neg) > abs(q_val_neg):
+                alpha_neg = alpha_quad_neg
+                q_val_neg = q_val_quad_neg
+        if abs(q_val_pos) >= abs(q_val_neg) and abs(q_val_pos) > abs(q_val):
+            step = np.clip(alpha_pos * xpt[:, k], xl, xu)
+            q_val = q_val_pos
+        elif abs(q_val_neg) > abs(q_val_pos) and abs(q_val_neg) > abs(q_val):
+            step = np.clip(alpha_neg * xpt[:, k], xl, xu)
+            q_val = q_val_neg
+
+    if debug:
+        assert np.all(xl <= step)
+        assert np.all(step <= xu)
+        assert np.linalg.norm(step) < 1.1 * delta
+    return step
+
+
+def _cauchy_geom(const, grad, curv, xl, xu, delta, debug):
+    """
+    Same as `bound_constrained_cauchy_step` without the absolute value.
+    """
+    # Calculate the initial active set.
+    fixed_xl = (xl < 0.0) & (grad > 0.0)
+    fixed_xu = (xu > 0.0) & (grad < 0.0)
+
+    # Calculate the Cauchy step.
+    cauchy_step = np.zeros_like(grad)
+    cauchy_step[fixed_xl] = xl[fixed_xl]
+    cauchy_step[fixed_xu] = xu[fixed_xu]
+    if np.linalg.norm(cauchy_step) > delta:
+        working = fixed_xl | fixed_xu
+        while True:
+            # Calculate the Cauchy step for the directions in the working set.
+            g_norm = np.linalg.norm(grad[working])
+            delta_reduced = np.sqrt(
+                delta**2.0 - cauchy_step[~working] @ cauchy_step[~working]
+            )
+            if g_norm > TINY * abs(delta_reduced):
+                mu = max(delta_reduced / g_norm, 0.0)
+            else:
+                break
+            cauchy_step[working] = mu * grad[working]
+
+            # Update the working set.
+            fixed_xl = working & (cauchy_step < xl)
+            fixed_xu = working & (cauchy_step > xu)
+            if not np.any(fixed_xl) and not np.any(fixed_xu):
+                # Stop the calculations as the Cauchy step is now feasible.
+                break
+            cauchy_step[fixed_xl] = xl[fixed_xl]
+            cauchy_step[fixed_xu] = xu[fixed_xu]
+            working = working & ~(fixed_xl | fixed_xu)
+
+    # Calculate the step that maximizes the quadratic along the Cauchy step.
+    grad_step = grad @ cauchy_step
+    if grad_step >= 0.0:
+        # Set alpha_tr to the step size for the trust-region constraint.
+        s_norm = np.linalg.norm(cauchy_step)
+        if s_norm > TINY * delta:
+            alpha_tr = max(delta / s_norm, 0.0)
+        else:
+            # The Cauchy step is basically zero.
+            alpha_tr = 0.0
+
+        # Set alpha_quad to the step size for the maximization problem.
+        curv_step = curv(cauchy_step)
+        if curv_step < -TINY * grad_step:
+            alpha_quad = max(-grad_step / curv_step, 0.0)
+        else:
+            alpha_quad = np.inf
+
+        # Set alpha_bd to the step size for the bound constraints.
+        i_xl = (xl > -np.inf) & (cauchy_step < TINY * xl)
+        i_xu = (xu < np.inf) & (cauchy_step > TINY * xu)
+        alpha_xl = np.min(xl[i_xl] / cauchy_step[i_xl], initial=np.inf)
+        alpha_xu = np.min(xu[i_xu] / cauchy_step[i_xu], initial=np.inf)
+        alpha_bd = min(alpha_xl, alpha_xu)
+
+        # Calculate the solution and the corresponding function value.
+        alpha = min(alpha_tr, alpha_quad, alpha_bd)
+        step = np.clip(alpha * cauchy_step, xl, xu)
+        q_val = const + alpha * grad_step + 0.5 * alpha**2.0 * curv_step
+    else:
+        # This case is never reached in exact arithmetic. It prevents this
+        # function to return a step that decreases the objective function.
+        step = np.zeros_like(grad)
+        q_val = const
+
+    if debug:
+        assert np.all(xl <= step)
+        assert np.all(step <= xu)
+        assert np.linalg.norm(step) < 1.1 * delta
+    return step, q_val
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/subsolvers/optim.py b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/subsolvers/optim.py
new file mode 100644
index 0000000000000000000000000000000000000000..c4a960396fb2e992cf76bac0baf171b5af9b7717
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/subsolvers/optim.py
@@ -0,0 +1,1203 @@
+import inspect
+
+import numpy as np
+from scipy.linalg import qr
+
+from ..utils import get_arrays_tol
+
+
+TINY = np.finfo(float).tiny
+EPS = np.finfo(float).eps
+
+
+def tangential_byrd_omojokun(grad, hess_prod, xl, xu, delta, debug, **kwargs):
+    r"""
+    Minimize approximately a quadratic function subject to bound constraints in
+    a trust region.
+
+    This function solves approximately
+
+    .. math::
+
+        \min_{s \in \mathbb{R}^n} \quad g^{\mathsf{T}} s + \frac{1}{2}
+        s^{\mathsf{T}} H s \quad \text{s.t.} \quad
+        \left\{ \begin{array}{l}
+            l \le s \le u\\
+            \lVert s \rVert \le \Delta,
+        \end{array} \right.
+
+    using an active-set variation of the truncated conjugate gradient method.
+
+    Parameters
+    ----------
+    grad : `numpy.ndarray`, shape (n,)
+        Gradient :math:`g` as shown above.
+    hess_prod : callable
+        Product of the Hessian matrix :math:`H` with any vector.
+
+            ``hess_prod(s) -> `numpy.ndarray`, shape (n,)``
+
+        returns the product :math:`H s`.
+    xl : `numpy.ndarray`, shape (n,)
+        Lower bounds :math:`l` as shown above.
+    xu : `numpy.ndarray`, shape (n,)
+        Upper bounds :math:`u` as shown above.
+    delta : float
+        Trust-region radius :math:`\Delta` as shown above.
+    debug : bool
+        Whether to make debugging tests during the execution.
+
+    Returns
+    -------
+    `numpy.ndarray`, shape (n,)
+        Approximate solution :math:`s`.
+
+    Other Parameters
+    ----------------
+    improve_tcg : bool, optional
+        If True, a solution generated by the truncated conjugate gradient
+        method that is on the boundary of the trust region is improved by
+        moving around the trust-region boundary on the two-dimensional space
+        spanned by the solution and the gradient of the quadratic function at
+        the solution (default is True).
+
+    Notes
+    -----
+    This function implements Algorithm 6.2 of [1]_. It is assumed that the
+    origin is feasible with respect to the bound constraints and that `delta`
+    is finite and positive.
+
+    References
+    ----------
+    .. [1] T. M. Ragonneau. *Model-Based Derivative-Free Optimization Methods
+       and Software*. PhD thesis, Department of Applied Mathematics, The Hong
+       Kong Polytechnic University, Hong Kong, China, 2022. URL:
+       https://theses.lib.polyu.edu.hk/handle/200/12294.
+    """
+    if debug:
+        assert isinstance(grad, np.ndarray) and grad.ndim == 1
+        assert inspect.signature(hess_prod).bind(grad)
+        assert isinstance(xl, np.ndarray) and xl.shape == grad.shape
+        assert isinstance(xu, np.ndarray) and xu.shape == grad.shape
+        assert isinstance(delta, float)
+        assert isinstance(debug, bool)
+        tol = get_arrays_tol(xl, xu)
+        assert np.all(xl <= tol)
+        assert np.all(xu >= -tol)
+        assert np.isfinite(delta) and delta > 0.0
+    xl = np.minimum(xl, 0.0)
+    xu = np.maximum(xu, 0.0)
+
+    # Copy the arrays that may be modified by the code below.
+    n = grad.size
+    grad = np.copy(grad)
+    grad_orig = np.copy(grad)
+
+    # Calculate the initial active set.
+    free_bd = ((xl < 0.0) | (grad < 0.0)) & ((xu > 0.0) | (grad > 0.0))
+
+    # Set the initial iterate and the initial search direction.
+    step = np.zeros_like(grad)
+    sd = np.zeros_like(step)
+    sd[free_bd] = -grad[free_bd]
+
+    k = 0
+    reduct = 0.0
+    boundary_reached = False
+    while k < np.count_nonzero(free_bd):
+        # Stop the computations if sd is not a descent direction.
+        grad_sd = grad @ sd
+        if grad_sd >= -10.0 * EPS * n * max(1.0, np.linalg.norm(grad)):
+            break
+
+        # Set alpha_tr to the step size for the trust-region constraint.
+        try:
+            alpha_tr = _alpha_tr(step, sd, delta)
+        except ZeroDivisionError:
+            break
+
+        # Stop the computations if a step along sd is expected to give a
+        # relatively small reduction in the objective function.
+        if -alpha_tr * grad_sd <= 1e-8 * reduct:
+            break
+
+        # Set alpha_quad to the step size for the minimization problem.
+        hess_sd = hess_prod(sd)
+        curv_sd = sd @ hess_sd
+        if curv_sd > TINY * abs(grad_sd):
+            alpha_quad = max(-grad_sd / curv_sd, 0.0)
+        else:
+            alpha_quad = np.inf
+
+        # Stop the computations if the reduction in the objective function
+        # provided by an unconstrained step is small.
+        alpha = min(alpha_tr, alpha_quad)
+        if -alpha * (grad_sd + 0.5 * alpha * curv_sd) <= 1e-8 * reduct:
+            break
+
+        # Set alpha_bd to the step size for the bound constraints.
+        i_xl = (xl > -np.inf) & (sd < -TINY * np.abs(xl - step))
+        i_xu = (xu < np.inf) & (sd > TINY * np.abs(xu - step))
+        all_alpha_xl = np.full_like(step, np.inf)
+        all_alpha_xu = np.full_like(step, np.inf)
+        all_alpha_xl[i_xl] = np.maximum(
+            (xl[i_xl] - step[i_xl]) / sd[i_xl],
+            0.0,
+        )
+        all_alpha_xu[i_xu] = np.maximum(
+            (xu[i_xu] - step[i_xu]) / sd[i_xu],
+            0.0,
+        )
+        alpha_xl = np.min(all_alpha_xl)
+        alpha_xu = np.min(all_alpha_xu)
+        alpha_bd = min(alpha_xl, alpha_xu)
+
+        # Update the iterate.
+        alpha = min(alpha, alpha_bd)
+        if alpha > 0.0:
+            step[free_bd] = np.clip(
+                step[free_bd] + alpha * sd[free_bd],
+                xl[free_bd],
+                xu[free_bd],
+            )
+            grad += alpha * hess_sd
+            reduct -= alpha * (grad_sd + 0.5 * alpha * curv_sd)
+
+        if alpha < min(alpha_tr, alpha_bd):
+            # The current iteration is a conjugate gradient iteration. Update
+            # the search direction so that it is conjugate (with respect to H)
+            # to all the previous search directions.
+            beta = (grad[free_bd] @ hess_sd[free_bd]) / curv_sd
+            sd[free_bd] = beta * sd[free_bd] - grad[free_bd]
+            sd[~free_bd] = 0.0
+            k += 1
+        elif alpha < alpha_tr:
+            # The iterate is restricted by a bound constraint. Add this bound
+            # constraint to the active set, and restart the calculations.
+            if alpha_xl <= alpha:
+                i_new = np.argmin(all_alpha_xl)
+                step[i_new] = xl[i_new]
+            else:
+                i_new = np.argmin(all_alpha_xu)
+                step[i_new] = xu[i_new]
+            free_bd[i_new] = False
+            sd[free_bd] = -grad[free_bd]
+            sd[~free_bd] = 0.0
+            k = 0
+        else:
+            # The current iterate is on the trust-region boundary. Add all the
+            # active bounds to the working set to prepare for the improvement
+            # of the solution, and stop the iterations.
+            if alpha_xl <= alpha:
+                i_new = _argmin(all_alpha_xl)
+                step[i_new] = xl[i_new]
+                free_bd[i_new] = False
+            if alpha_xu <= alpha:
+                i_new = _argmin(all_alpha_xu)
+                step[i_new] = xu[i_new]
+                free_bd[i_new] = False
+            boundary_reached = True
+            break
+
+    # Attempt to improve the solution on the trust-region boundary.
+    if kwargs.get("improve_tcg", True) and boundary_reached:
+        step_base = np.copy(step)
+        step_comparator = grad_orig @ step_base + 0.5 * step_base @ hess_prod(
+            step_base
+        )
+
+        while np.count_nonzero(free_bd) > 0:
+            # Check whether a substantial reduction in the objective function
+            # is possible, and set the search direction.
+            step_sq = step[free_bd] @ step[free_bd]
+            grad_sq = grad[free_bd] @ grad[free_bd]
+            grad_step = grad[free_bd] @ step[free_bd]
+            grad_sd = -np.sqrt(max(step_sq * grad_sq - grad_step**2.0, 0.0))
+            sd[free_bd] = grad_step * step[free_bd] - step_sq * grad[free_bd]
+            sd[~free_bd] = 0.0
+            if grad_sd >= -1e-8 * reduct or np.any(
+                grad_sd >= -TINY * np.abs(sd[free_bd])
+            ):
+                break
+            sd[free_bd] /= -grad_sd
+
+            # Calculate an upper bound for the tangent of half the angle theta
+            # of this alternative iteration. The step will be updated as:
+            # step = cos(theta) * step + sin(theta) * sd.
+            temp_xl = np.zeros(n)
+            temp_xu = np.zeros(n)
+            temp_xl[free_bd] = (
+                step[free_bd] ** 2.0 + sd[free_bd] ** 2.0 - xl[free_bd] ** 2.0
+            )
+            temp_xu[free_bd] = (
+                step[free_bd] ** 2.0 + sd[free_bd] ** 2.0 - xu[free_bd] ** 2.0
+            )
+            temp_xl[temp_xl > 0.0] = (
+                np.sqrt(temp_xl[temp_xl > 0.0]) - sd[temp_xl > 0.0]
+            )
+            temp_xu[temp_xu > 0.0] = (
+                np.sqrt(temp_xu[temp_xu > 0.0]) + sd[temp_xu > 0.0]
+            )
+            dist_xl = np.maximum(step - xl, 0.0)
+            dist_xu = np.maximum(xu - step, 0.0)
+            i_xl = temp_xl > TINY * dist_xl
+            i_xu = temp_xu > TINY * dist_xu
+            all_t_xl = np.ones(n)
+            all_t_xu = np.ones(n)
+            all_t_xl[i_xl] = np.minimum(
+                all_t_xl[i_xl],
+                dist_xl[i_xl] / temp_xl[i_xl],
+            )
+            all_t_xu[i_xu] = np.minimum(
+                all_t_xu[i_xu],
+                dist_xu[i_xu] / temp_xu[i_xu],
+            )
+            t_xl = np.min(all_t_xl)
+            t_xu = np.min(all_t_xu)
+            t_bd = min(t_xl, t_xu)
+
+            # Calculate some curvature information.
+            hess_step = hess_prod(step)
+            hess_sd = hess_prod(sd)
+            curv_step = step @ hess_step
+            curv_sd = sd @ hess_sd
+            curv_step_sd = step @ hess_sd
+
+            # For a range of equally spaced values of tan(0.5 * theta),
+            # calculate the reduction in the objective function that would be
+            # obtained by accepting the corresponding angle.
+            n_samples = 20
+            n_samples = int((n_samples - 3) * t_bd + 3)
+            t_samples = np.linspace(t_bd / n_samples, t_bd, n_samples)
+            sin_values = 2.0 * t_samples / (1.0 + t_samples**2.0)
+            all_reduct = sin_values * (
+                grad_step * t_samples
+                - grad_sd
+                - t_samples * curv_step
+                + sin_values
+                * (t_samples * curv_step_sd - 0.5 * (curv_sd - curv_step))
+            )
+            if np.all(all_reduct <= 0.0):
+                # No reduction in the objective function is obtained.
+                break
+
+            # Accept the angle that provides the largest reduction in the
+            # objective function, and update the iterate.
+            i_max = np.argmax(all_reduct)
+            cos_value = (1.0 - t_samples[i_max] ** 2.0) / (
+                1.0 + t_samples[i_max] ** 2.0
+            )
+            step[free_bd] = (
+                cos_value * step[free_bd] + sin_values[i_max] * sd[free_bd]
+            )
+            grad += (cos_value - 1.0) * hess_step + sin_values[i_max] * hess_sd
+            reduct += all_reduct[i_max]
+
+            # If the above angle is restricted by bound constraints, add them
+            # to the working set, and restart the alternative iteration.
+            # Otherwise, the calculations are terminated.
+            if t_bd < 1.0 and i_max == n_samples - 1:
+                if t_xl <= t_bd:
+                    i_new = _argmin(all_t_xl)
+                    step[i_new] = xl[i_new]
+                    free_bd[i_new] = False
+                if t_xu <= t_bd:
+                    i_new = _argmin(all_t_xu)
+                    step[i_new] = xu[i_new]
+                    free_bd[i_new] = False
+            else:
+                break
+
+        # Ensure that the alternative iteration improves the objective
+        # function.
+        if grad_orig @ step + 0.5 * step @ hess_prod(step) > step_comparator:
+            step = step_base
+
+    if debug:
+        assert np.all(xl <= step)
+        assert np.all(step <= xu)
+        assert np.linalg.norm(step) < 1.1 * delta
+    return step
+
+
+def constrained_tangential_byrd_omojokun(
+    grad,
+    hess_prod,
+    xl,
+    xu,
+    aub,
+    bub,
+    aeq,
+    delta,
+    debug,
+    **kwargs,
+):
+    r"""
+    Minimize approximately a quadratic function subject to bound and linear
+    constraints in a trust region.
+
+    This function solves approximately
+
+    .. math::
+
+        \min_{s \in \mathbb{R}^n} \quad g^{\mathsf{T}} s + \frac{1}{2}
+        s^{\mathsf{T}} H s \quad \text{s.t.} \quad
+        \left\{ \begin{array}{l}
+            l \le s \le u,\\
+            A_{\scriptscriptstyle I} s \le b_{\scriptscriptstyle I},\\
+            A_{\scriptscriptstyle E} s = 0,\\
+            \lVert s \rVert \le \Delta,
+        \end{array} \right.
+
+    using an active-set variation of the truncated conjugate gradient method.
+
+    Parameters
+    ----------
+    grad : `numpy.ndarray`, shape (n,)
+        Gradient :math:`g` as shown above.
+    hess_prod : callable
+        Product of the Hessian matrix :math:`H` with any vector.
+
+            ``hess_prod(s) -> `numpy.ndarray`, shape (n,)``
+
+        returns the product :math:`H s`.
+    xl : `numpy.ndarray`, shape (n,)
+        Lower bounds :math:`l` as shown above.
+    xu : `numpy.ndarray`, shape (n,)
+        Upper bounds :math:`u` as shown above.
+    aub : `numpy.ndarray`, shape (m_linear_ub, n)
+        Coefficient matrix :math:`A_{\scriptscriptstyle I}` as shown above.
+    bub : `numpy.ndarray`, shape (m_linear_ub,)
+        Right-hand side :math:`b_{\scriptscriptstyle I}` as shown above.
+    aeq : `numpy.ndarray`, shape (m_linear_eq, n)
+        Coefficient matrix :math:`A_{\scriptscriptstyle E}` as shown above.
+    delta : float
+        Trust-region radius :math:`\Delta` as shown above.
+    debug : bool
+        Whether to make debugging tests during the execution.
+
+    Returns
+    -------
+    `numpy.ndarray`, shape (n,)
+        Approximate solution :math:`s`.
+
+    Other Parameters
+    ----------------
+    improve_tcg : bool, optional
+        If True, a solution generated by the truncated conjugate gradient
+        method that is on the boundary of the trust region is improved by
+        moving around the trust-region boundary on the two-dimensional space
+        spanned by the solution and the gradient of the quadratic function at
+        the solution (default is True).
+
+    Notes
+    -----
+    This function implements Algorithm 6.3 of [1]_. It is assumed that the
+    origin is feasible with respect to the bound and linear constraints, and
+    that `delta` is finite and positive.
+
+    References
+    ----------
+    .. [1] T. M. Ragonneau. *Model-Based Derivative-Free Optimization Methods
+       and Software*. PhD thesis, Department of Applied Mathematics, The Hong
+       Kong Polytechnic University, Hong Kong, China, 2022. URL:
+       https://theses.lib.polyu.edu.hk/handle/200/12294.
+    """
+    if debug:
+        assert isinstance(grad, np.ndarray) and grad.ndim == 1
+        assert inspect.signature(hess_prod).bind(grad)
+        assert isinstance(xl, np.ndarray) and xl.shape == grad.shape
+        assert isinstance(xu, np.ndarray) and xu.shape == grad.shape
+        assert (
+            isinstance(aub, np.ndarray)
+            and aub.ndim == 2
+            and aub.shape[1] == grad.size
+        )
+        assert (
+            isinstance(bub, np.ndarray)
+            and bub.ndim == 1
+            and bub.size == aub.shape[0]
+        )
+        assert (
+            isinstance(aeq, np.ndarray)
+            and aeq.ndim == 2
+            and aeq.shape[1] == grad.size
+        )
+        assert isinstance(delta, float)
+        assert isinstance(debug, bool)
+        tol = get_arrays_tol(xl, xu)
+        assert np.all(xl <= tol)
+        assert np.all(xu >= -tol)
+        assert np.all(bub >= -tol)
+        assert np.isfinite(delta) and delta > 0.0
+    xl = np.minimum(xl, 0.0)
+    xu = np.maximum(xu, 0.0)
+    bub = np.maximum(bub, 0.0)
+
+    # Copy the arrays that may be modified by the code below.
+    n = grad.size
+    grad = np.copy(grad)
+    grad_orig = np.copy(grad)
+
+    # Calculate the initial active set.
+    free_xl = (xl < 0.0) | (grad < 0.0)
+    free_xu = (xu > 0.0) | (grad > 0.0)
+    free_ub = (bub > 0.0) | (aub @ grad > 0.0)
+    n_act, q = qr_tangential_byrd_omojokun(aub, aeq, free_xl, free_xu, free_ub)
+
+    # Set the initial iterate and the initial search direction.
+    step = np.zeros_like(grad)
+    sd = -q[:, n_act:] @ (q[:, n_act:].T @ grad)
+    resid = np.copy(bub)
+
+    k = 0
+    reduct = 0.0
+    boundary_reached = False
+    while k < n - n_act:
+        # Stop the computations if sd is not a descent direction.
+        grad_sd = grad @ sd
+        if grad_sd >= -10.0 * EPS * n * max(1.0, np.linalg.norm(grad)):
+            break
+
+        # Set alpha_tr to the step size for the trust-region constraint.
+        try:
+            alpha_tr = _alpha_tr(step, sd, delta)
+        except ZeroDivisionError:
+            break
+
+        # Stop the computations if a step along sd is expected to give a
+        # relatively small reduction in the objective function.
+        if -alpha_tr * grad_sd <= 1e-8 * reduct:
+            break
+
+        # Set alpha_quad to the step size for the minimization problem.
+        hess_sd = hess_prod(sd)
+        curv_sd = sd @ hess_sd
+        if curv_sd > TINY * abs(grad_sd):
+            alpha_quad = max(-grad_sd / curv_sd, 0.0)
+        else:
+            alpha_quad = np.inf
+
+        # Stop the computations if the reduction in the objective function
+        # provided by an unconstrained step is small.
+        alpha = min(alpha_tr, alpha_quad)
+        if -alpha * (grad_sd + 0.5 * alpha * curv_sd) <= 1e-8 * reduct:
+            break
+
+        # Set alpha_bd to the step size for the bound constraints.
+        i_xl = free_xl & (xl > -np.inf) & (sd < -TINY * np.abs(xl - step))
+        i_xu = free_xu & (xu < np.inf) & (sd > TINY * np.abs(xu - step))
+        all_alpha_xl = np.full_like(step, np.inf)
+        all_alpha_xu = np.full_like(step, np.inf)
+        all_alpha_xl[i_xl] = np.maximum(
+            (xl[i_xl] - step[i_xl]) / sd[i_xl],
+            0.0,
+        )
+        all_alpha_xu[i_xu] = np.maximum(
+            (xu[i_xu] - step[i_xu]) / sd[i_xu],
+            0.0,
+        )
+        alpha_xl = np.min(all_alpha_xl)
+        alpha_xu = np.min(all_alpha_xu)
+        alpha_bd = min(alpha_xl, alpha_xu)
+
+        # Set alpha_ub to the step size for the linear constraints.
+        aub_sd = aub @ sd
+        i_ub = free_ub & (aub_sd > TINY * np.abs(resid))
+        all_alpha_ub = np.full_like(bub, np.inf)
+        all_alpha_ub[i_ub] = resid[i_ub] / aub_sd[i_ub]
+        alpha_ub = np.min(all_alpha_ub, initial=np.inf)
+
+        # Update the iterate.
+        alpha = min(alpha, alpha_bd, alpha_ub)
+        if alpha > 0.0:
+            step = np.clip(step + alpha * sd, xl, xu)
+            grad += alpha * hess_sd
+            resid = np.maximum(0.0, resid - alpha * aub_sd)
+            reduct -= alpha * (grad_sd + 0.5 * alpha * curv_sd)
+
+        if alpha < min(alpha_tr, alpha_bd, alpha_ub):
+            # The current iteration is a conjugate gradient iteration. Update
+            # the search direction so that it is conjugate (with respect to H)
+            # to all the previous search directions.
+            grad_proj = q[:, n_act:] @ (q[:, n_act:].T @ grad)
+            beta = (grad_proj @ hess_sd) / curv_sd
+            sd = beta * sd - grad_proj
+            k += 1
+        elif alpha < alpha_tr:
+            # The iterate is restricted by a bound/linear constraint. Add this
+            # constraint to the active set, and restart the calculations.
+            if alpha_xl <= alpha:
+                i_new = np.argmin(all_alpha_xl)
+                step[i_new] = xl[i_new]
+                free_xl[i_new] = False
+            elif alpha_xu <= alpha:
+                i_new = np.argmin(all_alpha_xu)
+                step[i_new] = xu[i_new]
+                free_xu[i_new] = False
+            else:
+                i_new = np.argmin(all_alpha_ub)
+                free_ub[i_new] = False
+            n_act, q = qr_tangential_byrd_omojokun(
+                aub,
+                aeq,
+                free_xl,
+                free_xu,
+                free_ub,
+            )
+            sd = -q[:, n_act:] @ (q[:, n_act:].T @ grad)
+            k = 0
+        else:
+            # The current iterate is on the trust-region boundary. Add all the
+            # active bound/linear constraints to the working set to prepare for
+            # the improvement of the solution, and stop the iterations.
+            if alpha_xl <= alpha:
+                i_new = _argmin(all_alpha_xl)
+                step[i_new] = xl[i_new]
+                free_xl[i_new] = False
+            if alpha_xu <= alpha:
+                i_new = _argmin(all_alpha_xu)
+                step[i_new] = xu[i_new]
+                free_xu[i_new] = False
+            if alpha_ub <= alpha:
+                i_new = _argmin(all_alpha_ub)
+                free_ub[i_new] = False
+            n_act, q = qr_tangential_byrd_omojokun(
+                aub,
+                aeq,
+                free_xl,
+                free_xu,
+                free_ub,
+            )
+            boundary_reached = True
+            break
+
+    # Attempt to improve the solution on the trust-region boundary.
+    if kwargs.get("improve_tcg", True) and boundary_reached and n_act < n:
+        step_base = np.copy(step)
+        while n_act < n:
+            # Check whether a substantial reduction in the objective function
+            # is possible, and set the search direction.
+            step_proj = q[:, n_act:] @ (q[:, n_act:].T @ step)
+            grad_proj = q[:, n_act:] @ (q[:, n_act:].T @ grad)
+            step_sq = step_proj @ step_proj
+            grad_sq = grad_proj @ grad_proj
+            grad_step = grad_proj @ step_proj
+            grad_sd = -np.sqrt(max(step_sq * grad_sq - grad_step**2.0, 0.0))
+            sd = q[:, n_act:] @ (
+                q[:, n_act:].T @ (grad_step * step - step_sq * grad)
+            )
+            if grad_sd >= -1e-8 * reduct or np.any(
+                grad_sd >= -TINY * np.abs(sd)
+            ):
+                break
+            sd /= -grad_sd
+
+            # Calculate an upper bound for the tangent of half the angle theta
+            # of this alternative iteration for the bound constraints. The step
+            # will be updated as:
+            # step += (cos(theta) - 1) * step_proj + sin(theta) * sd.
+            temp_xl = np.zeros(n)
+            temp_xu = np.zeros(n)
+            dist_xl = np.maximum(step - xl, 0.0)
+            dist_xu = np.maximum(xu - step, 0.0)
+            temp_xl[free_xl] = sd[free_xl] ** 2.0 - dist_xl[free_xl] * (
+                dist_xl[free_xl] - 2.0 * step_proj[free_xl]
+            )
+            temp_xu[free_xu] = sd[free_xu] ** 2.0 - dist_xu[free_xu] * (
+                dist_xu[free_xu] + 2.0 * step_proj[free_xu]
+            )
+            temp_xl[temp_xl > 0.0] = (
+                np.sqrt(temp_xl[temp_xl > 0.0]) - sd[temp_xl > 0.0]
+            )
+            temp_xu[temp_xu > 0.0] = (
+                np.sqrt(temp_xu[temp_xu > 0.0]) + sd[temp_xu > 0.0]
+            )
+            i_xl = temp_xl > TINY * dist_xl
+            i_xu = temp_xu > TINY * dist_xu
+            all_t_xl = np.ones(n)
+            all_t_xu = np.ones(n)
+            all_t_xl[i_xl] = np.minimum(
+                all_t_xl[i_xl],
+                dist_xl[i_xl] / temp_xl[i_xl],
+            )
+            all_t_xu[i_xu] = np.minimum(
+                all_t_xu[i_xu],
+                dist_xu[i_xu] / temp_xu[i_xu],
+            )
+            t_xl = np.min(all_t_xl)
+            t_xu = np.min(all_t_xu)
+            t_bd = min(t_xl, t_xu)
+
+            # Calculate an upper bound for the tangent of half the angle theta
+            # of this alternative iteration for the linear constraints.
+            temp_ub = np.zeros_like(resid)
+            aub_step = aub @ step_proj
+            aub_sd = aub @ sd
+            temp_ub[free_ub] = aub_sd[free_ub] ** 2.0 - resid[free_ub] * (
+                resid[free_ub] + 2.0 * aub_step[free_ub]
+            )
+            temp_ub[temp_ub > 0.0] = (
+                np.sqrt(temp_ub[temp_ub > 0.0]) + aub_sd[temp_ub > 0.0]
+            )
+            i_ub = temp_ub > TINY * resid
+            all_t_ub = np.ones_like(resid)
+            all_t_ub[i_ub] = np.minimum(
+                all_t_ub[i_ub],
+                resid[i_ub] / temp_ub[i_ub],
+            )
+            t_ub = np.min(all_t_ub, initial=1.0)
+            t_min = min(t_bd, t_ub)
+
+            # Calculate some curvature information.
+            hess_step = hess_prod(step_proj)
+            hess_sd = hess_prod(sd)
+            curv_step = step_proj @ hess_step
+            curv_sd = sd @ hess_sd
+            curv_step_sd = step_proj @ hess_sd
+
+            # For a range of equally spaced values of tan(0.5 * theta),
+            # calculate the reduction in the objective function that would be
+            # obtained by accepting the corresponding angle.
+            n_samples = 20
+            n_samples = int((n_samples - 3) * t_min + 3)
+            t_samples = np.linspace(t_min / n_samples, t_min, n_samples)
+            sin_values = 2.0 * t_samples / (1.0 + t_samples**2.0)
+            all_reduct = sin_values * (
+                grad_step * t_samples
+                - grad_sd
+                - sin_values
+                * (
+                    0.5 * t_samples**2.0 * curv_step
+                    - 2.0 * t_samples * curv_step_sd
+                    + 0.5 * curv_sd
+                )
+            )
+            if np.all(all_reduct <= 0.0):
+                # No reduction in the objective function is obtained.
+                break
+
+            # Accept the angle that provides the largest reduction in the
+            # objective function, and update the iterate.
+            i_max = np.argmax(all_reduct)
+            cos_value = (1.0 - t_samples[i_max] ** 2.0) / (
+                1.0 + t_samples[i_max] ** 2.0
+            )
+            step = np.clip(
+                step + (cos_value - 1.0) * step_proj + sin_values[i_max] * sd,
+                xl,
+                xu,
+            )
+            grad += (cos_value - 1.0) * hess_step + sin_values[i_max] * hess_sd
+            resid = np.maximum(
+                0.0,
+                resid
+                - (cos_value - 1.0) * aub_step
+                - sin_values[i_max] * aub_sd,
+            )
+            reduct += all_reduct[i_max]
+
+            # If the above angle is restricted by bound constraints, add them
+            # to the working set, and restart the alternative iteration.
+            # Otherwise, the calculations are terminated.
+            if t_min < 1.0 and i_max == n_samples - 1:
+                if t_xl <= t_min:
+                    i_new = _argmin(all_t_xl)
+                    step[i_new] = xl[i_new]
+                    free_xl[i_new] = False
+                if t_xu <= t_min:
+                    i_new = _argmin(all_t_xu)
+                    step[i_new] = xu[i_new]
+                    free_xl[i_new] = False
+                if t_ub <= t_min:
+                    i_new = _argmin(all_t_ub)
+                    free_ub[i_new] = False
+                n_act, q = qr_tangential_byrd_omojokun(
+                    aub,
+                    aeq,
+                    free_xl,
+                    free_xu,
+                    free_ub,
+                )
+            else:
+                break
+
+        # Ensure that the alternative iteration improves the objective
+        # function.
+        if grad_orig @ step + 0.5 * step @ hess_prod(
+            step
+        ) > grad_orig @ step_base + 0.5 * step_base @ hess_prod(step_base):
+            step = step_base
+
+    if debug:
+        tol = get_arrays_tol(xl, xu)
+        assert np.all(xl <= step)
+        assert np.all(step <= xu)
+        assert np.all(aub @ step <= bub + tol)
+        assert np.all(np.abs(aeq @ step) <= tol)
+        assert np.linalg.norm(step) < 1.1 * delta
+    return step
+
+
+def normal_byrd_omojokun(aub, bub, aeq, beq, xl, xu, delta, debug, **kwargs):
+    r"""
+    Minimize approximately a linear constraint violation subject to bound
+    constraints in a trust region.
+
+    This function solves approximately
+
+    .. math::
+
+        \min_{s \in \mathbb{R}^n} \quad \frac{1}{2} \big( \lVert \max \{
+        A_{\scriptscriptstyle I} s - b_{\scriptscriptstyle I}, 0 \} \rVert^2 +
+        \lVert A_{\scriptscriptstyle E} s - b_{\scriptscriptstyle E} \rVert^2
+        \big) \quad \text{s.t.}
+        \quad
+        \left\{ \begin{array}{l}
+            l \le s \le u,\\
+            \lVert s \rVert \le \Delta,
+        \end{array} \right.
+
+    using a variation of the truncated conjugate gradient method.
+
+    Parameters
+    ----------
+    aub : `numpy.ndarray`, shape (m_linear_ub, n)
+        Matrix :math:`A_{\scriptscriptstyle I}` as shown above.
+    bub : `numpy.ndarray`, shape (m_linear_ub,)
+        Vector :math:`b_{\scriptscriptstyle I}` as shown above.
+    aeq : `numpy.ndarray`, shape (m_linear_eq, n)
+        Matrix :math:`A_{\scriptscriptstyle E}` as shown above.
+    beq : `numpy.ndarray`, shape (m_linear_eq,)
+        Vector :math:`b_{\scriptscriptstyle E}` as shown above.
+    xl : `numpy.ndarray`, shape (n,)
+        Lower bounds :math:`l` as shown above.
+    xu : `numpy.ndarray`, shape (n,)
+        Upper bounds :math:`u` as shown above.
+    delta : float
+        Trust-region radius :math:`\Delta` as shown above.
+    debug : bool
+        Whether to make debugging tests during the execution.
+
+    Returns
+    -------
+    `numpy.ndarray`, shape (n,)
+        Approximate solution :math:`s`.
+
+    Other Parameters
+    ----------------
+    improve_tcg : bool, optional
+        If True, a solution generated by the truncated conjugate gradient
+        method that is on the boundary of the trust region is improved by
+        moving around the trust-region boundary on the two-dimensional space
+        spanned by the solution and the gradient of the quadratic function at
+        the solution (default is True).
+
+    Notes
+    -----
+    This function implements Algorithm 6.4 of [1]_. It is assumed that the
+    origin is feasible with respect to the bound constraints and that `delta`
+    is finite and positive.
+
+    References
+    ----------
+    .. [1] T. M. Ragonneau. *Model-Based Derivative-Free Optimization Methods
+       and Software*. PhD thesis, Department of Applied Mathematics, The Hong
+       Kong Polytechnic University, Hong Kong, China, 2022. URL:
+       https://theses.lib.polyu.edu.hk/handle/200/12294.
+    """
+    if debug:
+        assert isinstance(aub, np.ndarray) and aub.ndim == 2
+        assert (
+            isinstance(bub, np.ndarray)
+            and bub.ndim == 1
+            and bub.size == aub.shape[0]
+        )
+        assert (
+            isinstance(aeq, np.ndarray)
+            and aeq.ndim == 2
+            and aeq.shape[1] == aub.shape[1]
+        )
+        assert (
+            isinstance(beq, np.ndarray)
+            and beq.ndim == 1
+            and beq.size == aeq.shape[0]
+        )
+        assert isinstance(xl, np.ndarray) and xl.shape == (aub.shape[1],)
+        assert isinstance(xu, np.ndarray) and xu.shape == (aub.shape[1],)
+        assert isinstance(delta, float)
+        assert isinstance(debug, bool)
+        tol = get_arrays_tol(xl, xu)
+        assert np.all(xl <= tol)
+        assert np.all(xu >= -tol)
+        assert np.isfinite(delta) and delta > 0.0
+    xl = np.minimum(xl, 0.0)
+    xu = np.maximum(xu, 0.0)
+
+    # Calculate the initial active set.
+    m_linear_ub, n = aub.shape
+    grad = np.r_[aeq.T @ -beq, np.maximum(0.0, -bub)]
+    free_xl = (xl < 0.0) | (grad[:n] < 0.0)
+    free_xu = (xu > 0.0) | (grad[:n] > 0.0)
+    free_slack = bub < 0.0
+    free_ub = (bub > 0.0) | (aub @ grad[:n] - grad[n:] > 0.0)
+    n_act, q = qr_normal_byrd_omojokun(
+        aub,
+        free_xl,
+        free_xu,
+        free_slack,
+        free_ub,
+    )
+
+    # Calculate an upper bound on the norm of the slack variables. It is not
+    # used in the original algorithm, but it may prevent undesired behaviors
+    # engendered by computer rounding errors.
+    delta_slack = np.sqrt(beq @ beq + grad[n:] @ grad[n:])
+
+    # Set the initial iterate and the initial search direction.
+    step = np.zeros(n)
+    sd = -q[:, n_act:] @ (q[:, n_act:].T @ grad)
+    resid = bub + grad[n:]
+
+    k = 0
+    reduct = 0.0
+    boundary_reached = False
+    while k < n + m_linear_ub - n_act:
+        # Stop the computations if sd is not a descent direction.
+        grad_sd = grad @ sd
+        if grad_sd >= -10.0 * EPS * n * max(1.0, np.linalg.norm(grad)):
+            break
+
+        # Set alpha_tr to the step size for the trust-region constraint.
+        try:
+            alpha_tr = _alpha_tr(step, sd[:n], delta)
+        except ZeroDivisionError:
+            alpha_tr = np.inf
+
+        # Prevent undesired behaviors engendered by computer rounding errors by
+        # considering the trust-region constraint on the slack variables.
+        try:
+            alpha_tr = min(alpha_tr, _alpha_tr(grad[n:], sd[n:], delta_slack))
+        except ZeroDivisionError:
+            pass
+
+        # Stop the computations if a step along sd is expected to give a
+        # relatively small reduction in the objective function.
+        if -alpha_tr * grad_sd <= 1e-8 * reduct:
+            break
+
+        # Set alpha_quad to the step size for the minimization problem.
+        hess_sd = np.r_[aeq.T @ (aeq @ sd[:n]), sd[n:]]
+        curv_sd = sd @ hess_sd
+        if curv_sd > TINY * abs(grad_sd):
+            alpha_quad = max(-grad_sd / curv_sd, 0.0)
+        else:
+            alpha_quad = np.inf
+
+        # Stop the computations if the reduction in the objective function
+        # provided by an unconstrained step is small.
+        alpha = min(alpha_tr, alpha_quad)
+        if -alpha * (grad_sd + 0.5 * alpha * curv_sd) <= 1e-8 * reduct:
+            break
+
+        # Set alpha_bd to the step size for the bound constraints.
+        i_xl = free_xl & (xl > -np.inf) & (sd[:n] < -TINY * np.abs(xl - step))
+        i_xu = free_xu & (xu < np.inf) & (sd[:n] > TINY * np.abs(xu - step))
+        i_slack = free_slack & (sd[n:] < -TINY * np.abs(grad[n:]))
+        all_alpha_xl = np.full_like(step, np.inf)
+        all_alpha_xu = np.full_like(step, np.inf)
+        all_alpha_slack = np.full_like(bub, np.inf)
+        all_alpha_xl[i_xl] = np.maximum(
+            (xl[i_xl] - step[i_xl]) / sd[:n][i_xl],
+            0.0,
+        )
+        all_alpha_xu[i_xu] = np.maximum(
+            (xu[i_xu] - step[i_xu]) / sd[:n][i_xu],
+            0.0,
+        )
+        all_alpha_slack[i_slack] = np.maximum(
+            -grad[n:][i_slack] / sd[n:][i_slack],
+            0.0,
+        )
+        alpha_xl = np.min(all_alpha_xl)
+        alpha_xu = np.min(all_alpha_xu)
+        alpha_slack = np.min(all_alpha_slack, initial=np.inf)
+        alpha_bd = min(alpha_xl, alpha_xu, alpha_slack)
+
+        # Set alpha_ub to the step size for the linear constraints.
+        aub_sd = aub @ sd[:n] - sd[n:]
+        i_ub = free_ub & (aub_sd > TINY * np.abs(resid))
+        all_alpha_ub = np.full_like(bub, np.inf)
+        all_alpha_ub[i_ub] = resid[i_ub] / aub_sd[i_ub]
+        alpha_ub = np.min(all_alpha_ub, initial=np.inf)
+
+        # Update the iterate.
+        alpha = min(alpha, alpha_bd, alpha_ub)
+        if alpha > 0.0:
+            step = np.clip(step + alpha * sd[:n], xl, xu)
+            grad += alpha * hess_sd
+            resid = np.maximum(0.0, resid - alpha * aub_sd)
+            reduct -= alpha * (grad_sd + 0.5 * alpha * curv_sd)
+
+        if alpha < min(alpha_tr, alpha_bd, alpha_ub):
+            # The current iteration is a conjugate gradient iteration. Update
+            # the search direction so that it is conjugate (with respect to H)
+            # to all the previous search directions.
+            grad_proj = q[:, n_act:] @ (q[:, n_act:].T @ grad)
+            beta = (grad_proj @ hess_sd) / curv_sd
+            sd = beta * sd - grad_proj
+            k += 1
+        elif alpha < alpha_tr:
+            # The iterate is restricted by a bound/linear constraint. Add this
+            # constraint to the active set, and restart the calculations.
+            if alpha_xl <= alpha:
+                i_new = np.argmin(all_alpha_xl)
+                step[i_new] = xl[i_new]
+                free_xl[i_new] = False
+            elif alpha_xu <= alpha:
+                i_new = np.argmin(all_alpha_xu)
+                step[i_new] = xu[i_new]
+                free_xu[i_new] = False
+            elif alpha_slack <= alpha:
+                i_new = np.argmin(all_alpha_slack)
+                free_slack[i_new] = False
+            else:
+                i_new = np.argmin(all_alpha_ub)
+                free_ub[i_new] = False
+            n_act, q = qr_normal_byrd_omojokun(
+                aub, free_xl, free_xu, free_slack, free_ub
+            )
+            sd = -q[:, n_act:] @ (q[:, n_act:].T @ grad)
+            k = 0
+        else:
+            # The current iterate is on the trust-region boundary. Add all the
+            # active bound constraints to the working set to prepare for the
+            # improvement of the solution, and stop the iterations.
+            if alpha_xl <= alpha:
+                i_new = _argmin(all_alpha_xl)
+                step[i_new] = xl[i_new]
+                free_xl[i_new] = False
+            if alpha_xu <= alpha:
+                i_new = _argmin(all_alpha_xu)
+                step[i_new] = xu[i_new]
+                free_xu[i_new] = False
+            boundary_reached = True
+            break
+
+    # Attempt to improve the solution on the trust-region boundary.
+    if kwargs.get("improve_tcg", True) and boundary_reached:
+        step_base = np.copy(step)
+        free_bd = free_xl & free_xu
+        grad = aub.T @ np.maximum(aub @ step - bub, 0.0) + aeq.T @ (
+            aeq @ step - beq
+        )
+        sd = np.zeros(n)
+        while np.count_nonzero(free_bd) > 0:
+            # Check whether a substantial reduction in the objective function
+            # is possible, and set the search direction.
+            step_sq = step[free_bd] @ step[free_bd]
+            grad_sq = grad[free_bd] @ grad[free_bd]
+            grad_step = grad[free_bd] @ step[free_bd]
+            grad_sd = -np.sqrt(max(step_sq * grad_sq - grad_step**2.0, 0.0))
+            sd[free_bd] = grad_step * step[free_bd] - step_sq * grad[free_bd]
+            sd[~free_bd] = 0.0
+            if grad_sd >= -1e-8 * reduct or np.any(
+                grad_sd >= -TINY * np.abs(sd[free_bd])
+            ):
+                break
+            sd[free_bd] /= -grad_sd
+
+            # Calculate an upper bound for the tangent of half the angle theta
+            # of this alternative iteration. The step will be updated as:
+            # step = cos(theta) * step + sin(theta) * sd.
+            temp_xl = np.zeros(n)
+            temp_xu = np.zeros(n)
+            temp_xl[free_bd] = (
+                step[free_bd] ** 2.0 + sd[free_bd] ** 2.0 - xl[free_bd] ** 2.0
+            )
+            temp_xu[free_bd] = (
+                step[free_bd] ** 2.0 + sd[free_bd] ** 2.0 - xu[free_bd] ** 2.0
+            )
+            temp_xl[temp_xl > 0.0] = (
+                np.sqrt(temp_xl[temp_xl > 0.0]) - sd[temp_xl > 0.0]
+            )
+            temp_xu[temp_xu > 0.0] = (
+                np.sqrt(temp_xu[temp_xu > 0.0]) + sd[temp_xu > 0.0]
+            )
+            dist_xl = np.maximum(step - xl, 0.0)
+            dist_xu = np.maximum(xu - step, 0.0)
+            i_xl = temp_xl > TINY * dist_xl
+            i_xu = temp_xu > TINY * dist_xu
+            all_t_xl = np.ones(n)
+            all_t_xu = np.ones(n)
+            all_t_xl[i_xl] = np.minimum(
+                all_t_xl[i_xl],
+                dist_xl[i_xl] / temp_xl[i_xl],
+            )
+            all_t_xu[i_xu] = np.minimum(
+                all_t_xu[i_xu],
+                dist_xu[i_xu] / temp_xu[i_xu],
+            )
+            t_xl = np.min(all_t_xl)
+            t_xu = np.min(all_t_xu)
+            t_bd = min(t_xl, t_xu)
+
+            # For a range of equally spaced values of tan(0.5 * theta),
+            # calculate the reduction in the objective function that would be
+            # obtained by accepting the corresponding angle.
+            n_samples = 20
+            n_samples = int((n_samples - 3) * t_bd + 3)
+            t_samples = np.linspace(t_bd / n_samples, t_bd, n_samples)
+            resid_ub = np.maximum(aub @ step - bub, 0.0)
+            resid_eq = aeq @ step - beq
+            step_proj = np.copy(step)
+            step_proj[~free_bd] = 0.0
+            all_reduct = np.empty(n_samples)
+            for i in range(n_samples):
+                sin_value = 2.0 * t_samples[i] / (1.0 + t_samples[i] ** 2.0)
+                step_alt = np.clip(
+                    step + sin_value * (sd - t_samples[i] * step_proj),
+                    xl,
+                    xu,
+                )
+                resid_ub_alt = np.maximum(aub @ step_alt - bub, 0.0)
+                resid_eq_alt = aeq @ step_alt - beq
+                all_reduct[i] = 0.5 * (
+                    resid_ub @ resid_ub
+                    + resid_eq @ resid_eq
+                    - resid_ub_alt @ resid_ub_alt
+                    - resid_eq_alt @ resid_eq_alt
+                )
+            if np.all(all_reduct <= 0.0):
+                # No reduction in the objective function is obtained.
+                break
+
+            # Accept the angle that provides the largest reduction in the
+            # objective function, and update the iterate.
+            i_max = np.argmax(all_reduct)
+            cos_value = (1.0 - t_samples[i_max] ** 2.0) / (
+                1.0 + t_samples[i_max] ** 2.0
+            )
+            sin_value = (2.0 * t_samples[i_max]
+                         / (1.0 + t_samples[i_max] ** 2.0))
+            step[free_bd] = cos_value * step[free_bd] + sin_value * sd[free_bd]
+            grad = aub.T @ np.maximum(aub @ step - bub, 0.0) + aeq.T @ (
+                aeq @ step - beq
+            )
+            reduct += all_reduct[i_max]
+
+            # If the above angle is restricted by bound constraints, add them
+            # to the working set, and restart the alternative iteration.
+            # Otherwise, the calculations are terminated.
+            if t_bd < 1.0 and i_max == n_samples - 1:
+                if t_xl <= t_bd:
+                    i_new = _argmin(all_t_xl)
+                    step[i_new] = xl[i_new]
+                    free_bd[i_new] = False
+                if t_xu <= t_bd:
+                    i_new = _argmin(all_t_xu)
+                    step[i_new] = xu[i_new]
+                    free_bd[i_new] = False
+            else:
+                break
+
+        # Ensure that the alternative iteration improves the objective
+        # function.
+        resid_ub = np.maximum(aub @ step - bub, 0.0)
+        resid_ub_base = np.maximum(aub @ step_base - bub, 0.0)
+        resid_eq = aeq @ step - beq
+        resid_eq_base = aeq @ step_base - beq
+        if (
+            resid_ub @ resid_ub + resid_eq @ resid_eq
+            > resid_ub_base @ resid_ub_base + resid_eq_base @ resid_eq_base
+        ):
+            step = step_base
+
+    if debug:
+        assert np.all(xl <= step)
+        assert np.all(step <= xu)
+        assert np.linalg.norm(step) < 1.1 * delta
+    return step
+
+
+def qr_tangential_byrd_omojokun(aub, aeq, free_xl, free_xu, free_ub):
+    n = free_xl.size
+    identity = np.eye(n)
+    q, r, _ = qr(
+        np.block(
+            [
+                [aeq],
+                [aub[~free_ub, :]],
+                [-identity[~free_xl, :]],
+                [identity[~free_xu, :]],
+            ]
+        ).T,
+        pivoting=True,
+    )
+    n_act = np.count_nonzero(
+        np.abs(np.diag(r))
+        >= 10.0
+        * EPS
+        * n
+        * np.linalg.norm(r[: np.min(r.shape), : np.min(r.shape)], axis=0)
+    )
+    return n_act, q
+
+
+def qr_normal_byrd_omojokun(aub, free_xl, free_xu, free_slack, free_ub):
+    m_linear_ub, n = aub.shape
+    identity_n = np.eye(n)
+    identity_m = np.eye(m_linear_ub)
+    q, r, _ = qr(
+        np.block(
+            [
+                [
+                    aub[~free_ub, :],
+                    -identity_m[~free_ub, :],
+                ],
+                [
+                    np.zeros((m_linear_ub - np.count_nonzero(free_slack), n)),
+                    -identity_m[~free_slack, :],
+                ],
+                [
+                    -identity_n[~free_xl, :],
+                    np.zeros((n - np.count_nonzero(free_xl), m_linear_ub)),
+                ],
+                [
+                    identity_n[~free_xu, :],
+                    np.zeros((n - np.count_nonzero(free_xu), m_linear_ub)),
+                ],
+            ]
+        ).T,
+        pivoting=True,
+    )
+    n_act = np.count_nonzero(
+        np.abs(np.diag(r))
+        >= 10.0
+        * EPS
+        * (n + m_linear_ub)
+        * np.linalg.norm(r[: np.min(r.shape), : np.min(r.shape)], axis=0)
+    )
+    return n_act, q
+
+
+def _alpha_tr(step, sd, delta):
+    step_sd = step @ sd
+    sd_sq = sd @ sd
+    dist_tr_sq = delta**2.0 - step @ step
+    temp = np.sqrt(max(step_sd**2.0 + sd_sq * dist_tr_sq, 0.0))
+    if step_sd <= 0.0 and sd_sq > TINY * abs(temp - step_sd):
+        alpha_tr = max((temp - step_sd) / sd_sq, 0.0)
+    elif abs(temp + step_sd) > TINY * dist_tr_sq:
+        alpha_tr = max(dist_tr_sq / (temp + step_sd), 0.0)
+    else:
+        raise ZeroDivisionError
+    return alpha_tr
+
+
+def _argmax(x):
+    return np.flatnonzero(x >= np.max(x))
+
+
+def _argmin(x):
+    return np.flatnonzero(x <= np.min(x))
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/utils/__init__.py b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/utils/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..fe6b4841ddff3a04bda5cbff744e30681b6963b9
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/utils/__init__.py
@@ -0,0 +1,18 @@
+from .exceptions import (
+    MaxEvalError,
+    TargetSuccess,
+    CallbackSuccess,
+    FeasibleSuccess,
+)
+from .math import get_arrays_tol, exact_1d_array
+from .versions import show_versions
+
+__all__ = [
+    "MaxEvalError",
+    "TargetSuccess",
+    "CallbackSuccess",
+    "FeasibleSuccess",
+    "get_arrays_tol",
+    "exact_1d_array",
+    "show_versions",
+]
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/utils/exceptions.py b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/utils/exceptions.py
new file mode 100644
index 0000000000000000000000000000000000000000..c85094894f378a8e3934ad109ea6166e33e4366b
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/utils/exceptions.py
@@ -0,0 +1,22 @@
+class MaxEvalError(Exception):
+    """
+    Exception raised when the maximum number of evaluations is reached.
+    """
+
+
+class TargetSuccess(Exception):
+    """
+    Exception raised when the target value is reached.
+    """
+
+
+class CallbackSuccess(StopIteration):
+    """
+    Exception raised when the callback function raises a ``StopIteration``.
+    """
+
+
+class FeasibleSuccess(Exception):
+    """
+    Exception raised when a feasible point of a feasible problem is found.
+    """
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/utils/math.py b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/utils/math.py
new file mode 100644
index 0000000000000000000000000000000000000000..1b16ae98a0df38752815f5a69d56da20f856f9f9
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/utils/math.py
@@ -0,0 +1,77 @@
+import numpy as np
+
+
+EPS = np.finfo(float).eps
+
+
+def get_arrays_tol(*arrays):
+    """
+    Get a relative tolerance for a set of arrays.
+
+    Parameters
+    ----------
+    *arrays: tuple
+        Set of `numpy.ndarray` to get the tolerance for.
+
+    Returns
+    -------
+    float
+        Relative tolerance for the set of arrays.
+
+    Raises
+    ------
+    ValueError
+        If no array is provided.
+    """
+    if len(arrays) == 0:
+        raise ValueError("At least one array must be provided.")
+    size = max(array.size for array in arrays)
+    weight = max(
+        np.max(np.abs(array[np.isfinite(array)]), initial=1.0)
+        for array in arrays
+    )
+    return 10.0 * EPS * max(size, 1.0) * weight
+
+
+def exact_1d_array(x, message):
+    """
+    Preprocess a 1-dimensional array.
+
+    Parameters
+    ----------
+    x : array_like
+        Array to be preprocessed.
+    message : str
+        Error message if `x` cannot be interpreter as a 1-dimensional array.
+
+    Returns
+    -------
+    `numpy.ndarray`
+        Preprocessed array.
+    """
+    x = np.atleast_1d(np.squeeze(x)).astype(float)
+    if x.ndim != 1:
+        raise ValueError(message)
+    return x
+
+
+def exact_2d_array(x, message):
+    """
+    Preprocess a 2-dimensional array.
+
+    Parameters
+    ----------
+    x : array_like
+        Array to be preprocessed.
+    message : str
+        Error message if `x` cannot be interpreter as a 2-dimensional array.
+
+    Returns
+    -------
+    `numpy.ndarray`
+        Preprocessed array.
+    """
+    x = np.atleast_2d(x).astype(float)
+    if x.ndim != 2:
+        raise ValueError(message)
+    return x
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/utils/versions.py b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/utils/versions.py
new file mode 100644
index 0000000000000000000000000000000000000000..94a0f8f5cef626354f40901cbe06a84287291c1c
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/cobyqa/utils/versions.py
@@ -0,0 +1,67 @@
+import os
+import platform
+import sys
+from importlib.metadata import PackageNotFoundError, version
+
+
+def _get_sys_info():
+    """
+    Get useful system information.
+
+    Returns
+    -------
+    dict
+        Useful system information.
+    """
+    return {
+        "python": sys.version.replace(os.linesep, " "),
+        "executable": sys.executable,
+        "machine": platform.platform(),
+    }
+
+
+def _get_deps_info():
+    """
+    Get the versions of the dependencies.
+
+    Returns
+    -------
+    dict
+        Versions of the dependencies.
+    """
+    deps = ["cobyqa", "numpy", "scipy", "setuptools", "pip"]
+    deps_info = {}
+    for module in deps:
+        try:
+            deps_info[module] = version(module)
+        except PackageNotFoundError:
+            deps_info[module] = None
+    return deps_info
+
+
+def show_versions():
+    """
+    Display useful system and dependencies information.
+
+    When reporting issues, please include this information.
+    """
+    print("System settings")
+    print("---------------")
+    sys_info = _get_sys_info()
+    print(
+        "\n".join(
+            f"{k:>{max(map(len, sys_info.keys())) + 1}}: {v}"
+            for k, v in sys_info.items()
+        )
+    )
+
+    print()
+    print("Python dependencies")
+    print("-------------------")
+    deps_info = _get_deps_info()
+    print(
+        "\n".join(
+            f"{k:>{max(map(len, deps_info.keys())) + 1}}: {v}"
+            for k, v in deps_info.items()
+        )
+    )
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/deprecation.py b/.venv/lib/python3.11/site-packages/scipy/_lib/deprecation.py
new file mode 100644
index 0000000000000000000000000000000000000000..ea60d3809bf9b11309f8a6613caffb6aaab7e667
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/deprecation.py
@@ -0,0 +1,274 @@
+from inspect import Parameter, signature
+import functools
+import warnings
+from importlib import import_module
+from scipy._lib._docscrape import FunctionDoc
+
+
+__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, dep_version="1.16.0"):
+    """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``.
+    dep_version : str, optional
+        Version in which deprecated attributes will be removed.
+    """
+    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 {dep_version}, 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 = f"`{old_name}` is deprecated!"
+    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,
+                               deprecated_args=None, custom_message=""):
+    """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.
+    deprecated_args : set of str, optional
+        Arguments to deprecate - whether passed by position or keyword.
+    custom_message : str, optional
+        Custom message to add to deprecation warning and documentation.
+    """
+    if version is None:
+        msg = "Need to specify a version where signature will be changed"
+        raise ValueError(msg)
+
+    deprecated_args = set() if deprecated_args is None else set(deprecated_args)
+
+    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)
+
+        def warn_deprecated_args(kwargs):
+            intersection = deprecated_args.intersection(kwargs)
+            if intersection:
+                message = (f"Arguments {intersection} are deprecated, whether passed "
+                           "by position or keyword. They will be removed in SciPy "
+                           f"{version}. ")
+                message += custom_message
+                warnings.warn(message, category=DeprecationWarning, stacklevel=3)
+
+        @functools.wraps(f)
+        def inner_f(*args, **kwargs):
+
+            extra_args = len(args) - len(all_args)
+            if extra_args <= 0:
+                warn_deprecated_args(kwargs)
+                return f(*args, **kwargs)
+
+            # extra_args > 0
+            kwonly_extra_args = set(kwonly_args[:extra_args]) - deprecated_args
+            args_msg = ", ".join(kwonly_extra_args)
+            warnings.warn(
+                (
+                    f"You are passing as positional arguments: {args_msg}. "
+                    "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))
+            warn_deprecated_args(kwargs)
+            return f(**kwargs)
+
+        doc = FunctionDoc(inner_f)
+        kwonly_extra_args = set(kwonly_args) - deprecated_args
+        admonition = f"""
+.. deprecated:: {version}
+    Use of argument(s) ``{kwonly_extra_args}`` by position is deprecated; beginning in 
+    SciPy {version}, these will be keyword-only. """
+        if deprecated_args:
+            admonition += (f"Argument(s) ``{deprecated_args}`` are deprecated, whether "
+                           "passed by position or keyword; they will be removed in "
+                           f"SciPy {version}. ")
+        admonition += custom_message
+        doc['Extended Summary'] += [admonition]
+
+        doc = str(doc).split("\n", 1)[1].lstrip(" \n")  # remove signature
+        inner_f.__doc__ = str(doc)
+
+        return inner_f
+
+    if func is not None:
+        return _inner_deprecate_positional_args(func)
+
+    return _inner_deprecate_positional_args
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/doccer.py b/.venv/lib/python3.11/site-packages/scipy/_lib/doccer.py
new file mode 100644
index 0000000000000000000000000000000000000000..7fd8fef0b4c49a60fdd71427d134528aee00a216
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/doccer.py
@@ -0,0 +1,366 @@
+"""Utilities to allow inserting docstring fragments for common
+parameters into function and method docstrings."""
+
+from collections.abc import Callable, Iterable, Mapping
+from typing import Protocol, TypeVar
+import sys
+
+__all__ = [
+    "docformat",
+    "inherit_docstring_from",
+    "indentcount_lines",
+    "filldoc",
+    "unindent_dict",
+    "unindent_string",
+    "extend_notes_in_docstring",
+    "replace_notes_in_docstring",
+    "doc_replace",
+]
+
+_F = TypeVar("_F", bound=Callable[..., object])
+
+
+class Decorator(Protocol):
+    """A decorator of a function."""
+
+    def __call__(self, func: _F, /) -> _F: ...
+
+
+def docformat(docstring: str, docdict: Mapping[str, str] | None = None) -> str:
+    """Fill a function docstring from variables in dictionary.
+
+    Adapt the indent of the inserted docs
+
+    Parameters
+    ----------
+    docstring : str
+        A docstring from a function, possibly with dict formatting strings.
+    docdict : dict[str, str], optional
+        A 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
+    -------
+    docstring : str
+        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()
+        indented[name] = ("\n" + indent).join(lines)
+    return docstring % indented
+
+
+def inherit_docstring_from(cls: object) -> Decorator:
+    """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 : type or object
+        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
+    -------
+    decfunc : 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: _F) -> _F:
+        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: object, notes: str) -> Decorator:
+    """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`.
+
+    Parameters
+    ----------
+    cls : type or object
+        A class with a method with the same name as the decorated method.
+        The docstring of the method in this class replaces the docstring of the
+        decorated method.
+    notes : str
+        Additional notes to append to the 'Notes' section of the docstring.
+
+    Returns
+    -------
+    decfunc : function
+        The decorator function that modifies the __doc__ attribute
+        of its argument.
+    """
+
+    def _doc(func: _F) -> _F:
+        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: object, notes: str) -> Decorator:
+    """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`.
+
+    Parameters
+    ----------
+    cls : type or object
+        A class with a method with the same name as the decorated method.
+        The docstring of the method in this class replaces the docstring of the
+        decorated method.
+    notes : str
+        The notes to replace the existing 'Notes' section with.
+
+    Returns
+    -------
+    decfunc : function
+        The decorator function that modifies the __doc__ attribute
+        of its argument.
+    """
+
+    def _doc(func: _F) -> _F:
+        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: Iterable[str]) -> int:
+    """Minimum indent for all lines in line list
+
+    Parameters
+    ----------
+    lines : Iterable[str]
+        The lines to find the minimum indent of.
+
+    Returns
+    -------
+    indent : int
+        The minimum indent.
+
+
+    Examples
+    --------
+    >>> 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: Mapping[str, str], unindent_params: bool = True) -> Decorator:
+    """Return docstring decorator using docdict variable dictionary.
+
+    Parameters
+    ----------
+    docdict : dict[str, str]
+        A dictionary containing name, docstring fragment pairs.
+    unindent_params : bool, optional
+        If True, strip common indentation from all parameters in docdict.
+        Default is False.
+
+    Returns
+    -------
+    decfunc : function
+        The decorator function that applies dictionary to its
+        argument's __doc__ attribute.
+    """
+    if unindent_params:
+        docdict = unindent_dict(docdict)
+
+    def decorate(func: _F) -> _F:
+        # __doc__ may be None for optimized Python (-OO)
+        doc = func.__doc__ or ""
+        func.__doc__ = docformat(doc, docdict)
+        return func
+
+    return decorate
+
+
+def unindent_dict(docdict: Mapping[str, str]) -> dict[str, str]:
+    """Unindent all strings in a docdict.
+
+    Parameters
+    ----------
+    docdict : dict[str, str]
+        A dictionary with string values to unindent.
+
+    Returns
+    -------
+    docdict : dict[str, str]
+        The `docdict` dictionary but each of its string values are unindented.
+    """
+    can_dict: dict[str, str] = {}
+    for name, dstr in docdict.items():
+        can_dict[name] = unindent_string(dstr)
+    return can_dict
+
+
+def unindent_string(docstring: str) -> str:
+    """Set docstring to minimum indent for all lines, including first.
+
+    Parameters
+    ----------
+    docstring : str
+        The input docstring to unindent.
+
+    Returns
+    -------
+    docstring : str
+        The unindented docstring.
+
+    Examples
+    --------
+    >>> 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: object, oldval: str, newval: str) -> Decorator:
+    """Decorator to take the docstring from obj, with oldval replaced by newval
+
+    Equivalent to ``func.__doc__ = obj.__doc__.replace(oldval, newval)``
+
+    Parameters
+    ----------
+    obj : object
+        A class or object whose docstring will be used as the basis for the
+        replacement operation.
+    oldval : str
+        The string to search for in the docstring.
+    newval : str
+        The string to replace `oldval` with in the docstring.
+
+    Returns
+    -------
+    decfunc : function
+        A decorator function that replaces occurrences of `oldval` with `newval`
+        in the docstring of the decorated function.
+    """
+    # __doc__ may be None for optimized Python (-OO)
+    doc = (obj.__doc__ or "").replace(oldval, newval)
+
+    def inner(func: _F) -> _F:
+        func.__doc__ = doc
+        return func
+
+    return inner
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/messagestream.cpython-311-x86_64-linux-gnu.so b/.venv/lib/python3.11/site-packages/scipy/_lib/messagestream.cpython-311-x86_64-linux-gnu.so
new file mode 100644
index 0000000000000000000000000000000000000000..f12b050d46a4b842687b84ee0731c97555921a0c
Binary files /dev/null and b/.venv/lib/python3.11/site-packages/scipy/_lib/messagestream.cpython-311-x86_64-linux-gnu.so differ
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/__init__.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..577e1d266162d70023cdee4cc0bfe72ce0796712
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/__init__.py
@@ -0,0 +1,212 @@
+# Bounds may appear unused in this file but we need to import it to make it available to the user
+from scipy.optimize import NonlinearConstraint, LinearConstraint, Bounds
+from .common._nonlinear_constraints import process_nl_constraints
+from .common._linear_constraints import (
+    combine_multiple_linear_constraints,
+    separate_LC_into_eq_and_ineq,
+)
+from .common._bounds import process_bounds
+from enum import Enum
+from .common._project import _project
+from .common.linalg import get_arrays_tol
+from .cobyla.cobyla import cobyla
+import numpy as np
+from collections.abc import Iterable
+
+
+class ConstraintType(Enum):
+    LINEAR_OBJECT = 5
+    NONLINEAR_OBJECT = 10
+    LINEAR_DICT = 15
+    NONLINEAR_DICT = 20
+
+
+def get_constraint_type(constraint):
+    if isinstance(constraint, dict) and ("A" in constraint) and ("lb" in constraint) and ("ub" in constraint):
+        return ConstraintType.LINEAR_DICT
+    elif isinstance(constraint, dict) and ("fun" in constraint) and ("lb" in constraint) and ("ub" in constraint):
+        return ConstraintType.NONLINEAR_DICT
+    elif hasattr(constraint, "A") and hasattr(constraint, "lb") and hasattr(constraint, "ub"):
+        return ConstraintType.LINEAR_OBJECT
+    elif hasattr(constraint, "fun") and hasattr(constraint, "lb") and hasattr(constraint, "ub"):
+        return ConstraintType.NONLINEAR_OBJECT
+    else:
+        raise ValueError(f"Constraint type {type(constraint)} not recognized")
+
+
+def process_constraints(constraints):
+    # First throw it back if it's an empty tuple
+    if not constraints:
+        return None, None
+    # Next figure out if it's a list of constraints or a single constraint
+    # If it's a single constraint, make it a list, and then the remaining logic
+    # doesn't have to change
+    if not isinstance(constraints, Iterable):
+        constraints = [constraints]
+
+    # Separate out the linear and nonlinear constraints
+    linear_constraints = []
+    nonlinear_constraints = []
+    for constraint in constraints:
+        constraint_type = get_constraint_type(constraint)
+        if constraint_type is ConstraintType.LINEAR_OBJECT:
+            linear_constraints.append(constraint)
+        elif constraint_type is ConstraintType.NONLINEAR_OBJECT:
+            nonlinear_constraints.append(constraint)
+        elif constraint_type == ConstraintType.LINEAR_DICT:
+            linear_constraints.append(LinearConstraint(constraint["A"], constraint["lb"], constraint["ub"]))
+        elif constraint_type == ConstraintType.NONLINEAR_DICT:
+            nonlinear_constraints.append(NonlinearConstraint(constraint["fun"], constraint["lb"], constraint["ub"]))
+        else:
+            raise ValueError("Constraint type not recognized")
+
+    if len(nonlinear_constraints) > 0:
+        nonlinear_constraint_function = process_nl_constraints(nonlinear_constraints)
+    else:
+        nonlinear_constraint_function = None
+
+    # Determine if we have multiple linear constraints, just 1, or none, and process accordingly
+    if len(linear_constraints) > 1:
+        linear_constraint = combine_multiple_linear_constraints(linear_constraints)
+    elif len(linear_constraints) == 1:
+        linear_constraint = linear_constraints[0]
+    else:
+        linear_constraint = None
+
+    return linear_constraint, nonlinear_constraint_function
+
+
+def minimize(fun, x0, args=(), method=None, bounds=None, constraints=(), callback=None, options=None):
+
+    linear_constraint, nonlinear_constraint_function = process_constraints(constraints)
+
+    options = {'quiet': True} if options is None else options
+    quiet = options.get("quiet", True)
+
+    if method is None:
+        if nonlinear_constraint_function is not None:
+            if not quiet: print("Nonlinear constraints detected, applying COBYLA")
+            method = "cobyla"
+        elif linear_constraint is not None:
+            if not quiet: print("Linear constraints detected without nonlinear constraints, applying LINCOA")
+            method = "lincoa"
+        elif bounds is not None:
+            if not quiet: print("Bounds without linear or nonlinear constraints detected, applying BOBYQA")
+            method = "bobyqa"
+        else:
+            if not quiet: print("No bounds or constraints detected, applying NEWUOA")
+            method = "newuoa"
+    else:
+        # Raise some errors if methods were called with inappropriate options
+        method = method.lower()
+        if method not in ('newuoa', 'uobyqa', 'bobyqa', 'cobyla', 'lincoa'):
+            raise ValueError(f"Method must be one of NEWUOA, UOBYQA, BOBYQA, COBYLA, or LINCOA, not '{method}'")
+        if method != "cobyla" and nonlinear_constraint_function is not None:
+            raise ValueError("Nonlinear constraints were provided for an algorithm that cannot handle them")
+        if method not in ("cobyla", "lincoa") and linear_constraint is not None:
+            raise ValueError("Linear constraints were provided for an algorithm that cannot handle them")
+        if method not in ("cobyla", "bobyqa", "lincoa") and bounds is not None:
+            raise ValueError("Bounds were provided for an algorithm that cannot handle them")
+
+    # Try to get the length of x0. If we can't that likely means it's a scalar, and
+    # in that case we turn it into an array and wrap the original function so that it
+    # can accept an array and return a scalar.
+    try:
+        lenx0 = len(x0)
+    except TypeError:
+        x0 = np.array([x0])
+        original_scalar_fun = fun
+        def scalar_fun(x):
+            return original_scalar_fun(x[0], *args)
+        fun = scalar_fun
+        lenx0 = 1
+
+    lb, ub = process_bounds(bounds, lenx0)
+
+    # Check which variables are fixed and eliminate them from the problem.
+    # Save the indices and values so that we can call the original function with
+    # an array of the appropriate size, and so that we can add the fixed values to the
+    # result when COBYLA returns.
+    tol = get_arrays_tol(lb, ub)
+    _fixed_idx = (
+        (lb <= ub)
+        & (np.abs(lb - ub) < tol)
+    )
+    if any(_fixed_idx):
+        _fixed_values = 0.5 * (
+            lb[_fixed_idx] + ub[_fixed_idx]
+        )
+        _fixed_values = np.clip(
+            _fixed_values,
+            lb[_fixed_idx],
+            ub[_fixed_idx],
+        )
+        x0 = x0[~_fixed_idx]
+        lb = lb[~_fixed_idx]
+        ub = ub[~_fixed_idx]
+        original_fun = fun
+        def fixed_fun(x):
+            newx = np.zeros(lenx0)
+            newx[_fixed_idx] = _fixed_values
+            newx[~_fixed_idx] = x
+            return original_fun(newx, *args)
+        fun = fixed_fun
+
+
+    # Project x0 onto the feasible set
+    if nonlinear_constraint_function is None:
+        result = _project(x0, lb, ub, {"linear": linear_constraint, "nonlinear": None})
+        x0 = result.x
+    
+    if linear_constraint is not None:
+        A_eq, b_eq, A_ineq, b_ineq = separate_LC_into_eq_and_ineq(linear_constraint)
+    else:
+        A_eq, b_eq, A_ineq, b_ineq = None, None, None, None
+
+    if nonlinear_constraint_function is not None:
+        # If there is a nonlinear constraint function, we will call COBYLA, which needs the number of nonlinear
+        # constraints (m_nlcon). In order to get this number we need to evaluate the constraint function at x0.
+        # The constraint value at x0 (nlconstr0) is not discarded but passed down to the Fortran backend, as its
+        # evaluation is assumed to be expensive. We also evaluate the objective function at x0 and pass the result
+        # (f0) down to the Fortran backend, which expects nlconstr0 and f0 to be provided in sync.
+        def calcfc(x):
+            f = fun(x, *args)
+            nlconstr = nonlinear_constraint_function(x)
+            return f, nlconstr
+    else:
+        def calcfc(x):
+            f = fun(x, *args)
+            constr = np.zeros(0)
+            return f, constr
+
+    f0, nlconstr0 = calcfc(x0)
+
+    if 'quiet' in options:
+        del options['quiet']
+
+    if 'maxfev' in options:
+        options['maxfun'] = options['maxfev']
+        del options['maxfev']
+
+    result = cobyla(
+        calcfc,
+        len(nlconstr0),
+        x0,
+        A_ineq,
+        b_ineq,
+        A_eq,
+        b_eq,
+        lb,
+        ub,
+        f0=f0,
+        nlconstr0=nlconstr0,
+        callback=callback,
+        **options
+    )
+
+    if any(_fixed_idx):
+        newx = np.zeros(lenx0)
+        newx[_fixed_idx] = _fixed_values
+        newx[~_fixed_idx] = result.x
+        result.x = newx
+    return result
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/cobyla/__init__.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/cobyla/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/cobyla/cobyla.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/cobyla/cobyla.py
new file mode 100644
index 0000000000000000000000000000000000000000..11772a249e71ea23663b4d78479cd3792e1e7c43
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/cobyla/cobyla.py
@@ -0,0 +1,559 @@
+'''
+This module provides Powell's COBYLA algorithm.
+
+Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
+
+Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
+
+Python translation by Nickolai Belakovski.
+
+N.B.:
+
+1. The modern-Fortran reference implementation in PRIMA contains bug fixes and improvements over the
+original Fortran 77 implementation by Powell. Consequently, the PRIMA implementation behaves differently
+from the original Fortran 77 implementation by Powell. Therefore, it is important to point out that
+you are using PRIMA rather than the original solvers if you want your results to be reproducible.
+
+2. Compared to Powell's Fortran 77 implementation, the modern-Fortran implementation and hence any
+faithful translation like this one generally produce better solutions with fewer function evaluations,
+making them preferable for applications with expensive function evaluations. However, if function
+evaluations are not the dominant cost in your application, the Fortran 77 solvers are likely to be
+faster, as they are more efficient in terms of memory usage and flops thanks to the careful and
+ingenious (but unmaintained and unmaintainable) implementation by Powell.
+
+See the PRIMA documentation (www.libprima.net) for more information.
+'''
+
+from ..common.evaluate import evaluate, moderatex, moderatef, moderatec
+from ..common.consts import (EPS, RHOBEG_DEFAULT, RHOEND_DEFAULT, CTOL_DEFAULT,
+                                   CWEIGHT_DEFAULT, FTARGET_DEFAULT, IPRINT_DEFAULT,
+                                   MAXFUN_DIM_DEFAULT, DEBUGGING, BOUNDMAX,
+                                   ETA1_DEFAULT, ETA2_DEFAULT, GAMMA1_DEFAULT,
+                                   GAMMA2_DEFAULT)
+from ..common.preproc import preproc
+from ..common.present import present
+from ..common.linalg import matprod
+from .cobylb import cobylb
+import numpy as np
+from dataclasses import dataclass
+from copy import copy
+
+
+@dataclass
+class COBYLAResult:
+    x: np.ndarray
+    f: float
+    constr: np.ndarray
+    cstrv: float
+    nf: int
+    xhist: np.ndarray | None
+    fhist: np.ndarray | None
+    chist: np.ndarray | None
+    conhist: np.ndarray | None
+    info: int
+
+
+def cobyla(calcfc, m_nlcon, x, Aineq=None, bineq=None, Aeq=None, beq=None,
+           xl=None, xu=None, f0=None, nlconstr0=None, rhobeg=None, rhoend=None,
+           ftarget=FTARGET_DEFAULT, ctol=CTOL_DEFAULT, cweight=CWEIGHT_DEFAULT,
+           maxfun=None, iprint=IPRINT_DEFAULT, eta1=None, eta2=None,
+           gamma1=GAMMA1_DEFAULT, gamma2=GAMMA2_DEFAULT, maxhist=None, maxfilt=2000,
+           callback=None):
+    """
+    Among all the arguments, only CALCFC, M_NLCON, and X are obligatory. The others are
+    OPTIONAL and you can neglect them unless you are familiar with the algorithm. Any
+    unspecified optional input will take the default value detailed below. For
+    instance, we may invoke the solver as follows.
+
+    # First define CALCFC, M_NLCON, and X, and then do the following.
+    result = cobyla(calcfc, m_nlcon, x)
+
+    or
+
+    # First define CALCFC, M_NLCON, X, Aineq, and Bineq, and then do the following.
+    result = cobyla(calcfc, m_nlcon, x, Aineq=Aineq, bineq=bineq, rhobeg=1.0e0,
+        rhoend=1.0e-6)
+
+    ####################################################################################
+    # IMPORTANT NOTICE: The user must set M_NLCON correctly to the number of nonlinear
+    # constraints, namely the size of NLCONSTR introduced below. Set it to 0 if there
+    # is no nonlinear constraint.
+    ####################################################################################
+
+    See examples/cobyla/cobyla_example.py for a concrete example.
+
+    A detailed introduction to the arguments is as follows.
+
+    ####################################################################################
+    # INPUTS
+    ####################################################################################
+
+    CALCFC
+      Input, function.
+      f, nlconstr = CALCFC(X) should evaluate the objective function and nonlinear
+      constraints at the given vector X; it should return a tuple consisting of the
+      objective function value and the nonlinear constraint value. It must be provided
+      by the user, and its definition must conform to the following interface:
+      #-------------------------------------------------------------------------#
+       def calcfc(x):
+           f = 0.0
+           nlconstr = np.zeros(m_nlcon)
+           return f, nlconstr
+      #-------------------------------------------------------------------------#
+
+    M_NLCON
+      Input, scalar.
+      M_NLCON must be set to the number of nonlinear constraints, namely the size of
+      NLCONSTR(X).
+      N.B.:
+      1. Why don't we define M_NLCON as optional and default it to 0 when it is absent?
+      This is because we need to allocate memory for CONSTR_LOC using M_NLCON. To
+      ensure that the size of CONSTR_LOC is correct, we require the user to specify
+      M_NLCON explicitly.
+
+    X
+      Input, vector.
+      As an input, X should be an N-dimensional vector that contains the starting
+      point, N being the dimension of the problem.
+
+    Aineq, Bineq
+      Input, matrix of size [Mineq, N] and vector of size Mineq unless they are both
+      empty, default: None and None.
+      Aineq and Bineq represent the linear inequality constraints: Aineq*X <= Bineq.
+
+    Aeq, Beq
+      Input, matrix of size [Meq, N] and vector of size Meq unless they are both
+      empty, default: None and None.
+      Aeq and Beq represent the linear equality constraints: Aeq*X = Beq.
+
+    XL, XU
+      Input, vectors of size N unless they are both None, default: None and None.
+      XL is the lower bound for X. If XL is None, X has no
+      lower bound. Any entry of XL that is NaN or below -BOUNDMAX will be taken as
+      -BOUNDMAX, which effectively means there is no lower bound for the corresponding
+      entry of X. The value of BOUNDMAX is 0.25*HUGE(X), which is about 8.6E37 for
+      single precision and 4.5E307 for double precision. XU is similar.
+
+    F0
+      Input, scalar.
+      F0, if present, should be set to the objective function value of the starting X.
+
+    NLCONSTR0
+      Input, vector.
+      NLCONSTR0, if present, should be set to the nonlinear constraint value at the
+      starting X; in addition, SIZE(NLCONSTR0) must be M_NLCON, or the solver will
+      abort.
+
+    RHOBEG, RHOEND
+      Inputs, scalars, default: RHOBEG = 1, RHOEND = 10^-6. RHOBEG and RHOEND must be
+      set to the initial and final values of a trust-region radius, both being positive
+      and RHOEND <= RHOBEG. Typically RHOBEG should be about one tenth of the greatest
+      expected change to a variable, and RHOEND should indicate the accuracy that is
+      required in the final values of the variables.
+
+    FTARGET
+      Input, scalar, default: -Inf.
+      FTARGET is the target function value. The algorithm will terminate when a
+      feasible point with a function value <= FTARGET is found.
+
+    CTOL
+      Input, scalar, default: sqrt(machine epsilon).
+      CTOL is the tolerance of constraint violation. X is considered feasible if
+      CSTRV(X) <= CTOL.
+      N.B.:
+        1. CTOL is absolute, not relative.
+        2. CTOL is used only when selecting the returned X. It does not affect the
+           iterations of the algorithm.
+
+    CWEIGHT
+      Input, scalar, default: CWEIGHT_DFT defined in common/consts.py.
+      CWEIGHT is the weight that the constraint violation takes in the selection of the
+      returned X.
+
+    MAXFUN
+      Input, integer scalar, default: MAXFUN_DIM_DFT*N with MAXFUN_DIM_DFT defined in
+      common/consts.py. MAXFUN is the maximal number of calls of CALCFC.
+
+    IPRINT
+      Input, integer scalar, default: 0.
+      The value of IPRINT should be set to 0, 1, -1, 2, -2, 3, or -3, which controls
+      how much information will be printed during the computation:
+      0: there will be no printing;
+      1: a message will be printed to the screen at the return, showing the best vector
+         of variables found and its objective function value;
+      2: in addition to 1, each new value of RHO is printed to the screen, with the
+         best vector of variables so far and its objective function value; each new
+         value of CPEN is also printed;
+      3: in addition to 2, each function evaluation with its variables will be printed
+         to the screen; -1, -2, -3: the same information as 1, 2, 3 will be printed,
+         not to the screen but to a file named COBYLA_output.txt; the file will be
+         created if it does not exist; the new output will be appended to the end of
+         this file if it already exists.
+      Note that IPRINT = +/-3 can be costly in terms of time and/or space.
+
+    ETA1, ETA2, GAMMA1, GAMMA2
+      Input, scalars, default: ETA1 = 0.1, ETA2 = 0.7, GAMMA1 = 0.5, and GAMMA2 = 2.
+      ETA1, ETA2, GAMMA1, and GAMMA2 are parameters in the updating scheme of the
+      trust-region radius detailed in the subroutine TRRAD in trustregion.py. Roughly
+      speaking, the trust-region radius is contracted by a factor of GAMMA1 when the
+      reduction ratio is below ETA1, and enlarged by a factor of GAMMA2 when the
+      reduction ratio is above ETA2. It is required that 0 < ETA1 <= ETA2 < 1 and
+      0 < GAMMA1 < 1 < GAMMA2. Normally, ETA1 <= 0.25. It is NOT advised to set
+      ETA1 >= 0.5.
+
+    MAXFILT
+      Input, scalar.
+      MAXFILT is a nonnegative integer indicating the maximal length of the filter used
+      for selecting the returned solution; default: MAXFILT_DFT (a value lower than
+      MIN_MAXFILT is not recommended);
+      see common/consts.py for the definitions of MAXFILT_DFT and MIN_MAXFILT.
+
+    CALLBACK
+      Input, function to report progress and optionally request termination.
+
+
+    ####################################################################################
+    # OUTPUTS
+    ####################################################################################
+
+    The output is a single data structure, COBYLAResult, with the following fields:
+
+    X
+      Output, vector.
+      As an output, X will be set to an approximate minimizer.
+
+    F
+      Output, scalar.
+      F will be set to the objective function value of X at exit.
+
+    CONSTR
+      Output, vector.
+      CONSTR will be set to the constraint value of X at exit.
+
+    CSTRV
+      Output, scalar.
+      CSTRV will be set to the constraint violation of X at exit, i.e.,
+      max([0, XL - X, X - XU, Aineq*X - Bineq, ABS(Aeq*X -Beq), NLCONSTR(X)]).
+
+    NF
+      Output, scalar.
+      NF will be set to the number of calls of CALCFC at exit.
+
+    XHIST, FHIST, CHIST, CONHIST, MAXHIST
+      XHIST: Output, rank 2 array;
+      FHIST: Output, rank 1 array;
+      CHIST: Output, rank 1 array;
+      CONHIST: Output, rank 2 array;
+      MAXHIST: Input, scalar, default: MAXFUN
+      XHIST, if present, will output the history of iterates; FHIST, if present, will
+      output the history function values; CHIST, if present, will output the history of
+      constraint violations; CONHIST, if present, will output the history of constraint
+      values; MAXHIST should be a nonnegative integer, and XHIST/FHIST/CHIST/CONHIST
+      will output only the history of the last MAXHIST iterations.
+      Therefore, MAXHIST= 0 means XHIST/FHIST/CONHIST/CHIST will output
+      nothing, while setting MAXHIST = MAXFUN requests XHIST/FHIST/CHIST/CONHIST to
+      output all the history. If XHIST is present, its size at exit will be
+      (N, min(NF, MAXHIST)); if FHIST is present, its size at exit will be
+      min(NF, MAXHIST); if CHIST is present, its size at exit will be min(NF, MAXHIST);
+      if CONHIST is present, its size at exit will be (M, min(NF, MAXHIST)).
+
+      IMPORTANT NOTICE:
+      Setting MAXHIST to a large value can be costly in terms of memory for large
+      problems.
+      MAXHIST will be reset to a smaller value if the memory needed exceeds MAXHISTMEM
+      defined in common/consts.py
+      Use *HIST with caution!!! (N.B.: the algorithm is NOT designed for large
+      problems).
+
+    INFO
+      Output, scalar.
+      INFO is the exit flag. It will be set to one of the following values defined in
+      common/infos.py:
+      SMALL_TR_RADIUS: the lower bound for the trust region radius is reached;
+      FTARGET_ACHIEVED: the target function value is reached;
+      MAXFUN_REACHED: the objective function has been evaluated MAXFUN times;
+      MAXTR_REACHED: the trust region iteration has been performed MAXTR times (MAXTR = 2*MAXFUN);
+      NAN_INF_X: NaN or Inf occurs in X;
+      DAMAGING_ROUNDING: rounding errors are becoming damaging.
+      #--------------------------------------------------------------------------#
+      The following case(s) should NEVER occur unless there is a bug.
+      NAN_INF_F: the objective function returns NaN or +Inf;
+      NAN_INF_MODEL: NaN or Inf occurs in the model;
+      TRSUBP_FAILED: a trust region step failed to reduce the model
+      #--------------------------------------------------------------------------#
+    """
+
+    # Local variables
+    solver = "COBYLA"
+    srname = "COBYLA"
+
+    # Sizes
+    mineq = len(bineq) if present(bineq) else 0
+    meq = len(beq) if present(beq) else 0
+    mxl = sum(xl > -BOUNDMAX) if present(xl) else 0
+    mxu = sum(xu < BOUNDMAX) if present(xu) else 0
+    mmm = mxu + mxl + 2*meq + mineq + m_nlcon
+    num_vars = len(x)
+
+    # Preconditions
+    if DEBUGGING:
+        assert m_nlcon >= 0, f'{srname} M_NLCON >= 0'
+        assert num_vars >= 1, f'{srname} N >= 1'
+
+        assert present(Aineq) == present(bineq), \
+            f'{srname} Aineq and Bineq are both present or both absent'
+        if (present(Aineq)):
+            assert Aineq.shape == (mineq, num_vars), f'{srname} SIZE(Aineq) == [Mineq, N]'
+
+        assert present(Aeq) == present(beq), \
+            f'{srname} Aeq and Beq are both present or both absent'
+        if (present(Aeq)):
+            assert Aeq.shape == (meq, num_vars), f'{srname} SIZE(Aeq) == [Meq, N]'
+
+        if (present(xl)):
+            assert len(xl) == num_vars, f'{srname} SIZE(XL) == N'
+        if (present(xu)):
+            assert len(xu) == num_vars, f'{srname} SIZE(XU) == N'
+
+
+        # N.B.: If NLCONSTR0 is present, then F0 must be present, and we assume that
+        # F(X0) = F0 even if F0 is NaN; if NLCONSTR0 is absent, then F0 must be either
+        # absent or NaN, both of which will be interpreted as F(X0) is not provided.
+        if present(nlconstr0):
+            assert present(f0), f'{srname} If NLCONSTR0 is present, then F0 is present'
+        if present(f0):
+            assert np.isnan(f0) or present(nlconstr0), \
+                f'{srname} If F0 is present and not NaN, then NLCONSTR0 is present'
+
+
+
+    # Exit if the size of NLCONSTR0 is inconsistent with M_NLCON.
+    if present(nlconstr0):
+        assert np.size(nlconstr0) == m_nlcon
+
+    # Read the inputs.
+
+    if xl is not None:
+      xl = copy(xl)
+      xl[np.isnan(xl)] = -BOUNDMAX
+      xl[xl < -BOUNDMAX] = -BOUNDMAX
+
+    if xu is not None:
+      xu = copy(xu)
+      xu[np.isnan(xu)] = BOUNDMAX
+      xu[xu > BOUNDMAX] = BOUNDMAX
+
+    # Wrap the linear and bound constraints into a single constraint: AMAT@X <= BVEC.
+    amat, bvec = get_lincon(Aeq, Aineq, beq, bineq, xl, xu)
+
+    # Create constraint vector
+    constr = np.zeros(mmm)
+
+    # Set [F_LOC, CONSTR_LOC] to [F(X0), CONSTR(X0)] after evaluating the latter if
+    # needed. In this way, COBYLB only needs one interface.
+    # N.B.: Due to the preconditions above, there are two possibilities for F0 and
+    # NLCONSTR0.
+    # If NLCONSTR0 is present, then F0 must be present, and we assume that F(X0) = F0
+    # even if F0 is NaN.
+    # If NLCONSTR0 is absent, then F0 must be either absent or NaN, both of which will
+    # be interpreted as F(X0) is not provided and we have to evaluate F(X0) and
+    # NLCONSTR(X0) now.
+    if (present(f0) and present(nlconstr0) and all(np.isfinite(x))):
+        f = moderatef(f0)
+        if amat is not None:
+          constr[:mmm - m_nlcon] = moderatec(matprod(amat, x) - bvec)
+        constr[mmm - m_nlcon:] = moderatec(nlconstr0)
+    else:
+        x = moderatex(x)
+        f, constr = evaluate(calcfc, x, m_nlcon, amat, bvec)
+        constr[:mmm - m_nlcon] = moderatec(constr[:mmm - m_nlcon])
+        # N.B.: Do NOT call FMSG, SAVEHIST, or SAVEFILT for the function/constraint evaluation at X0.
+        # They will be called during the initialization, which will read the function/constraint at X0.
+    cstrv = max(np.append(0, constr))
+
+
+    # If RHOBEG is present, use it; otherwise, RHOBEG takes the default value for
+    # RHOBEG, taking the value of RHOEND into account. Note that RHOEND is considered
+    # only if it is present and it is VALID (i.e., finite and positive). The other
+    # inputs are read similarly.
+    if present(rhobeg):
+        rhobeg = rhobeg
+    elif present(rhoend) and np.isfinite(rhoend) and rhoend > 0:
+        rhobeg = max(10 * rhoend, RHOBEG_DEFAULT)
+    else:
+        rhobeg = RHOBEG_DEFAULT
+
+    if present(rhoend):
+        rhoend = rhoend
+    elif rhobeg > 0:
+        rhoend = max(EPS, min(RHOEND_DEFAULT/RHOBEG_DEFAULT * rhobeg, RHOEND_DEFAULT))
+    else:
+        rhoend = RHOEND_DEFAULT
+
+    maxfun = maxfun if present(maxfun) else MAXFUN_DIM_DEFAULT * num_vars
+
+    if present(eta1):
+        eta1 = eta1
+    elif present(eta2) and 0 < eta2 < 1:
+        eta1 = max(EPS, eta2 / 7)
+    else:
+        eta1 = ETA1_DEFAULT
+
+    if present(eta2):
+        eta2 = eta2
+    elif 0 < eta1 < 1:
+        eta2 = (eta1 + 2) / 3
+    else:
+        eta2 = ETA2_DEFAULT
+
+    maxhist = (
+        maxhist
+        if present(maxhist)
+        else max(maxfun, num_vars + 2, MAXFUN_DIM_DEFAULT * num_vars)
+    )
+
+    # Preprocess the inputs in case some of them are invalid. It does nothing if all
+    # inputs are valid.
+    (
+        iprint,
+        maxfun,
+        maxhist,
+        ftarget,
+        rhobeg,
+        rhoend,
+        npt,  # Unused in COBYLA
+        maxfilt,
+        ctol,
+        cweight,
+        eta1,
+        eta2,
+        gamma1,
+        gamma2,
+        _x0,  # Unused in COBYLA
+    ) = preproc(
+        solver,
+        num_vars,
+        iprint,
+        maxfun,
+        maxhist,
+        ftarget,
+        rhobeg,
+        rhoend,
+        num_constraints=mmm,
+        maxfilt=maxfilt,
+        ctol=ctol,
+        cweight=cweight,
+        eta1=eta1,
+        eta2=eta2,
+        gamma1=gamma1,
+        gamma2=gamma2,
+        is_constrained=(mmm > 0),
+    )
+
+    # Further revise MAXHIST according to MAXHISTMEM, and allocate memory for the history.
+    # In MATLAB/Python/Julia/R implementation, we should simply set MAXHIST = MAXFUN and initialize
+    # CHIST = NaN(1, MAXFUN), CONHIST = NaN(M, MAXFUN), FHIST = NaN(1, MAXFUN), XHIST = NaN(N, MAXFUN)
+    # if they are requested; replace MAXFUN with 0 for the history that is not requested.
+    # prehist(maxhist, num_vars, present(xhist), xhist_loc, present(fhist), fhist_loc, &
+    #     & present(chist), chist_loc, m, present(conhist), conhist_loc)
+
+    # call cobylb, which performs the real calculations
+    x, f, constr, cstrv, nf, xhist, fhist, chist, conhist, info = cobylb(
+        calcfc,
+        iprint,
+        maxfilt,
+        maxfun,
+        amat,
+        bvec,
+        ctol,
+        cweight,
+        eta1,
+        eta2,
+        ftarget,
+        gamma1,
+        gamma2,
+        rhobeg,
+        rhoend,
+        constr,
+        f,
+        x,
+        maxhist,
+        callback
+    )
+
+    return COBYLAResult(x, f, constr, cstrv, nf, xhist, fhist, chist, conhist, info)
+
+
+def get_lincon(Aeq=None, Aineq=None, beq=None, bineq=None, xl=None, xu=None):
+    """
+    This subroutine wraps the linear and bound constraints into a single constraint:
+        AMAT*X <= BVEC.
+
+    N.B.:
+
+    LINCOA normalizes the linear constraints so that each constraint has a gradient
+    of norm 1. However, COBYLA does not do this.
+    """
+
+    # Sizes
+    if Aeq is not None:
+        num_vars = Aeq.shape[1]
+    elif Aineq is not None:
+        num_vars = Aineq.shape[1]
+    elif xl is not None:
+        num_vars = len(xl)
+    elif xu is not None:
+        num_vars = len(xu)
+    else:
+        return None, None
+
+    # Preconditions
+    if DEBUGGING:
+        assert Aineq is None or Aineq.shape == (len(bineq), num_vars)
+        assert Aeq is None or Aeq.shape == (len(beq), num_vars)
+        assert (xl is None or xu is None) or len(xl) == len(xu) == num_vars
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    # Define the indices of the nontrivial bound constraints.
+    ixl = np.where(xl > -BOUNDMAX)[0] if xl is not None else None
+    ixu = np.where(xu < BOUNDMAX)[0] if xu is not None else None
+
+    # Wrap the linear constraints.
+    # The bound constraint XL <= X <= XU is handled as two constraints:
+    # -X <= -XL, X <= XU.
+    # The equality constraint Aeq*X = Beq is handled as two constraints:
+    # -Aeq*X <= -Beq, Aeq*X <= Beq.
+    # N.B.:
+    # 1. The treatment of the equality constraints is naive. One may choose to
+    #    eliminate them instead.
+    idmat = np.eye(num_vars)
+    amat = np.vstack([
+        -idmat[ixl, :] if ixl is not None else np.empty((0, num_vars)),
+        idmat[ixu, :] if ixu is not None else np.empty((0, num_vars)),
+        -Aeq if Aeq is not None else np.empty((0, num_vars)),
+        Aeq if Aeq is not None else np.empty((0, num_vars)),
+        Aineq if Aineq is not None else np.empty((0, num_vars))
+    ])
+    bvec = np.hstack([
+        -xl[ixl] if ixl is not None else np.empty(0),
+        xu[ixu] if ixu is not None else np.empty(0),
+        -beq if beq is not None else np.empty(0),
+        beq if beq is not None else np.empty(0),
+        bineq if bineq is not None else np.empty(0)
+    ])
+
+    amat = amat if amat.shape[0] > 0 else None
+    bvec = bvec if bvec.shape[0] > 0 else None
+
+    #==================#
+    # Calculation ends #
+    #==================#
+
+    # Postconditions
+    if DEBUGGING:
+        assert (amat is None and bvec is None) or amat.shape == (len(bvec), num_vars)
+
+    return amat, bvec
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/cobyla/cobylb.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/cobyla/cobylb.py
new file mode 100644
index 0000000000000000000000000000000000000000..91012c3b3b2bca1c8827db3c4e2b7245735d23c2
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/cobyla/cobylb.py
@@ -0,0 +1,714 @@
+'''
+This module performs the major calculations of COBYLA.
+
+Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
+
+Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
+
+Python translation by Nickolai Belakovski.
+'''
+
+import numpy as np
+from ..common.checkbreak import checkbreak_con
+from ..common.consts import REALMAX, EPS, DEBUGGING, MIN_MAXFILT
+from ..common.infos import INFO_DEFAULT, MAXTR_REACHED, DAMAGING_ROUNDING, \
+                    SMALL_TR_RADIUS, CALLBACK_TERMINATE
+from ..common.evaluate import evaluate
+from ..common.history import savehist
+from ..common.linalg import isinv, matprod, inprod, norm, primasum, primapow2
+from ..common.message import fmsg, retmsg, rhomsg
+from ..common.ratio import redrat
+from ..common.redrho import redrho
+from ..common.selectx import savefilt, selectx
+from .update import updatepole, findpole, updatexfc
+from .geometry import setdrop_tr, geostep
+from .trustregion import trstlp, trrad
+from .initialize import initxfc, initfilt
+
+
+def cobylb(calcfc, iprint, maxfilt, maxfun, amat, bvec, ctol, cweight, eta1, eta2,
+           ftarget, gamma1, gamma2, rhobeg, rhoend, constr, f, x, maxhist, callback):
+    '''
+    This subroutine performs the actual computations of COBYLA.
+    '''
+
+    # Outputs
+    xhist = []
+    fhist = []
+    chist = []
+    conhist = []
+
+    # Local variables
+    solver = 'COBYLA'
+    A = np.zeros((np.size(x), np.size(constr))) # A contains the approximate gradient for the constraints
+    distsq = np.zeros(np.size(x) + 1)
+    # CPENMIN is the minimum of the penalty parameter CPEN for the L-infinity
+    # constraint violation in the merit function. Note that CPENMIN = 0 in Powell's
+    # implementation, which allows CPEN to be 0. Here, we take CPENMIN > 0 so that CPEN
+    # is always positive. This avoids the situation where PREREM becomes 0 when
+    # PREREF = 0 = CPEN. It brings two advantages as follows.
+    # 1. If the trust-region subproblem solver works correctly and the trust-region
+    #    center is not optimal for the subproblem, then PREREM > 0 is guaranteed. This
+    #    is because, in theory, PREREC >= 0 and MAX(PREREC, PREREF) > 0, and the
+    #    definition of CPEN in GETCPEN ensures that PREREM > 0.
+    # 2. There is no need to revise ACTREM and PREREM when CPEN = 0 and F = FVAL(N+1)
+    #    as in lines 312--314 of Powell's cobylb.f code. Powell's code revises ACTREM
+    #    to CVAL(N + 1) - CSTRV and PREREM to PREREC in this case, which is crucial for
+    #    feasibility problems.
+    cpenmin = EPS
+
+    # Sizes
+    m_lcon = np.size(bvec) if bvec is not None else 0
+    num_constraints = np.size(constr)
+    m_nlcon = num_constraints - m_lcon
+    num_vars = np.size(x)
+
+    # Preconditions
+    if DEBUGGING:
+        assert abs(iprint) <= 3
+        assert num_constraints >= m_lcon and m_lcon >= 0
+        assert num_vars >= 1
+        assert maxfun >= num_vars + 2
+        assert rhobeg >= rhoend and rhoend > 0
+        assert all(np.isfinite(x))
+        assert 0 <= eta1 <= eta2 < 1
+        assert 0 < gamma1 < 1 < gamma2
+        assert 0 <= ctol
+        assert 0 <= cweight
+        assert 0 <= maxhist <= maxfun
+        assert amat is None or np.shape(amat) == (m_lcon, num_vars)
+        assert min(MIN_MAXFILT, maxfun) <= maxfilt <= maxfun
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    # Initialize SIM, FVAL, CONMAT, and CVAL, together with the history.
+    # After the initialization, SIM[:, NUM_VARS] holds the vertex of the initial
+    # simplex with the smallest function value (regardless of the constraint
+    # violation), and SIM[:, :NUM_VARS] holds the displacements from the other vertices
+    # to SIM[:, NUM_VARS]. FVAL, CONMAT, and CVAL hold the function values, constraint
+    # values, and constraint violations on the vertices in the order corresponding to
+    # SIM.
+    evaluated, conmat, cval, sim, simi, fval, nf, subinfo = initxfc(calcfc, iprint,
+      maxfun, constr, amat, bvec, ctol, f, ftarget, rhobeg, x,
+      xhist, fhist, chist, conhist, maxhist)
+
+    # Initialize the filter, including xfilt, ffilt, confilt, cfilt, and nfilt.
+    # N.B.: The filter is used only when selecting which iterate to return. It does not
+    # interfere with the iterations. COBYLA is NOT a filter method but a trust-region
+    # method based on an L-infinity merit function. Powell's implementation does not
+    # use a filter to select the iterate, possibly returning a suboptimal iterate.
+    cfilt = np.zeros(np.minimum(np.maximum(maxfilt, 1), maxfun))
+    confilt = np.zeros((np.size(constr), np.size(cfilt)))
+    ffilt = np.zeros(np.size(cfilt))
+    xfilt = np.zeros((np.size(x), np.size(cfilt)))
+    nfilt = initfilt(conmat, ctol, cweight, cval, fval, sim, evaluated, cfilt, confilt,
+                     ffilt, xfilt)
+
+    # Check whether to return due to abnormal cases that may occur during the initialization.
+    if subinfo != INFO_DEFAULT:
+        info = subinfo
+        # Return the best calculated values of the variables
+        # N.B: Selectx and findpole choose X by different standards, one cannot replace the other
+        kopt = selectx(ffilt[:nfilt], cfilt[:nfilt], cweight, ctol)
+        x = xfilt[:, kopt]
+        f = ffilt[kopt]
+        constr = confilt[:, kopt]
+        cstrv = cfilt[kopt]
+        # print a return message according to IPRINT.
+        retmsg(solver, info, iprint, nf, f, x, cstrv, constr)
+        # Postconditions
+        if DEBUGGING:
+            assert nf <= maxfun
+            assert np.size(x) == num_vars and not any(np.isnan(x))
+            assert not (np.isnan(f) or np.isposinf(f))
+            # assert np.size(xhist, 0) == n and np.size(xhist, 1) == maxxhist
+            # assert not any(np.isnan(xhist(:, 1:min(nf, maxxhist))))
+            # The last calculated X can be Inf (finite + finite can be Inf numerically).
+            # assert np.size(fhist) == maxfhist
+            # assert not any(np.isnan(fhist(1:min(nf, maxfhist))) or np.isposinf(fhist(1:min(nf, maxfhist))))
+            # assert np.size(conhist, 0) == m and np.size(conhist, 1) == maxconhist
+            # assert not any(np.isnan(conhist(:, 1:min(nf, maxconhist))) or np.isneginf(conhist(:, 1:min(nf, maxconhist))))
+            # assert np.size(chist) == maxchist
+            # assert not any(chist(1:min(nf, maxchist)) < 0 or np.isnan(chist(1:min(nf, maxchist))) or np.isposinf(chist(1:min(nf, maxchist))))
+            # nhist = minval([nf, maxfhist, maxchist])
+            # assert not any(isbetter(fhist(1:nhist), chist(1:nhist), f, cstrv, ctol))
+        return x, f, constr, cstrv, nf, xhist, fhist, chist, conhist, info
+
+
+    # Set some more initial values.
+    # We must initialize shortd, ratio, and jdrop_tr because these get defined on
+    # branches that are not guaranteed to be executed, but their values are used later.
+    # Our initialization of CPEN differs from Powell's in two ways. First, we use the
+    # ratio defined in (13) of Powell's COBYLA paper to initialize CPEN. Second, we
+    # impose CPEN >= CPENMIN > 0. Powell's code simply initializes CPEN to 0.
+    rho = rhobeg
+    delta = rhobeg
+    cpen = np.maximum(cpenmin, np.minimum(1.0E3, fcratio(conmat, fval)))  # Powell's code: CPEN = ZERO
+    shortd = False
+    ratio = -1
+    jdrop_tr = 0
+
+    # If DELTA <= GAMMA3*RHO after an update, we set DELTA to RHO. GAMMA3 must be less
+    # than GAMMA2. The reason is as follows. Imagine a very successful step with
+    # DNORM = the un-updated DELTA = RHO. The TRRAD will update DELTA to GAMMA2*RHO.
+    # If GAMMA3 >= GAMMA2, then DELTA will be reset to RHO, which is not reasonable as
+    # D is very successful. See paragraph two of Sec 5.2.5 in T. M. Ragonneau's thesis:
+    # "Model-Based Derivative-Free Optimization Methods and Software." According to
+    # test on 20230613, for COBYLA, this Powellful updating scheme of DELTA works
+    # slightly better than setting directly DELTA = max(NEW_DELTA, RHO).
+    gamma3 = np.maximum(1, np.minimum(0.75 * gamma2, 1.5))
+
+    # MAXTR is the maximal number of trust region iterations. Each trust-region
+    # iteration takes 1 or 2 function evaluations unless the trust-region step is short
+    # or the trust-region subproblem solver fails but the geometry step is not invoked.
+    # Thus the following MAXTR is unlikely to be reached.
+    maxtr = 10 * maxfun
+    info = MAXTR_REACHED
+
+    # Begin the iterative procedure
+    # After solving a trust-region subproblem, we use three boolean variables to
+    # control the workflow.
+    # SHORTD - Is the trust-region trial step too short to invoke # a function
+    #          evaluation?
+    # IMPROVE_GEO - Will we improve the model after the trust-region iteration? If yes,
+    #               a geometry step will be taken, corresponding to the "Branch (Delta)"
+    #               in the COBYLA paper.
+    # REDUCE_RHO - Will we reduce rho after the trust-region iteration?
+    # COBYLA never sets IMPROVE_GEO and REDUCE_RHO to True simultaneously.
+    for tr in range(maxtr):
+        # Increase the penalty parameter CPEN, if needed, so that
+        # PREREM = PREREF + CPEN * PREREC > 0.
+        # This is the first (out of two) update of CPEN, where CPEN increases or
+        # remains the same.
+        # N.B.: CPEN and the merit function PHI = FVAL + CPEN*CVAL are used in three
+        # places only.
+        # 1. In FINDPOLE/UPDATEPOLE, deciding the optimal vertex of the current simplex.
+        # 2. After the trust-region trial step, calculating the reduction ratio.
+        # 3. In GEOSTEP, deciding the direction of the geometry step.
+        # They do not appear explicitly in the trust-region subproblem, though the
+        # trust-region center (i.e. the current optimal vertex) is defined by them.
+        cpen = getcpen(amat, bvec, conmat, cpen, cval, delta, fval, rho, sim, simi)
+
+        # Switch the best vertex of the current simplex to SIM[:, NUM_VARS].
+        conmat, cval, fval, sim, simi, subinfo = updatepole(cpen, conmat, cval, fval,
+                                                            sim, simi)
+        # Check whether to exit due to damaging rounding in UPDATEPOLE.
+        if subinfo == DAMAGING_ROUNDING:
+            info = subinfo
+            break  # Better action to take? Geometry step, or simply continue?
+
+        # Does the interpolation set have adequate geometry? It affects improve_geo and
+        # reduce_rho.
+        adequate_geo = all(primasum(primapow2(sim[:, :num_vars]), axis=0) <= 4 * primapow2(delta))
+
+        # Calculate the linear approximations to the objective and constraint functions.
+        # N.B.: TRSTLP accesses A mostly by columns, so it is more reasonable to save A
+        # instead of A^T.
+        # Zaikun 2023108: According to a test on 2023108, calculating G and
+        # A(:, M_LCON+1:M) by solving the linear systems SIM^T*G = FVAL(1:N)-FVAL(N+1)
+        # and SIM^T*A = CONMAT(:, 1:N)-CONMAT(:, N+1) does not seem to improve or worsen
+        # the performance of COBYLA in terms of the number of function evaluations. The
+        # system was solved by SOLVE in LINALG_MOD based on a QR factorization of SIM
+        # (not necessarily a good algorithm). No preconditioning or scaling was used.
+        g = matprod((fval[:num_vars] - fval[num_vars]), simi)
+        A[:, :m_lcon] = amat.T if amat is not None else amat
+        A[:, m_lcon:] = matprod((conmat[m_lcon:, :num_vars] -
+                          np.tile(conmat[m_lcon:, num_vars], (num_vars, 1)).T), simi).T
+
+        # Calculate the trust-region trial step d. Note that d does NOT depend on cpen.
+        d = trstlp(A, -conmat[:, num_vars], delta, g)
+        dnorm = min(delta, norm(d))
+
+        # Is the trust-region trial step short? N.B.: we compare DNORM with RHO, not
+        # DELTA. Powell's code especially defines SHORTD by SHORTD = (DNORM < 0.5 *
+        # RHO). In our tests 1/10 seems to work better than 1/2 or 1/4, especially for
+        # linearly constrained problems. Note that LINCOA has a slightly more
+        # sophisticated way of defining SHORTD, taking into account whether D causes a
+        # change to the active set. Should we try the same here?
+        shortd = (dnorm <= 0.1 * rho)
+
+        # Predict the change to F (PREREF) and to the constraint violation (PREREC) due
+        # to D. We have the following in precise arithmetic. They may fail to hold due
+        # to rounding errors.
+        # 1. B[:NUM_CONSTRAINTS] = -CONMAT[:, NUM_VARS] and hence
+        # np.max(np.append(B[:NUM_CONSTRAINTS] - D@A[:, :NUM_CONSTRAINTS], 0)) is the
+        # L-infinity violation of the linearized constraints corresponding to D. When
+        # D=0, the violation is np.max(np.append(B[:NUM_CONSTRAINTS], 0)) =
+        # CVAL[NUM_VARS]. PREREC is the reduction of this violation achieved by D,
+        # which is nonnegative in theory; PREREC = 0 iff B[:NUM_CONSTRAINTS] <= 0, i.e.
+        # the trust-region center satisfies the linearized constraints.
+        # 2. PREREF may be negative or 0, but it is positive when PREREC = 0 and shortd
+        # is False
+        # 3. Due to 2, in theory, max(PREREC, PREREF) > 0 if shortd is False.
+        preref = -inprod(d, g)  # Can be negative
+        prerec = cval[num_vars] - np.max(np.append(0, conmat[:, num_vars] + matprod(d, A)))
+
+        # Evaluate PREREM, which is the predicted reduction in the merit function.
+        # In theory, PREREM >= 0 and it is 0 iff CPEN = 0 = PREREF. This may not be true
+        # numerically.
+        prerem = preref + cpen * prerec
+        trfail = not (prerem > 1.0E-6 * min(cpen, 1) * rho)
+
+        if shortd or trfail:
+            # Reduce DELTA if D is short or if D fails to render PREREM > 0. The latter
+            # can only happen due to rounding errors. This seems quite important for
+            # performance
+            delta *= 0.1
+            if delta <= gamma3 * rho:
+                delta = rho  # set delta to rho when it is close to or below
+        else:
+            # Calculate the next value of the objective and constraint functions.
+            # If X is close to one of the points in the interpolation set, then we do
+            # not evaluate the objective and constraints at X, assuming them to have
+            # the values at the closest point.
+            # N.B.: If this happens, do NOT include X into the filter, as F and CONSTR
+            # are inaccurate.
+            x = sim[:, num_vars] + d
+            distsq[num_vars] = primasum(primapow2(x - sim[:, num_vars]))
+            distsq[:num_vars] = primasum(primapow2(x.reshape(num_vars, 1) -
+                (sim[:, num_vars].reshape(num_vars, 1) + sim[:, :num_vars])), axis=0)
+            j = np.argmin(distsq)
+            if distsq[j] <= primapow2(1e-4 * rhoend):
+                f = fval[j]
+                constr = conmat[:, j]
+                cstrv = cval[j]
+            else:
+                # Evaluate the objective and constraints at X, taking care of possible
+                # inf/nan values.
+                f, constr = evaluate(calcfc, x, m_nlcon, amat, bvec)
+                cstrv = np.max(np.append(0, constr))
+                nf += 1
+                # Save X, F, CONSTR, CSTRV into the history.
+                savehist(maxhist, x, xhist, f, fhist, cstrv, chist, constr, conhist)
+                # Save X, F, CONSTR, CSTRV into the filter.
+                nfilt, cfilt, ffilt, xfilt, confilt = savefilt(cstrv, ctol, cweight, f,
+                                                               x, nfilt, cfilt, ffilt,
+                                                               xfilt, constr, confilt)
+
+            # Print a message about the function/constraint evaluation according to
+            # iprint
+            fmsg(solver, 'Trust region', iprint, nf, delta, f, x, cstrv, constr)
+
+            # Evaluate ACTREM, which is the actual reduction in the merit function
+            actrem = (fval[num_vars] + cpen * cval[num_vars]) - (f + cpen * cstrv)
+
+            # Calculate the reduction  ratio by redrat, which hands inf/nan carefully
+            ratio = redrat(actrem, prerem, eta1)
+
+            # Update DELTA. After this, DELTA < DNORM may hold.
+            # N.B.:
+            # 1. Powell's code uses RHO as the trust-region radius and updates it as
+            #    follows.
+            #    Reduce RHO to GAMMA1*RHO if ADEQUATE_GEO is TRUE and either SHORTD is
+            #    TRUE or RATIO < ETA1, and then revise RHO to RHOEND if its new value is
+            #    not more than GAMMA3*RHOEND; RHO remains unchanged in all other cases;
+            #    in particular, RHO is never increased.
+            # 2. Our implementation uses DELTA as the trust-region radius, while using
+            #    RHO as a lower bound for DELTA. DELTA is updated in a way that is
+            #    typical for trust-region methods, and it is revised to RHO if its new
+            #    value is not more than GAMMA3*RHO. RHO reflects the current resolution
+            #    of the algorithm; its update is essentially the same as the update of
+            #    RHO in Powell's code (see the definition of REDUCE_RHO below). Our
+            #    implementation aligns with UOBYQA/NEWUOA/BOBYQA/LINCOA and improves the
+            #    performance of COBYLA.
+            # 3. The same as Powell's code, we do not reduce RHO unless ADEQUATE_GEO is
+            #    TRUE. This is also how Powell updated RHO in
+            #    UOBYQA/NEWUOA/BOBYQA/LINCOA. What about we also use ADEQUATE_GEO ==
+            #    TRUE as a prerequisite for reducing DELTA? The argument would be that
+            #    the bad (small) value of RATIO may be because of a bad geometry (and
+            #    hence a bad model) rather than an improperly large DELTA, and it might
+            #    be good to try improving the geometry first without reducing DELTA.
+            #    However, according to a test on 20230206, it does not improve the
+            #    performance if we skip the update of DELTA when ADEQUATE_GEO is FALSE
+            #    and RATIO < 0.1. Therefore, we choose to update DELTA without checking
+            #    ADEQUATE_GEO.
+
+            delta = trrad(delta, dnorm, eta1, eta2, gamma1, gamma2, ratio)
+            if delta <= gamma3*rho:
+                delta = rho  # Set delta to rho when it is close to or below.
+
+            # Is the newly generated X better than the current best point?
+            ximproved = actrem > 0  # If ACTREM is NaN, then XIMPROVED should and will be False
+
+            # Set JDROP_TR to the index of the vertex to be replaced with X. JDROP_TR = 0 means there
+            # is no good point to replace, and X will not be included into the simplex; in this case,
+            # the geometry of the simplex likely needs improvement, which will be handled below.
+            jdrop_tr = setdrop_tr(ximproved, d, delta, rho, sim, simi)
+
+            # Update SIM, SIMI, FVAL, CONMAT, and CVAL so that SIM[:, JDROP_TR] is replaced with D.
+            # UPDATEXFC does nothing if JDROP_TR is None, as the algorithm decides to discard X.
+            sim, simi, fval, conmat, cval, subinfo = updatexfc(jdrop_tr, constr, cpen, cstrv, d, f, conmat, cval, fval, sim, simi)
+            # Check whether to break due to damaging rounding in UPDATEXFC
+            if subinfo == DAMAGING_ROUNDING:
+                info = subinfo
+                break  # Better action to take? Geometry step, or a RESCUE as in BOBYQA?
+
+            # Check whether to break due to maxfun, ftarget, etc.
+            subinfo = checkbreak_con(maxfun, nf, cstrv, ctol, f, ftarget, x)
+            if subinfo != INFO_DEFAULT:
+                info = subinfo
+                break
+        # End of if SHORTD or TRFAIL. The normal trust-region calculation ends.
+
+        # Before the next trust-region iteration, we possibly improve the geometry of the simplex or
+        # reduce RHO according to IMPROVE_GEO and REDUCE_RHO. Now we decide these indicators.
+        # N.B.: We must ensure that the algorithm does not set IMPROVE_GEO = True at infinitely many
+        # consecutive iterations without moving SIM[:, NUM_VARS] or reducing RHO. Otherwise, the algorithm
+        # will get stuck in repetitive invocations of GEOSTEP. This is ensured by the following facts:
+        # 1. If an iteration sets IMPROVE_GEO to True, it must also reduce DELTA or set DELTA to RHO.
+        # 2. If SIM[:, NUM_VARS] and RHO remain unchanged, then ADEQUATE_GEO will become True after at
+        # most NUM_VARS invocations of GEOSTEP.
+
+        # BAD_TRSTEP: Is the last trust-region step bad?
+        bad_trstep = shortd or trfail or ratio <= 0 or jdrop_tr is None
+        # IMPROVE_GEO: Should we take a geometry step to improve the geometry of the interpolation set?
+        improve_geo = bad_trstep and not adequate_geo
+        # REDUCE_RHO: Should we enhance the resolution by reducing rho?
+        reduce_rho = bad_trstep and adequate_geo and max(delta, dnorm) <= rho
+
+        # COBYLA never sets IMPROVE_GEO and REDUCE_RHO to True simultaneously.
+        # assert not (IMPROVE_GEO and REDUCE_RHO), 'IMPROVE_GEO or REDUCE_RHO are not both TRUE, COBYLA'
+
+        # If SHORTD or TRFAIL is True, then either IMPROVE_GEO or REDUCE_RHO is True unless ADEQUATE_GEO
+        # is True and max(DELTA, DNORM) > RHO.
+        # assert not (shortd or trfail) or (improve_geo or reduce_rho or (adequate_geo and max(delta, dnorm) > rho)), \
+        #     'If SHORTD or TRFAIL is TRUE, then either IMPROVE_GEO or REDUCE_RHO is TRUE unless ADEQUATE_GEO is TRUE and MAX(DELTA, DNORM) > RHO'
+
+        # Comments on BAD_TRSTEP:
+        # 1. Powell's definition of BAD_TRSTEP is as follows. The one used above seems to work better,
+        # especially for linearly constrained problems due to the factor TENTH (= ETA1).
+        # !bad_trstep = (shortd .or. actrem <= 0 .or. actrem < TENTH * prerem .or. jdrop_tr == 0)
+        # Besides, Powell did not check PREREM > 0 in BAD_TRSTEP, which is reasonable to do but has
+        # little impact upon the performance.
+        # 2. NEWUOA/BOBYQA/LINCOA would define BAD_TRSTEP, IMPROVE_GEO, and REDUCE_RHO as follows. Two
+        # different thresholds are used in BAD_TRSTEP. It outperforms Powell's version.
+        # !bad_trstep = (shortd .or. trfail .or. ratio <= eta1 .or. jdrop_tr == 0)
+        # !improve_geo = bad_trstep .and. .not. adequate_geo
+        # !bad_trstep = (shortd .or. trfail .or. ratio <= 0 .or. jdrop_tr == 0)
+        # !reduce_rho = bad_trstep .and. adequate_geo .and. max(delta, dnorm) <= rho
+        # 3. Theoretically, JDROP_TR > 0 when ACTREM > 0 (guaranteed by RATIO > 0). However, in Powell's
+        # implementation, JDROP_TR may be 0 even RATIO > 0 due to NaN. The modernized code has rectified
+        # this in the function SETDROP_TR. After this rectification, we can indeed simplify the
+        # definition of BAD_TRSTEP by removing the condition JDROP_TR == 0. We retain it for robustness.
+
+        # Comments on REDUCE_RHO:
+        # When SHORTD is TRUE, UOBYQA/NEWUOA/BOBYQA/LINCOA all set REDUCE_RHO to TRUE if the recent
+        # models are sufficiently accurate according to certain criteria. See the paragraph around (37)
+        # in the UOBYQA paper and the discussions about Box 14 in the NEWUOA paper. This strategy is
+        # crucial for the performance of the solvers. However, as of 20221111, we have not managed to
+        # make it work in COBYLA. As in NEWUOA, we recorded the errors of the recent models, and set
+        # REDUCE_RHO to true if they are small (e.g., ALL(ABS(MODERR_REC) <= 0.1 * MAXVAL(ABS(A))*RHO) or
+        # ALL(ABS(MODERR_REC) <= RHO**2)) when SHORTD is TRUE. It made little impact on the performance.
+
+
+        # Since COBYLA never sets IMPROVE_GEO and REDUCE_RHO to TRUE simultaneously, the following
+        # two blocks are exchangeable: IF (IMPROVE_GEO) ... END IF and IF (REDUCE_RHO) ... END IF.
+
+        # Improve the geometry of the simplex by removing a point and adding a new one.
+        # If the current interpolation set has acceptable geometry, then we skip the geometry step.
+        # The code has a small difference from Powell's original code here: If the current geometry
+        # is acceptable, then we will continue with a new trust-region iteration; however, at the
+        # beginning of the iteration, CPEN may be updated, which may alter the pole point SIM(:, N+1)
+        # by UPDATEPOLE; the quality of the interpolation point depends on SIM(:, N + 1), meaning
+        # that the same interpolation set may have good or bad geometry with respect to different
+        # "poles"; if the geometry turns out bad with the new pole, the original COBYLA code will
+        # take a geometry step, but our code here will NOT do it but continue to take a trust-region
+        # step. The argument is this: even if the geometry step is not skipped in the first place, the
+        # geometry may turn out bad again after the pole is altered due to an update to CPEN; should
+        # we take another geometry step in that case? If no, why should we do it here? Indeed, this
+        # distinction makes no practical difference for CUTEst problems with at most 100 variables
+        # and 5000 constraints, while the algorithm framework is simplified.
+        if improve_geo and not all(primasum(primapow2(sim[:, :num_vars]), axis=0) <= 4 * primapow2(delta)):
+            # Before the geometry step, updatepole has been called either implicitly by UPDATEXFC or
+            # explicitly after CPEN is updated, so that SIM[:, :NUM_VARS] is the optimal vertex.
+
+            # Decide a vertex to drop from the simplex. It will be replaced with SIM[:, NUM_VARS] + D to
+            # improve the geometry of the simplex.
+            # N.B.:
+            # 1. COBYLA never sets JDROP_GEO = num_vars.
+            # 2. The following JDROP_GEO comes from UOBYQA/NEWUOA/BOBYQA/LINCOA.
+            # 3. In Powell's original algorithm, the geometry of the simplex is considered acceptable
+            # iff the distance between any vertex and the pole is at most 2.1*DELTA, and the distance
+            # between any vertex and the opposite face of the simplex is at least 0.25*DELTA, as
+            # specified in (14) of the COBYLA paper. Correspondingly, JDROP_GEO is set to the index of
+            # the vertex with the largest distance to the pole provided that the distance is larger than
+            # 2.1*DELTA, or the vertex with the smallest distance to the opposite face of the simplex,
+            # in which case the distance must be less than 0.25*DELTA, as the current simplex does not
+            # have acceptable geometry (see (15)--(16) of the COBYLA paper). Once JDROP_GEO is set, the
+            # algorithm replaces SIM(:, JDROP_GEO) with D specified in (17) of the COBYLA paper, which
+            # is orthogonal to the face opposite to SIM(:, JDROP_GEO) and has a length of 0.5*DELTA,
+            # intending to improve the geometry of the simplex as per (14).
+            # 4. Powell's geometry-improving procedure outlined above has an intrinsic flaw: it may lead
+            # to infinite cycling, as was observed in a test on 20240320. In this test, the geometry-
+            # improving point introduced in the previous iteration was replaced with the trust-region
+            # trial point in the current iteration, which was then replaced with the same geometry-
+            # improving point in the next iteration, and so on. In this process, the simplex alternated
+            # between two configurations, neither of which had acceptable geometry. Thus RHO was never
+            # reduced, leading to infinite cycling. (N.B.: Our implementation uses DELTA as the trust
+            # region radius, with RHO being its lower bound. When the infinite cycling occurred in this
+            # test, DELTA = RHO and it could not be reduced due to the requirement that DELTA >= RHO.)
+            jdrop_geo = np.argmax(primasum(primapow2(sim[:, :num_vars]), axis=0), axis=0)
+
+            # Calculate the geometry step D.
+            delbar = delta/2
+            d = geostep(jdrop_geo, amat, bvec, conmat, cpen, cval, delbar, fval, simi)
+
+            # Calculate the next value of the objective and constraint functions.
+            # If X is close to one of the points in the interpolation set, then we do not evaluate the
+            # objective and constraints at X, assuming them to have the values at the closest point.
+            # N.B.:
+            # 1. If this happens, do NOT include X into the filter, as F and CONSTR are inaccurate.
+            # 2. In precise arithmetic, the geometry improving step ensures that the distance between X
+            # and any interpolation point is at least DELBAR, yet X may be close to them due to
+            # rounding. In an experiment with single precision on 20240317, X = SIM(:, N+1) occurred.
+            x = sim[:, num_vars] + d
+            distsq[num_vars] = primasum(primapow2(x - sim[:, num_vars]))
+            distsq[:num_vars] = primasum(primapow2(x.reshape(num_vars, 1) -
+                (sim[:, num_vars].reshape(num_vars, 1) + sim[:, :num_vars])), axis=0)
+            j = np.argmin(distsq)
+            if distsq[j] <= primapow2(1e-4 * rhoend):
+                f = fval[j]
+                constr = conmat[:, j]
+                cstrv = cval[j]
+            else:
+                # Evaluate the objective and constraints at X, taking care of possible
+                # inf/nan values.
+                f, constr = evaluate(calcfc, x, m_nlcon, amat, bvec)
+                cstrv = np.max(np.append(0, constr))
+                nf += 1
+                # Save X, F, CONSTR, CSTRV into the history.
+                savehist(maxhist, x, xhist, f, fhist, cstrv, chist, constr, conhist)
+                # Save X, F, CONSTR, CSTRV into the filter.
+                nfilt, cfilt, ffilt, xfilt, confilt = savefilt(cstrv, ctol, cweight, f,
+                                                               x, nfilt, cfilt, ffilt,
+                                                               xfilt, constr, confilt)
+
+            # Print a message about the function/constraint evaluation according to iprint
+            fmsg(solver, 'Geometry', iprint, nf, delta, f, x, cstrv, constr)
+            # Update SIM, SIMI, FVAL, CONMAT, and CVAL so that SIM(:, JDROP_GEO) is replaced with D.
+            sim, simi, fval, conmat, cval, subinfo = updatexfc(jdrop_geo, constr, cpen, cstrv, d, f, conmat, cval, fval, sim, simi)
+            # Check whether to break due to damaging rounding in UPDATEXFC
+            if subinfo == DAMAGING_ROUNDING:
+                info = subinfo
+                break  # Better action to take? Geometry step, or simply continue?
+
+            # Check whether to break due to maxfun, ftarget, etc.
+            subinfo = checkbreak_con(maxfun, nf, cstrv, ctol, f, ftarget, x)
+            if subinfo != INFO_DEFAULT:
+                info = subinfo
+                break
+        # end of if improve_geo. The procedure of improving the geometry ends.
+
+        # The calculations with the current RHO are complete. Enhance the resolution of the algorithm
+        # by reducing RHO; update DELTA and CPEN at the same time.
+        if reduce_rho:
+            if rho <= rhoend:
+                info = SMALL_TR_RADIUS
+                break
+            delta = max(0.5 * rho, redrho(rho, rhoend))
+            rho = redrho(rho, rhoend)
+            # THe second (out of two) updates of CPEN, where CPEN decreases or remains the same.
+            # Powell's code: cpen = min(cpen, fcratio(fval, conmat)), which may set CPEN to 0.
+            cpen = np.maximum(cpenmin, np.minimum(cpen, fcratio(conmat, fval)))
+            # Print a message about the reduction of rho according to iprint
+            rhomsg(solver, iprint, nf, fval[num_vars], rho, sim[:, num_vars], cval[num_vars], conmat[:, num_vars], cpen)
+            conmat, cval, fval, sim, simi, subinfo = updatepole(cpen, conmat, cval, fval, sim, simi)
+            # Check whether to break due to damaging rounding detected in updatepole
+            if subinfo == DAMAGING_ROUNDING:
+                info = subinfo
+                break  # Better action to take? Geometry step, or simply continue?
+        # End of if reduce_rho. The procedure of reducing RHO ends.
+        # Report the current best value, and check if user asks for early termination.
+        if callback:
+            terminate = callback(sim[:, num_vars], fval[num_vars], nf, tr, cval[num_vars], conmat[:, num_vars])
+            if terminate:
+                info = CALLBACK_TERMINATE
+                break
+    # End of for loop. The iterative procedure ends
+
+    # Return from the calculation, after trying the last trust-region step if it has not been tried yet.
+    # Ensure that D has not been updated after SHORTD == TRUE occurred, or the code below is incorrect.
+    x = sim[:, num_vars] + d
+    if (info == SMALL_TR_RADIUS and
+            shortd and
+            norm(x - sim[:, num_vars]) > 1.0E-3 * rhoend and
+            nf < maxfun):
+        # Zaikun 20230615: UPDATEXFC or UPDATEPOLE is not called since the last trust-region step. Hence
+        # SIM[:, NUM_VARS] remains unchanged. Otherwise SIM[:, NUM_VARS] + D would not make sense.
+        f, constr = evaluate(calcfc, x, m_nlcon, amat, bvec)
+        cstrv = np.max(np.append(0, constr))
+        nf += 1
+        savehist(maxhist, x, xhist, f, fhist, cstrv, chist, constr, conhist)
+        nfilt, cfilt, ffilt, xfilt, confilt = savefilt(cstrv, ctol, cweight, f, x, nfilt, cfilt, ffilt, xfilt, constr, confilt)
+        # Zaikun 20230512: DELTA has been updated. RHO is only indicative here. TO BE IMPROVED.
+        fmsg(solver, 'Trust region', iprint, nf, rho, f, x, cstrv, constr)
+
+    # Return the best calculated values of the variables
+    # N.B.: SELECTX and FINDPOLE choose X by different standards, one cannot replace the other.
+    kopt = selectx(ffilt[:nfilt], cfilt[:nfilt], max(cpen, cweight), ctol)
+    x = xfilt[:, kopt]
+    f = ffilt[kopt]
+    constr = confilt[:, kopt]
+    cstrv = cfilt[kopt]
+
+    # Print a return message according to IPRINT.
+    retmsg(solver, info, iprint, nf, f, x, cstrv, constr)
+    return x, f, constr, cstrv, nf, xhist, fhist, chist, conhist, info
+
+
+
+def getcpen(amat, bvec, conmat, cpen, cval, delta, fval, rho, sim, simi):
+    '''
+    This function gets the penalty parameter CPEN so that PREREM = PREREF + CPEN * PREREC > 0.
+    See the discussions around equation (9) of the COBYLA paper.
+    '''
+
+    # Even after nearly all of the pycutest problems were showing nearly bit for bit
+    # identical results between Python and the Fortran bindings, HS102 was still off by
+    # more than machine epsilon. It turned out to be due to the fact that getcpen was
+    # modifying fval, among other. It just goes to show that even when you're nearly
+    # perfect, you can still have non trivial bugs.
+    conmat = conmat.copy()
+    cval = cval.copy()
+    fval = fval.copy()
+    sim = sim.copy()
+    simi = simi.copy()
+
+    # Intermediate variables
+    A = np.zeros((np.size(sim, 0), np.size(conmat, 0)))
+    itol = 1
+
+    # Sizes
+    m_lcon = np.size(bvec) if bvec is not None else 0
+    num_constraints = np.size(conmat, 0)
+    num_vars = np.size(sim, 0)
+
+    # Preconditions
+    if DEBUGGING:
+        assert num_constraints >= 0
+        assert num_vars >= 1
+        assert cpen > 0
+        assert np.size(conmat, 0) == num_constraints and np.size(conmat, 1) == num_vars + 1
+        assert not (np.isnan(conmat) | np.isneginf(conmat)).any()
+        assert np.size(cval) == num_vars + 1 and \
+            not any(cval < 0 | np.isnan(cval) | np.isposinf(cval))
+        assert np.size(fval) == num_vars + 1 and not any(np.isnan(fval) | np.isposinf(fval))
+        assert np.size(sim, 0) == num_vars and np.size(sim, 1) == num_vars + 1
+        assert np.isfinite(sim).all()
+        assert all(np.max(abs(sim[:, :num_vars]), axis=0) > 0)
+        assert np.size(simi, 0) == num_vars and np.size(simi, 1) == num_vars
+        assert np.isfinite(simi).all()
+        assert isinv(sim[:, :num_vars], simi, itol)
+        assert delta >= rho and rho > 0
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    # Initialize INFO which is needed in the postconditions
+    info = INFO_DEFAULT
+
+    # Increase CPEN if necessary to ensure PREREM > 0. Branch back for the next loop
+    # if this change alters the optimal vertex of the current simplex.
+    # Note the following:
+    # 1. In each loop, CPEN is changed only if PREREC > 0 > PREREF, in which case
+    #    PREREM is guaranteed positive after the update. Note that PREREC >= 0 and
+    #    max(PREREC, PREREF) > 0 in theory. If this holds numerically as well then CPEN
+    #    is not changed only if PREREC = 0 or PREREF >= 0, in which case PREREM is
+    #    currently positive, explaining why CPEN needs no update.
+    # 2. Even without an upper bound for the loop counter, the loop can occur at most
+    #    NUM_VARS+1 times. This is because the update of CPEN does not decrease CPEN,
+    #    and hence it can make vertex J (J <= NUM_VARS) become the new optimal vertex
+    #    only if CVAL[J] is less than CVAL[NUM_VARS], which can happen at most NUM_VARS
+    #    times. See the paragraph below (9) in the COBYLA paper. After the "correct"
+    #    optimal vertex is found, one more loop is needed to calculate CPEN, and hence
+    #    the loop can occur at most NUM_VARS+1 times.
+    for iter in range(num_vars + 1):
+        # Switch the best vertex of the current simplex to SIM[:, NUM_VARS]
+        conmat, cval, fval, sim, simi, info = updatepole(cpen, conmat, cval, fval, sim,
+                                                         simi)
+        # Check whether to exit due to damaging rounding in UPDATEPOLE
+        if info == DAMAGING_ROUNDING:
+            break
+
+        # Calculate the linear approximations to the objective and constraint functions.
+        g = matprod(fval[:num_vars] - fval[num_vars], simi)
+        A[:, :m_lcon] = amat.T if amat is not None else amat
+        A[:, m_lcon:] = matprod((conmat[m_lcon:, :num_vars] -
+                          np.tile(conmat[m_lcon:, num_vars], (num_vars, 1)).T), simi).T
+
+        # Calculate the trust-region trial step D. Note that D does NOT depend on CPEN.
+        d = trstlp(A, -conmat[:, num_vars], delta, g)
+
+        # Predict the change to F (PREREF) and to the constraint violation (PREREC) due
+        # to D.
+        preref = -inprod(d, g)  # Can be negative
+        prerec = cval[num_vars] - np.max(np.append(0, conmat[:, num_vars] + matprod(d, A)))
+
+        # PREREC <= 0 or PREREF >=0 or either is NaN
+        if not (prerec > 0 and preref < 0):
+            break
+
+        # Powell's code defines BARMU = -PREREF / PREREC, and CPEN is increased to
+        # 2*BARMU if and only if it is currently less than 1.5*BARMU, a very
+        # "Powellful" scheme. In our implementation, however, we set CPEN directly to
+        # the maximum between its current value and 2*BARMU while handling possible
+        # overflow. The simplifies the scheme without worsening the performance of
+        # COBYLA.
+        cpen = max(cpen, min(-2 * preref / prerec, REALMAX))
+
+        if findpole(cpen, cval, fval) == num_vars:
+            break
+
+    #==================#
+    # Calculation ends #
+    #==================#
+
+    # Postconditions
+    if DEBUGGING:
+        assert cpen >= cpen and cpen > 0
+        assert preref + cpen * prerec > 0 or info == DAMAGING_ROUNDING or \
+            not (prerec >= 0 and np.maximum(prerec, preref) > 0) or not np.isfinite(preref)
+
+    return cpen
+
+
+def fcratio(conmat, fval):
+    '''
+    This function calculates the ratio between the "typical change" of F and that of CONSTR.
+    See equations (12)-(13) in Section 3 of the COBYLA paper for the definition of the ratio.
+    '''
+
+    # Preconditions
+    if DEBUGGING:
+        assert np.size(fval) >= 1
+        assert np.size(conmat, 1) == np.size(fval)
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    cmin = np.min(-conmat, axis=1)
+    cmax = np.max(-conmat, axis=1)
+    fmin = min(fval)
+    fmax = max(fval)
+    if any(cmin < 0.5 * cmax) and fmin < fmax:
+        denom = np.min(np.maximum(cmax, 0) - cmin, where=cmin < 0.5 * cmax, initial=np.inf)
+        # Powell mentioned the following alternative in section 4 of his COBYLA paper. According to a test
+        # on 20230610, it does not make much difference to the performance.
+        # denom = np.max(max(*cmax, 0) - cmin, mask=(cmin < 0.5 * cmax))
+        r = (fmax - fmin) / denom
+    else:
+        r = 0
+
+    #==================#
+    # Calculation ends #
+    #==================#
+
+    # Postconditions
+    if DEBUGGING:
+        assert r >= 0
+
+    return r
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/cobyla/geometry.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/cobyla/geometry.py
new file mode 100644
index 0000000000000000000000000000000000000000..c03e5626696f5a40939792b6448972d304b31d71
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/cobyla/geometry.py
@@ -0,0 +1,226 @@
+'''
+This module contains subroutines concerning the geometry-improving of the interpolation set.
+
+Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
+
+Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
+
+Python translation by Nickolai Belakovski.
+'''
+
+from ..common.consts import DEBUGGING
+from ..common.linalg import isinv, matprod, inprod, norm, primasum, primapow2
+import numpy as np
+
+
+def setdrop_tr(ximproved, d, delta, rho, sim, simi):
+    '''
+    This function finds (the index) of a current interpolation point to be replaced with
+    the trust-region trial point. See (19)-(22) of the COBYLA paper.
+    N.B.:
+    1. If XIMPROVED == True, then JDROP > 0 so that D is included into XPT. Otherwise,
+       it is a bug.
+    2. COBYLA never sets JDROP = NUM_VARS
+    TODO: Check whether it improves the performance if JDROP = NUM_VARS is allowed when
+    XIMPROVED is True. Note that UPDATEXFC should be revised accordingly.
+    '''
+
+    # Local variables
+    itol = 0.1
+
+    # Sizes
+    num_vars = np.size(sim, 0)
+
+    # Preconditions
+    if DEBUGGING:
+        assert num_vars >= 1
+        assert np.size(d) == num_vars and all(np.isfinite(d))
+        assert delta >= rho and rho > 0
+        assert np.size(sim, 0) == num_vars and np.size(sim, 1) == num_vars + 1
+        assert np.isfinite(sim).all()
+        assert all(np.max(abs(sim[:, :num_vars]), axis=0) > 0)
+        assert np.size(simi, 0) == num_vars and np.size(simi, 1) == num_vars
+        assert np.isfinite(simi).all()
+        assert isinv(sim[:, :num_vars], simi, itol)
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    # -------------------------------------------------------------------------------------------------- #
+    #  The following code is Powell's scheme for defining JDROP.
+    # -------------------------------------------------------------------------------------------------- #
+    # ! JDROP = 0 by default. It cannot be removed, as JDROP may not be set below in some cases (e.g.,
+    # ! when XIMPROVED == FALSE, MAXVAL(ABS(SIMID)) <= 1, and MAXVAL(VETA) <= EDGMAX).
+    # jdrop = 0
+    #
+    # ! SIMID(J) is the value of the J-th Lagrange function at D. It is the counterpart of VLAG in UOBYQA
+    # ! and DEN in NEWUOA/BOBYQA/LINCOA, but it excludes the value of the (N+1)-th Lagrange function.
+    # simid = matprod(simi, d)
+    # if (any(abs(simid) > 1) .or. (ximproved .and. any(.not. is_nan(simid)))) then
+    #     jdrop = int(maxloc(abs(simid), mask=(.not. is_nan(simid)), dim=1), kind(jdrop))
+    #     !!MATLAB: [~, jdrop] = max(simid, [], 'omitnan');
+    # end if
+    #
+    # ! VETA(J) is the distance from the J-th vertex of the simplex to the best vertex, taking the trial
+    # ! point SIM(:, N+1) + D into account.
+    # if (ximproved) then
+    #     veta = sqrt(sum((sim(:, 1:n) - spread(d, dim=2, ncopies=n))**2, dim=1))
+    #     !!MATLAB: veta = sqrt(sum((sim(:, 1:n) - d).^2));  % d should be a column! Implicit expansion
+    # else
+    #     veta = sqrt(sum(sim(:, 1:n)**2, dim=1))
+    # end if
+    #
+    # ! VSIG(J) (J=1, .., N) is the Euclidean distance from vertex J to the opposite face of the simplex.
+    # vsig = ONE / sqrt(sum(simi**2, dim=2))
+    # sigbar = abs(simid) * vsig
+    #
+    # ! The following JDROP will overwrite the previous one if its premise holds.
+    # mask = (veta > factor_delta * delta .and. (sigbar >= factor_alpha * delta .or. sigbar >= vsig))
+    # if (any(mask)) then
+    #     jdrop = int(maxloc(veta, mask=mask, dim=1), kind(jdrop))
+    #     !!MATLAB: etamax = max(veta(mask)); jdrop = find(mask & ~(veta < etamax), 1, 'first');
+    # end if
+    #
+    # ! Powell's code does not include the following instructions. With Powell's code, if SIMID consists
+    # ! of only NaN, then JDROP can be 0 even when XIMPROVED == TRUE (i.e., D reduces the merit function).
+    # ! With the following code, JDROP cannot be 0 when XIMPROVED == TRUE, unless VETA is all NaN, which
+    # ! should not happen if X0 does not contain NaN, the trust-region/geometry steps never contain NaN,
+    # ! and we exit once encountering an iterate containing Inf (due to overflow).
+    # if (ximproved .and. jdrop <= 0) then  ! Write JDROP <= 0 instead of JDROP == 0 for robustness.
+    #     jdrop = int(maxloc(veta, mask=(.not. is_nan(veta)), dim=1), kind(jdrop))
+    #     !!MATLAB: [~, jdrop] = max(veta, [], 'omitnan');
+    # end if
+    # -------------------------------------------------------------------------------------------------- #
+    #  Powell's scheme ends here.
+    # -------------------------------------------------------------------------------------------------- #
+
+    # The following definition of JDROP is inspired by SETDROP_TR in UOBYQA/NEWUOA/BOBYQA/LINCOA.
+    # It is simpler and works better than Powell's scheme. Note that we allow JDROP to be NUM_VARS+1 if
+    # XIMPROVED is True, whereas Powell's code does not.
+    # See also (4.1) of Scheinberg-Toint-2010: Self-Correcting Geometry in Model-Based Algorithms for
+    # Derivative-Free Unconstrained Optimization, which refers to the strategy here as the "combined
+    # distance/poisedness criteria".
+
+    # DISTSQ[j] is the square of the distance from the jth vertex of the simplex to get "best" point so
+    # far, taking the trial point SIM[:, NUM_VARS] + D into account.
+    distsq = np.zeros(np.size(sim, 1))
+    if ximproved:
+        distsq[:num_vars] = primasum(primapow2(sim[:, :num_vars] - np.tile(d, (num_vars, 1)).T), axis=0)
+        distsq[num_vars] = primasum(d*d)
+    else:
+        distsq[:num_vars] = primasum(primapow2(sim[:, :num_vars]), axis=0)
+        distsq[num_vars] = 0
+
+    weight = np.maximum(1, distsq / primapow2(np.maximum(rho, delta/10)))  # Similar to Powell's NEWUOA code.
+
+    # Other possible definitions of weight. They work almost the same as the one above.
+    # weight = distsq  # Similar to Powell's LINCOA code, but WRONG. See comments in LINCOA/geometry.f90.
+    # weight = max(1, max(25 * distsq / delta**2))  # Similar to Powell's BOBYQA code, works well.
+    # weight = max(1, max(10 * distsq / delta**2))
+    # weight = max(1, max(1e2 * distsq / delta**2))
+    # weight = max(1, max(distsq / rho**2))  ! Similar to Powell's UOBYQA
+
+    # If 0 <= j < NUM_VARS, SIMID[j] is the value of the jth Lagrange function at D; the value of the
+    # (NUM_VARS+1)th Lagrange function is 1 - sum(SIMID). [SIMID, 1 - sum(SIMID)] is the counterpart of
+    # VLAG in UOBYQA and DEN in NEWUOA/BOBYQA/LINCOA.
+    simid = matprod(simi, d)
+    score = weight * abs(np.array([*simid, 1 - primasum(simid)]))
+
+    # If XIMPROVED = False (D does not render a better X), set SCORE[NUM_VARS] = -1 to avoid JDROP = NUM_VARS.
+    if not ximproved:
+        score[num_vars] = -1
+
+    # score[j] is NaN implies SIMID[j] is NaN, but we want abs(SIMID) to be big. So we
+    # exclude such j.
+    score[np.isnan(score)] = -1
+
+    jdrop = None
+    # The following if statement works a bit better than
+    # `if any(score > 1) or (any(score > 0) and ximproved)` from Powell's UOBYQA and
+    # NEWUOA code.
+    if any(score > 0):  # Powell's BOBYQA and LINCOA code.
+        jdrop = np.argmax(score)
+
+    if (ximproved and jdrop is None):
+        jdrop = np.argmax(distsq)
+
+    #==================#
+    # Calculation ends #
+    #==================#
+
+    # Postconditions
+    if DEBUGGING:
+        assert jdrop is None or (0 <= jdrop < num_vars + 1)
+        assert jdrop <= num_vars or ximproved
+        assert jdrop >= 0 or not ximproved
+        # JDROP >= 1 when XIMPROVED = TRUE unless NaN occurs in DISTSQ, which should not happen if the
+        # starting point does not contain NaN and the trust-region/geometry steps never contain NaN.
+
+    return jdrop
+
+
+
+
+def geostep(jdrop, amat, bvec, conmat, cpen, cval, delbar, fval, simi):
+    '''
+    This function calculates a geometry step so that the geometry of the interpolation set is improved
+    when SIM[: JDROP_GEO] is replaced with SIM[:, NUM_VARS] + D. See (15)--(17) of the COBYLA paper.
+    '''
+
+    # Sizes
+    m_lcon = np.size(bvec, 0) if bvec is not None else 0
+    num_constraints = np.size(conmat, 0)
+    num_vars = np.size(simi, 0)
+
+    # Preconditions
+    if DEBUGGING:
+        assert num_constraints >= m_lcon >= 0
+        assert num_vars >= 1
+        assert delbar > 0
+        assert cpen > 0
+        assert np.size(simi, 0) == num_vars and np.size(simi, 1) == num_vars
+        assert np.isfinite(simi).all()
+        assert np.size(fval) == num_vars + 1 and not any(np.isnan(fval) | np.isposinf(fval))
+        assert np.size(conmat, 0) == num_constraints and np.size(conmat, 1) == num_vars + 1
+        assert not np.any(np.isnan(conmat) | np.isposinf(conmat))
+        assert np.size(cval) == num_vars + 1 and not any(cval < 0 | np.isnan(cval) | np.isposinf(cval))
+        assert 0 <= jdrop < num_vars
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    # SIMI[JDROP, :] is a vector perpendicular to the face of the simplex to the opposite of vertex
+    # JDROP. Set D to the vector in this direction and with length DELBAR.
+    d = simi[jdrop, :]
+    d = delbar * (d / norm(d))
+
+    # The code below chooses the direction of D according to an approximation of the merit function.
+    # See (17) of the COBYLA paper and  line 225 of Powell's cobylb.f.
+
+    # Calculate the coefficients of the linear approximations to the objective and constraint functions.
+    # N.B.: CONMAT and SIMI have been updated after the last trust-region step, but G and A have not.
+    # So we cannot pass G and A from outside.
+    g = matprod(fval[:num_vars] - fval[num_vars], simi)
+    A = np.zeros((num_vars, num_constraints))
+    A[:, :m_lcon] = amat.T if amat is not None else amat
+    A[:, m_lcon:] = matprod((conmat[m_lcon:, :num_vars] -
+                          np.tile(conmat[m_lcon:, num_vars], (num_vars, 1)).T), simi).T
+    # CVPD and CVND are the predicted constraint violation of D and -D by the linear models.
+    cvpd = np.max(np.append(0, conmat[:, num_vars] + matprod(d, A)))
+    cvnd = np.max(np.append(0, conmat[:, num_vars] - matprod(d, A)))
+    if -inprod(d, g) + cpen * cvnd < inprod(d, g) + cpen * cvpd:
+        d *= -1
+
+    #==================#
+    # Calculation ends #
+    #==================#
+
+    # Postconditions
+    if DEBUGGING:
+        assert np.size(d) == num_vars and all(np.isfinite(d))
+        # In theory, ||S|| == DELBAR, which may be false due to rounding, but not too far.
+        # It is crucial to ensure that the geometry step is nonzero, which holds in theory.
+        assert 0.9 * delbar < np.linalg.norm(d) <= 1.1 * delbar
+    return d
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/cobyla/initialize.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/cobyla/initialize.py
new file mode 100644
index 0000000000000000000000000000000000000000..c8c3da8e2c7b0ec09bb30b7bf67ec55238022a94
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/cobyla/initialize.py
@@ -0,0 +1,215 @@
+'''
+This module contains subroutines for initialization.
+
+Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
+
+Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
+
+Python translation by Nickolai Belakovski.
+'''
+
+from ..common.checkbreak import checkbreak_con
+from ..common.consts import DEBUGGING, REALMAX
+from ..common.infos import INFO_DEFAULT
+from ..common.evaluate import evaluate
+from ..common.history import savehist
+from ..common.linalg import inv
+from ..common.message import fmsg
+from ..common.selectx import savefilt
+
+import numpy as np
+
+def initxfc(calcfc, iprint, maxfun, constr0, amat, bvec, ctol, f0, ftarget, rhobeg, x0,
+            xhist, fhist, chist, conhist, maxhist):
+    '''
+    This subroutine does the initialization concerning X, function values, and
+    constraints.
+    '''
+
+    # Local variables
+    solver = 'COBYLA'
+    srname = "INITIALIZE"
+
+    # Sizes
+    num_constraints = np.size(constr0)
+    m_lcon = np.size(bvec) if bvec is not None else 0
+    m_nlcon = num_constraints - m_lcon
+    num_vars = np.size(x0)
+
+    # Preconditions
+    if DEBUGGING:
+        assert num_constraints >= 0, f'M >= 0 {srname}'
+        assert num_vars >= 1, f'N >= 1 {srname}'
+        assert abs(iprint) <= 3, f'IPRINT is 0, 1, -1, 2, -2, 3, or -3 {srname}'
+        # assert conmat.shape == (num_constraints , num_vars + 1), f'CONMAT.shape = [M, N+1] {srname}'
+        # assert cval.size == num_vars + 1, f'CVAL.size == N+1 {srname}'
+        # assert maxchist * (maxchist - maxhist) == 0, f'CHIST.shape == 0 or MAXHIST {srname}'
+        # assert conhist.shape[0] == num_constraints and maxconhist * (maxconhist - maxhist) == 0, 'CONHIST.shape[0] == num_constraints, SIZE(CONHIST, 2) == 0 or MAXHIST {srname)}'
+        # assert maxfhist * (maxfhist - maxhist) == 0, f'FHIST.shape == 0 or MAXHIST {srname}'
+        # assert xhist.shape[0] == num_vars and maxxhist * (maxxhist - maxhist) == 0, 'XHIST.shape[0] == N, SIZE(XHIST, 2) == 0 or MAXHIST {srname)}'
+        assert all(np.isfinite(x0)), f'X0 is finite {srname}'
+        assert rhobeg > 0, f'RHOBEG > 0 {srname}'
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    # Initialize info to the default value. At return, a value different from this
+    # value will indicate an abnormal return
+    info = INFO_DEFAULT
+
+    # Initialize the simplex. It will be revised during the initialization.
+    sim = np.eye(num_vars, num_vars+1) * rhobeg
+    sim[:, num_vars] = x0
+
+    # Initialize the matrix simi. In most cases simi is overwritten, but not always.
+    simi = np.eye(num_vars) / rhobeg
+
+    # evaluated[j] = True iff the function/constraint of SIM[:, j] has been evaluated.
+    evaluated = np.zeros(num_vars+1, dtype=bool)
+
+    # Initialize fval
+    fval = np.zeros(num_vars+1) + REALMAX
+    cval = np.zeros(num_vars+1) + REALMAX
+    conmat = np.zeros((num_constraints, num_vars+1)) + REALMAX
+
+
+    for k in range(num_vars + 1):
+        x = sim[:, num_vars].copy()
+        # We will evaluate F corresponding to SIM(:, J).
+        if k == 0:
+            j = num_vars
+            f = f0
+            constr = constr0
+        else:
+            j = k - 1
+            x[j] += rhobeg
+            f, constr = evaluate(calcfc, x, m_nlcon, amat, bvec)
+        cstrv = np.max(np.append(0, constr))
+
+        # Print a message about the function/constraint evaluation according to IPRINT.
+        fmsg(solver, 'Initialization', iprint, k, rhobeg, f, x, cstrv, constr)
+
+        # Save X, F, CONSTR, CSTRV into the history.
+        savehist(maxhist, x, xhist, f, fhist, cstrv, chist, constr, conhist)
+
+        # Save F, CONSTR, and CSTRV to FVAL, CONMAT, and CVAL respectively.
+        evaluated[j] = True
+        fval[j] = f
+        conmat[:, j] = constr
+        cval[j] = cstrv
+
+        # Check whether to exit.
+        subinfo = checkbreak_con(maxfun, k, cstrv, ctol, f, ftarget, x)
+        if subinfo != INFO_DEFAULT:
+            info = subinfo
+            break
+
+        # Exchange the new vertex of the initial simplex with the optimal vertex if necessary.
+        # This is the ONLY part that is essentially non-parallel.
+        if j < num_vars and fval[j] < fval[num_vars]:
+            fval[j], fval[num_vars] = fval[num_vars], fval[j]
+            cval[j], cval[num_vars] = cval[num_vars], cval[j]
+            conmat[:, [j, num_vars]] = conmat[:, [num_vars, j]]
+            sim[:, num_vars] = x
+            sim[j, :j+1] = -rhobeg  # SIM[:, :j+1] is lower triangular
+
+    nf = np.count_nonzero(evaluated)
+
+    if evaluated.all():
+        # Initialize SIMI to the inverse of SIM[:, :num_vars]
+        simi = inv(sim[:, :num_vars])
+
+    #==================#
+    # Calculation ends #
+    #==================#
+
+    # Postconditions
+    if DEBUGGING:
+        assert nf <= maxfun, f'NF <= MAXFUN {srname}'
+        assert evaluated.size == num_vars + 1, f'EVALUATED.size == Num_vars + 1 {srname}'
+        # assert chist.size == maxchist, f'CHIST.size == MAXCHIST {srname}'
+        # assert conhist.shape== (num_constraints, maxconhist), f'CONHIST.shape == [M, MAXCONHIST] {srname}'
+        assert conmat.shape == (num_constraints, num_vars + 1), f'CONMAT.shape = [M, N+1] {srname}'
+        assert not (np.isnan(conmat).any() or np.isneginf(conmat).any()), f'CONMAT does not contain NaN/-Inf {srname}'
+        assert cval.size == num_vars + 1 and not (any(cval < 0) or any(np.isnan(cval)) or any(np.isposinf(cval))), f'CVAL.shape == Num_vars+1 and CVAL does not contain negative values or NaN/+Inf {srname}'
+        # assert fhist.shape == maxfhist, f'FHIST.shape == MAXFHIST {srname}'
+        # assert maxfhist * (maxfhist - maxhist) == 0, f'FHIST.shape == 0 or MAXHIST {srname}'
+        assert fval.size == num_vars + 1 and not (any(np.isnan(fval)) or any(np.isposinf(fval))), f'FVAL.shape == Num_vars+1 and FVAL is not NaN/+Inf {srname}'
+        # assert xhist.shape == (num_vars, maxxhist), f'XHIST.shape == [N, MAXXHIST] {srname}'
+        assert sim.shape == (num_vars, num_vars + 1), f'SIM.shape == [N, N+1] {srname}'
+        assert np.isfinite(sim).all(), f'SIM is finite {srname}'
+        assert all(np.max(abs(sim[:, :num_vars]), axis=0) > 0), f'SIM(:, 1:N) has no zero column {srname}'
+        assert simi.shape == (num_vars, num_vars), f'SIMI.shape == [N, N] {srname}'
+        assert np.isfinite(simi).all(), f'SIMI is finite {srname}'
+        assert np.allclose(sim[:, :num_vars] @ simi, np.eye(num_vars), rtol=0.1, atol=0.1) or not all(evaluated), f'SIMI = SIM(:, 1:N)^{-1} {srname}'
+
+    return evaluated, conmat, cval, sim, simi, fval, nf, info
+
+
+def initfilt(conmat, ctol, cweight, cval, fval, sim, evaluated, cfilt, confilt, ffilt, xfilt):
+    '''
+    This function initializes the filter (XFILT, etc) that will be used when selecting
+    x at the end of the solver.
+    N.B.:
+    1. Why not initialize the filters using XHIST, etc? Because the history is empty if
+    the user chooses not to output it.
+    2. We decouple INITXFC and INITFILT so that it is easier to parallelize the former
+    if needed.
+    '''
+
+    # Sizes
+    num_constraints = conmat.shape[0]
+    num_vars = sim.shape[0]
+    maxfilt = len(ffilt)
+
+    # Preconditions
+    if DEBUGGING:
+        assert num_constraints >= 0
+        assert num_vars >= 1
+        assert maxfilt >= 1
+        assert np.size(confilt, 0) == num_constraints and np.size(confilt, 1) == maxfilt
+        assert np.size(cfilt) == maxfilt
+        assert np.size(xfilt, 0) == num_vars and np.size(xfilt, 1) == maxfilt
+        assert np.size(ffilt) == maxfilt
+        assert np.size(conmat, 0) == num_constraints and np.size(conmat, 1) == num_vars + 1
+        assert not (np.isnan(conmat) | np.isneginf(conmat)).any()
+        assert np.size(cval) == num_vars + 1 and not any(cval < 0 | np.isnan(cval) | np.isposinf(cval))
+        assert np.size(fval) == num_vars + 1 and not any(np.isnan(fval) | np.isposinf(fval))
+        assert np.size(sim, 0) == num_vars and np.size(sim, 1) == num_vars + 1
+        assert np.isfinite(sim).all()
+        assert all(np.max(abs(sim[:, :num_vars]), axis=0) > 0)
+        assert np.size(evaluated) == num_vars + 1
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+
+    nfilt = 0
+    for i in range(num_vars+1):
+        if evaluated[i]:
+            if i < num_vars:
+                x = sim[:, i] + sim[:, num_vars]
+            else:
+                x = sim[:, i]  # i == num_vars, i.e. the last column
+            nfilt, cfilt, ffilt, xfilt, confilt = savefilt(cval[i], ctol, cweight, fval[i], x, nfilt, cfilt, ffilt, xfilt, conmat[:, i], confilt)
+
+    #==================#
+    # Calculation ends #
+    #==================#
+
+    # Postconditions
+    if DEBUGGING:
+        assert nfilt <= maxfilt
+        assert np.size(confilt, 0) == num_constraints and np.size(confilt, 1) == maxfilt
+        assert not (np.isnan(confilt[:, :nfilt]) | np.isneginf(confilt[:, :nfilt])).any()
+        assert np.size(cfilt) == maxfilt
+        assert not any(cfilt[:nfilt] < 0 | np.isnan(cfilt[:nfilt]) | np.isposinf(cfilt[:nfilt]))
+        assert np.size(xfilt, 0) == num_vars and np.size(xfilt, 1) == maxfilt
+        assert not (np.isnan(xfilt[:, :nfilt])).any()
+        # The last calculated X can be Inf (finite + finite can be Inf numerically).
+        assert np.size(ffilt) == maxfilt
+        assert not any(np.isnan(ffilt[:nfilt]) | np.isposinf(ffilt[:nfilt]))
+
+    return nfilt
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/cobyla/trustregion.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/cobyla/trustregion.py
new file mode 100644
index 0000000000000000000000000000000000000000..3e97d7c0b5066a94e96168bd2f42e72197f4b632
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/cobyla/trustregion.py
@@ -0,0 +1,492 @@
+'''
+This module provides subroutines concerning the trust-region calculations of COBYLA.
+
+Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
+
+Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
+
+Python translation by Nickolai Belakovski.
+'''
+
+import numpy as np
+import numpy.typing as npt
+from ..common.consts import DEBUGGING, REALMIN, REALMAX, EPS
+from ..common.powalg import qradd_Rdiag, qrexc_Rdiag
+from ..common.linalg import isminor, matprod, inprod, lsqr, primasum
+
+
+def trstlp(A, b, delta, g):
+    '''
+    This function calculated an n-component vector d by the following two stages. In the first
+    stage, d is set to the shortest vector that minimizes the greatest violation of the constraints
+        A.T @ D <= B,  K = 1, 2, 3, ..., M,
+    subject to the Euclidean length of d being at most delta. If its length is strictly less than
+    delta, then the second stage uses the resultant freedom in d to minimize the objective function
+        G.T @ D
+    subject to no increase in any greatest constraint violation.
+
+    It is possible but rare that a degeneracy may prevent d from attaining the target length delta.
+
+    cviol is the largest constraint violation of the current d: max(max(A.T@D - b), 0)
+    icon is the index of a most violated constraint if cviol is positive.
+
+    nact is the number of constraints in the active set and iact[0], ..., iact[nact-1] are their indices,
+    while the remainder of the iact contains a permutation of the remaining constraint indicies.
+    N.B.: nact <= min(num_constraints, num_vars). Obviously nact <= num_constraints. In addition, the constraints
+    in iact[0, ..., nact-1] have linearly independent gradients (see the comments above the instruction
+    that delete a constraint from the active set to make room for the new active constraint with index iact[icon]);
+    it can also be seen from the update of nact: starting from 0, nact is incremented only if nact < n.
+
+    Further, Z is an orthogonal matrix whose first nact columns can be regarded as the result of
+    Gram-Schmidt applied to the active constraint gradients. For j = 0, 1, ..., nact-1, the number
+    zdota[j] is the scalar product of the jth column of Z with the gradient of the jth active
+    constraint. d is the current vector of variables and here the residuals of the active constraints
+    should be zero. Further, the active constraints have nonnegative Lagrange multipliers that are
+    held at the beginning of vmultc. The remainder of this vector holds the residuals of the inactive
+    constraints at d, the ordering of the components of vmultc being in agreement with the permutation
+    of the indices of the constraints that is in iact. All these residuals are nonnegative, which is
+    achieved by the shift cviol that makes the least residual zero.
+
+    N.B.:
+    0. In Powell's implementation, the constraints are A.T @ D >= B. In other words, the A and B in
+    our implementation are the negative of those in Powell's implementation.
+    1. The algorithm was NOT documented in the COBYLA paper. A note should be written to introduce it!
+    2. As a major part of the algorithm (see trstlp_sub), the code maintains and updates the QR
+    factorization of A[iact[:nact]], i.e. the gradients of all the active (linear) constraints. The
+    matrix Z is indeed Q, and the vector zdota is the diagonal of R. The factorization is updated by
+    Givens rotations when an index is added in or removed from iact.
+    3. There are probably better algorithms available for the trust-region linear programming problem.
+    '''
+
+    # Sizes
+    num_constraints = A.shape[1]
+    num_vars = A.shape[0]
+
+    # Preconditions
+    if DEBUGGING:
+        assert num_vars >= 1
+        assert num_constraints >= 0
+        assert np.size(g) == num_vars
+        assert np.size(b) == num_constraints
+        assert delta > 0
+
+
+    vmultc = np.zeros(num_constraints + 1)
+    iact = np.zeros(num_constraints + 1, dtype=int)
+    nact = 0
+    d = np.zeros(num_vars)
+    z = np.zeros((num_vars, num_vars))
+
+    # ==================
+    # Calculation starts
+    # ==================
+
+    # Form A_aug and B_aug. This allows the gradient of the objective function to be regarded as the
+    # gradient of a constraint in the second stage.
+    A_aug = np.hstack([A, g.reshape((num_vars, 1))])
+    b_aug = np.hstack([b, 0])
+
+
+    # Scale the problem if A contains large values. Otherwise floating point exceptions may occur.
+    # Note that the trust-region step is scale invariant.
+    for i in range(num_constraints+1):  # Note that A_aug.shape[1] == num_constraints+1
+        if (maxval:=max(abs(A_aug[:, i]))) > 1e12:
+            modscal = max(2*REALMIN, 1/maxval)
+            A_aug[:, i] *= modscal
+            b_aug[i] *= modscal
+
+    # Stage 1: minimize the 1+infinity constraint violation of the linearized constraints.
+    iact[:num_constraints], nact, d, vmultc[:num_constraints], z = trstlp_sub(iact[:num_constraints], nact, 1, A_aug[:, :num_constraints], b_aug[:num_constraints], delta, d, vmultc[:num_constraints], z)
+
+    # Stage 2: minimize the linearized objective without increasing the 1_infinity constraint violation.
+    iact, nact, d, vmultc, z = trstlp_sub(iact, nact, 2, A_aug, b_aug, delta, d, vmultc, z)
+
+    # ================
+    # Calculation ends
+    # ================
+
+    # Postconditions
+    if DEBUGGING:
+        assert all(np.isfinite(d))
+        # Due to rounding, it may happen that ||D|| > DELTA, but ||D|| > 2*DELTA is highly improbable.
+        assert np.linalg.norm(d) <= 2 * delta
+
+    return d
+
+def trstlp_sub(iact: npt.NDArray, nact: int, stage, A, b, delta, d, vmultc, z):
+    '''
+    This subroutine does the real calculations for trstlp, both stage 1 and stage 2.
+    Major differences between stage 1 and stage 2:
+    1. Initialization. Stage 2 inherits the values of some variables from stage 1, so they are
+    initialized in stage 1 but not in stage 2.
+    2. cviol. cviol is updated after at iteration in stage 1, while it remains a constant in stage2.
+    3. sdirn. See the definition of sdirn in the code for details.
+    4. optnew. The two stages have different objectives, so optnew is updated differently.
+    5. step. step <= cviol in stage 1.
+    '''
+    zdasav = np.zeros(z.shape[1])
+    vmultd = np.zeros(np.size(vmultc))
+    zdota = np.zeros(np.size(z, 1))
+
+    # Sizes
+    mcon = np.size(A, 1)
+    num_vars = np.size(A, 0)
+
+    # Preconditions
+    if DEBUGGING:
+        assert num_vars >= 1
+        assert stage == 1 or stage == 2
+        assert (mcon >= 0 and stage == 1) or (mcon >= 1 and stage == 2)
+        assert np.size(b) == mcon
+        assert np.size(iact) == mcon
+        assert np.size(vmultc) == mcon
+        assert np.size(d) == num_vars
+        assert np.size(z, 0) == num_vars and np.size(z, 1) == num_vars
+        assert delta > 0
+        if stage == 2:
+            assert all(np.isfinite(d)) and np.linalg.norm(d) <= 2 * delta
+            assert nact >= 0 and nact <= np.minimum(mcon, num_vars)
+            assert all(vmultc[:mcon]) >= 0
+            # N.B.: Stage 1 defines only VMULTC(1:M); VMULTC(M+1) is undefined!
+
+
+    # Initialize according to stage
+    if stage == 1:
+        iact = np.linspace(0, mcon-1, mcon, dtype=int)
+        nact = 0
+        d = np.zeros(num_vars)
+        cviol = np.max(np.append(0, -b))
+        vmultc = cviol + b
+        z = np.eye(num_vars)
+        if mcon == 0 or cviol <= 0:
+            # Check whether a quick return is possible. Make sure the in-outputs have been initialized.
+            return iact, nact, d, vmultc, z
+
+        if all(np.isnan(b)):
+            return iact, nact, d, vmultc, z
+        else:
+            icon = np.nanargmax(-b)
+        num_constraints = mcon
+        sdirn = np.zeros(len(d))
+    else:
+        if inprod(d, d) >= delta*delta:
+            # Check whether a quick return is possible.
+            return iact, nact, d, vmultc, z
+
+        iact[mcon-1] = mcon-1
+        vmultc[mcon-1] = 0
+        num_constraints = mcon - 1
+        icon = mcon - 1
+
+        # In Powell's code, stage 2 uses the zdota and cviol calculated by stage1. Here we recalculate
+        # them so that they need not be passed from stage 1 to 2, and hence the coupling is reduced.
+        cviol = np.max(np.append(0, matprod(d, A[:, :num_constraints]) - b[:num_constraints]))
+    zdota[:nact] = [inprod(z[:, k], A[:, iact[k]]) for k in range(nact)]
+
+    # More initialization
+    optold = REALMAX
+    nactold = nact
+    nfail = 0
+
+    # Zaikun 20211011: vmultd is computed from scratch at each iteration, but vmultc is inherited
+
+    # Powell's code can encounter infinite cycling, which did happen when testing the following CUTEst
+    # problems: DANWOODLS, GAUSS1LS, GAUSS2LS, GAUSS3LS, KOEBHELB, TAX13322, TAXR13322. Indeed, in all
+    # these cases, Inf/NaN appear in d due to extremely large values in A (up to 10^219). To resolve
+    # this, we set the maximal number of iterations to maxiter, and terminate if Inf/NaN occurs in d.
+    maxiter = np.minimum(10000, 100*max(num_constraints, num_vars))
+    for iter in range(maxiter):
+        if DEBUGGING:
+            assert all(vmultc >= 0)
+        if stage == 1:
+            optnew = cviol
+        else:
+            optnew = inprod(d, A[:, mcon-1])
+
+        # End the current stage of the calculation if 3 consecutive iterations have either failed to
+        # reduce the best calculated value of the objective function or to increase the number of active
+        # constraints since the best value was calculated. This strategy prevents cycling, but there is
+        # a remote possibility that it will cause premature termination.
+        if optnew < optold or nact > nactold:
+            nactold = nact
+            nfail = 0
+        else:
+            nfail += 1
+        optold = np.minimum(optold, optnew)
+        if nfail == 3:
+            break
+
+        # If icon exceeds nact, then we add the constraint with index iact[icon] to the active set.
+        if icon >= nact:  # In Python this needs to be >= since Python is 0-indexed (in Fortran we have 1 > 0, in Python we need 0 >= 0)
+            zdasav[:nact] = zdota[:nact]
+            nactsav = nact
+            z, zdota, nact = qradd_Rdiag(A[:, iact[icon]], z, zdota, nact)  # May update nact to nact+1
+            # Indeed it suffices to pass zdota[:min(num_vars, nact+1)] to qradd as follows:
+            # qradd(A[:, iact[icon]], z, zdota[:min(num_vars, nact+1)], nact)
+
+            if nact == nactsav + 1:
+                # N.B.: It is possible to index arrays using [nact, icon] when nact == icon.
+                # Zaikun 20211012: Why should vmultc[nact] = 0?
+                if nact != (icon + 1):  # Need to add 1 to Python for 0 indexing
+                    vmultc[[icon, nact-1]] = vmultc[nact-1], 0
+                    iact[[icon, nact-1]] = iact[[nact-1, icon]]
+                else:
+                    vmultc[nact-1] = 0
+            else:
+                # Zaikun 20211011:
+                # 1. VMULTD is calculated from scratch for the first time (out of 2) in one iteration.
+                # 2. Note that IACT has not been updated to replace IACT[NACT] with IACT[ICON]. Thus
+                # A[:, IACT[:NACT]] is the UNUPDATED version before QRADD (note Z[:, :NACT] remains the
+                # same before and after QRADD). Therefore if we supply ZDOTA to LSQR (as Rdiag) as
+                # Powell did, we should use the UNUPDATED version, namely ZDASAV.
+                # vmultd[:nact] = lsqr(A[:, iact[:nact]], A[:, iact[icon]], z[:, :nact], zdasav[:nact])
+                vmultd[:nact] = lsqr(A[:, iact[:nact]], A[:, iact[icon]], z[:, :nact], zdasav[:nact])
+                if not any(np.logical_and(vmultd[:nact] > 0, iact[:nact] <= num_constraints)):
+                    # N.B.: This can be triggered by NACT == 0 (among other possibilities)! This is
+                    # important, because NACT will be used as an index in the sequel.
+                    break
+                # vmultd[NACT+1:mcon] is not used, but we have to initialize it in Fortran, or compilers
+                # complain about the where construct below (another solution: restrict where to 1:NACT).
+                vmultd[nact:mcon] = -1  # len(vmultd) == mcon
+
+                # Revise the Lagrange multipliers. The revision is not applicable to vmultc[nact:num_constraints].
+                fracmult = [vmultc[i]/vmultd[i] if vmultd[i] > 0 and iact[i] <= num_constraints else REALMAX for i in range(nact)]
+                # Only the places with vmultd > 0 and iact <= m is relevant below, if any.
+                frac = min(fracmult[:nact])  # fracmult[nact:mcon] may contain garbage
+                vmultc[:nact] = np.maximum(np.zeros(len(vmultc[:nact])), vmultc[:nact] - frac*vmultd[:nact])
+
+                # Reorder the active constraints so that the one to be replaced is at the end of the list.
+                # Exit if the new value of zdota[nact] is not acceptable. Powell's condition for the
+                # following If: not abs(zdota[nact]) > 0. Note that it is different from
+                # 'abs(zdota[nact]) <=0)' as zdota[nact] can be NaN.
+                # N.B.: We cannot arrive here with nact == 0, which should have triggered a break above
+                if np.isnan(zdota[nact - 1]) or abs(zdota[nact - 1]) <= EPS**2:
+                    break
+                vmultc[[icon, nact - 1]] = 0, frac  # vmultc[[icon, nact]] is valid as icon > nact
+                iact[[icon, nact - 1]] = iact[[nact - 1, icon]]
+            # end if nact == nactsav + 1
+
+            # In stage 2, ensure that the objective continues to be treated as the last active constraint.
+            # Zaikun 20211011, 20211111: Is it guaranteed for stage 2 that iact[nact-1] = mcon when
+            # iact[nact] != mcon??? If not, then how does the following procedure ensure that mcon is
+            # the last of iact[:nact]?
+            if stage == 2 and iact[nact - 1] != (mcon - 1):
+                if nact <= 1:
+                    # We must exit, as nact-2 is used as an index below. Powell's code does not have this.
+                    break
+                z, zdota[:nact] = qrexc_Rdiag(A[:, iact[:nact]], z, zdota[:nact], nact - 2)  # We pass nact-2 in Python instead of nact-1
+                # Indeed, it suffices to pass Z[:, :nact] to qrexc as follows:
+                # z[:, :nact], zdota[:nact] = qrexc(A[:, iact[:nact]], z[:, :nact], zdota[:nact], nact - 1)
+                iact[[nact-2, nact-1]] = iact[[nact-1, nact-2]]
+                vmultc[[nact-2, nact-1]] = vmultc[[nact-1, nact-2]]
+            # Zaikun 20211117: It turns out that the last few lines do not guarantee iact[nact] == num_vars in
+            # stage 2; the following test cannot be passed. IS THIS A BUG?!
+            # assert iact[nact] == mcon or stage == 1, 'iact[nact] must == mcon in stage 2'
+
+            # Powell's code does not have the following. It avoids subsequent floating points exceptions.
+            if np.isnan(zdota[nact-1]) or abs(zdota[nact-1]) <= EPS**2:
+                break
+
+            # Set sdirn to the direction of the next change to the current vector of variables
+            # Usually during stage 1 the vector sdirn gives a search direction that reduces all the
+            # active constraint violations by one simultaneously.
+            if stage == 1:
+                sdirn -= ((inprod(sdirn, A[:, iact[nact-1]]) + 1)/zdota[nact-1])*z[:, nact-1]
+            else:
+                sdirn = -1/zdota[nact-1]*z[:, nact-1]
+        else:  # icon < nact
+            # Delete the constraint with the index iact[icon] from the active set, which is done by
+            # reordering iact[icon:nact] into [iact[icon+1:nact], iact[icon]] and then reduce nact to
+            # nact - 1. In theory, icon > 0.
+            # assert icon > 0, "icon > 0 is required"  #  For Python I think this is irrelevant
+            z, zdota[:nact] = qrexc_Rdiag(A[:, iact[:nact]], z, zdota[:nact], icon)  # qrexc does nothing if icon == nact
+            # Indeed, it suffices to pass Z[:, :nact] to qrexc as follows:
+            # z[:, :nact], zdota[:nact] = qrexc(A[:, iact[:nact]], z[:, :nact], zdota[:nact], icon)
+            iact[icon:nact] = [*iact[icon+1:nact], iact[icon]]
+            vmultc[icon:nact] = [*vmultc[icon+1:nact], vmultc[icon]]
+            nact -= 1
+
+            # Powell's code does not have the following. It avoids subsequent exceptions.
+            # Zaikun 20221212: In theory, nact > 0 in stage 2, as the objective function should always
+            # be considered as an "active constraint" --- more precisely, iact[nact] = mcon. However,
+            # looking at the code, I cannot see why in stage 2 nact must be positive after the reduction
+            # above. It did happen in stage 1 that nact became 0 after the reduction --- this is
+            # extremely rare, and it was never observed until 20221212, after almost one year of
+            # random tests. Maybe nact is theoretically positive even in stage 1?
+            if stage == 2 and nact < 0:
+                break  # If this case ever occurs, we have to break, as nact is used as an index below.
+            if nact > 0:
+                if np.isnan(zdota[nact-1]) or abs(zdota[nact-1]) <= EPS**2:
+                    break
+
+            # Set sdirn to the direction of the next change to the current vector of variables.
+            if stage == 1:
+                sdirn -= inprod(sdirn, z[:, nact]) * z[:, nact]
+                # sdirn is orthogonal to z[:, nact+1]
+            else:
+                sdirn = -1/zdota[nact-1] * z[:, nact-1]
+        # end if icon > nact
+
+        # Calculate the step to the trust region boundary or take the step that reduces cviol to 0.
+        # ----------------------------------------------------------------------------------------- #
+        # The following calculation of step is adopted from NEWUOA/BOBYQA/LINCOA. It seems to improve
+        # the performance of COBYLA. We also found that removing the precaution about underflows is
+        # beneficial to the overall performance of COBYLA --- the underflows are harmless anyway.
+        dd = delta*delta - inprod(d, d)
+        ss = inprod(sdirn, sdirn)
+        sd = inprod(sdirn, d)
+        if dd <= 0 or ss <= EPS * delta*delta or np.isnan(sd):
+            break
+        # sqrtd: square root of a discriminant. The max avoids sqrtd < abs(sd) due to underflow
+        sqrtd = max(np.sqrt(ss*dd + sd*sd), abs(sd), np.sqrt(ss * dd))
+        if sd > 0:
+            step = dd / (sqrtd + sd)
+        else:
+            step = (sqrtd - sd) / ss
+        # step < 0 should not happen. Step can be 0 or NaN when, e.g., sd or ss becomes inf
+        if step <= 0 or not np.isfinite(step):
+            break
+
+        # Powell's approach and comments are as follows.
+        # -------------------------------------------------- #
+        # The two statements below that include the factor eps prevent
+        # some harmless underflows that occurred in a test calculation
+        # (Zaikun: here, eps is the machine epsilon; Powell's original
+        # code used 1.0e-6, and Powell's code was written in single
+        # precision). Further, we skip the step if it could be 0 within
+        # a reasonable tolerance for computer rounding errors.
+
+        #  !dd = delta*delta - sum(d**2, mask=(abs(d) >= EPS * delta))
+        #  !ss = inprod(sdirn, sdirn)
+        #  !if (dd  <= 0) then
+        #  !    exit
+        #  !end if
+        #  !sd = inprod(sdirn, d)
+        #  !if (abs(sd) >= EPS * sqrt(ss * dd)) then
+        #  !    step = dd / (sqrt(ss * dd + sd*sd) + sd)
+        #  !else
+        #  !    step = dd / (sqrt(ss * dd) + sd)
+        #  !end if
+        # -------------------------------------------------- #
+
+        if stage == 1:
+            if isminor(cviol, step):
+                break
+            step = min(step, cviol)
+
+        # Set dnew to the new variables if step is the steplength, and reduce cviol to the corresponding
+        # maximum residual if stage 1 is being done
+        dnew = d + step * sdirn
+        if stage == 1:
+            cviol = np.max(np.append(0, matprod(dnew, A[:, iact[:nact]]) - b[iact[:nact]]))
+            # N.B.: cviol will be used when calculating vmultd[nact+1:mcon].
+
+        # Zaikun 20211011:
+        # 1. vmultd is computed from scratch for the second (out of 2) time in one iteration.
+        # 2. vmultd[:nact] and vmultd[nact:mcon] are calculated separately with no coupling.
+        # 3. vmultd will be calculated from scratch again in the next iteration.
+        # Set vmultd to the vmultc vector that would occur if d became dnew. A device is included to
+        # force vmultd[k] = 0 if deviations from this value can be attributed to computer rounding
+        # errors. First calculate the new Lagrange multipliers.
+        vmultd[:nact] = -lsqr(A[:, iact[:nact]], dnew, z[:, :nact], zdota[:nact])
+        if stage == 2:
+            vmultd[nact-1] = max(0, vmultd[nact-1])  # This seems never activated.
+        # Complete vmultd by finding the new constraint residuals. (Powell wrote "Complete vmultc ...")
+        cvshift = cviol - (matprod(dnew, A[:, iact]) - b[iact])  # Only cvshift[nact+1:mcon] is needed
+        cvsabs = matprod(abs(dnew), abs(A[:, iact])) + abs(b[iact]) + cviol
+        cvshift[isminor(cvshift, cvsabs)] = 0
+        vmultd[nact:mcon] = cvshift[nact:mcon]
+
+        # Calculate the fraction of the step from d to dnew that will be taken
+        fracmult = [vmultc[i]/(vmultc[i] - vmultd[i]) if vmultd[i] < 0 else REALMAX for i in range(len(vmultd))]
+        # Only the places with vmultd < 0 are relevant below, if any.
+        icon = np.argmin(np.append(1, fracmult)) - 1
+        frac = min(np.append(1, fracmult))
+
+        # Update d, vmultc, and cviol
+        dold = d
+        d = (1 - frac)*d + frac * dnew
+        vmultc = np.maximum(0, (1 - frac)*vmultc + frac*vmultd)
+        # Break in the case of inf/nan in d or vmultc.
+        if not (np.isfinite(primasum(abs(d))) and np.isfinite(primasum(abs(vmultc)))):
+            d = dold  # Should we restore also iact, nact, vmultc, and z?
+            break
+
+        if stage == 1:
+            # cviol = (1 - frac) * cvold + frac * cviol  # Powell's version
+            # In theory, cviol = np.max(np.append(d@A - b, 0)), yet the
+            # cviol updated as above can be quite different from this value if A has huge entries (e.g., > 1e20)
+            cviol = np.max(np.append(0, matprod(d, A) - b))
+
+        if icon < 0 or icon >= mcon:
+            # In Powell's code, the condition is icon == 0. Indeed, icon < 0 cannot hold unless
+            # fracmult contains only nan, which should not happen; icon >= mcon should never occur.
+            break
+
+    #==================#
+    # Calculation ends #
+    #==================#
+
+    # Postconditions
+    if DEBUGGING:
+        assert np.size(iact) == mcon
+        assert np.size(vmultc) == mcon
+        assert all(vmultc >= 0)
+        assert np.size(d) == num_vars
+        assert all(np.isfinite(d))
+        assert np.linalg.norm(d) <= 2 * delta
+        assert np.size(z, 0) == num_vars and np.size(z, 1) == num_vars
+        assert nact >= 0 and nact <= np.minimum(mcon, num_vars)
+
+    return iact, nact, d, vmultc, z
+
+
+def trrad(delta_in, dnorm, eta1, eta2, gamma1, gamma2, ratio):
+    '''
+    This function updates the trust region radius according to RATIO and DNORM.
+    '''
+
+    # Preconditions
+    if DEBUGGING:
+        assert delta_in >= dnorm > 0
+        assert 0 <= eta1 <= eta2 < 1
+        assert 0 < gamma1 < 1 < gamma2
+        # By the definition of RATIO in ratio.f90, RATIO cannot be NaN unless the
+        # actual reduction is NaN, which should NOT happen due to the moderated extreme
+        # barrier.
+        assert not np.isnan(ratio)
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    if ratio <= eta1:
+        delta = gamma1 * dnorm  # Powell's UOBYQA/NEWUOA
+        # delta = gamma1 * delta_in  # Powell's COBYLA/LINCOA
+        # delta = min(gamma1 * delta_in, dnorm)  # Powell's BOBYQA
+    elif ratio <= eta2:
+        delta = max(gamma1 * delta_in, dnorm)  # Powell's UOBYQA/NEWUOA/BOBYQA/LINCOA
+    else:
+        delta = max(gamma1 * delta_in, gamma2 * dnorm)  # Powell's NEWUOA/BOBYQA
+        # delta = max(delta_in, gamma2 * dnorm)  # Modified version. Works well for UOBYQA
+        # For noise-free CUTEst problems of <= 100 variables, Powell's version works slightly better
+        # than the modified one.
+        # delta = max(delta_in, 1.25*dnorm, dnorm + rho)  # Powell's UOBYQA
+        # delta = min(max(gamma1 * delta_in, gamma2 * dnorm), gamma3 * delta_in)  # Powell's LINCOA, gamma3 = np.sqrt(2)
+
+    # For noisy problems, the following may work better.
+    # if ratio <= eta1:
+    #     delta = gamma1 * dnorm
+    # elseif ratio <= eta2:  # Ensure DELTA >= DELTA_IN
+    #     delta = delta_in
+    # else:  # Ensure DELTA > DELTA_IN with a constant factor
+    #     delta = max(delta_in * (1 + gamma2) / 2, gamma2 * dnorm)
+
+    #==================#
+    # Calculation ends #
+    #==================#
+
+    # Postconditions
+    if DEBUGGING:
+        assert delta > 0
+    return delta
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/cobyla/update.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/cobyla/update.py
new file mode 100644
index 0000000000000000000000000000000000000000..66a3fa3623f9373b27346ebe052ddbf6f3ba68a4
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/cobyla/update.py
@@ -0,0 +1,289 @@
+'''
+This module contains subroutines concerning the update of the interpolation set.
+
+Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
+
+Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
+
+Python translation by Nickolai Belakovski.
+'''
+
+from ..common.consts import DEBUGGING
+from ..common.infos import DAMAGING_ROUNDING, INFO_DEFAULT
+from ..common.linalg import isinv, matprod, outprod, inprod, inv, primasum
+import numpy as np
+
+
+def updatexfc(jdrop, constr, cpen, cstrv, d, f, conmat, cval, fval, sim, simi):
+    '''
+    This function revises the simplex by updating the elements of SIM, SIMI, FVAL, CONMAT, and CVAL
+    '''
+
+    # Local variables
+    itol = 1
+
+    # Sizes
+    num_constraints = np.size(constr)
+    num_vars = np.size(sim, 0)
+
+    # Preconditions
+    if DEBUGGING:
+        assert num_constraints >= 0
+        assert num_vars >= 1
+        assert jdrop >= 0 and jdrop <= num_vars + 1
+        assert not any(np.isnan(constr) | np.isneginf(constr))
+        assert not (np.isnan(cstrv) | np.isposinf(cstrv))
+        assert np.size(d) == num_vars and all(np.isfinite(d))
+        assert not (np.isnan(f) | np.isposinf(f))
+        assert np.size(conmat, 0) == num_constraints and np.size(conmat, 1) == num_vars + 1
+        assert not (np.isnan(conmat) | np.isneginf(conmat)).any()
+        assert np.size(cval) == num_vars + 1 and not any(cval < 0 | np.isnan(cval) | np.isposinf(cval))
+        assert np.size(fval) == num_vars + 1 and not any(np.isnan(fval) | np.isposinf(fval))
+        assert np.size(sim, 0) == num_vars and np.size(sim, 1) == num_vars + 1
+        assert np.isfinite(sim).all()
+        assert all(primasum(abs(sim[:, :num_vars]), axis=0) > 0)
+        assert np.size(simi, 0) == num_vars and np.size(simi, 1) == num_vars
+        assert np.isfinite(simi).all()
+        assert isinv(sim[:, :num_vars], simi, itol)
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+
+    # Do nothing when JDROP is None. This can only happen after a trust-region step.
+    if jdrop is None:  # JDROP is None is impossible if the input is correct.
+        return conmat, cval, fval, sim, simi, INFO_DEFAULT
+
+    sim_old = sim
+    simi_old = simi
+    if jdrop < num_vars:
+        sim[:, jdrop] = d
+        simi_jdrop = simi[jdrop, :] / inprod(simi[jdrop, :], d)
+        simi -= outprod(matprod(simi, d), simi_jdrop)
+        simi[jdrop, :] = simi_jdrop
+    else:  # jdrop == num_vars
+        sim[:, num_vars] += d
+        sim[:, :num_vars] -= np.tile(d, (num_vars, 1)).T
+        simid = matprod(simi, d)
+        sum_simi = primasum(simi, axis=0)
+        simi += outprod(simid, sum_simi / (1 - sum(simid)))
+
+    # Check whether SIMI is a poor approximation to the inverse of SIM[:, :NUM_VARS]
+    # Calculate SIMI from scratch if the current one is damaged by rounding errors.
+    itol = 1
+    erri = np.max(abs(matprod(simi, sim[:, :num_vars]) - np.eye(num_vars)))  # np.max returns NaN if any input is NaN
+    if erri > 0.1 * itol or np.isnan(erri):
+        simi_test = inv(sim[:, :num_vars])
+        erri_test = np.max(abs(matprod(simi_test, sim[:, :num_vars]) - np.eye(num_vars)))
+        if erri_test < erri or (np.isnan(erri) and not np.isnan(erri_test)):
+            simi = simi_test
+            erri = erri_test
+
+    # If SIMI is satisfactory, then update FVAL, CONMAT, CVAL, and the pole position. Otherwise restore
+    # SIM and SIMI, and return with INFO = DAMAGING_ROUNDING.
+    if erri <= itol:
+        fval[jdrop] = f
+        conmat[:, jdrop] = constr
+        cval[jdrop] = cstrv
+        # Switch the best vertex to the pole position SIM[:, NUM_VARS] if it is not there already
+        conmat, cval, fval, sim, simi, info = updatepole(cpen, conmat, cval, fval, sim, simi)
+    else:
+        info = DAMAGING_ROUNDING
+        sim = sim_old
+        simi = simi_old
+
+    #==================#
+    # Calculation ends #
+    #==================#
+
+    # Postconditions
+    if DEBUGGING:
+        assert np.size(conmat, 0) == num_constraints and np.size(conmat, 1) == num_vars + 1
+        assert not (np.isnan(conmat) | np.isneginf(conmat)).any()
+        assert np.size(cval) == num_vars + 1 and not any(cval < 0 | np.isnan(cval) | np.isposinf(cval))
+        assert np.size(fval) == num_vars + 1 and not any(np.isnan(fval) | np.isposinf(fval))
+        assert np.size(sim, 0) == num_vars and np.size(sim, 1) == num_vars + 1
+        assert np.isfinite(sim).all()
+        assert all(primasum(abs(sim[:, :num_vars]), axis=0) > 0)
+        assert np.size(simi, 0) == num_vars and np.size(simi, 1) == num_vars
+        assert np.isfinite(simi).all()
+        assert isinv(sim[:, :num_vars], simi, itol) or info == DAMAGING_ROUNDING
+
+    return sim, simi, fval, conmat, cval, info
+
+def findpole(cpen, cval, fval):
+    '''
+    This subroutine identifies the best vertex of the current simplex with respect to the merit
+    function PHI = F + CPEN * CSTRV.
+    '''
+
+    # Size
+    num_vars = np.size(fval) - 1
+
+    # Preconditions
+    if DEBUGGING:
+        assert cpen > 0
+        assert np.size(cval) == num_vars + 1 and not any(cval < 0 | np.isnan(cval) | np.isposinf(cval))
+        assert np.size(fval) == num_vars + 1 and not any(np.isnan(fval) | np.isposinf(fval))
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    # Identify the optimal vertex of the current simplex
+    jopt = np.size(fval) - 1
+    phi = fval + cpen * cval
+    phimin = min(phi)
+    # Essentially jopt = np.argmin(phi). However, we keep jopt = num_vars unless there
+    # is a strictly better choice. When there are multiple choices, we choose the jopt
+    # with the smallest value of cval.
+    if phimin < phi[jopt] or any((cval < cval[jopt]) & (phi <= phi[jopt])):
+        # While we could use argmin(phi), there may be two places where phi achieves
+        # phimin, and in that case we should choose the one with the smallest cval.
+        jopt = np.ma.array(cval, mask=(phi > phimin)).argmin()
+
+    #==================#
+    # Calculation ends #
+    #==================#
+
+    # Postconditions
+    if DEBUGGING:
+        assert jopt >= 0 and jopt < num_vars + 1
+        assert jopt == num_vars or phi[jopt] < phi[num_vars] or (phi[jopt] <= phi[num_vars] and cval[jopt] < cval[num_vars])
+    return jopt
+
+
+def updatepole(cpen, conmat, cval, fval, sim, simi):
+    #--------------------------------------------------------------------------------------------------!
+    # This subroutine identifies the best vertex of the current simplex with respect to the merit
+    # function PHI = F + CPEN * CSTRV, and then switch this vertex to SIM[:, NUM_VARS], which Powell called
+    # the "pole position" in his comments. CONMAT, CVAL, FVAL, and SIMI are updated accordingly.
+    #
+    # N.B. 1: In precise arithmetic, the following two procedures produce the same results:
+    # 1) apply UPDATEPOLE to SIM twice, first with CPEN = CPEN1 and then with CPEN = CPEN2;
+    # 2) apply UPDATEPOLE to SIM with CPEN = CPEN2.
+    # In finite-precision arithmetic, however, they may produce different results unless CPEN1 = CPEN2.
+    #
+    # N.B. 2: When JOPT == N+1, the best vertex is already at the pole position, so there is nothing to
+    # switch. However, as in Powell's code, the code below will check whether SIMI is good enough to
+    # work as the inverse of SIM(:, 1:N) or not. If not, Powell's code would invoke an error return of
+    # COBYLB; our implementation, however, will try calculating SIMI from scratch; if the recalculated
+    # SIMI is still of poor quality, then UPDATEPOLE will return with INFO = DAMAGING_ROUNDING,
+    # informing COBYLB that SIMI is poor due to damaging rounding errors.
+    #
+    # N.B. 3: UPDATEPOLE should be called when and only when FINDPOLE can potentially returns a value
+    # other than N+1. The value of FINDPOLE is determined by CPEN, CVAL, and FVAL, the latter two being
+    # decided by SIM. Thus UPDATEPOLE should be called after CPEN or SIM changes. COBYLA updates CPEN at
+    # only two places: the beginning of each trust-region iteration, and when REDRHO is called;
+    # SIM is updated only by UPDATEXFC, which itself calls UPDATEPOLE internally. Therefore, we only
+    # need to call UPDATEPOLE after updating CPEN at the beginning of each trust-region iteration and
+    # after each invocation of REDRHO.
+
+    # Local variables
+    itol = 1
+
+    # Sizes
+    num_constraints = conmat.shape[0]
+    num_vars = sim.shape[0]
+
+    # Preconditions
+    if DEBUGGING:
+        assert num_constraints >= 0
+        assert num_vars >= 1
+        assert cpen > 0
+        assert np.size(conmat, 0) == num_constraints and np.size(conmat, 1) == num_vars + 1
+        assert not (np.isnan(conmat) | np.isneginf(conmat)).any()
+        assert np.size(cval) == num_vars + 1 and not any(cval < 0 | np.isnan(cval) | np.isposinf(cval))
+        assert np.size(fval) == num_vars + 1 and not any(np.isnan(fval) | np.isposinf(fval))
+        assert np.size(sim, 0) == num_vars and np.size(sim, 1) == num_vars + 1
+        assert np.isfinite(sim).all()
+        assert all(primasum(abs(sim[:, :num_vars]), axis=0) > 0)
+        assert np.size(simi, 0) == num_vars and np.size(simi, 1) == num_vars
+        assert np.isfinite(simi).all()
+        assert isinv(sim[:, :num_vars], simi, itol)
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    # INFO must be set, as it is an output.
+    info = INFO_DEFAULT
+
+    # Identify the optimal vertex of the current simplex.
+    jopt = findpole(cpen, cval, fval)
+
+    # Switch the best vertex to the pole position SIM[:, NUM_VARS] if it is not there already and update
+    # SIMI. Before the update, save a copy of SIM and SIMI. If the update is unsuccessful due to
+    # damaging rounding errors, we restore them and return with INFO = DAMAGING_ROUNDING.
+    sim_old = sim.copy()
+    simi_old = simi.copy()
+    if 0 <= jopt < num_vars:
+        # Unless there is a bug in FINDPOLE it is guaranteed that JOPT >= 0
+        # When JOPT == NUM_VARS, there is nothing to switch; in addition SIMI[JOPT, :] will be illegal.
+        # fval[[jopt, -1]] = fval[[-1, jopt]]
+        # conmat[:, [jopt, -1]] = conmat[:, [-1, jopt]] # Exchange CONMAT[:, JOPT] AND CONMAT[:, -1]
+        # cval[[jopt, -1]] = cval[[-1, jopt]]
+        sim[:, num_vars] += sim[:, jopt]
+        sim_jopt = sim[:, jopt].copy()
+        sim[:, jopt] = 0  # np.zeros(num_constraints)?
+        sim[:, :num_vars] -= np.tile(sim_jopt, (num_vars, 1)).T
+        # The above update is equivalent to multiplying SIM[:, :NUM_VARS] from the right side by a matrix whose
+        # JOPT-th row is [-1, -1, ..., -1], while all the other rows are the same as those of the
+        # identity matrix. It is easy to check that the inverse of this matrix is itself. Therefore,
+        # SIMI should be updated by a multiplication with this matrix (i.e. its inverse) from the left
+        # side, as is done in the following line. The JOPT-th row of the updated SIMI is minus the sum
+        # of all rows of the original SIMI, whereas all the other rows remain unchanged.
+        # NDB 20250114: In testing the cutest problem 'SYNTHES2' between the Python implementation and
+        # the Fortran bindings, I saw a difference between the following for loop and the
+        # np.sum command. The differences were small, on the order of 1e-16, i.e. epsilon.
+        # According to numpy documentation, np.sum sometimes uses partial pairwise summation,
+        # depending on the memory layout of the array and the axis specified.
+        # for i in range(simi.shape[1]):
+        #     simi[jopt, i] = -sum(simi[:, i])
+        simi[jopt, :] = -primasum(simi, axis=0)
+
+    # Check whether SIMI is a poor approximation to the inverse of SIM[:, :NUM_VARS]
+    # Calculate SIMI from scratch if the current one is damaged by rounding errors.
+    erri = np.max(abs(matprod(simi, sim[:, :num_vars]) - np.eye(num_vars)))  # np.max returns NaN if any input is NaN
+    itol = 1
+    if erri > 0.1 * itol or np.isnan(erri):
+        simi_test = inv(sim[:, :num_vars])
+        erri_test = np.max(abs(matprod(simi_test, sim[:, :num_vars]) - np.eye(num_vars)))
+        if erri_test < erri or (np.isnan(erri) and not np.isnan(erri_test)):
+            simi = simi_test
+            erri = erri_test
+
+
+    # If SIMI is satisfactory, then update FVAL, CONMAT, and CVAL. Otherwise restore SIM and SIMI, and
+    # return with INFO = DAMAGING_ROUNDING.
+    if erri <= itol:
+        if 0 <= jopt < num_vars:
+            fval[[jopt, num_vars]] = fval[[num_vars, jopt]]
+            conmat[:, [jopt, num_vars]] = conmat[:, [num_vars, jopt]]
+            cval[[jopt, num_vars]] = cval[[num_vars, jopt]]
+    else:  # erri > itol or erri is NaN
+        info = DAMAGING_ROUNDING
+        sim = sim_old
+        simi = simi_old
+
+    #==================#
+    # Calculation ends #
+    #==================#
+
+    # Postconditions
+    if DEBUGGING:
+        assert findpole(cpen, cval, fval) == num_vars or info == DAMAGING_ROUNDING
+        assert np.size(conmat, 0) == num_constraints and np.size(conmat, 1) == num_vars + 1
+        assert not (np.isnan(conmat) | np.isneginf(conmat)).any()
+        assert np.size(cval) == num_vars + 1 and not any(cval < 0 | np.isnan(cval) | np.isposinf(cval))
+        assert np.size(fval) == num_vars + 1 and not any(np.isnan(fval) | np.isposinf(fval))
+        assert np.size(sim, 0) == num_vars and np.size(sim, 1) == num_vars + 1
+        assert np.isfinite(sim).all()
+        assert all(primasum(abs(sim[:, :num_vars]), axis=0) > 0)
+        assert np.size(simi, 0) == num_vars and np.size(simi, 1) == num_vars
+        assert np.isfinite(simi).all()
+        # Do not check SIMI = SIM[:, :num_vars]^{-1}, as it may not be true due to damaging rounding.
+        assert isinv(sim[:, :num_vars], simi, itol) or info == DAMAGING_ROUNDING
+
+    return conmat, cval, fval, sim, simi, info
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/__init__.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/_bounds.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/_bounds.py
new file mode 100644
index 0000000000000000000000000000000000000000..89afe9529f21235fc659d661df5f6037688bfdcf
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/_bounds.py
@@ -0,0 +1,34 @@
+import numpy as np
+from scipy.optimize import Bounds
+
+def process_bounds(bounds, lenx0):
+    '''
+    `bounds` can either be an object with the properties lb and ub, or a list of tuples
+    indicating a lower bound and an upper bound for each variable. If the list contains
+    fewer entries than the length of x0, the remaining entries will generated as -/+ infinity.
+    Some examples of valid lists of tuple, assuming len(x0) == 3:
+    [(0, 1), (2, 3), (4, 5)] -> returns [0, 2, 4], [1, 3, 5]
+    [(0, 1), (None, 3)]      -> returns [0, -inf, -inf], [1, 3, inf]
+    [(0, 1), (-np.inf, 3)]   -> returns [0, -inf, -inf], [1, 3, inf]
+    '''
+
+    if bounds is None:
+        lb = np.array([-np.inf]*lenx0, dtype=np.float64)
+        ub = np.array([np.inf]*lenx0, dtype=np.float64)
+        return lb, ub
+    
+    if isinstance(bounds, Bounds):
+        lb = np.array(bounds.lb, dtype=np.float64)
+        ub = np.array(bounds.ub, dtype=np.float64)
+        lb = np.concatenate((lb, -np.inf*np.ones(lenx0 - len(lb))))
+        ub = np.concatenate((ub, np.inf*np.ones(lenx0 - len(ub))))
+        return lb, ub
+    
+    # If neither of the above conditions are true, we assume that bounds is a list of tuples
+    lb = np.array([bound[0] if bound[0] is not None else -np.inf for bound in bounds], dtype=np.float64)
+    ub = np.array([bound[1] if bound[1] is not None else np.inf for bound in bounds], dtype=np.float64)
+    # If there were fewer bounds than variables, pad the rest with -/+ infinity
+    lb = np.concatenate((lb, -np.inf*np.ones(lenx0 - len(lb))))
+    ub = np.concatenate((ub, np.inf*np.ones(lenx0 - len(ub))))
+    
+    return lb, ub
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/_linear_constraints.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/_linear_constraints.py
new file mode 100644
index 0000000000000000000000000000000000000000..be508dced7edd7cda998d840bf195411a140b2ad
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/_linear_constraints.py
@@ -0,0 +1,46 @@
+import numpy as np
+from scipy.optimize import LinearConstraint
+
+
+def combine_multiple_linear_constraints(constraints):
+    full_A = constraints[0].A
+    full_lb = constraints[0].lb
+    full_ub = constraints[0].ub
+    for constraint in constraints[1:]:
+        full_A = np.concatenate((full_A, constraint.A), axis=0)
+        full_lb = np.concatenate((full_lb, constraint.lb), axis=0)
+        full_ub = np.concatenate((full_ub, constraint.ub), axis=0)
+    return LinearConstraint(full_A, full_lb, full_ub)
+
+
+def separate_LC_into_eq_and_ineq(linear_constraint):
+    # The Python interface receives linear constraints lb <= A*x <= ub, but the
+    # Fortran backend of PRIMA expects that the linear constraints are specified
+    # as A_eq*x = b_eq, A_ineq*x <= b_ineq.
+    # As such, we must:
+    # 1. for constraints with lb == ub, rewrite them as A_eq*x = lb;
+    # 2. for constraints with lb < ub, rewrite them as A_ineq*x <= b_ineq.
+
+    # We suppose lb == ub if ub <= lb + 2*epsilon, assuming that the preprocessing
+    # ensures lb <= ub.
+    epsilon = np.finfo(np.float64).eps
+
+    eq_indices = (linear_constraint.ub <= (linear_constraint.lb + 2*epsilon))
+    A_eq = linear_constraint.A[eq_indices]
+    b_eq = (linear_constraint.lb[eq_indices] + linear_constraint.ub[eq_indices])/2.0
+
+    ineq_lb_indices = (linear_constraint.lb > -np.inf)
+    A_ineq_lb = -linear_constraint.A[~eq_indices & ineq_lb_indices]
+    b_ineq_lb = -linear_constraint.lb[~eq_indices & ineq_lb_indices]
+    ineq_ub_indices = (linear_constraint.ub < np.inf)
+    A_ineq_ub = linear_constraint.A[~eq_indices & ineq_ub_indices]
+    b_ineq_ub = linear_constraint.ub[~eq_indices & ineq_ub_indices]
+    A_ineq = np.concatenate((A_ineq_lb, A_ineq_ub))
+    b_ineq = np.concatenate((b_ineq_lb, b_ineq_ub))
+
+    # Ensure dtype is float64, or set to None if empty
+    A_eq = np.array(A_eq, dtype=np.float64) if len(A_eq) > 0 else None
+    b_eq = np.array(b_eq, dtype=np.float64) if len(b_eq) > 0 else None
+    A_ineq = np.array(A_ineq, dtype=np.float64) if len(A_ineq) > 0 else None
+    b_ineq = np.array(b_ineq, dtype=np.float64) if len(b_ineq) > 0 else None
+    return A_eq, b_eq, A_ineq, b_ineq
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/_nonlinear_constraints.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/_nonlinear_constraints.py
new file mode 100644
index 0000000000000000000000000000000000000000..bfee3b7f2cf2e591b5d956cf3a1ae75cc79f1a19
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/_nonlinear_constraints.py
@@ -0,0 +1,54 @@
+import numpy as np
+        
+def transform_constraint_function(nlc):
+    '''
+    The Python interfaces receives the constraints as lb <= constraint(x) <= ub, 
+    but the Fortran backend expects the nonlinear constraints to be constraint(x) <= 0.
+    Thus a conversion is needed.
+    
+    In addition to the conversion, we add a check to ensure that the provided lower/upper bounds
+    have a shape consistent with the output of the constraint function.
+    '''
+
+    def newconstraint(x):
+        values = np.atleast_1d(np.array(nlc.fun(x), dtype=np.float64))
+        
+        # Upgrade the lower/upper bounds to vectors if necessary
+        lb = nlc.lb
+        try:
+            _ = len(lb)
+        except TypeError:
+            lb = np.array([nlc.lb]*len(values), dtype=np.float64)
+
+        ub = nlc.ub
+        try:
+            _ = len(ub)
+        except TypeError:
+            ub = np.array([nlc.ub]*len(values), dtype=np.float64)
+        
+        
+        # Check the shapes and raise an exception if they do not match
+        if len(values) != len(lb):
+            raise ValueError("The number of elements in the constraint function's output does not match the number of elements in the lower bound.")
+        if len(values) != len(ub):
+            raise ValueError("The number of elements in the constraint function's output does not match the number of elements in the upper bound.")
+        
+        # Combine the upper and lower bounds to transform the function into the form
+        # expected by the Fortran backend.
+        return np.concatenate(([lb_ii - vi for lb_ii, vi in zip(lb, values) if lb_ii > -np.inf],
+                               [vi - ub_ii for ub_ii, vi in zip(ub, values) if ub_ii < np.inf],
+                            ))
+    return newconstraint
+
+
+def process_nl_constraints(nlcs):
+    functions = []
+    for nlc in nlcs:
+        fun_i = transform_constraint_function(nlc)
+        functions.append(fun_i)
+    def constraint_function(x):
+        values = np.empty(0, dtype=np.float64)
+        for fun in functions:
+            values = np.concatenate((values, fun(x)))
+        return values
+    return constraint_function
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/_project.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/_project.py
new file mode 100644
index 0000000000000000000000000000000000000000..233a4e32e905d9a4d3b91f3918de766f51a085fc
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/_project.py
@@ -0,0 +1,173 @@
+'''
+This module provides the _project function that attempts to project the initial guess
+onto the feasible set.
+
+Adapted from the corresponding function in the PDFO package (https://www.pdfo.net) by
+Tom M. Ragonneau (https://ragonneau.github.io) and Zaikun Zhang (https://www.zhangzk.net).
+'''
+
+import numpy as np
+from ._linear_constraints import LinearConstraint
+from scipy.optimize import OptimizeResult
+
+# All the accepted scalar types; np.generic correspond to all NumPy types.
+scalar_types = (int, float, np.generic)
+eps = np.finfo(np.float64).eps
+
+def _project(x0, lb, ub, constraints):
+    """Projection of the initial guess onto the feasible set.
+
+    Parameters
+    ----------
+    x0: ndarray, shape (n,)
+        The same as in prepdfo.
+    lb: ndarray, shape (n,)
+        The same as in prepdfo.
+    ub: ndarray, shape (n,)
+        The same as in prepdfo.
+    constraints: dict
+        The general constraints of the problem, defined as a dictionary with
+        fields:
+            linear: LinearConstraint
+                The linear constraints of the problem.
+            nonlinear: dict
+                The nonlinear constraints of the problem. When ``_project`` is called, the nonlinear constraints are
+                None.
+
+    Returns
+    -------
+    result: OptimizeResult
+        The result of the projection.
+
+    Authors
+    -------
+    Tom M. RAGONNEAU (ragonneau.github.io)
+    and Zaikun ZHANG (www.zhangzk.net)
+
+    Dedicated to the late Professor M. J. D. Powell FRS (1936--2015).
+    """
+    invoker = 'prima'
+
+    # Validate x0.
+    if isinstance(x0, scalar_types):
+        x0_c = [x0]
+    elif hasattr(x0, '__len__'):
+        x0_c = x0
+    else:
+        raise ValueError('{}: UNEXPECTED ERROR: x0 should be a vector.'.format(invoker))
+    try:
+        x0_c = np.asarray(x0_c, dtype=np.float64)
+    except ValueError:
+        raise ValueError('{}: UNEXPECTED ERROR: x0 should contain only scalars.'.format(invoker))
+    if len(x0_c.shape) != 1:
+        raise ValueError('{}: UNEXPECTED ERROR: x0 should be a vector.'.format(invoker))
+    lenx0 = x0_c.size
+
+    # Validate lb.
+    if isinstance(lb, scalar_types):
+        lb_c = [lb]
+    elif hasattr(lb, '__len__'):
+        lb_c = lb
+    else:
+        raise ValueError('{}: UNEXPECTED ERROR: lb should be a vector.'.format(invoker))
+    try:
+        lb_c = np.asarray(lb_c, dtype=np.float64)
+    except ValueError:
+        raise ValueError('{}: UNEXPECTED ERROR: lb should contain only scalars.'.format(invoker))
+    if len(lb_c.shape) != 1 or lb_c.size != lenx0:
+        raise ValueError('{}: UNEXPECTED ERROR: the size of lb is inconsistent with x0.'.format(invoker))
+
+    # Validate ub.
+    if isinstance(ub, scalar_types):
+        ub_c = [ub]
+    elif hasattr(ub, '__len__'):
+        ub_c = ub
+    else:
+        raise ValueError('{}: UNEXPECTED ERROR: ub should be a vector.'.format(invoker))
+    try:
+        ub_c = np.asarray(ub_c, dtype=np.float64)
+    except ValueError:
+        raise ValueError('{}: UNEXPECTED ERROR: ub should contain only scalars.'.format(invoker))
+    if len(ub_c.shape) != 1 or ub_c.size != lenx0:
+        raise ValueError('{}: UNEXPECTED ERROR: the size of ub is inconsistent with x0.'.format(invoker))
+
+    # Validate constraints.
+    if not isinstance(constraints, dict) or not ({'linear', 'nonlinear'} <= set(constraints.keys())) or \
+            not (isinstance(constraints['linear'], LinearConstraint) or constraints['linear'] is None):
+        # the nonlinear constraints will not be taken into account in this function and are, therefore, not validated
+        raise ValueError('{}: UNEXPECTED ERROR: The constraints are ill-defined.'.format(invoker))
+
+    max_con = 1e20  # Decide whether an inequality constraint can be ignored
+
+    # Project onto the feasible set.
+    if constraints['linear'] is None:
+        # Direct projection onto the bound constraints
+        x_proj = np.nanmin((np.nanmax((x0_c, lb_c), axis=0), ub_c), axis=0)
+        return OptimizeResult(x=x_proj)
+    elif all(np.less_equal(np.abs(constraints['linear'].ub - constraints['linear'].lb), eps)) and \
+            np.max(lb_c) <= -max_con and np.min(ub_c) >= max_con:
+        # The linear constraints are all equality constraints. The projection can therefore be done by solving the
+        # least-squares problem: min ||A*x - (b - A*x_0)||.
+        a = constraints['linear'].A
+        b = (constraints['linear'].lb + constraints['linear'].ub) / 2
+        xi, _, _, _ = np.linalg.lstsq(a, b - np.dot(a, x0_c), rcond=None)
+
+        # The problem is not bounded. However, if the least-square solver returned values bigger in absolute value
+        # than max_con, they will be reduced to this bound.
+        x_proj = np.nanmin((np.nanmax((x0_c + xi, lb_c), axis=0), ub_c), axis=0)
+
+        return OptimizeResult(x=x_proj)
+
+    if constraints['linear'] is not None:
+        try:
+            # Project the initial guess onto the linear constraints via SciPy.
+            from scipy.optimize import minimize
+            from scipy.optimize import Bounds as ScipyBounds
+            from scipy.optimize import LinearConstraint as ScipyLinearConstraint
+
+            linear = constraints['linear']
+
+            # To be more efficient, SciPy asks to separate the equality and the inequality constraints into two
+            # different LinearConstraint structures
+            pc_args_ineq, pc_args_eq = dict(), dict()
+            pc_args_ineq['A'], pc_args_eq['A'] = np.asarray([[]]), np.asarray([[]])
+            pc_args_ineq['A'] = pc_args_ineq['A'].reshape(0, linear.A.shape[1])
+            pc_args_eq['A'] = pc_args_eq['A'].reshape(0, linear.A.shape[1])
+            pc_args_ineq['lb'], pc_args_eq['lb'] = np.asarray([]), np.asarray([])
+            pc_args_ineq['ub'], pc_args_eq['ub'] = np.asarray([]), np.asarray([])
+
+            for i in range(linear.lb.size):
+                if linear.lb[i] != linear.ub[i]:
+                    pc_args_ineq['A'] = np.concatenate((pc_args_ineq['A'], linear.A[i:i+1, :]), axis=0)
+                    pc_args_ineq['lb'] = np.r_[pc_args_ineq['lb'], linear.lb[i]]
+                    pc_args_ineq['ub'] = np.r_[pc_args_ineq['ub'], linear.ub[i]]
+                else:
+                    pc_args_eq['A'] = np.concatenate((pc_args_eq['A'], linear.A[i:i+1, :]), axis=0)
+                    pc_args_eq['lb'] = np.r_[pc_args_eq['lb'], linear.lb[i]]
+                    pc_args_eq['ub'] = np.r_[pc_args_eq['ub'], linear.ub[i]]
+
+            if pc_args_ineq['A'].size > 0 and pc_args_ineq['lb'].size > 0 and pc_args_eq['lb'].size > 0:
+                project_constraints = [ScipyLinearConstraint(**pc_args_ineq), ScipyLinearConstraint(**pc_args_eq)]
+            elif pc_args_ineq['A'].size > 0 and pc_args_ineq['lb'].size > 0:
+                project_constraints = ScipyLinearConstraint(**pc_args_ineq)
+            elif pc_args_eq['A'].size > 0:
+                project_constraints = ScipyLinearConstraint(**pc_args_eq)
+            else:
+                project_constraints = ()
+
+            # Perform the actual projection.
+            ax_ineq = np.dot(pc_args_ineq['A'], x0_c)
+            ax_eq = np.dot(pc_args_eq['A'], x0_c)
+            if np.greater(ax_ineq, pc_args_ineq['ub']).any() or np.greater(pc_args_ineq['lb'], ax_ineq).any() or \
+                    np.not_equal(ax_eq, pc_args_eq['lb']).any() or \
+                    np.greater(x0_c, ub_c).any() or np.greater(lb_c, x0_c).any():
+                return minimize(lambda x: np.dot(x - x0_c, x - x0_c) / 2, x0_c, jac=lambda x: (x - x0_c),
+                                bounds=ScipyBounds(lb_c, ub_c), constraints=project_constraints)
+            else:
+                # Do not perform any projection if the initial guess is feasible.
+                return OptimizeResult(x=x0_c)
+
+        except ImportError:
+            return OptimizeResult(x=x0_c)
+
+    return OptimizeResult(x=x0_c)
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/checkbreak.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/checkbreak.py
new file mode 100644
index 0000000000000000000000000000000000000000..95efd6c14b3dc6e572b3d83a7f47b419ac050c93
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/checkbreak.py
@@ -0,0 +1,93 @@
+'''
+This module checks whether to break out of the solver loop.
+
+Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
+
+Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
+
+Python translation by Nickolai Belakovski.
+'''
+
+from .infos import INFO_DEFAULT, NAN_INF_X, NAN_INF_F, FTARGET_ACHIEVED, MAXFUN_REACHED
+
+import numpy as np
+
+def checkbreak_unc(maxfun, nf, f, ftarget, x):
+    '''
+    This module checks whether to break out of the solver loop in the unconstrained case.
+    '''
+
+    # Outputs
+    info = INFO_DEFAULT
+
+    # Local variables
+    srname = "CHECKbreak_UNC"
+
+    # Preconditions
+    assert INFO_DEFAULT not in [NAN_INF_X, NAN_INF_F, FTARGET_ACHIEVED, MAXFUN_REACHED], f'NAN_INF_X, NAN_INF_F, FTARGET_ACHIEVED, and MAXFUN_REACHED differ from INFO_DFT {srname}'
+    # X does not contain NaN if the initial X does not contain NaN and the subroutines generating
+    # trust-region/geometry steps work properly so that they never produce a step containing NaN/Inf.
+    assert not any(np.isnan(x)), f'X does not contain NaN {srname}'
+    # With the moderated extreme barrier, F cannot be NaN/+Inf.
+    assert not (any(np.isnan(f)) or any(np.isposinf(f))), f'F is not NaN/+Inf {srname}'
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    # Although X should not contain NaN unless there is a bug, we include the following for security.
+    # X can be Inf, as finite + finite can be Inf numerically.
+    if any(np.isnan(x)) or any(np.isinf(x)):
+        info = NAN_INF_X
+
+    # Although NAN_INF_F should not happen unless there is a bug, we include the following for security.
+    if any(np.isnan(f)) or any(np.isposinf(f)):
+        info = NAN_INF_F
+
+    if f <= ftarget:
+        info = FTARGET_ACHIEVED
+
+    if nf >= maxfun:
+        info = MAXFUN_REACHED
+
+    return info
+
+def checkbreak_con(maxfun, nf, cstrv, ctol, f, ftarget, x):
+    '''
+    This module checks whether to break out of the solver loop in the constrained case.
+    '''
+
+    # Outputs
+    info = INFO_DEFAULT
+
+    # Local variables
+    srname = "CHECKbreak_CON"
+
+    # Preconditions
+    assert INFO_DEFAULT not in [NAN_INF_X, NAN_INF_F, FTARGET_ACHIEVED, MAXFUN_REACHED], f'NAN_INF_X, NAN_INF_F, FTARGET_ACHIEVED, and MAXFUN_REACHED differ from INFO_DFT {srname}'
+    # X does not contain NaN if the initial X does not contain NaN and the subroutines generating
+    # trust-region/geometry steps work properly so that they never produce a step containing NaN/Inf.
+    assert not any(np.isnan(x)), f'X does not contain NaN {srname}'
+    # With the moderated extreme barrier, F or CSTRV cannot be NaN/+Inf.
+    assert not (np.isnan(f) or np.isposinf(f) or np.isnan(cstrv) or np.isposinf(cstrv)), f'F or CSTRV is not NaN/+Inf {srname}'
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    # Although X should not contain NaN unless there is a bug, we include the following for security.
+    # X can be Inf, as finite + finite can be Inf numerically.
+    if any(np.isnan(x)) or any(np.isinf(x)):
+        info = NAN_INF_X
+
+    # Although NAN_INF_F should not happen unless there is a bug, we include the following for security.
+    if np.isnan(f) or np.isposinf(f) or np.isnan(cstrv) or np.isposinf(cstrv):
+        info = NAN_INF_F
+
+    if cstrv <= ctol and f <= ftarget:
+        info = FTARGET_ACHIEVED
+
+    if nf >= maxfun:
+        info = MAXFUN_REACHED
+
+    return info
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/consts.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/consts.py
new file mode 100644
index 0000000000000000000000000000000000000000..1916bae39cf08a495af8008cd623eb45680914e8
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/consts.py
@@ -0,0 +1,47 @@
+'''
+This is a module defining some constants.
+
+Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
+
+Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
+
+Python translation by Nickolai Belakovski.
+'''
+
+import numpy as np
+import os
+
+DEBUGGING = bool(os.getenv('PRIMA_DEBUGGING'))
+
+REALMIN = np.finfo(float).tiny
+REALMAX = np.finfo(float).max
+FUNCMAX = 10.0**30
+CONSTRMAX = FUNCMAX
+EPS = np.finfo(float).eps
+
+# Any bound with an absolute value at least BOUNDMAX is considered as no bound.
+BOUNDMAX = REALMAX/4
+
+# Some default values
+RHOBEG_DEFAULT = 1
+RHOEND_DEFAULT = 1e-6
+FTARGET_DEFAULT = -REALMAX
+CTOL_DEFAULT = np.sqrt(EPS)
+CWEIGHT_DEFAULT = 1e8
+ETA1_DEFAULT = 0.1
+ETA2_DEFAULT = 0.7
+GAMMA1_DEFAULT = 0.5
+GAMMA2_DEFAULT = 2
+IPRINT_DEFAULT = 0
+MAXFUN_DIM_DEFAULT = 500
+
+PRIMA_MAX_HIST_MEM_MB = 300  # 1MB > 10^5*REAL64. 100 can be too small.
+
+# Maximal amount of memory (Byte) allowed for XHIST, FHIST, CONHIST, CHIST, and the filters.
+MHM = PRIMA_MAX_HIST_MEM_MB * 10**6
+# Make sure that MAXHISTMEM does not exceed HUGE(0) to avoid overflow and memory errors.
+MAXHISTMEM = min(MHM, np.iinfo(np.int32).max)
+
+# Maximal length of the filter used in constrained solvers.
+MIN_MAXFILT = 200  # Should be positive; < 200 is not recommended.
+MAXFILT_DEFAULT = 10 * MIN_MAXFILT
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/evaluate.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/evaluate.py
new file mode 100644
index 0000000000000000000000000000000000000000..075e1d42fa30fcd0eaeb7b40a491f41623e27293
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/evaluate.py
@@ -0,0 +1,99 @@
+'''
+This is a module evaluating the objective/constraint function with Nan/Inf handling.
+
+Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
+
+Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
+
+Python translation by Nickolai Belakovski.
+'''
+
+import numpy as np
+from .consts import FUNCMAX, CONSTRMAX, REALMAX, DEBUGGING
+from .linalg import matprod, primasum
+
+# This is a module evaluating the objective/constraint function with Nan/Inf handling.
+
+
+def moderatex(x):
+    '''
+    This function moderates a decision variable. It replaces NaN by 0 and Inf/-Inf by
+    REALMAX/-REALMAX.
+    '''
+    x[np.isnan(x)] = 0
+    x = np.clip(x, -REALMAX, REALMAX)
+    return x
+
+def moderatef(f):
+    """
+    This function moderates the function value of a MINIMIZATION problem. It replaces
+    NaN and any value above FUNCMAX by FUNCMAX.
+    """
+    f = FUNCMAX if np.isnan(f) else f
+    f = np.clip(f, -REALMAX, FUNCMAX)
+    # We may moderate huge negative function values as follows, but we decide not to.
+    # f = np.clip(f, -FUNCMAX, FUNCMAX)
+    return f
+
+
+def moderatec(c):
+    """
+    This function moderates the constraint value, the constraint demanding this value
+    to be NONNEGATIVE. It replaces any value below -CONSTRMAX by -CONSTRMAX, and any
+    NaN or value above CONSTRMAX by CONSTRMAX.
+    """
+    np.nan_to_num(c, copy=False, nan=CONSTRMAX)
+    c = np.clip(c, -CONSTRMAX, CONSTRMAX)
+    return c
+
+
+def evaluate(calcfc, x, m_nlcon, amat, bvec):
+    """
+    This function evaluates CALCFC at X, returning the objective function value and the
+    constraint value. Nan/Inf are handled by a moderated extreme barrier.
+    """
+
+    # Sizes
+    m_lcon = len(bvec) if bvec is not None else 0
+
+    # Preconditions
+    if DEBUGGING:
+        # X should not contain NaN if the initial X does not contain NaN and the
+        # subroutines generating # trust-region/geometry steps work properly so that
+        # they never produce a step containing NaN/Inf.
+        assert not any(np.isnan(x))
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    constr = np.zeros(m_lcon + m_nlcon)
+    if amat is not None:
+        constr[:m_lcon] = matprod(x, amat.T) - bvec
+
+    if any(np.isnan(x)):
+        # Although this should not happen unless there is a bug, we include this case
+        # for robustness.
+        f = primasum(x)
+        constr = np.ones(m_nlcon) * f
+    else:
+        f, constr[m_lcon:] = calcfc(moderatex(x))
+
+        # Moderated extreme barrier: replace NaN/huge objective or constraint values
+        # with a large but finite value. This is naive, and better approaches surely
+        # exist.
+        f = moderatef(f)
+        constr[m_lcon:] = moderatec(constr[m_lcon:])
+
+    #==================#
+    # Calculation ends #
+    #==================#
+
+    # Postconditions
+    if DEBUGGING:
+        # With X not containing NaN, and with the moderated extreme barrier, F cannot
+        # be NaN/+Inf, and CONSTR cannot be NaN/-Inf.
+        assert not (np.isnan(f) or np.isposinf(f))
+        assert not any(np.isnan(constr) | np.isposinf(constr))
+
+    return f, constr
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/history.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/history.py
new file mode 100644
index 0000000000000000000000000000000000000000..212db7305d890d0133afb312ed6c73b79f8bb0fa
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/history.py
@@ -0,0 +1,38 @@
+'''
+This module provides subroutines that handle the X/F/C histories of the solver, taking into
+account that MAXHIST may be smaller than NF.
+
+Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
+
+Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
+
+Python translation by Nickolai Belakovski.
+'''
+
+def savehist(maxhist, x, xhist, f, fhist, cstrv, chist, constr, conhist):
+    '''
+    Save the data values to the history lists.
+
+    The implementation of this function is vastly different from the Fortran implementation.
+    This is mostly due to the ease of creating and appending to lists in Python
+
+    However just like the Fortran version we should be concerned about both performance
+    and memory constraints. It will probably be better to initialize an array of NaN for
+    each of the histories and keep track of how many indices we have stored. Not needed for
+    the moment.
+    '''
+    if len(xhist) < maxhist:
+        xhist.append(x)
+        fhist.append(f)
+        chist.append(cstrv)
+        conhist.append(constr)
+    else:
+        # This effectively accomplishes what rangehist does in the Fortran implementation
+        xhist.pop(0)
+        fhist.pop(0)
+        chist.pop(0)
+        conhist.pop(0)
+        xhist.append(x)
+        fhist.append(f)
+        chist.append(cstrv)
+        conhist.append(constr)
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/infos.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/infos.py
new file mode 100644
index 0000000000000000000000000000000000000000..27d0f1e144bdfabf1633ccfccaab2ac5dd93781f
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/infos.py
@@ -0,0 +1,30 @@
+'''
+This is a module defining exit flags.
+
+Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
+
+Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
+
+Python translation by Nickolai Belakovski.
+'''
+
+INFO_DEFAULT = 0
+SMALL_TR_RADIUS = 0
+FTARGET_ACHIEVED = 1
+TRSUBP_FAILED = 2
+MAXFUN_REACHED = 3
+MAXTR_REACHED = 20
+NAN_INF_X = -1
+NAN_INF_F = -2
+NAN_INF_MODEL = -3
+NO_SPACE_BETWEEN_BOUNDS = 6
+DAMAGING_ROUNDING = 7
+ZERO_LINEAR_CONSTRAINT = 8
+CALLBACK_TERMINATE = 30
+
+# Stop-codes.
+# The following codes are used by ERROR STOP as stop-codes, which should be default integers.
+INVALID_INPUT = 100
+ASSERTION_FAILS = 101
+VALIDATION_FAILS = 102
+MEMORY_ALLOCATION_FAILS = 103
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/linalg.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/linalg.py
new file mode 100644
index 0000000000000000000000000000000000000000..a5ee756aaf867ca76afe9cbb5f768c2097ecbadc
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/linalg.py
@@ -0,0 +1,435 @@
+'''
+This module provides some basic linear algebra procedures.
+
+Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
+
+Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
+
+Python translation by Nickolai Belakovski.
+'''
+
+import numpy as np
+from .consts import DEBUGGING, EPS, REALMAX, REALMIN
+from .present import present
+
+
+# We use naive implementations of matrix multiplication and other routines for two
+# reasons:
+# 1. When Fortran is compiled in debug mode, and Python is using these routines, we
+#    can get bit for bit identical results as compared to Fortran. This is helpful
+#    for comparing the two implementations. It will be particularly helpful when porting
+#    the other implementations like LINCOA, etc.
+# 2. On some problems this algorithm is very sensitive to errors in finite precision
+#    arithmetic. Switching to naive implementation will slow down the algorithm, but
+#    may be more stable.
+USE_NAIVE_MATH = False
+
+
+def inprod(x, y):
+    if not USE_NAIVE_MATH:
+        return np.dot(x, y)
+    result = 0
+    for i in range(len(x)):
+        result += x[i] * y[i]
+    return result
+
+
+def matprod12(x, y):
+    result = np.zeros(y.shape[1])
+    for i in range(y.shape[1]):
+        result[i] = inprod(x, y[:, i])
+    return result
+
+
+def matprod21(x, y):
+    result = np.zeros(x.shape[0])
+    for i in range(x.shape[1]):
+        result += x[:, i] * y[i]
+    return result
+
+
+def matprod22(x, y):
+    result = np.zeros((x.shape[0], y.shape[1]))
+    for i in range(y.shape[1]):
+        for j in range(x.shape[1]):
+            result[:, j] += x[:, i] * y[i, j]
+    return result
+
+
+def matprod(x, y):
+    if not USE_NAIVE_MATH:
+        return x@y
+    if len(x.shape) == 1 and len(y.shape) == 1:
+        return inprod(x, y)
+    elif len(x.shape) == 1 and len(y.shape) == 2:
+        return matprod12(x, y)
+    elif len(x.shape) == 2 and len(y.shape) == 1:
+        return matprod21(x, y)
+    elif len(x.shape) == 2 and len(y.shape) == 2:
+        return matprod22(x, y)
+    else:
+        raise ValueError(f'Invalid shapes for x and y: {x.shape} and {y.shape}')
+
+
+def outprod(x, y):
+    if not USE_NAIVE_MATH:
+        return np.outer(x, y)
+    result = np.zeros((len(x), len(y)))
+    for i in range(len(x)):
+            result[:, i] = x * y[i]
+    return result
+
+
+def lsqr(A, b, Q, Rdiag):
+    if not USE_NAIVE_MATH:
+        return np.linalg.lstsq(A, b, rcond=None)[0]
+
+    m = A.shape[0]
+    n = A.shape[1]
+
+    rank = min(m, n)
+
+    x = np.zeros(n)
+    y = b.copy()
+
+    for i in range(rank - 1, -1, -1):
+        yq = inprod(y, Q[:, i])
+        yqa = inprod(np.abs(y), np.abs(Q[:, i]))
+        if isminor(yq, yqa):
+            x[i] = 0
+        else:
+            x[i] = yq / Rdiag[i]
+            y = y - x[i] * A[:, i]
+    return x
+
+
+def hypot(x1, x2):
+    if not USE_NAIVE_MATH:
+        return np.hypot(x1, x2)
+    if not np.isfinite(x1):
+        r = abs(x1)
+    elif not np.isfinite(x2):
+        r = abs(x2)
+    else:
+        y = abs(np.array([x1, x2]))
+        y = np.array([min(y), max(y)])
+        if y[0] > np.sqrt(REALMIN) and y[1] < np.sqrt(REALMAX/2.1):
+            r = np.sqrt(sum(y*y))
+        elif y[1] > 0:
+            r = y[1] * np.sqrt((y[0]/y[1])*(y[0]/y[1]) + 1)
+        else:
+            r = 0
+    return r
+
+
+def norm(x):
+    if not USE_NAIVE_MATH:
+        return np.linalg.norm(x)
+    # NOTE: Avoid np.pow! And exponentiation in general!
+    # It appears that in Fortran, x*x and x**2 are the same, but in Python they are not!
+    # Try it with x = 5 - 1e-15
+    result = np.sqrt(sum([xi*xi for xi in x]))
+    return result
+
+
+def istril(A, tol=0):
+    return primasum(abs(A) - np.tril(abs(A))) <= tol
+
+def istriu(A, tol=0):
+    return primasum(abs(A) - np.triu(abs(A))) <= tol
+
+
+def inv(A):
+    if not USE_NAIVE_MATH:
+        return np.linalg.inv(A)
+    A = A.copy()
+    n = A.shape[0]
+    if istril(A):
+        # This case is invoked in COBYLA.
+        R = A.T
+        B = np.zeros((n, n))
+        for i in range(n):
+            B[i, i] = 1 / R[i, i]
+            B[:i, i] = -matprod(B[:i, :i], R[:i, i]) / R[i, i]
+        return B.T
+    elif istriu(A):
+        B = np.zeros((n, n))
+        for i in range(n):
+            B[i, i] = 1 / A[i, i]
+            B[:i, i] = -matprod(B[:i, :i], A[:i, i]) / A[i, i]
+    else:
+        # This is NOT the best algorithm for the inverse, but since the QR subroutine is available ...
+        Q, R, P = qr(A)
+        R = R.T
+        B = np.zeros((n, n))
+        for i in range(n - 1, -1, -1):
+            B[:, i] = (Q[:, i] - matprod(B[:, i + 1:n], R[i + 1:n, i])) / R[i, i]
+        InvP = np.zeros(n, dtype=int)
+        InvP[P] = np.linspace(0, n-1, n)
+        B = B[:, InvP].T
+    return B
+
+
+def qr(A):
+    m = A.shape[0]
+    n = A.shape[1]
+
+    Q = np.eye(m)
+    T = A.T
+    P = np.linspace(0, n-1, n, dtype=int)
+
+    for j in range(n):
+        k = np.argmax(primasum(primapow2(T[j:n+1, j:m+1]), axis=1), axis=0)
+        if k > 0 and k <= n - j - 1:
+            k += j
+            P[j], P[k] = P[k], P[j]
+            T[[j, k], :] = T[[k, j], :]
+        for i in range(m-1, j, -1):
+            G = planerot(T[j, [j, i]]).T
+            T[j, [j, i]] = np.append(hypot(T[j, j], T[j, i]), 0)
+            T[j + 1:n + 1, [j, i]] = matprod(T[j + 1:n + 1, [j, i]], G)
+            Q[:, [j, i]] = matprod(Q[:, [j, i]], G)
+
+    R = T.T
+
+    return Q, R, P
+
+
+def primasum(x, axis=None):
+    '''
+    According to its documentation, np.sum will sometimes do partial pairwise summation.
+    For our purposes, when comparing, we want don't want to do anything fancy, and we
+    just want to add things up one at a time.
+    '''
+    if not USE_NAIVE_MATH:
+        return np.sum(x, axis=axis)
+    if axis is None:
+        if x.ndim == 2:
+            # Sum columns first, then sum the result
+            return sum(primasum(x, axis=0))
+        else:
+            return sum(x)
+    elif axis == 0:
+        result = np.zeros(x.shape[1])
+        for i in range(x.shape[1]):
+            result[i] = sum(x[:, i])
+        return result
+    elif axis == 1:
+        result = np.zeros(x.shape[0])
+        for i in range(x.shape[0]):
+            result[i] = sum(x[i, :])
+        return result
+
+
+def primapow2(x):
+    '''
+    Believe it or now, x**2 is not always the same as x*x in Python. In Fortran they
+    appear to be identical. Here's a quick one-line to find an example on your system
+    (well, two liner after importing numpy):
+    list(filter(lambda x: x[1], [(x:=np.random.random(), x**2 - x*x != 0) for _ in range(10000)]))
+    '''
+    return x*x
+
+
+def planerot(x):
+    '''
+    As in MATLAB, planerot(x) returns a 2x2 Givens matrix G for x in R2 so that Y=G@x has Y[1] = 0.
+    Roughly speaking, G = np.array([[x[0]/R, x[1]/R], [-x[1]/R, x[0]/R]]), where R = np.linalg.norm(x).
+    0. We need to take care of the possibilities of R=0, Inf, NaN, and over/underflow.
+    1. The G defined above is continuous with respect to X except at 0. Following this definition,
+    G = np.array([[np.sign(x[0]), 0], [0, np.sign(x[0])]]) if x[1] == 0,
+    G = np.array([[0, np.sign(x[1])], [np.sign(x[1]), 0]]) if x[0] == 0
+    Yet some implementations ignore the signs, leading to discontinuity and numerical instability.
+    2. Difference from MATLAB: if x contains NaN of consists of only Inf, MATLAB returns a NaN matrix,
+    but we return an identity matrix or a matrix of +/-np.sqrt(2). We intend to keep G always orthogonal.
+    '''
+
+    # Preconditions
+    if DEBUGGING:
+        assert len(x) == 2, "x must be a 2-vector"
+
+    # ==================
+    # Calculation starts
+    # ==================
+
+    # Define C = X(1) / R and S = X(2) / R with R = HYPOT(X(1), X(2)). Handle Inf/NaN, over/underflow.
+    if (any(np.isnan(x))):
+        # In this case, MATLAB sets G to NaN(2, 2). We refrain from doing so to keep G orthogonal.
+        c = 1
+        s = 0
+    elif (all(np.isinf(x))):
+        # In this case, MATLAB sets G to NaN(2, 2). We refrain from doing so to keep G orthogonal.
+        c = 1 / np.sqrt(2) * np.sign(x[0])
+        s = 1 / np.sqrt(2) * np.sign(x[1])
+    elif (abs(x[0]) <= 0 and abs(x[1]) <= 0): # X(1) == 0 == X(2).
+        c = 1
+        s = 0
+    elif (abs(x[1]) <= EPS * abs(x[0])):
+        # N.B.:
+        # 0. With <= instead of <, this case covers X(1) == 0 == X(2), which is treated above separately
+        # to avoid the confusing SIGN(., 0) (see 1).
+        # 1. SIGN(A, 0) = ABS(A) in Fortran but sign(0) = 0 in MATLAB, Python, Julia, and R#
+        # 2. Taking SIGN(X(1)) into account ensures the continuity of G with respect to X except at 0.
+        c = np.sign(x[0])
+        s = 0
+    elif (abs(x[0]) <= EPS * abs(x[1])):
+        # N.B.: SIGN(A, X) = ABS(A) * sign of X /= A * sign of X # Therefore, it is WRONG to define G
+        # as SIGN(RESHAPE([ZERO, -ONE, ONE, ZERO], [2, 2]), X(2)). This mistake was committed on
+        # 20211206 and took a whole day to debug! NEVER use SIGN on arrays unless you are really sure.
+        c = 0
+        s = np.sign(x[1])
+    else:
+        # Here is the normal case. It implements the Givens rotation in a stable & continuous way as in:
+        # Bindel, D., Demmel, J., Kahan, W., and Marques, O. (2002). On computing Givens rotations
+        # reliably and efficiently. ACM Transactions on Mathematical Software (TOMS), 28(2), 206-238.
+        # N.B.: 1. Modern compilers compute SQRT(REALMIN) and SQRT(REALMAX/2.1) at compilation time.
+        # 2. The direct calculation without involving T and U seems to work better; use it if possible.
+        if (all(np.logical_and(np.sqrt(REALMIN) < np.abs(x), np.abs(x) < np.sqrt(REALMAX / 2.1)))):
+            # Do NOT use HYPOTENUSE here; the best implementation for one may be suboptimal for the other
+            r = norm(x)
+            c = x[0] / r
+            s = x[1] / r
+        elif (abs(x[0]) > abs(x[1])):
+            t = x[1] / x[0]
+            u = max(1, abs(t), np.sqrt(1 + t*t))  # MAXVAL: precaution against rounding error.
+            u *= np.sign(x[0]) ##MATLAB: u = sign(x(1))*sqrt(1 + t**2)
+            c = 1 / u
+            s = t / u
+        else:
+            t = x[0] / x[1]
+            u = max([1, abs(t), np.sqrt(1 + t*t)])  # MAXVAL: precaution against rounding error.
+            u *= np.sign(x[1]) ##MATLAB: u = sign(x(2))*sqrt(1 + t**2)
+            c = t / u
+            s = 1 / u
+
+    G = np.array([[c, s], [-s, c]]) #  MATLAB: G = [c, s; -s, c]
+
+    #====================#
+    #  Calculation ends  #
+    #====================#
+
+    # Postconditions
+    if DEBUGGING:
+        assert G.shape == (2,2)
+        assert np.all(np.isfinite(G))
+        assert abs(G[0, 0] - G[1, 1]) + abs(G[0, 1] + G[1, 0]) <= 0
+        tol = np.maximum(1.0E-10, np.minimum(1.0E-1, 1.0E6 * EPS))
+        assert isorth(G, tol)
+        if all(np.logical_and(np.isfinite(x), np.abs(x) < np.sqrt(REALMAX / 2.1))):
+            r = np.linalg.norm(x)
+            assert max(abs(G@x - [r, 0])) <= max(tol, tol * r), 'G @ X = [||X||, 0]'
+
+    return G
+
+
+def isminor(x, ref):
+    '''
+    This function tests whether x is minor compared to ref. It is used by Powell, e.g., in COBYLA.
+    In precise arithmetic, isminor(x, ref) is true if and only if x == 0; in floating point
+    arithmetic, isminor(x, ref) is true if x is 0 or its nonzero value can be attributed to
+    computer rounding errors according to ref.
+    Larger sensitivity means the function is more strict/precise, the value 0.1 being due to Powell.
+
+    For example:
+    isminor(1e-20, 1e300) -> True, because in floating point arithmetic 1e-20 cannot be added to
+    1e300 without being rounded to 1e300.
+    isminor(1e300, 1e-20) -> False, because in floating point arithmetic adding 1e300 to 1e-20
+    dominates the latter number.
+    isminor(3, 4) -> False, because 3 can be added to 4 without being rounded off
+    '''
+
+    sensitivity = 0.1
+    refa = abs(ref) + sensitivity * abs(x)
+    refb = abs(ref) + 2 * sensitivity * abs(x)
+    return np.logical_or(abs(ref) >= refa, refa >= refb)
+
+
+def isinv(A, B, tol=None):
+    '''
+    This procedure tests whether A = B^{-1} up to the tolerance TOL.
+    '''
+
+    # Sizes
+    n = np.size(A, 0)
+
+    # Preconditions
+    if DEBUGGING:
+        assert np.size(A, 0) == np.size(A, 1)
+        assert np.size(B, 0) == np.size(B, 1)
+        assert np.size(A, 0) == np.size(B, 0)
+        if present(tol):
+            assert tol >= 0
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    tol = tol if present(tol) else np.minimum(1e-3, 1e2 * EPS * np.maximum(np.size(A, 0), np.size(A, 1)))
+    tol = np.max([tol, tol * np.max(abs(A)), tol * np.max(abs(B))])
+    is_inv = ((abs(matprod(A, B)) - np.eye(n)) <= tol).all() or ((abs(matprod(B, A) - np.eye(n))) <= tol).all()
+
+    #===================#
+    #  Calculation ends #
+    #===================#
+    return is_inv
+
+
+def isorth(A, tol=None):
+    '''
+    This function tests whether the matrix A has orthonormal columns up to the tolerance TOL.
+    '''
+
+    # Preconditions
+    if DEBUGGING:
+        if present(tol):
+            assert tol >= 0
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    num_vars = np.size(A, 1)
+
+    if num_vars > np.size(A, 0):
+        is_orth = False
+    elif (np.isnan(primasum(abs(A)))):
+        is_orth = False
+    else:
+        if present(tol):
+            is_orth = (abs(matprod(A.T, A) - np.eye(num_vars)) <= np.maximum(tol, tol * np.max(abs(A)))).all()
+        else:
+            is_orth = (abs(matprod(A.T, A) - np.eye(num_vars)) <= 0).all()
+
+    #====================#
+    #  Calculation ends  #
+    #====================#
+    return is_orth
+
+
+def get_arrays_tol(*arrays):
+    """
+    Get a relative tolerance for a set of arrays. Borrowed from COBYQA
+
+    Parameters
+    ----------
+    *arrays: tuple
+        Set of `numpy.ndarray` to get the tolerance for.
+
+    Returns
+    -------
+    float
+        Relative tolerance for the set of arrays.
+
+    Raises
+    ------
+    ValueError
+        If no array is provided.
+    """
+    if len(arrays) == 0:
+        raise ValueError("At least one array must be provided.")
+    size = max(array.size for array in arrays)
+    weight = max(
+        np.max(np.abs(array[np.isfinite(array)]), initial=1.0)
+        for array in arrays
+    )
+    return 10.0 * EPS * max(size, 1.0) * weight
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/message.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/message.py
new file mode 100644
index 0000000000000000000000000000000000000000..899d5fa63daa2b55bc6a1050f27786b569c0e92c
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/message.py
@@ -0,0 +1,290 @@
+'''
+This module provides some functions that print messages to terminal/files.
+
+Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
+
+Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
+
+Python translation by Nickolai Belakovski.
+
+N.B.:
+1. In case parallelism is desirable (especially during initialization), the functions may
+have to be modified or disabled due to the IO operations.
+2. IPRINT indicates the level of verbosity, which increases with the absolute value of IPRINT.
+IPRINT = +/-3 can be expensive due to high IO operations.
+'''
+
+from .consts import DEBUGGING
+from .infos import FTARGET_ACHIEVED, MAXFUN_REACHED, MAXTR_REACHED, \
+    SMALL_TR_RADIUS, TRSUBP_FAILED, NAN_INF_F, NAN_INF_X, NAN_INF_MODEL, DAMAGING_ROUNDING, \
+    NO_SPACE_BETWEEN_BOUNDS, ZERO_LINEAR_CONSTRAINT, CALLBACK_TERMINATE
+from .present import present
+import numpy as np
+
+spaces = '   '
+
+
+def get_info_string(solver, info):
+    if info == FTARGET_ACHIEVED:
+        reason = 'the target function value is achieved.'
+    elif info == MAXFUN_REACHED:
+        reason = 'the objective function has been evaluated MAXFUN times.'
+    elif info == MAXTR_REACHED:
+        reason = 'the maximal number of trust region iterations has been reached.'
+    elif info == SMALL_TR_RADIUS:
+        reason = 'the trust region radius reaches its lower bound.'
+    elif info == TRSUBP_FAILED:
+        reason = 'a trust region step has failed to reduce the quadratic model.'
+    elif info == NAN_INF_X:
+        reason = 'NaN or Inf occurs in x.'
+    elif info == NAN_INF_F:
+        reason = 'the objective function returns NaN/+Inf.'
+    elif info == NAN_INF_MODEL:
+        reason = 'NaN or Inf occurs in the models.'
+    elif info == DAMAGING_ROUNDING:
+        reason = 'rounding errors are becoming damaging.'
+    elif info == NO_SPACE_BETWEEN_BOUNDS:
+        reason = 'there is no space between the lower and upper bounds of variable.'
+    elif info == ZERO_LINEAR_CONSTRAINT:
+        reason = 'one of the linear constraints has a zero gradient'
+    elif info == CALLBACK_TERMINATE:
+        reason = 'the callback function requested termination'
+    else:
+        reason = 'UNKNOWN EXIT FLAG'
+    ret_message = f'Return from {solver} because {reason.strip()}'
+    return ret_message
+
+
+def retmsg(solver, info, iprint, nf, f, x, cstrv=None, constr=None):
+    '''
+    This function prints messages at return.
+    '''
+    # Local variables
+    valid_exit_codes = [FTARGET_ACHIEVED, MAXFUN_REACHED, MAXTR_REACHED,
+        SMALL_TR_RADIUS, TRSUBP_FAILED, NAN_INF_F, NAN_INF_X, NAN_INF_MODEL, DAMAGING_ROUNDING,
+        NO_SPACE_BETWEEN_BOUNDS, ZERO_LINEAR_CONSTRAINT, CALLBACK_TERMINATE]
+
+    # Preconditions
+    if DEBUGGING:
+        assert info in valid_exit_codes
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    if abs(iprint) < 1:  # No printing (iprint == 0)
+        return
+    elif iprint > 0:  # Print the message to the standard out.
+        fname = ''
+    else:  # Print the message to a file named FNAME.
+        fname = f'{solver}_output.txt'
+
+    # Decide whether the problem is truly constrained.
+    if present(constr):
+        is_constrained = (np.size(constr) > 0)
+    else:
+        is_constrained = present(cstrv)
+
+    # Decide the constraint violation.
+    if present(cstrv):
+        cstrv_loc = cstrv
+    elif present(constr):
+        cstrv_loc = np.max(np.append(0, -constr))  # N.B.: We assume that the constraint is CONSTR >= 0.
+    else:
+        cstrv_loc = 0
+
+    # Decide the return message.
+    ret_message = get_info_string(solver, info)
+
+    if np.size(x) <= 2:
+        x_message = f'\nThe corresponding X is: {x}'  # Printed in one line
+    else:
+        x_message = f'\nThe corresponding X is:\n{x}'
+
+    if is_constrained:
+        nf_message = (f'\nNumber of function values = {nf}{spaces}'
+            f'Least value of F = {f}{spaces}Constraint violation = {cstrv_loc}')
+    else:
+        nf_message = f'\nNumber of function values = {nf}{spaces}Least value of F = {f}'
+
+    if is_constrained and present(constr):
+        if np.size(constr) <= 2:
+            constr_message = f'\nThe constraint value is: {constr}'  # Printed in one line
+        else:
+            constr_message = f'\nThe constraint value is:\n{constr}'
+    else:
+        constr_message = ''
+
+    # Print the message.
+    if abs(iprint) >= 2:
+        message = f'\n{ret_message}{nf_message}{x_message}{constr_message}\n'
+    else:
+        message = f'{ret_message}{nf_message}{x_message}{constr_message}\n'
+    if len(fname) > 0:
+        with open(fname, 'a') as f: f.write(message)
+    else:
+        print(message)
+
+
+def rhomsg(solver, iprint, nf, delta, f, rho, x, cstrv=None, constr=None, cpen=None):
+    '''
+    This function prints messages when RHO is updated.
+    '''
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    if abs(iprint) < 2:  # No printing
+        return
+    elif iprint > 0:  # Print the message to the standard out.
+        fname = ''
+    else:  # Print the message to a file named FNAME.
+        fname = f'{solver.strip()}_output.txt'
+
+    # Decide whether the problem is truly constrained.
+    if present(constr):
+        is_constrained = (np.size(constr) > 0)
+    else:
+        is_constrained = present(cstrv)
+
+    # Decide the constraint violation.
+    if present(cstrv):
+        cstrv_loc = cstrv
+    elif present(constr):
+        cstrv_loc = np.max(np.append(0, -constr))  # N.B.: We assume that the constraint is CONSTR >= 0.
+    else:
+        cstrv_loc = 0
+
+    if present(cpen):
+        rho_message = (f'\nNew RHO = {rho}{spaces}Delta = {delta}{spaces}'
+            f'CPEN = {cpen}')
+    else:
+        rho_message = f'\nNew RHO = {rho}{spaces}Delta = {delta}'
+
+    if np.size(x) <= 2:
+        x_message = f'\nThe corresponding X is: {x}'  # Printed in one line
+    else:
+        x_message = f'\nThe corresponding X is:\n{x}'
+
+    if is_constrained:
+        nf_message = (f'\nNumber of function values = {nf}{spaces}'
+            f'Least value of F = {f}{spaces}Constraint violation = {cstrv_loc}')
+    else:
+        nf_message = f'\nNumber of function values = {nf}{spaces}Least value of F = {f}'
+
+    if is_constrained and present(constr):
+        if np.size(constr) <= 2:
+            constr_message = f'\nThe constraint value is: {constr}'  # Printed in one line
+        else:
+            constr_message = f'\nThe constraint value is:\n{constr}'
+    else:
+        constr_message = ''
+
+    # Print the message.
+    if abs(iprint) >= 3:
+        message = f'\n{rho_message}{nf_message}{x_message}{constr_message}'
+    else:
+        message = f'{rho_message}{nf_message}{x_message}{constr_message}'
+    if len(fname) > 0:
+        with open(fname, 'a') as f: f.write(message)
+    else:
+        print(message)
+
+    #====================#
+    #  Calculation ends  #
+    #====================#
+
+
+def cpenmsg(solver, iprint, cpen):
+    '''
+    This function prints a message when CPEN is updated.
+    '''
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    if abs(iprint) < 2:  # No printing
+        return
+    elif iprint > 0:  # Print the message to the standard out.
+        fname = ''
+    else:  # Print the message to a file named FNAME.
+        fname = f'{solver.strip()}_output.txt'
+
+    # Print the message.
+    if abs(iprint) >= 3:
+        message = f'\nSet CPEN to {cpen}'
+    else:
+        message = f'\n\nSet CPEN to {cpen}'
+    if len(fname) > 0:
+        with open(fname, 'a') as f: f.write(message)
+    else:
+        print(message)
+
+    #====================#
+    #  Calculation ends  #
+    #====================#
+
+
+def fmsg(solver, state, iprint, nf, delta, f, x, cstrv=None, constr=None):
+    '''
+    This subroutine prints messages for each evaluation of the objective function.
+    '''
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    if abs(iprint) < 2:  # No printing
+        return
+    elif iprint > 0:  # Print the message to the standard out.
+        fname = ''
+    else:  # Print the message to a file named FNAME.
+        fname = f'{solver.strip()}_output.txt'
+
+    # Decide whether the problem is truly constrained.
+    if present(constr):
+        is_constrained = (np.size(constr) > 0)
+    else:
+        is_constrained = present(cstrv)
+
+    # Decide the constraint violation.
+    if present(cstrv):
+        cstrv_loc = cstrv
+    elif present(constr):
+        cstrv_loc = np.max(np.append(0, -constr))  # N.B.: We assume that the constraint is CONSTR >= 0.
+    else:
+        cstrv_loc = 0
+
+    delta_message = f'\n{state} step with radius = {delta}'
+
+    if is_constrained:
+        nf_message = (f'\nNumber of function values = {nf}{spaces}'
+            f'Least value of F = {f}{spaces}Constraint violation = {cstrv_loc}')
+    else:
+        nf_message = f'\nNumber of function values = {nf}{spaces}Least value of F = {f}'
+
+    if np.size(x) <= 2:
+        x_message = f'\nThe corresponding X is: {x}'  # Printed in one line
+    else:
+        x_message = f'\nThe corresponding X is:\n{x}'
+
+    if is_constrained and present(constr):
+        if np.size(constr) <= 2:
+            constr_message = f'\nThe constraint value is: {constr}'  # Printed in one line
+        else:
+            constr_message = f'\nThe constraint value is:\n{constr}'
+    else:
+        constr_message = ''
+
+    # Print the message.
+    message = f'{delta_message}{nf_message}{x_message}{constr_message}'
+    if len(fname) > 0:
+        with open(fname, 'a') as f: f.write(message)
+    else:
+        print(message)
+
+    #====================#
+    #  Calculation ends  #
+    #====================#
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/powalg.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/powalg.py
new file mode 100644
index 0000000000000000000000000000000000000000..1a8268b2631ba830f8bd36129dd10a3fb9133dc1
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/powalg.py
@@ -0,0 +1,131 @@
+'''
+This module provides some Powell-style linear algebra procedures.
+
+Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
+
+Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
+
+Python translation by Nickolai Belakovski.
+'''
+
+import numpy as np
+from .linalg import isminor, planerot, matprod, inprod, hypot
+from .consts import DEBUGGING, EPS
+
+
+def qradd_Rdiag(c, Q, Rdiag, n):
+    '''
+    This function updates the QR factorization of an MxN matrix A of full column rank, attempting to
+    add a new column C to this matrix as the LAST column while maintaining the full-rankness.
+    Case 1. If C is not in range(A) (theoretically, it implies N < M), then the new matrix is np.hstack([A, C])
+    Case 2. If C is in range(A), then the new matrix is np.hstack([A[:, :n-1], C])
+    N.B.:
+    0. Instead of R, this subroutine updates Rdiag, which is np.diag(R), with a size at most M and at
+    least min(m, n+1). The number is min(m, n+1) rather than min(m, n) as n may be augmented by 1 in
+    the function.
+    1. With the two cases specified as above, this function does not need A as an input.
+    2. The function changes only Q[:, nsave:m] (nsave is the original value of n) and
+    R[:, n-1] (n takes the updated value)
+    3. Indeed, when C is in range(A), Powell wrote in comments that "set iOUT to the index of the
+    constraint (here, column of A --- Zaikun) to be deleted, but branch if no suitable index can be
+    found". The idea is to replace a column of A by C so that the new matrix still has full rank
+    (such a column must exist unless C = 0). But his code essentially sets iout=n always. Maybe he
+    found this worked well enough in practice. Meanwhile, Powell's code includes a snippet that can
+    never be reached, which was probably intended to deal with the case that IOUT != n
+    '''
+    m = Q.shape[1]
+    nsave = n  # Needed for debugging (only)
+
+    # As in Powell's COBYLA, CQ is set to 0 at the positions with CQ being negligible as per ISMINOR.
+    # This may not be the best choice if the subroutine is used in other contexts, e.g. LINCOA.
+    cq = matprod(c, Q)
+    cqa = matprod(abs(c), abs(Q))
+    # The line below basically makes an element of cq 0 if adding it to the corresponding element of
+    # cqa does not change the latter.
+    cq = np.array([0 if isminor(cqi, cqai) else cqi for cqi, cqai in zip(cq, cqa)])
+
+    # Update Q so that the columns of Q[:, n+1:m] are orthogonal to C. This is done by applying a 2D
+    # Givens rotation to Q[:, [k, k+1]] from the right to zero C' @ Q[:, k+1] out for K=n+1, ... m-1.
+    # Nothing will be done if n >= m-1
+    for k in range(m-2, n-1, -1):
+        if abs(cq[k+1]) > 0:
+            # Powell wrote cq[k+1] != 0 instead of abs. The two differ if cq[k+1] is NaN.
+            # If we apply the rotation below when cq[k+1] = 0, then cq[k] will get updated to |cq[k]|.
+            G = planerot(cq[k:k+2])
+            Q[:, [k, k+1]] = matprod(Q[:, [k, k+1]], G.T)
+            cq[k] = hypot(*cq[k:k+2])
+
+    # Augment n by 1 if C is not in range(A)
+    if n < m:
+        # Powell's condition for the following if: cq[n+1] != 0
+        if abs(cq[n]) > EPS**2 and not isminor(cq[n], cqa[n]):
+            n += 1
+
+    # Update Rdiag so that Rdiag[n] = cq[n] = np.dot(c, q[:, n]). Note that N may be been augmented.
+    if n - 1 >= 0 and n - 1 < m:  # n >= m should not happen unless the input is wrong
+        Rdiag[n - 1] = cq[n - 1]
+
+    if DEBUGGING:
+        assert nsave <= n <= min(nsave + 1, m)
+        assert n <= len(Rdiag) <= m
+        assert Q.shape == (m, m)
+
+    return Q, Rdiag, n
+
+
+def qrexc_Rdiag(A, Q, Rdiag, i):  # Used in COBYLA
+    '''
+    This function updates the QR factorization for an MxN matrix A=Q@R so that the updated Q and
+    R form a QR factorization of [A_0, ..., A_{I-1}, A_{I+1}, ..., A_{N-1}, A_I] which is the matrix
+    obtained by rearranging columns [I, I+1, ... N-1] of A to [I+1, ..., N-1, I]. Here A is ASSUMED TO
+    BE OF FULL COLUMN RANK, Q is a matrix whose columns are orthogonal, and R, which is not present,
+    is an upper triangular matrix whose diagonal entries are nonzero. Q and R need not be square.
+    N.B.:
+    0. Instead of R, this function updates Rdiag, which is np.diag(R), the size being n.
+    1. With L = Q.shape[1] = R.shape[0], we have M >= L >= N. Most often L = M or N.
+    2. This function changes only Q[:, i:] and Rdiag[i:]
+    3. (NDB 20230919) In Python, i is either icon or nact - 2, whereas in FORTRAN it is either icon or nact - 1.
+    '''
+
+    # Sizes
+    m, n = A.shape
+
+    # Preconditions
+    assert n >= 1 and n <= m
+    assert i >= 0 and i < n
+    assert len(Rdiag) == n
+    assert Q.shape[0] == m and Q.shape[1] >= n and Q.shape[1] <= m
+    # tol = max(1.0E-8, min(1.0E-1, 1.0E8 * EPS * m + 1))
+    # assert isorth(Q, tol)  # Costly!
+
+
+    if i < 0 or i >= n:
+        return Q, Rdiag
+
+    # Let R be the upper triangular matrix in the QR factorization, namely R = Q.T@A.
+    # For each k, find the Givens rotation G with G@(R[k:k+2, :]) = [hypt, 0], and update Q[:, k:k+2]
+    # to Q[:, k:k+2]@(G.T). Then R = Q.T@A is an upper triangular matrix as long as A[:, [k, k+1]] is
+    # updated to A[:, [k+1, k]]. Indeed, this new upper triangular matrix can be obtained by first
+    # updating R[[k, k+1], :] to G@(R[[k, k+1], :]) and then exchanging its columns K and K+1; at the same
+    # time, entries k and k+1 of R's diagonal Rdiag become [hypt, -(Rdiag[k+1]/hypt)*RDiag[k]].
+    # After this is done for each k = 0, ..., n-2, we obtain the QR factorization of the matrix that
+    # rearranges columns [i, i+1, ... n-1] of A as [i+1, ..., n-1, i].
+    # Powell's code, however, is slightly different: before everything, he first exchanged columns k and
+    # k+1 of Q (as well as rows k and k+1 of R). This makes sure that the entries of the update Rdiag
+    # are all positive if it is the case for the original Rdiag.
+    for k in range(i, n-1):
+        G = planerot([Rdiag[k+1], inprod(Q[:, k], A[:, k+1])])
+        Q[:, [k, k+1]] = matprod(Q[:, [k+1, k]], (G.T))
+        # Powell's code updates Rdiag in the following way:
+        # hypt = np.sqrt(Rdiag[k+1]**2 + np.dot(Q[:, k], A[:, k+1])**2)
+        # Rdiag[[k, k+1]] = [hypt, (Rdiag[k+1]/hypt)*Rdiag[k]]
+        # Note that Rdiag[n-1] inherits all rounding in Rdiag[i:n-1] and Q[:, i:n-1] and hence contains
+        # significant errors. Thus we may modify Powell's code to set only Rdiag[k] = hypt here and then
+        # calculate Rdiag[n] by an inner product after the loop. Nevertheless, we simple calculate RDiag
+        # from scratch below.
+
+    # Calculate Rdiag(i:n) from scratch
+    Rdiag[i:n-1] = [inprod(Q[:, k], A[:, k+1]) for k in range(i, n-1)]
+    Rdiag[n-1] = inprod(Q[:, n-1], A[:, i])
+
+    return Q, Rdiag
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/preproc.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/preproc.py
new file mode 100644
index 0000000000000000000000000000000000000000..465b0b2de289b61be4c560dec89482887002aa24
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/preproc.py
@@ -0,0 +1,277 @@
+'''
+This is a module that preprocesses the inputs.
+
+Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
+
+Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
+
+Python translation by Nickolai Belakovski.
+'''
+
+from .consts import DEBUGGING, EPS, IPRINT_DEFAULT, FTARGET_DEFAULT, \
+    MIN_MAXFILT, MAXFILT_DEFAULT, MAXHISTMEM, ETA1_DEFAULT, ETA2_DEFAULT, \
+    GAMMA1_DEFAULT, GAMMA2_DEFAULT, RHOBEG_DEFAULT, RHOEND_DEFAULT, \
+    CTOL_DEFAULT, CWEIGHT_DEFAULT
+from .present import present
+from warnings import warn
+import numpy as np
+
+
+def preproc(solver, num_vars, iprint, maxfun, maxhist, ftarget, rhobeg, rhoend,
+            num_constraints=None, npt=None, maxfilt=None, ctol=None, cweight=None,
+            eta1=None, eta2=None, gamma1=None, gamma2=None, is_constrained=None, has_rhobeg=None,
+            honour_x0=None, xl=None, xu=None, x0=None):
+    '''
+    This subroutine preprocesses the inputs. It does nothing to the inputs that are valid.
+    '''
+    # Preconditions
+    if DEBUGGING:
+        assert num_vars >= 1
+        if present(num_constraints):
+            assert num_constraints >= 0
+            assert num_constraints == 0 or solver.lower() == 'cobyla'
+        if solver.lower() == 'cobyla' and present(num_constraints) and present(is_constrained):
+            assert num_constraints == 0 or is_constrained
+        if solver.lower() == 'bobyqa':
+            assert present(xl) and present(xu)
+            assert all(xu - xl >= 2 * EPS)
+        if present(honour_x0):
+            assert solver.lower() == 'bobyqa' and present(has_rhobeg) and present(xl) and present(xu) and present(x0)
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    # Read num_constraints, if necessary
+    num_constraints = num_constraints if (present(num_constraints) and solver.lower() == 'cobyla') else 0
+
+    # Decide whether the problem is truly constrained
+    is_constrained = is_constrained if (present(is_constrained)) else num_constraints > 0
+
+    # Validate IPRINT
+    if np.abs(iprint) > 3:
+        iprint = IPRINT_DEFAULT
+        warn(f'{solver}: Invalid IPRINT; it should be 0, 1, -1, 2, -2, 3, or -3; it is set to {iprint}')
+
+    # Validate MAXFUN
+    if solver.lower() == 'uobyqa':
+        min_maxfun = (num_vars + 1) * (num_vars + 2) / 2 + 1
+        min_maxfun_str = '(N+1)(N+2)/2 + 1'
+    elif solver.lower() == 'cobyla':
+        min_maxfun = num_vars + 2
+        min_maxfun_str = 'num_vars + 2'
+    else:  # CASE ('NEWUOA', 'BOBYQA', 'LINCOA')
+        min_maxfun = num_vars + 3
+        min_maxfun_str = 'num_vars + 3'
+    if maxfun < min_maxfun:
+        maxfun = min_maxfun
+        warn(f'{solver}: Invalid MAXFUN; it should be at least {min_maxfun_str}; it is set to {maxfun}')
+
+    # Validate MAXHIST
+    if maxhist < 0:
+        maxhist = maxfun
+        warn(f'{solver}: Invalid MAXHIST; it should be a nonnegative integer; it is set to {maxhist}')
+    maxhist = min(maxhist, maxfun)  # MAXHIST > MAXFUN is never needed.
+
+    # Validate FTARGET
+    if np.isnan(ftarget):
+        ftarget = FTARGET_DEFAULT
+        warn(f'{solver}: Invalid FTARGET; it should be a real number; it is set to {ftarget}')
+
+    # Validate NPT
+    if (solver.lower() == 'newuoa' or solver.lower() == 'bobyqa' or solver.lower() == 'lincoa') and present(npt):
+        if (npt < num_vars + 2 or npt > min(maxfun - 1, ((num_vars + 2) * (num_vars + 1)) / 2)):
+            npt = int(min(maxfun - 1, 2 * num_vars + 1))
+            warn(f'{solver}: Invalid NPT; it should be an integer in the interval [N+2, (N+1)(N+2)/2] and less than MAXFUN; it is set to {npt}')
+
+    # Validate MAXFILT
+    if present(maxfilt) and (solver.lower() == 'lincoa' or solver.lower() == 'cobyla'):
+        maxfilt_in = maxfilt
+        if maxfilt < 1:
+            maxfilt = MAXFILT_DEFAULT  # The inputted MAXFILT is obviously wrong.
+        else:
+            maxfilt = max(MIN_MAXFILT, maxfilt)  # The inputted MAXFILT is too small.
+        # Further revise MAXFILT according to MAXHISTMEM.
+        if solver.lower() == 'lincoa':
+            unit_memo = (num_vars + 2) * np.dtype(float).itemsize
+        elif solver.lower() == 'cobyla':
+            unit_memo = (num_constraints + num_vars + 2) * np.dtype(float).itemsize
+        else:
+            unit_memo = 1
+        # We cannot simply set MAXFILT = MIN(MAXFILT, MAXHISTMEM/...), as they may not have
+        # the same kind, and compilers may complain. We may convert them, but overflow may occur.
+        if maxfilt > MAXHISTMEM / unit_memo:
+            maxfilt = int(MAXHISTMEM / unit_memo)  # Integer division.
+        maxfilt = min(maxfun, max(MIN_MAXFILT, maxfilt))
+        if is_constrained:
+            if maxfilt_in < 1:
+                warn(f'{solver}: Invalid MAXFILT; it should be a positive integer; it is set to  {maxfilt}')
+            elif maxfilt_in < min(maxfun, MIN_MAXFILT):
+                warn(f'{solver}: MAXFILT is too small; it is set to {maxfilt}')
+            elif maxfilt < min(maxfilt_in, maxfun):
+                warn(f'{solver}: MAXFILT is set to {maxfilt} due to memory limit')
+
+    # Validate ETA1 and ETA2
+    eta1_local = eta1 if present(eta1) else ETA1_DEFAULT
+    eta2_local = eta2 if present(eta2) else ETA2_DEFAULT
+
+    # When the difference between ETA1 and ETA2 is tiny, we force them to equal.
+    # See the explanation around RHOBEG and RHOEND for the reason.
+    if present(eta1) and present(eta2):
+        if np.abs(eta1 - eta2) < 1.0E2 * EPS * max(np.abs(eta1), 1):
+            eta2 = eta1
+
+    if present(eta1):
+        if np.isnan(eta1):
+            # In this case, we take the value hard coded in Powell's original code
+            # without any warning. It is useful when interfacing with MATLAB/Python.
+            eta1 = ETA1_DEFAULT
+        elif eta1 < 0 or eta1 >= 1:
+            # Take ETA2 into account if it has a valid value.
+            if present(eta2) and eta2_local > 0 and eta2_local <= 1:
+                eta1 = max(EPS, eta2 / 7.0)
+            else:
+                eta1 = ETA1_DEFAULT
+            warn(f'{solver}: Invalid ETA1; it should be in the interval [0, 1) and not more than ETA2; it is set to {eta1}')
+
+    if present(eta2):
+        if np.isnan(eta2):
+            # In this case, we take the value hard coded in Powell's original code
+            # without any warning. It is useful when interfacing with MATLAB/Python.
+            eta2 = ETA2_DEFAULT
+        elif present(eta1) and (eta2 < eta1_local or eta2 > 1):
+            eta2 = (eta1 + 2) / 3.0
+            warn(f'{solver}: Invalid ETA2; it should be in the interval [0, 1) and not less than ETA1; it is set to {eta2}')
+
+    # Validate GAMMA1 and GAMMA2
+    if present(gamma1):
+        if np.isnan(gamma1):
+            # In this case, we take the value hard coded in Powell's original code
+            # without any warning. It is useful when interfacing with MATLAB/Python.
+            gamma1 = GAMMA1_DEFAULT
+        elif gamma1 <= 0 or gamma1 >= 1:
+            gamma1 = GAMMA1_DEFAULT
+            warn(f'{solver}: Invalid GAMMA1; it should in the interval (0, 1); it is set to {gamma1}')
+
+    if present(gamma2):
+        if np.isnan(gamma2):
+            # In this case, we take the value hard coded in Powell's original code
+            # without any warning. It is useful when interfacing with MATLAB/Python.
+            gamma2 = GAMMA2_DEFAULT
+        elif gamma2 < 1 or np.isinf(gamma2):
+            gamma2 = GAMMA2_DEFAULT
+            warn(f'{solver}: Invalid GAMMA2; it should be a real number not less than 1; it is set to {gamma2}')
+
+    # Validate RHOBEG and RHOEND
+
+    if np.abs(rhobeg - rhoend) < 1.0e2 * EPS * np.maximum(np.abs(rhobeg), 1):
+        # When the data is passed from the interfaces (e.g., MEX) to the Fortran code, RHOBEG, and RHOEND
+        # may change a bit. It was observed in a MATLAB test that MEX passed 1 to Fortran as
+        # 0.99999999999999978. Therefore, if we set RHOEND = RHOBEG in the interfaces, then it may happen
+        # that RHOEND > RHOBEG, which is considered as an invalid input. To avoid this situation, we
+        # force RHOBEG and RHOEND to equal when the difference is tiny.
+        rhoend = rhobeg
+
+    # Revise the default values for RHOBEG/RHOEND according to the solver.
+    if solver.lower() == 'bobyqa':
+        rhobeg_default = np.maximum(EPS, np.min(RHOBEG_DEFAULT, np.min(xu - xl) / 4.0))
+        rhoend_default = np.maximum(EPS, np.min(0.1 * rhobeg_default, RHOEND_DEFAULT))
+    else:
+        rhobeg_default = RHOBEG_DEFAULT
+        rhoend_default = RHOEND_DEFAULT
+
+    if solver.lower() == 'bobyqa':
+        # Do NOT merge the IF below into the ELIF above! Otherwise, XU and XL may be accessed even if
+        # the solver is not BOBYQA, because the logical evaluation is not short-circuit.
+        if rhobeg > np.min(xu - xl) / 2:
+            # Do NOT make this revision if RHOBEG not positive or not finite, because otherwise RHOBEG
+            # will get a huge value when XU or XL contains huge values that indicate unbounded variables.
+            rhobeg = np.min(xu - xl) / 4.0  # Here, we do not take RHOBEG_DEFAULT.
+            warn(f'{solver}: Invalid RHOBEG; {solver} requires 0 < RHOBEG <= np.min(XU-XL)/2; it is set to np.min(XU-XL)/4')
+    if rhobeg <= 0 or np.isnan(rhobeg) or np.isinf(rhobeg):
+        # Take RHOEND into account if it has a valid value. We do not do this if the solver is BOBYQA,
+        # which requires that RHOBEG <= (XU-XL)/2.
+        if np.isfinite(rhoend) and rhoend > 0 and solver.lower() != 'bobyqa':
+            rhobeg = max(10 * rhoend, rhobeg_default)
+        else:
+            rhobeg = rhobeg_default
+        warn(f'{solver}: Invalid RHOBEG; it should be a positive number; it is set to {rhobeg}')
+
+    if rhoend <= 0 or rhobeg < rhoend or np.isnan(rhoend) or np.isinf(rhoend):
+        rhoend = max(EPS, min(0.1 * rhobeg, rhoend_default))
+        warn(f'{solver}: Invalid RHOEND; it should be a positive number and RHOEND <= RHOBEG; it is set to {rhoend}')
+
+    # For BOBYQA, revise X0 or RHOBEG so that the distance between X0 and the inactive bounds is at
+    # least RHOBEG. If HONOUR_X0 == TRUE, revise RHOBEG if needed; otherwise, revise HONOUR_X0 if needed.
+    if present(honour_x0):
+        if honour_x0:
+            rhobeg_old = rhobeg;
+            lbx = np.isfinite(xl) & (x0 - xl <= EPS * np.maximum(1, np.abs(xl))) # X0 essentially equals XL
+            ubx = np.isfinite(xu) & (x0 - xu >= -EPS * np.maximum(1, np.abs(xu))) # X0 essentially equals XU
+            x0[lbx] = xl[lbx]
+            x0[ubx] = xu[ubx]
+            rhobeg = max(EPS, np.min([rhobeg, x0[~lbx] - xl[~lbx], xu[~ubx] - x0[~ubx]]))
+            if rhobeg_old - rhobeg > EPS * max(1, rhobeg_old):
+                rhoend = max(EPS, min(0.1 * rhobeg, rhoend)) # We do not revise RHOEND unless RHOBEG is truly revised.
+                if has_rhobeg:
+                    warn(f'{solver}: RHOBEG is revised to {rhobeg} and RHOEND to at most 0.1*RHOBEG so that the distance between X0 and the inactive bounds is at least RHOBEG')
+            else:
+                rhoend = np.minimum(rhoend, rhobeg)  # This may update RHOEND slightly.
+        else:
+            x0_old = x0  # Recorded to see whether X0 is really revised.
+            # N.B.: The following revision is valid only if XL <= X0 <= XU and RHOBEG <= MINVAL(XU-XL)/2,
+            # which should hold at this point due to the revision of RHOBEG and moderation of X0.
+            # The cases below are mutually exclusive in precise arithmetic as MINVAL(XU-XL) >= 2*RHOBEG.
+            lbx = x0 <= xl + 0.5 * rhobeg
+            lbx_plus = (x0 > xl + 0.5 * rhobeg) & (x0 < xl + rhobeg)
+            ubx = x0 >= xu - 0.5 * rhobeg
+            ubx_minus = (x0 < xu - 0.5 * rhobeg) & (x0 > xu - rhobeg)
+            x0[lbx] = xl[lbx]
+            x0[lbx_plus] = xl[lbx_plus] + rhobeg
+            x0[ubx] = xu[ubx]
+            x0[ubx_minus] = xu[ubx_minus] - rhobeg
+
+            if (any(np.abs(x0_old - x0) > 0)):
+                warn(f'{solver}: X0 is revised so that the distance between X0 and the inactive bounds is at least RHOBEG set HONOUR_X0 to .TRUE. if you prefer to keep X0 unchanged')
+
+    # Validate CTOL (it can be 0)
+    if (present(ctol)):
+        if (np.isnan(ctol) or ctol < 0):
+            ctol = CTOL_DEFAULT
+            if (is_constrained):
+                warn(f'{solver}: Invalid CTOL; it should be a nonnegative number; it is set to {ctol}')
+
+    # Validate CWEIGHT (it can be +Inf)
+    if (present(cweight)):
+        if (np.isnan(cweight) or cweight < 0):
+            cweight = CWEIGHT_DEFAULT
+            if (is_constrained):
+                warn(f'{solver}: Invalid CWEIGHT; it should be a nonnegative number; it is set to {cweight}')
+
+    #====================#
+    #  Calculation ends  #
+    #====================#
+
+    # Postconditions
+    if DEBUGGING:
+        assert abs(iprint) <= 3
+        assert maxhist >= 0 and maxhist <= maxfun
+        if present(npt):
+            assert maxfun >= npt + 1
+            assert npt >= 3
+        if present(maxfilt):
+            assert maxfilt >= np.minimum(MIN_MAXFILT, maxfun) and maxfilt <= maxfun
+        if present(eta1) and present(eta2):
+            assert eta1 >= 0 and eta1 <= eta2 and eta2 < 1
+        if present(gamma1) and present(gamma2):
+            assert gamma1 > 0 and gamma1 < 1 and gamma2 > 1
+        assert rhobeg >= rhoend and rhoend > 0
+        if solver.lower() == 'bobyqa':
+            assert all(rhobeg <= (xu - xl) / 2)
+            assert all(np.isfinite(x0))
+            assert all(x0 >= xl and (x0 <= xl or x0 >= xl + rhobeg))
+            assert all(x0 <= xu and (x0 >= xu or x0 <= xu - rhobeg))
+        if present(ctol):
+            assert ctol >= 0
+
+    return iprint, maxfun, maxhist, ftarget, rhobeg, rhoend, npt, maxfilt, ctol, cweight, eta1, eta2, gamma1, gamma2, x0
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/present.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/present.py
new file mode 100644
index 0000000000000000000000000000000000000000..15f8b47b6721994399ebafff419087afe582b2b8
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/present.py
@@ -0,0 +1,5 @@
+def present(x):
+    '''
+    This is a Python equivalent of the Fortran 'present' function for optional arguments.
+    '''
+    return x is not None
\ No newline at end of file
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/ratio.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/ratio.py
new file mode 100644
index 0000000000000000000000000000000000000000..c1c3a28ef4a49eeb47438c4a6ad3fb10e83dbc92
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/ratio.py
@@ -0,0 +1,54 @@
+'''
+This module calculates the reduction ratio for trust-region methods.
+
+Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
+
+Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
+
+Python translation by Nickolai Belakovski.
+'''
+
+from .consts import DEBUGGING, REALMAX
+import numpy as np
+
+def redrat(ared, pred, rshrink):
+    '''
+    This function evaluates the reduction ratio of a trust-region step, handling inf/nan properly.
+    '''
+
+    # Preconditions
+    if DEBUGGING:
+        assert rshrink >= 0
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    if np.isnan(ared):
+        # This should not happen in unconstrained problems due to the moderated extreme barrier.
+        ratio = -REALMAX
+    elif np.isnan(pred) or pred <= 0:
+        # The trust-region subproblem solver fails in this rare case. Instead of terminating as Powell's
+        # original code does, we set ratio as follows so that the solver may continue to progress.
+        if ared > 0:
+            # The trial point will be accepted, but the trust-region radius will be shrunk if rshrink>0
+            ratio = rshrink/2
+        else:
+            # Set the ration to a large negative number to signify a bad trust-region step, so that the
+            # solver will check whether to take a geometry step or reduce rho.
+            ratio = -REALMAX
+    elif np.isposinf(pred) and np.isposinf(ared):
+        ratio = 1  # ared/pred = NaN if calculated directly
+    elif np.isposinf(pred) and np.isneginf(ared):
+        ratio = -REALMAX  # ared/pred = NaN if calculated directly
+    else:
+        ratio = ared/pred
+
+    #==================#
+    # Calculation ends #
+    #==================#
+
+    # Postconditions
+    if DEBUGGING:
+        assert not np.isnan(ratio)
+    return ratio
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/redrho.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/redrho.py
new file mode 100644
index 0000000000000000000000000000000000000000..4b4c09b8400e694ed6f7016273aecfd384bf38a0
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/redrho.py
@@ -0,0 +1,47 @@
+'''
+This module provides a function that calculates RHO when it needs to be reduced.
+
+Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
+
+Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
+
+Python translation by Nickolai Belakovski.
+'''
+
+from .consts import DEBUGGING
+import numpy as np
+
+def redrho(rho_in, rhoend):
+    '''
+    This function calculates RHO when it needs to be reduced.
+    The scheme is shared by UOBYQA, NEWUOA, BOBYQA, LINCOA. For COBYLA, Powell's code reduces RHO by
+    'RHO *= 0.5; if RHO <= 1.5 * RHOEND: RHO = RHOEND' as specified in (11) of the COBYLA
+    paper. However, this scheme seems to work better, especially after we introduce DELTA.
+    '''
+
+    # Preconditions
+    if DEBUGGING:
+        assert rho_in > rhoend > 0
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    rho_ratio = rho_in / rhoend
+
+    if rho_ratio > 250:
+        rho = 0.1 * rho_in
+    elif rho_ratio <= 16:
+        rho = rhoend
+    else:
+        rho = np.sqrt(rho_ratio) * rhoend  # rho = np.sqrt(rho * rhoend)
+
+    #==================#
+    # Calculation ends #
+    #==================#
+
+    # Postconditions
+    if DEBUGGING:
+        assert rho_in > rho >= rhoend
+
+    return rho
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/selectx.py b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/selectx.py
new file mode 100644
index 0000000000000000000000000000000000000000..63d76ad709dac1c0a4d246ba24566f6c8e655124
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/pyprima/common/selectx.py
@@ -0,0 +1,296 @@
+'''
+This module provides subroutines that ensure the returned X is optimal among all the calculated
+points in the sense that no other point achieves both lower function value and lower constraint
+violation at the same time. This module is needed only in the constrained case.
+
+Translated from Zaikun Zhang's modern-Fortran reference implementation in PRIMA.
+
+Dedicated to late Professor M. J. D. Powell FRS (1936--2015).
+
+Python translation by Nickolai Belakovski.
+'''
+
+import numpy as np
+import numpy.typing as npt
+from .consts import DEBUGGING, EPS, CONSTRMAX, REALMAX, FUNCMAX
+from .present import present
+
+def isbetter(f1: float, c1: float, f2: float, c2: float, ctol: float) -> bool:
+    '''
+    This function compares whether FC1 = (F1, C1) is (strictly) better than FC2 = (F2, C2), which
+    basically means that (F1 < F2 and C1 <= C2) or (F1 <= F2 and C1 < C2).
+    It takes care of the cases where some of these values are NaN or Inf, even though some cases
+    should never happen due to the moderated extreme barrier.
+    At return, BETTER = TRUE if and only if (F1, C1) is better than (F2, C2).
+    Here, C means constraint violation, which is a nonnegative number.
+    '''
+
+    # Preconditions
+    if DEBUGGING:
+        assert not any(np.isnan([f1, c1]) | np.isposinf([f2, c2]))
+        assert not any(np.isnan([f2, c2]) | np.isposinf([f2, c2]))
+        assert c1 >= 0 and c2 >= 0
+        assert ctol >= 0
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    is_better = False
+    # Even though NaN/+Inf should not occur in FC1 or FC2 due to the moderated extreme barrier, for
+    # security and robustness, the code below does not make this assumption.
+    is_better = is_better or (any(np.isnan([f1, c1]) | np.isposinf([f1, c1])) and not any(np.isnan([f2, c2]) | np.isposinf([f2, c2])))
+
+    is_better = is_better or (f1 < f2 and c1 <= c2)
+    is_better = is_better or (f1 <= f2 and c1 < c2)
+
+    # If C1 <= CTOL and C2 is significantly larger/worse than CTOL, i.e., C2 > MAX(CTOL,CREF),
+    # then FC1 is better than FC2 as long as F1 < REALMAX. Normally CREF >= CTOL so MAX(CTOL, CREF)
+    # is indeed CREF. However, this may not be true if CTOL > 1E-1*CONSTRMAX.
+    cref = 10 * max(EPS, min(ctol, 1.0E-2 * CONSTRMAX))  # The MIN avoids overflow.
+    is_better = is_better or (f1 < REALMAX and c1 <= ctol and (c2 > max(ctol, cref) or np.isnan(c2)))
+
+    #==================#
+    # Calculation ends #
+    #==================#
+
+    # Postconditions
+    if DEBUGGING:
+        assert not (is_better and f1 >= f2 and c1 >= c2)
+        assert is_better or not (f1 <= f2 and c1 < c2)
+        assert is_better or not (f1 < f2 and c1 <= c2)
+
+    return is_better
+
+
+def savefilt(cstrv, ctol, cweight, f, x, nfilt, cfilt, ffilt, xfilt, constr=None, confilt=None):
+    '''
+    This subroutine saves X, F, and CSTRV in XFILT, FFILT, and CFILT (and CONSTR in CONFILT
+    if they are present), unless a vector in XFILT[:, :NFILT] is better than X.
+    If X is better than some vectors in XFILT[:, :NFILT] then these vectors will be
+    removed. If X is not better than any of XFILT[:, :NFILT], but NFILT == MAXFILT,
+    then we remove a column from XFILT according to the merit function
+    PHI = FFILT + CWEIGHT * max(CFILT - CTOL, 0)
+    N.B.:
+    1. Only XFILT[:, :NFILT] and FFILT[:, :NFILT] etc contains valid information,
+    while XFILT[:, NFILT+1:MAXFILT] and FFILT[:, NFILT+1:MAXFILT] etc are not
+    initialized yet.
+    2. We decide whether and X is better than another by the ISBETTER function
+    '''
+
+    # Sizes
+    if present(constr):
+        num_constraints = len(constr)
+    else:
+        num_constraints = 0
+    num_vars = len(x)
+    maxfilt = len(ffilt)
+
+    # Preconditions
+    if DEBUGGING:
+        # Check the size of X.
+        assert num_vars >= 1
+        # Check CWEIGHT and CTOL
+        assert cweight >= 0
+        assert ctol >= 0
+        # Check NFILT
+        assert nfilt >= 0 and nfilt <= maxfilt
+        # Check the sizes of XFILT, FFILT, CFILT.
+        assert maxfilt >= 1
+        assert np.size(xfilt, 0) == num_vars and np.size(xfilt, 1) == maxfilt
+        assert np.size(cfilt) == maxfilt
+        # Check the values of XFILT, FFILT, CFILT.
+        assert not (np.isnan(xfilt[:, :nfilt])).any()
+        assert not any(np.isnan(ffilt[:nfilt]) | np.isposinf(ffilt[:nfilt]))
+        assert not any(cfilt[:nfilt] < 0 | np.isnan(cfilt[:nfilt]) | np.isposinf(cfilt[:nfilt]))
+        # Check the values of X, F, CSTRV.
+        # X does not contain NaN if X0 does not and the trust-region/geometry steps are proper.
+        assert not any(np.isnan(x))
+        # F cannot be NaN/+Inf due to the moderated extreme barrier.
+        assert not (np.isnan(f) | np.isposinf(f))
+        # CSTRV cannot be NaN/+Inf due to the moderated extreme barrier.
+        assert not (cstrv < 0 | np.isnan(cstrv) | np.isposinf(cstrv))
+        # Check CONSTR and CONFILT.
+        assert present(constr) == present(confilt)
+        if present(constr):
+            # CONSTR cannot contain NaN/-Inf due to the moderated extreme barrier.
+            assert not any(np.isnan(constr) | np.isneginf(constr))
+            assert np.size(confilt, 0) == num_constraints and np.size(confilt, 1) == maxfilt
+            assert not (np.isnan(confilt[:, :nfilt]) | np.isneginf(confilt[:, :nfilt])).any()
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    # Return immediately if any column of XFILT is better than X.
+    if any((isbetter(ffilt_i, cfilt_i, f, cstrv, ctol) for ffilt_i, cfilt_i in zip(ffilt[:nfilt], cfilt[:nfilt]))) or \
+        any(np.logical_and(ffilt[:nfilt] <= f, cfilt[:nfilt] <= cstrv)):
+        return nfilt, cfilt, ffilt, xfilt, confilt
+
+    # Decide which columns of XFILT to keep.
+    keep = np.logical_not([isbetter(f, cstrv, ffilt_i, cfilt_i, ctol) for ffilt_i, cfilt_i in zip(ffilt[:nfilt], cfilt[:nfilt])])
+
+    # If NFILT == MAXFILT and X is not better than any column of XFILT, then we remove the worst column
+    # of XFILT according to the merit function PHI = FFILT + CWEIGHT * MAX(CFILT - CTOL, ZERO).
+    if sum(keep) == maxfilt:  # In this case, NFILT = SIZE(KEEP) = COUNT(KEEP) = MAXFILT > 0.
+        cfilt_shifted = np.maximum(cfilt - ctol, 0)
+        if cweight <= 0:
+            phi = ffilt
+        elif np.isposinf(cweight):
+            phi = cfilt_shifted
+            # We should not use CFILT here; if MAX(CFILT_SHIFTED) is attained at multiple indices, then
+            # we will check FFILT to exhaust the remaining degree of freedom.
+        else:
+            phi = np.maximum(ffilt, -REALMAX)
+            phi = np.nan_to_num(phi, nan=-REALMAX)  # Replace NaN with -REALMAX and +/- inf with large numbers
+            phi += cweight * cfilt_shifted
+        # We select X to maximize PHI. In case there are multiple maximizers, we take the one with the
+        # largest CSTRV_SHIFTED; if there are more than one choices, we take the one with the largest F;
+        # if there are several candidates, we take the one with the largest CSTRV; if the last comparison
+        # still leads to more than one possibilities, then they are equally bad and we choose the first.
+        # N.B.:
+        # 1. This process is the opposite of selecting KOPT in SELECTX.
+        # 2. In finite-precision arithmetic, PHI_1 == PHI_2 and CSTRV_SHIFTED_1 == CSTRV_SHIFTED_2 do
+        # not ensure that F_1 == F_2!
+        phimax = max(phi)
+        cref = max(cfilt_shifted[phi >= phimax])
+        fref = max(ffilt[cfilt_shifted >= cref])
+        kworst = np.ma.array(cfilt, mask=(ffilt > fref)).argmax()
+        if kworst < 0 or kworst >= len(keep):  #  For security. Should not happen.
+            kworst = 0
+        keep[kworst] = False
+
+    # Keep the good xfilt values and remove all the ones that are strictly worse than the new x.
+    nfilt = sum(keep)
+    index_to_keep = np.where(keep)[0]
+    xfilt[:, :nfilt] = xfilt[:, index_to_keep]
+    ffilt[:nfilt] = ffilt[index_to_keep]
+    cfilt[:nfilt] = cfilt[index_to_keep]
+    if confilt is not None and constr is not None:
+        confilt[:, :nfilt] = confilt[:, index_to_keep]
+
+    # Once we have removed all the vectors that are strictly worse than x,
+    # we add x to the filter.
+    xfilt[:, nfilt] = x
+    ffilt[nfilt] = f
+    cfilt[nfilt] = cstrv
+    if confilt is not None and constr is not None:
+        confilt[:, nfilt] = constr
+    nfilt += 1  # In Python we need to increment the index afterwards
+
+    #==================#
+    # Calculation ends #
+    #==================#
+
+    # Postconditions
+    if DEBUGGING:
+        # Check NFILT and the sizes of XFILT, FFILT, CFILT.
+        assert nfilt >= 1 and nfilt <= maxfilt
+        assert np.size(xfilt, 0) == num_vars and np.size(xfilt, 1) == maxfilt
+        assert np.size(ffilt) == maxfilt
+        assert np.size(cfilt) == maxfilt
+        # Check the values of XFILT, FFILT, CFILT.
+        assert not (np.isnan(xfilt[:, :nfilt])).any()
+        assert not any(np.isnan(ffilt[:nfilt]) | np.isposinf(ffilt[:nfilt]))
+        assert not any(cfilt[:nfilt] < 0 | np.isnan(cfilt[:nfilt]) | np.isposinf(cfilt[:nfilt]))
+        # Check that no point in the filter is better than X, and X is better than no point.
+        assert not any([isbetter(ffilt_i, cfilt_i, f, cstrv, ctol) for ffilt_i, cfilt_i in zip(ffilt[:nfilt], cfilt[:nfilt])])
+        assert not any([isbetter(f, cstrv, ffilt_i, cfilt_i, ctol) for ffilt_i, cfilt_i in zip(ffilt[:nfilt], cfilt[:nfilt])])
+        # Check CONFILT.
+        if present(confilt):
+            assert np.size(confilt, 0) == num_constraints and np.size(confilt, 1) == maxfilt
+            assert not (np.isnan(confilt[:, :nfilt]) | np.isneginf(confilt[:, :nfilt])).any()
+
+
+    return nfilt, cfilt, ffilt, xfilt, confilt
+
+
+def selectx(fhist: npt.NDArray, chist: npt.NDArray, cweight: float, ctol: float):
+    '''
+    This subroutine selects X according to FHIST and CHIST, which represents (a part of) history
+    of F and CSTRV. Normally, FHIST and CHIST are not the full history but only a filter, e.g. ffilt
+    and CFILT generated by SAVEFILT. However, we name them as FHIST and CHIST because the [F, CSTRV]
+    in a filter should not dominate each other, but this subroutine does NOT assume such a property.
+    N.B.: CTOL is the tolerance of the constraint violation (CSTRV). A point is considered feasible if
+    its constraint violation is at most CTOL. Not that CTOL is absolute, not relative.
+    '''
+
+    # Sizes
+    nhist = len(fhist)
+
+    # Preconditions
+    if DEBUGGING:
+        assert nhist >= 1
+        assert np.size(chist) == nhist
+        assert not any(np.isnan(fhist) | np.isposinf(fhist))
+        assert not any(chist < 0 | np.isnan(chist) | np.isposinf(chist))
+        assert cweight >= 0
+        assert ctol >= 0
+
+    #====================#
+    # Calculation starts #
+    #====================#
+
+    # We select X among the points with F < FREF and CSTRV < CREF.
+    # Do NOT use F <= FREF, because F == FREF (FUNCMAX or REALMAX) may mean F == INF in practice!
+    if any(np.logical_and(fhist < FUNCMAX, chist < CONSTRMAX)):
+        fref = FUNCMAX
+        cref = CONSTRMAX
+    elif any(np.logical_and(fhist < REALMAX, chist < CONSTRMAX)):
+        fref = REALMAX
+        cref = CONSTRMAX
+    elif any(np.logical_and(fhist < FUNCMAX, chist < REALMAX)):
+        fref = FUNCMAX
+        cref = REALMAX
+    else:
+        fref = REALMAX
+        cref = REALMAX
+
+    if not any(np.logical_and(fhist < fref, chist < cref)):
+        kopt = nhist - 1
+    else:
+        # Shift the constraint violations by ctol, so that cstrv <= ctol is regarded as no violation.
+        chist_shifted = np.maximum(chist - ctol, 0)
+        # cmin is the minimal shift constraint violation attained in the history.
+        cmin = np.min(chist_shifted[fhist < fref])
+        # We consider only the points whose shifted constraint violations are at most the cref below.
+        # N.B.: Without taking np.maximum(EPS, .), cref would be 0 if cmin = 0. In that case, asking for
+        # cstrv_shift < cref would be WRONG!
+        cref = np.maximum(EPS, 2*cmin)
+        # We use the following phi as our merit function to select X.
+        if cweight <= 0:
+            phi = fhist
+        elif np.isposinf(cweight):
+            phi = chist_shifted
+            # We should not use chist here; if np.minimum(chist_shifted) is attained at multiple indices, then
+            # we will check fhist to exhaust the remaining degree of freedom.
+        else:
+            phi = np.maximum(fhist, -REALMAX) + cweight * chist_shifted
+            # np.maximum(fhist, -REALMAX) makes sure that phi will not contain NaN (unless there is a bug).
+
+        # We select X to minimize phi subject to f < fref and cstrv_shift <= cref (see the comments
+        # above for the reason of taking "<" and "<=" in these two constraints). In case there are
+        # multiple minimizers, we take the one with the least cstrv_shift; if there is more than one
+        # choice, we take the one with the least f; if there are several candidates, we take the one
+        # with the least cstrv; if the last comparison still leads to more than one possibility, then
+        # they are equally good and we choose the first.
+        # N.B.:
+        # 1. This process is the opposite of selecting kworst in savefilt
+        # 2. In finite-precision arithmetic, phi_2 == phi_2 and cstrv_shift_1 == cstrv_shifted_2 do
+        # not ensure thatn f_1 == f_2!
+        phimin = np.min(phi[np.logical_and(fhist < fref, chist_shifted <= cref)])
+        cref = np.min(chist_shifted[np.logical_and(fhist < fref, phi <= phimin)])
+        fref = np.min(fhist[chist_shifted <= cref])
+        # Can't use argmin here because using it with a mask throws off the index
+        kopt = np.ma.array(chist, mask=(fhist > fref)).argmin()
+
+    #==================#
+    # Calculation ends #
+    #==================#
+
+    # Postconditions
+    if DEBUGGING:
+        assert kopt >= 0 and kopt < nhist
+        assert not any([isbetter(fhisti, chisti, fhist[kopt], chist[kopt], ctol) for fhisti, chisti in zip(fhist[:nhist], chist[:nhist])])
+
+    return kopt
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/tests/__init__.py b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test__gcutils.py b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test__gcutils.py
new file mode 100644
index 0000000000000000000000000000000000000000..0e397af4fb7e9bc69f31d1e39aa80716469d5470
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test__gcutils.py
@@ -0,0 +1,110 @@
+""" Test for assert_deallocated context manager and gc utilities
+"""
+import gc
+from threading import Lock
+
+from scipy._lib._gcutils import (set_gc_state, gc_state, assert_deallocated,
+                                 ReferenceError, IS_PYPY)
+
+from numpy.testing import assert_equal
+
+import pytest
+
+
+@pytest.fixture
+def gc_lock():
+    return Lock()
+
+
+def test_set_gc_state(gc_lock):
+    with gc_lock:
+        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(gc_lock):
+    # Test gc_state context manager
+    with gc_lock:
+        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(gc_lock):
+    # Ordinary use
+    class C:
+        def __init__(self, arg0, arg1, name='myname'):
+            self.name = name
+    with gc_lock:
+        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/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test__pep440.py b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test__pep440.py
new file mode 100644
index 0000000000000000000000000000000000000000..7f5b71c8f1e13b42de2e8e612a005dec409fc025
--- /dev/null
+++ b/.venv/lib/python3.11/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/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test__testutils.py b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test__testutils.py
new file mode 100644
index 0000000000000000000000000000000000000000..88db113d6d5a35c96ecc0a6a36ab42d74be49153
--- /dev/null
+++ b/.venv/lib/python3.11/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/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test__util.py b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test__util.py
new file mode 100644
index 0000000000000000000000000000000000000000..f78e36613f2a49a6e685a7775e117f80fe50affa
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test__util.py
@@ -0,0 +1,638 @@
+from multiprocessing import Pool
+from multiprocessing.pool import Pool as PWL
+import re
+import math
+import functools
+from fractions import Fraction
+
+import numpy as np
+from numpy.testing import assert_equal, assert_
+import pytest
+from pytest import raises as assert_raises
+from scipy.conftest import skip_xp_invalid_arg
+
+from scipy._lib._array_api import xp_assert_equal
+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, _workers_wrapper)
+import scipy._lib.array_api_extra as xpx
+from scipy._lib.array_api_extra.testing import lazy_xp_function
+from scipy import cluster, interpolate, linalg, optimize, sparse, spatial, stats
+
+
+lazy_xp_function(_contains_nan)
+
+
+@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)
+
+
+@_workers_wrapper
+def user_of_workers(x, b=1, workers=None):
+    assert workers is not None
+    assert isinstance(workers, MapWrapper)
+    return np.array(list(workers(np.sin, x * b)))
+
+
+def test__workers_wrapper():
+    arr = np.linspace(0, np.pi)
+    req = np.sin(arr * 2.0)
+
+    with Pool(2) as p:
+        v = user_of_workers(arr, workers=p.map, b=2)
+        assert_equal(v, req)
+
+    v = user_of_workers(arr, workers=None, b=2)
+    assert_equal(v, req)
+
+    v = user_of_workers(arr, workers=2, b=2)
+    assert_equal(v, req)
+
+    # assess if decorator works with partial functions
+    part_f = functools.partial(user_of_workers, b=2)
+    assert_equal(part_f(arr), req)
+
+    with Pool(2) as p:
+        part_f = functools.partial(user_of_workers, b=2, workers=p.map)
+        assert_equal(part_f(arr), req)
+
+
+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)
+
+    @pytest.fixture
+    def kwarg_lock(self):
+        from threading import Lock
+        return Lock()
+
+    def test_old_keyword_deprecated(self, kwarg_lock):
+        # 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)
+        # pytest warning filter is not thread-safe, enforce serialization
+        with kwarg_lock:
+            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 kwarg_lock:
+            with pytest.raises(TypeError, match=message), \
+                    pytest.warns(DeprecationWarning, match=dep_msg):
+                    # breakpoint()
+                    self.old_keyword_deprecated(10, old=10)
+        with kwarg_lock:
+            with pytest.raises(TypeError, match=message), \
+                    pytest.warns(DeprecationWarning, match=dep_msg):
+                    self.old_keyword_deprecated(new=10, old=10)
+
+
+class TestContainsNaN:
+    def test_policy(self):
+        data = np.array([1, 2, 3, np.nan])
+
+        assert _contains_nan(data)  # default policy is "propagate"
+        assert _contains_nan(data, nan_policy="propagate")
+        assert _contains_nan(data, nan_policy="omit")
+        assert not _contains_nan(data[:3])
+        assert not _contains_nan(data[:3], nan_policy="propagate")
+        assert not _contains_nan(data[:3], nan_policy="omit")
+
+        with pytest.raises(ValueError, match="The input contains nan values"):
+            _contains_nan(data, nan_policy="raise")
+        assert not _contains_nan(data[:3], nan_policy="raise")
+
+        with pytest.raises(ValueError, match="nan_policy must be one of"):
+            _contains_nan(data, nan_policy="nan")
+
+    def test_contains_nan(self):
+        # Special case: empty array
+        assert not _contains_nan(np.array([], dtype=float))
+
+        # Integer arrays cannot contain NaN
+        assert not _contains_nan(np.array([1, 2, 3]))
+        assert not _contains_nan(np.array([[1, 2], [3, 4]]))
+
+        assert not _contains_nan(np.array([1., 2., 3.]))
+        assert not _contains_nan(np.array([1., 2.j, 3.]))
+        assert _contains_nan(np.array([1., 2.j, np.nan]))
+        assert _contains_nan(np.array([1., 2., np.nan]))
+        assert _contains_nan(np.array([np.nan, 2., np.nan]))
+        assert not _contains_nan(np.array([[1., 2.], [3., 4.]]))
+        assert _contains_nan(np.array([[1., 2.], [3., np.nan]]))
+
+    @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)
+
+        data2 = np.array([1, 2, "3", np.nan], dtype='object')
+        assert _contains_nan(data2)
+
+        data3 = np.array([["1", 2], [3, np.nan]])  # converted to string "nan"
+        assert not _contains_nan(data3)
+
+        data4 = np.array([["1", 2], [3, np.nan]], dtype='object')
+        assert _contains_nan(data4)
+
+    @pytest.mark.skip_xp_backends(eager_only=True,
+                                  reason="lazy backends tested separately")
+    @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)
+        assert not _contains_nan(x, nan_policy)
+
+        x = xpx.at(x)[1, 2, 1].set(xp.nan)
+
+        if nan_policy == 'raise':
+            with pytest.raises(ValueError, match="The input contains nan values"):
+                _contains_nan(x, nan_policy)
+        elif nan_policy == 'omit':
+            assert _contains_nan(x, nan_policy, xp_omit_okay=True)
+        elif nan_policy == 'propagate':
+            assert _contains_nan(x, nan_policy)
+
+    @pytest.mark.skip_xp_backends("numpy", reason="lazy backends only")
+    @pytest.mark.skip_xp_backends("cupy", reason="lazy backends only")
+    @pytest.mark.skip_xp_backends("array_api_strict", reason="lazy backends only")
+    @pytest.mark.skip_xp_backends("torch", reason="lazy backends only")
+    def test_array_api_lazy(self, xp):
+        rng = np.random.default_rng(932347235892482)
+        x0 = rng.random(size=(2, 3, 4))
+        x = xp.asarray(x0)
+
+        xp_assert_equal(_contains_nan(x), xp.asarray(False))
+        xp_assert_equal(_contains_nan(x, "propagate"), xp.asarray(False))
+        xp_assert_equal(_contains_nan(x, "omit", xp_omit_okay=True), xp.asarray(False))
+        # Lazy arrays don't support "omit" and "raise" policies
+        match = "not supported for lazy arrays"
+        with pytest.raises(TypeError, match=match):
+            _contains_nan(x, "omit")
+        with pytest.raises(TypeError, match=match):
+            _contains_nan(x, "raise")
+
+        x = xpx.at(x)[1, 2, 1].set(np.nan)
+
+        xp_assert_equal(_contains_nan(x), xp.asarray(True))
+        xp_assert_equal(_contains_nan(x, "propagate"), xp.asarray(True))
+        xp_assert_equal(_contains_nan(x, "omit", xp_omit_okay=True), xp.asarray(True))
+        with pytest.raises(TypeError, match=match):
+            _contains_nan(x, "omit")
+        with pytest.raises(TypeError, match=match):
+            _contains_nan(x, "raise")
+
+
+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 TestTransitionToRNG:
+    def kmeans(self, **kwargs):
+        rng = np.random.default_rng(3458934594269824562)
+        return cluster.vq.kmeans2(rng.random(size=(20, 3)), 3, **kwargs)
+
+    def kmeans2(self, **kwargs):
+        rng = np.random.default_rng(3458934594269824562)
+        return cluster.vq.kmeans2(rng.random(size=(20, 3)), 3, **kwargs)
+
+    def barycentric(self, **kwargs):
+        rng = np.random.default_rng(3458934594269824562)
+        x1, x2, y1 = rng.random((3, 10))
+        f = interpolate.BarycentricInterpolator(x1, y1, **kwargs)
+        return f(x2)
+
+    def clarkson_woodruff_transform(self, **kwargs):
+        rng = np.random.default_rng(3458934594269824562)
+        return linalg.clarkson_woodruff_transform(rng.random((10, 10)), 3, **kwargs)
+
+    def basinhopping(self, **kwargs):
+        rng = np.random.default_rng(3458934594269824562)
+        return optimize.basinhopping(optimize.rosen, rng.random(3), **kwargs).x
+
+    def opt(self, fun, **kwargs):
+        rng = np.random.default_rng(3458934594269824562)
+        bounds = optimize.Bounds(-rng.random(3) * 10, rng.random(3) * 10)
+        return fun(optimize.rosen, bounds, **kwargs).x
+
+    def differential_evolution(self, **kwargs):
+        return self.opt(optimize.differential_evolution, **kwargs)
+
+    def dual_annealing(self, **kwargs):
+        return self.opt(optimize.dual_annealing, **kwargs)
+
+    def check_grad(self, **kwargs):
+        rng = np.random.default_rng(3458934594269824562)
+        x = rng.random(3)
+        return optimize.check_grad(optimize.rosen, optimize.rosen_der, x,
+                                   direction='random', **kwargs)
+
+    def random_array(self, **kwargs):
+        return sparse.random_array((10, 10), density=1.0, **kwargs).toarray()
+
+    def random(self, **kwargs):
+        return sparse.random(10, 10, density=1.0, **kwargs).toarray()
+
+    def rand(self, **kwargs):
+        return sparse.rand(10, 10, density=1.0, **kwargs).toarray()
+
+    def svds(self, **kwargs):
+        rng = np.random.default_rng(3458934594269824562)
+        A = rng.random((10, 10))
+        return sparse.linalg.svds(A, **kwargs)
+
+    def random_rotation(self, **kwargs):
+        return spatial.transform.Rotation.random(3, **kwargs).as_matrix()
+
+    def goodness_of_fit(self, **kwargs):
+        rng = np.random.default_rng(3458934594269824562)
+        data = rng.random(100)
+        return stats.goodness_of_fit(stats.laplace, data, **kwargs).pvalue
+
+    def permutation_test(self, **kwargs):
+        rng = np.random.default_rng(3458934594269824562)
+        data = tuple(rng.random((2, 100)))
+        def statistic(x, y, axis): return np.mean(x, axis=axis) - np.mean(y, axis=axis)
+        return stats.permutation_test(data, statistic, **kwargs).pvalue
+
+    def bootstrap(self, **kwargs):
+        rng = np.random.default_rng(3458934594269824562)
+        data = (rng.random(100),)
+        return stats.bootstrap(data, np.mean, **kwargs).confidence_interval
+
+    def dunnett(self, **kwargs):
+        rng = np.random.default_rng(3458934594269824562)
+        x, y, control = rng.random((3, 100))
+        return stats.dunnett(x, y, control=control, **kwargs).pvalue
+
+    def sobol_indices(self, **kwargs):
+        def f_ishigami(x): return (np.sin(x[0]) + 7 * np.sin(x[1]) ** 2
+                                   + 0.1 * (x[2] ** 4) * np.sin(x[0]))
+        dists = [stats.uniform(loc=-np.pi, scale=2 * np.pi),
+                 stats.uniform(loc=-np.pi, scale=2 * np.pi),
+                 stats.uniform(loc=-np.pi, scale=2 * np.pi)]
+        res = stats.sobol_indices(func=f_ishigami, n=1024, dists=dists, **kwargs)
+        return res.first_order
+
+    def qmc_engine(self, engine, **kwargs):
+        qrng = engine(d=1, **kwargs)
+        return qrng.random(4)
+
+    def halton(self, **kwargs):
+        return self.qmc_engine(stats.qmc.Halton, **kwargs)
+
+    def sobol(self, **kwargs):
+        return self.qmc_engine(stats.qmc.Sobol, **kwargs)
+
+    def latin_hypercube(self, **kwargs):
+        return self.qmc_engine(stats.qmc.LatinHypercube, **kwargs)
+
+    def poisson_disk(self, **kwargs):
+        return self.qmc_engine(stats.qmc.PoissonDisk, **kwargs)
+
+    def multivariate_normal_qmc(self, **kwargs):
+        X = stats.qmc.MultivariateNormalQMC([0], **kwargs)
+        return X.random(4)
+
+    def multinomial_qmc(self, **kwargs):
+        X = stats.qmc.MultinomialQMC([0.5, 0.5], 4, **kwargs)
+        return X.random(4)
+
+    def permutation_method(self, **kwargs):
+        rng = np.random.default_rng(3458934594269824562)
+        data = tuple(rng.random((2, 100)))
+        method = stats.PermutationMethod(**kwargs)
+        return stats.pearsonr(*data, method=method).pvalue
+
+    def bootstrap_method(self, **kwargs):
+        rng = np.random.default_rng(3458934594269824562)
+        data = tuple(rng.random((2, 100)))
+        res = stats.pearsonr(*data)
+        method = stats.BootstrapMethod(**kwargs)
+        return res.confidence_interval(method=method)
+
+    @pytest.mark.fail_slow(10)
+    @pytest.mark.slow
+    @pytest.mark.parametrize("method, arg_name", [
+        (kmeans, "seed"),
+        (kmeans2, "seed"),
+        (barycentric, "random_state"),
+        (clarkson_woodruff_transform, "seed"),
+        (basinhopping, "seed"),
+        (differential_evolution, "seed"),
+        (dual_annealing, "seed"),
+        (check_grad, "seed"),
+        (random_array, 'random_state'),
+        (random, 'random_state'),
+        (rand, 'random_state'),
+        (random_rotation, "random_state"),
+        (goodness_of_fit, "random_state"),
+        (permutation_test, "random_state"),
+        (bootstrap, "random_state"),
+        (permutation_method, "random_state"),
+        (bootstrap_method, "random_state"),
+        (dunnett, "random_state"),
+        (sobol_indices, "random_state"),
+        (halton, "seed"),
+        (sobol, "seed"),
+        (latin_hypercube, "seed"),
+        (poisson_disk, "seed"),
+        (multivariate_normal_qmc, "seed"),
+        (multinomial_qmc, "seed"),
+    ])
+    def test_rng_deterministic(self, method, arg_name):
+        np.random.seed(None)
+        seed = 2949672964
+
+        rng = np.random.default_rng(seed)
+        message = "got multiple values for argument now known as `rng`"
+        with pytest.raises(TypeError, match=message):
+            method(self, **{'rng': rng, arg_name: seed})
+
+        rng = np.random.default_rng(seed)
+        res1 = method(self, rng=rng)
+        res2 = method(self, rng=seed)
+        assert_equal(res2, res1)
+
+        if method.__name__ in {"dunnett", "sobol_indices"}:
+            # the two kwargs have essentially the same behavior for these functions
+            res3 = method(self, **{arg_name: seed})
+            assert_equal(res3, res1)
+            return
+
+        rng = np.random.RandomState(seed)
+        res1 = method(self, **{arg_name: rng})
+        res2 = method(self, **{arg_name: seed})
+
+        if method.__name__ in {"halton", "sobol", "latin_hypercube", "poisson_disk",
+                               "multivariate_normal_qmc", "multinomial_qmc"}:
+            # For these, passing `random_state=RandomState(seed)` is not the same as
+            # passing integer `seed`.
+            res1b = method(self, **{arg_name: np.random.RandomState(seed)})
+            assert_equal(res1b, res1)
+            res2b = method(self, **{arg_name: seed})
+            assert_equal(res2b, res2)
+            return
+
+        np.random.seed(seed)
+        res3 = method(self, **{arg_name: None})
+        assert_equal(res2, res1)
+        assert_equal(res3, res1)
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_array_api.py b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_array_api.py
new file mode 100644
index 0000000000000000000000000000000000000000..cb7735debae5a18640aca3c6c6c330e7f1802e81
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_array_api.py
@@ -0,0 +1,400 @@
+import re
+
+import numpy as np
+import pytest
+
+from importlib import import_module
+
+from scipy._lib._array_api import (
+    SCIPY_ARRAY_API, array_namespace, _asarray, xp_copy, xp_assert_equal, is_numpy,
+    np_compat, xp_default_dtype, xp_result_type, is_torch,
+    xp_capabilities_table, _xp_copy_to_numpy
+)
+from scipy._lib._array_api_docs_tables import is_named_function_like_object
+from scipy._lib import array_api_extra as xpx
+from scipy._lib._array_api_no_0d import xp_assert_equal as xp_assert_equal_no_0d
+from scipy._lib.array_api_extra.testing import lazy_xp_function
+
+# Run all tests in this module in the Array API CI,
+# including those without the `xp` fixture
+pytestmark = pytest.mark.array_api_backends
+
+lazy_xp_function(_asarray)
+lazy_xp_function(xp_copy)
+
+
+@pytest.mark.skipif(not 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__
+
+    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')
+
+    @pytest.mark.skip_xp_backends(np_only=True, reason="Array-likes")
+    def test_array_likes(self, xp):
+        """Test that if all parameters of array_namespace are Array-likes,
+        the output is array_api_compat.numpy
+        """
+        assert array_namespace([0, 1, 2]) is xp
+        assert array_namespace((0, 1, 2)) is xp
+        assert array_namespace(1, 2, 3) is xp
+        assert array_namespace(1) is xp
+        assert array_namespace(np.int64(1)) is xp
+        assert array_namespace([0, 1, 2], 3) is xp
+        assert array_namespace() is xp
+        assert array_namespace(None) is xp
+        assert array_namespace(1, None) is xp
+        assert array_namespace(None, 1) is xp
+
+        # This only works when xp is numpy!
+        assert array_namespace(np.asarray([1, 2]), [3, 4]) is xp
+        assert array_namespace(np.int64(1), [3, 4]) is xp
+
+    def test_array_and_array_likes_mix(self, xp):
+        """Test that if there is at least one Array API object among
+        the parameters of array_namespace, and all other parameters
+        are scalars, the output is its namespace.
+
+        If there are non-scalar Array-Likes, raise as in array-api-compat.
+        """
+        x = xp.asarray(1)
+        assert array_namespace(x) is xp
+        assert array_namespace(x, 1) is xp
+        assert array_namespace(1, x) is xp
+        assert array_namespace(None, x) is xp
+
+        if is_numpy(xp):
+            assert array_namespace(x, [1, 2]) is xp
+        else:
+            with pytest.raises(TypeError, match="Multiple namespaces"):
+                array_namespace(x, [1, 2])
+            with pytest.raises(TypeError, match="Multiple namespaces"):
+                array_namespace(x, np.int64(1))
+            with pytest.raises(TypeError, match="Multiple namespaces"):
+                 # Subclass of float; matches array_api_compat behavior
+                array_namespace(x, np.float64(1))
+            with pytest.raises(TypeError, match="Multiple namespaces"):
+                # Subclass of complex; matches array_api_compat behavior
+                array_namespace(x, np.complex128(1))
+
+    def test_array_api_extra_hook(self):
+        """Test that the `array_namespace` function used by
+        array-api-extra has been overridden by scipy
+        """
+        msg = "only boolean and numerical dtypes are supported"
+        with pytest.raises(TypeError, match=msg):
+            xpx.atleast_nd("abc", ndim=0)
+
+    def test_jax_zero_gradient_array(self):
+        """Test array_namespace special case for JAX zero-gradient arrays, which are
+        numpy arrays but must be treated as JAX arrays.
+        See matching code and tests in array_api_compat.
+        """
+        jax = pytest.importorskip("jax")
+        xp = pytest.importorskip("jax.numpy")
+        # Create numpy array with dtype=jax.float0
+        jax_zero = jax.vmap(jax.grad(xp.float32, allow_int=True))(xp.arange(4))
+        assert array_namespace(jax_zero) is xp
+
+    def test_void_but_not_jax_zero_gradient_array(self):
+        """A void dtype that is not a jax.float0 must not be caught in the
+        special case for JAX zero-gradient arrays.
+        """
+        void = np.empty(0, dtype=np.dtype([]))
+        with pytest.raises(TypeError, match="only boolean and numerical dtypes"):
+            array_namespace(void)
+        with pytest.raises(TypeError, match="only boolean and numerical dtypes"):
+            array_namespace([void, void])
+
+    def test_copy(self, xp):
+        for _xp in [xp, None]:
+            x = xp.asarray([1, 2, 3])
+            y = xp_copy(x, xp=_xp)
+            # with numpy we'd want to use np.shared_memory, but that's not specified
+            # in the array-api
+            assert id(x) != id(y)
+            try:
+                y[0] = 10
+            except (TypeError, ValueError):
+                pass
+            else:
+                assert x[0] != y[0]
+
+    @pytest.mark.parametrize(
+        "dtype",
+        ["float32", "float64", "complex64", "complex128", "int32", "int64"],
+    )
+    @pytest.mark.parametrize(
+        "data", [[], 1, [1, 2, 3], [[1, 2], [2, 3]]],
+    )
+    def test_copy_to_numpy(self, xp, data, dtype):
+        xp_dtype = getattr(xp, dtype)
+        np_dtype = getattr(np, dtype)
+        x = xp.asarray(data, dtype=xp_dtype)
+        y = _xp_copy_to_numpy(x)
+        assert isinstance(y, np.ndarray)
+        assert y.dtype == np_dtype
+        assert x.shape == y.shape
+        np.testing.assert_equal(y, np.asarray(data, dtype=np_dtype))
+        if is_numpy(xp):
+            # Ensure y is a copy when xp is numpy.
+            assert id(x) != id(y)
+
+    
+    @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)[()]
+
+        kwarg_names = ["check_namespace", "check_dtype", "check_shape", "check_0d"]
+        options = dict(zip(kwarg_names, [True, False, False, False]))
+        if is_numpy(xp):
+            xp_assert_equal(x, y, **options)
+        else:
+            with pytest.raises(
+                AssertionError,
+                match="Namespace of desired array does not match",
+            ):
+                xp_assert_equal(x, y, **options)
+            with pytest.raises(
+                AssertionError,
+                match="Namespace of actual and desired arrays do not match",
+            ):
+                xp_assert_equal(y, x, **options)
+
+        options = dict(zip(kwarg_names, [False, True, False, 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(zip(kwarg_names, [False, False, True, False]))
+        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, xp.asarray(y), **options)
+
+        options = dict(zip(kwarg_names, [False, False, False, True]))
+        if is_numpy(xp) and x.shape == y.shape:
+            xp_assert_equal(x, y, **options)
+        elif is_numpy(xp):
+            with pytest.raises(AssertionError, match="Array-ness does not match."):
+                xp_assert_equal(x, y, **options)
+
+    @pytest.mark.skip_xp_backends(np_only=True, reason="Scalars only exist in NumPy")
+    def test_check_scalar(self, xp):
+        # identity always passes
+        xp_assert_equal(xp.float64(0), xp.float64(0))
+        xp_assert_equal(xp.asarray(0.), xp.asarray(0.))
+        xp_assert_equal(xp.float64(0), xp.float64(0), check_0d=False)
+        xp_assert_equal(xp.asarray(0.), xp.asarray(0.), check_0d=False)
+
+        # Check default convention: 0d-arrays are distinguished from scalars
+        message = "Array-ness does not match:.*"
+        with pytest.raises(AssertionError, match=message):
+            xp_assert_equal(xp.asarray(0.), xp.float64(0))
+        with pytest.raises(AssertionError, match=message):
+            xp_assert_equal(xp.float64(0), xp.asarray(0.))
+        with pytest.raises(AssertionError, match=message):
+            xp_assert_equal(xp.asarray(42), xp.int64(42))
+        with pytest.raises(AssertionError, match=message):
+            xp_assert_equal(xp.int64(42), xp.asarray(42))
+
+        # with `check_0d=False`, scalars-vs-0d passes (if values match)
+        xp_assert_equal(xp.asarray(0.), xp.float64(0), check_0d=False)
+        xp_assert_equal(xp.float64(0), xp.asarray(0.), check_0d=False)
+        # also with regular python objects
+        xp_assert_equal(xp.asarray(0.), 0., check_0d=False)
+        xp_assert_equal(0., xp.asarray(0.), check_0d=False)
+        xp_assert_equal(xp.asarray(42), 42, check_0d=False)
+        xp_assert_equal(42, xp.asarray(42), check_0d=False)
+
+        # as an alternative to `check_0d=False`, explicitly expect scalar
+        xp_assert_equal(xp.float64(0), xp.asarray(0.)[()])
+
+    @pytest.mark.skip_xp_backends(np_only=True, reason="Scalars only exist in NumPy")
+    def test_check_scalar_no_0d(self, xp):
+        # identity passes, if first argument is not 0d (or check_0d=True)
+        xp_assert_equal_no_0d(xp.float64(0), xp.float64(0))
+        xp_assert_equal_no_0d(xp.float64(0), xp.float64(0), check_0d=True)
+        xp_assert_equal_no_0d(xp.asarray(0.), xp.asarray(0.), check_0d=True)
+
+        # by default, 0d values are forbidden as the first argument
+        message = "Result is a NumPy 0d-array.*"
+        with pytest.raises(AssertionError, match=message):
+            xp_assert_equal_no_0d(xp.asarray(0.), xp.asarray(0.))
+        with pytest.raises(AssertionError, match=message):
+            xp_assert_equal_no_0d(xp.asarray(0.), xp.float64(0))
+        with pytest.raises(AssertionError, match=message):
+            xp_assert_equal_no_0d(xp.asarray(42), xp.int64(42))
+
+        # Check default convention: 0d-arrays are NOT distinguished from scalars
+        xp_assert_equal_no_0d(xp.float64(0), xp.asarray(0.))
+        xp_assert_equal_no_0d(xp.int64(42), xp.asarray(42))
+
+        # opt in to 0d-check remains possible
+        message = "Array-ness does not match:.*"
+        with pytest.raises(AssertionError, match=message):
+            xp_assert_equal_no_0d(xp.asarray(0.), xp.float64(0), check_0d=True)
+        with pytest.raises(AssertionError, match=message):
+            xp_assert_equal_no_0d(xp.float64(0), xp.asarray(0.), check_0d=True)
+        with pytest.raises(AssertionError, match=message):
+            xp_assert_equal_no_0d(xp.asarray(42), xp.int64(0), check_0d=True)
+        with pytest.raises(AssertionError, match=message):
+            xp_assert_equal_no_0d(xp.int64(0), xp.asarray(42), check_0d=True)
+
+        # scalars-vs-0d passes (if values match) also with regular python objects
+        xp_assert_equal_no_0d(0., xp.asarray(0.))
+        xp_assert_equal_no_0d(42, xp.asarray(42))
+
+    def test_default_dtype(self, xp):
+        assert xp_default_dtype(xp) == xp.asarray(1.).dtype
+
+
+scalars = [1, 1., 1. + 1j]
+lists = [[1], [1.], [1. + 1j]]
+types = ('int8 int16 int32 int64 '
+         'uint8 uint16 uint32 uint64 '
+         'float32 float64 complex64 complex128').split()
+arrays = [np.asarray([1], dtype=getattr(np, t)) for t in types]
+
+
+def convert_type(x, xp):
+    # Convert NumPy array to xp-array
+    # Convert string to indicated dtype from xp
+    # Return Python scalars unchanged
+    if isinstance(x, np.ndarray):
+        return xp.asarray(x)
+    elif isinstance(x, str):
+        return getattr(xp, x)
+    return x
+
+
+def is_inexact(x, xp):
+    # Determine whether `x` is of inexact (real of complex floating) dtype
+    x = xp.asarray(x) if np.isscalar(x) or isinstance(x, list) else x
+    dtype = getattr(x, 'dtype', x)
+    return xp.isdtype(dtype, ('real floating', 'complex floating'))
+
+
+@pytest.mark.parametrize('x', scalars + lists + types + arrays)
+@pytest.mark.parametrize('y', scalars + lists + types + arrays)
+def test_xp_result_type_no_force(x, y, xp):
+    # When force_floating==False (default), behavior of `xp_result_type`
+    # should match that of `xp.result_type` on the same arguments after
+    # converting lists to arrays of type `xp`.
+    x = convert_type(x, xp)
+    y = convert_type(y, xp)
+    x_ref = xp.asarray(x) if isinstance(x, list) else x
+    y_ref = xp.asarray(y) if isinstance(y, list) else y
+
+    try:
+        dtype_ref = xp.result_type(x_ref, y_ref)
+        expected_error = None
+    except Exception as e:
+        expected_error = (type(e), str(e))
+
+    if expected_error is not None:
+        with pytest.raises(expected_error[0], match=re.escape(expected_error[1])):
+            xp_result_type(x, y, xp=xp)
+        return
+
+    dtype_res = xp_result_type(x, y, xp=xp)
+    assert dtype_res == dtype_ref
+
+
+@pytest.mark.parametrize('x', scalars + lists + types + arrays)
+@pytest.mark.parametrize('y', scalars + lists + types + arrays)
+def test_xp_result_type_force_floating(x, y, xp):
+    # When `force_floating==True`, behavior of `xp_result_type`
+    # should match that of `xp.result_type` with `1.0` appended to the set of
+    # arguments (after converting lists to arrays of type `xp`).
+    # If this raises a `TypeError`, which is the case when the result
+    # type is not defined by the standard, the result type should be
+    # the result type of any inexact (real or complex floating) arguments
+    # and the default floating point type.
+    if (is_torch(xp) and not(isinstance(x, str) or isinstance(y, str))
+            and np.isscalar(x) and np.isscalar(y)):
+        pytest.skip("See 3/27/2024 comment at  data-apis/array-api-compat#277")
+
+    x = convert_type(x, xp)
+    y = convert_type(y, xp)
+    x_ref = xp.asarray(x) if isinstance(x, list) else x
+    y_ref = xp.asarray(y) if isinstance(y, list) else y
+
+    expected_error = None
+    try:
+        dtype_ref = xp.result_type(x_ref, y_ref, 1.0)
+    except TypeError:
+        args = []
+        if is_inexact(x_ref, xp):
+            args.append(x_ref)
+        if is_inexact(y_ref, xp):
+            args.append(y_ref)
+        dtype_ref = xp.result_type(*args, xp.asarray(1.0))
+    except Exception as e:
+        expected_error = (type(e), str(e))
+
+    if expected_error is not None:
+        with pytest.raises(expected_error[0], match=expected_error[1]):
+            xp_result_type(x, y, xp=xp)
+        return
+
+    dtype_res = xp_result_type(x, y, force_floating=True, xp=xp)
+    assert dtype_res == dtype_ref
+
+# Test that the xp_capabilities decorator has been applied to all
+# functions and function-likes in the public API. Modules will be
+# added to the list of tested_modules below as decorator coverage
+# is added on a module by module basis. It remains for future work
+# to offer similar functionality to xp_capabilities for classes in
+# the public API.
+
+tested_modules = ["scipy.stats"]
+
+
+def collect_public_functions():
+    functions = []
+    for module_name in tested_modules:
+        module = import_module(module_name)
+        for name in module.__all__:
+            obj = getattr(module, name)
+            if not is_named_function_like_object(obj):
+                continue
+            functions.append(pytest.param(obj, id=f"{module_name}.{name}"))
+    return functions
+
+
+@pytest.mark.parametrize("func", collect_public_functions())
+def test_xp_capabilities_coverage(func):
+    assert func in xp_capabilities_table
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_bunch.py b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_bunch.py
new file mode 100644
index 0000000000000000000000000000000000000000..96008627e3afef540913071c95ea543c718a326c
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_bunch.py
@@ -0,0 +1,169 @@
+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)
+
+    def test_passes_polars_checks(self):
+        # gh-22450
+        Square = _make_tuple_bunch('Square', ['width', 'height'])
+        assert hasattr(Square, '_replace')
+        assert hasattr(Square, '_field_defaults')
+
+
+    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+    # 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/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_ccallback.py b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_ccallback.py
new file mode 100644
index 0000000000000000000000000000000000000000..e8a2c97c6f67732f55eea6ed5cad297f5068a714
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_ccallback.py
@@ -0,0 +1,196 @@
+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
+
+
+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):
+    cffi = pytest.importorskip("cffi")
+
+    # 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():
+    cffi = pytest.importorskip("cffi")
+    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/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_config.py b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_config.py
new file mode 100644
index 0000000000000000000000000000000000000000..b43d3f9f0da39a86b78cb2210cb21d670e5cf978
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_config.py
@@ -0,0 +1,44 @@
+"""
+Check the SciPy config is valid.
+"""
+import scipy
+import pytest
+from unittest.mock import patch
+
+pytestmark = pytest.mark.skipif(
+    not hasattr(scipy.__config__, "_built_with_meson"),
+    reason="Requires Meson builds",
+)
+
+
+class TestSciPyConfigs:
+    REQUIRED_CONFIG_KEYS = [
+        "Compilers",
+        "Machine Information",
+        "Python Information",
+    ]
+
+    @patch("scipy.__config__._check_pyyaml")
+    def test_pyyaml_not_found(self, mock_yaml_importer):
+        mock_yaml_importer.side_effect = ModuleNotFoundError()
+        with pytest.warns(UserWarning):
+            scipy.show_config()
+
+    def test_dict_mode(self):
+        config = scipy.show_config(mode="dicts")
+
+        assert isinstance(config, dict)
+        assert all([key in config for key in self.REQUIRED_CONFIG_KEYS]), (
+            "Required key missing,"
+            " see index of `False` with `REQUIRED_CONFIG_KEYS`"
+        )
+
+    def test_invalid_mode(self):
+        with pytest.raises(AttributeError):
+            scipy.show_config(mode="foo")
+
+    def test_warn_to_add_tests(self):
+        assert len(scipy.__config__.DisplayModes) == 2, (
+            "New mode detected,"
+            " please add UT if applicable and increment this count"
+        )
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_deprecation.py b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_deprecation.py
new file mode 100644
index 0000000000000000000000000000000000000000..36d4b671117ba54fa612e7de504382c238dc18ff
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_deprecation.py
@@ -0,0 +1,10 @@
+import pytest
+
+@pytest.mark.thread_unsafe(reason="deprecate_cython_api is not thread-safe")
+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/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_doccer.py b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_doccer.py
new file mode 100644
index 0000000000000000000000000000000000000000..d351b12da33e0f873e71b6ab6c11fa3aff5e9430
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_doccer.py
@@ -0,0 +1,144 @@
+''' Some tests for the documenting decorator and support functions '''
+
+import sys
+import warnings
+import pytest
+from numpy.testing import assert_equal
+
+from scipy._lib import doccer
+
+# python -OO strips docstrings
+DOCSTRINGS_STRIPPED = sys.flags.optimize > 1
+
+docstring = \
+"""Docstring
+    %(strtest1)s
+        %(strtest2)s
+     %(strtest3)s
+"""
+param_doc1 = \
+"""Another test
+   with some indent"""
+
+param_doc2 = \
+"""Another test, one line"""
+
+param_doc3 = \
+"""    Another test
+       with some indent"""
+
+doc_dict = {'strtest1':param_doc1,
+            'strtest2':param_doc2,
+            'strtest3':param_doc3}
+
+filled_docstring = \
+"""Docstring
+    Another test
+       with some indent
+        Another test, one line
+     Another test
+       with some indent
+"""
+
+
+def test_unindent():
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore", category=DeprecationWarning)
+        assert_equal(doccer.unindent_string(param_doc1), param_doc1)
+        assert_equal(doccer.unindent_string(param_doc2), param_doc2)
+        assert_equal(doccer.unindent_string(param_doc3), param_doc1)
+
+
+def test_unindent_dict():
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore", category=DeprecationWarning)
+        d2 = doccer.unindent_dict(doc_dict)
+    assert_equal(d2['strtest1'], doc_dict['strtest1'])
+    assert_equal(d2['strtest2'], doc_dict['strtest2'])
+    assert_equal(d2['strtest3'], doc_dict['strtest1'])
+
+
+def test_docformat():
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore", category=DeprecationWarning)
+        udd = doccer.unindent_dict(doc_dict)
+        formatted = doccer.docformat(docstring, udd)
+        assert_equal(formatted, filled_docstring)
+        single_doc = 'Single line doc %(strtest1)s'
+        formatted = doccer.docformat(single_doc, doc_dict)
+        # Note - initial indent of format string does not
+        # affect subsequent indent of inserted parameter
+        assert_equal(formatted, """Single line doc Another test
+   with some indent""")
+
+
+@pytest.mark.skipif(DOCSTRINGS_STRIPPED, reason="docstrings stripped")
+def test_decorator():
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore", category=DeprecationWarning)
+        # with unindentation of parameters
+        decorator = doccer.filldoc(doc_dict, True)
+
+        @decorator
+        def func():
+            """ Docstring
+            %(strtest3)s
+            """
+
+        def expected():
+            """ Docstring
+            Another test
+               with some indent
+            """
+        assert_equal(func.__doc__, expected.__doc__)
+
+        # without unindentation of parameters
+
+        # The docstring should be unindented for Python 3.13+
+        # because of https://github.com/python/cpython/issues/81283
+        decorator = doccer.filldoc(doc_dict, False if \
+                                   sys.version_info < (3, 13) else True)
+
+        @decorator
+        def func():
+            """ Docstring
+            %(strtest3)s
+            """
+        def expected():
+            """ Docstring
+                Another test
+                   with some indent
+            """
+        assert_equal(func.__doc__, expected.__doc__)
+
+
+@pytest.mark.skipif(DOCSTRINGS_STRIPPED, reason="docstrings stripped")
+def test_inherit_docstring_from():
+
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore", category=DeprecationWarning)
+
+        class Foo:
+            def func(self):
+                '''Do something useful.'''
+                return
+
+            def func2(self):
+                '''Something else.'''
+
+        class Bar(Foo):
+            @doccer.inherit_docstring_from(Foo)
+            def func(self):
+                '''%(super)sABC'''
+                return
+
+            @doccer.inherit_docstring_from(Foo)
+            def func2(self):
+                # No docstring.
+                return
+
+    assert_equal(Bar.func.__doc__, Foo.func.__doc__ + 'ABC')
+    assert_equal(Bar.func2.__doc__, Foo.func2.__doc__)
+    bar = Bar()
+    assert_equal(bar.func.__doc__, Foo.func.__doc__ + 'ABC')
+    assert_equal(bar.func2.__doc__, Foo.func2.__doc__)
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_import_cycles.py b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_import_cycles.py
new file mode 100644
index 0000000000000000000000000000000000000000..d1d7c2bfca4976019b77bfbbfbc0cc4ca96a6962
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_import_cycles.py
@@ -0,0 +1,26 @@
+import multiprocessing
+import subprocess
+import sys
+
+import pytest
+
+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.
+
+
+def _check_single_module(module):
+    pid = subprocess.Popen([sys.executable, '-X', 'faulthandler', '-c',
+                            f'import {module}'])
+    assert pid.wait() == 0, f'Failed to import {module}'
+
+
+@pytest.mark.fail_slow(40)
+@pytest.mark.slow
+def test_public_modules_importable_2():
+    # Ensure we use max 6 processes, to limit peak resource usage (memory, file handles)
+    # on resource-constrained systems (e.g., RISC-V - see gh-24163).
+    with multiprocessing.Pool(processes=6) as pool:
+        pool.map(_check_single_module, PUBLIC_MODULES)
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_public_api.py b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_public_api.py
new file mode 100644
index 0000000000000000000000000000000000000000..dfafb4f223364fd566a83bf3425e8077f96dafad
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_public_api.py
@@ -0,0 +1,433 @@
+"""
+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._lib._public_api import PUBLIC_MODULES
+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)))
+
+
+# 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.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',
+    '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',
+    'scipy.integrate.lsoda'
+]
+
+
+# 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.
+@pytest.mark.thread_unsafe(
+    reason=("crashes in pkgutil.walk_packages, see "
+            "https://github.com/data-apis/array-api-compat/issues/343"))
+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', 'torch', 'dask.array'}
+        for backend in backends:
+            path = f'array_api_compat.{backend}'
+            if path in name and backend not in xp_available_backends:
+                return
+        raise
+
+    modnames = []
+
+    with warnings.catch_warnings():
+        warnings.filterwarnings("ignore", "scipy.misc", DeprecationWarning)
+        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',
+    'scipy.integrate.lsoda'
+]
+
+
+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
+    with warnings.catch_warnings():
+        warnings.filterwarnings("ignore",  "scipy.misc", DeprecationWarning)
+        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.simplefilter('always', category=DeprecationWarning)
+        warnings.simplefilter('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.odepack', None),
+                          ('scipy.integrate.quadpack', None),
+                          ('scipy.integrate.vode', None),
+                          ('scipy.interpolate.dfitpack', None),
+                          ('scipy.interpolate.fitpack', None),
+                          ('scipy.interpolate.fitpack2', None),
+                          ('scipy.interpolate.interpnd', 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.ndimage.filters', None),
+                          ('scipy.ndimage.fourier', None),
+                          ('scipy.ndimage.interpolation', None),
+                          ('scipy.ndimage.measurements', None),
+                          ('scipy.ndimage.morphology', 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.spline', None),
+                          ('scipy.signal.waveforms', None),
+                          ('scipy.signal.wavelets', None),
+                          ('scipy.signal.windows.windows', 'windows'),
+                          ('scipy.sparse.base', None),
+                          ('scipy.sparse.bsr', None),
+                          ('scipy.sparse.compressed', None),
+                          ('scipy.sparse.construct', None),
+                          ('scipy.sparse.coo', None),
+                          ('scipy.sparse.csc', None),
+                          ('scipy.sparse.csr', None),
+                          ('scipy.sparse.data', None),
+                          ('scipy.sparse.dia', None),
+                          ('scipy.sparse.dok', None),
+                          ('scipy.sparse.extract', None),
+                          ('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__:
+        # 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")
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_scipy_version.py b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_scipy_version.py
new file mode 100644
index 0000000000000000000000000000000000000000..68e1a43c3fb329b6a4274ba76b53a215738da6ad
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_scipy_version.py
@@ -0,0 +1,28 @@
+import re
+
+import scipy
+import scipy.version
+
+
+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)|(\.dev0+\+git[0-9]{8}.[0-9a-f]{7}))"
+    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
+    assert scipy.__version__
+
+
+def test_version_submodule_members():
+    """`scipy.version` may not be quite public, but we install it.
+
+    So check that we don't silently change its contents.
+    """
+    for attr in ('version', 'full_version', 'short_version', 'git_revision', 'release'):
+        assert hasattr(scipy.version, attr)
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_tmpdirs.py b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_tmpdirs.py
new file mode 100644
index 0000000000000000000000000000000000000000..3ee90319aa4c7b6d73c64f0f16eadc1ab070c7ec
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_tmpdirs.py
@@ -0,0 +1,48 @@
+""" 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
+
+import pytest
+
+
+MY_PATH = abspath(__file__)
+MY_DIR = dirname(MY_PATH)
+
+
+@pytest.mark.thread_unsafe(reason="tempdir is not thread-safe")
+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))
+
+
+@pytest.mark.thread_unsafe(reason="in_tempdir is not thread-safe")
+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)
+
+
+@pytest.mark.thread_unsafe(reason="in_dir is not thread-safe")
+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/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_warnings.py b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_warnings.py
new file mode 100644
index 0000000000000000000000000000000000000000..2dd96428fe4106da2e90b65718ad78752d28cd0c
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/_lib/tests/test_warnings.py
@@ -0,0 +1,137 @@
+"""
+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 outside of test code
+            if argtext == "ignore" and "tests" not in self.__filename.parts:
+                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(40)
+@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
+    # warnings.catch_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('special', '_basic.py'),  # gh-21801
+        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', '_stats_py.py'),  # gh-20743
+        os.path.join('stats', '_variation.py'),  # gh-22827
+        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 filters should not be used outside of tests.\n"
+            "Found in:\n    {}".format(
+                "\n    ".join(bad_filters)))
+
diff --git a/.venv/lib/python3.11/site-packages/scipy/_lib/uarray.py b/.venv/lib/python3.11/site-packages/scipy/_lib/uarray.py
new file mode 100644
index 0000000000000000000000000000000000000000..b29fc713efb3e836cc179ac87ce41f87b51870ef
--- /dev/null
+++ b/.venv/lib/python3.11/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/.venv/lib/python3.11/site-packages/scipy/cluster/__init__.py b/.venv/lib/python3.11/site-packages/scipy/cluster/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..7b7784969a573d05cc6b98ee9066c42720156d1d
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/cluster/__init__.py
@@ -0,0 +1,31 @@
+"""
+=========================================
+Clustering package (:mod:`scipy.cluster`)
+=========================================
+
+.. currentmodule:: scipy.cluster
+
+Clustering algorithms are useful in information theory, target detection,
+communications, compression, and other areas. The `vq` module only
+supports vector quantization and the k-means algorithms.
+
+The `hierarchy` module provides functions for hierarchical and
+agglomerative clustering.  Its features include generating hierarchical
+clusters from distance matrices,
+calculating statistics on clusters, cutting linkages
+to generate flat clusters, and visualizing clusters with dendrograms.
+
+.. toctree::
+   :maxdepth: 1
+
+   cluster.vq
+   cluster.hierarchy
+
+"""
+__all__ = ['vq', 'hierarchy']
+
+from . import vq, hierarchy
+
+from scipy._lib._testutils import PytestTester
+test = PytestTester(__name__)
+del PytestTester
diff --git a/.venv/lib/python3.11/site-packages/scipy/cluster/_hierarchy.cpython-311-x86_64-linux-gnu.so b/.venv/lib/python3.11/site-packages/scipy/cluster/_hierarchy.cpython-311-x86_64-linux-gnu.so
new file mode 100644
index 0000000000000000000000000000000000000000..86fc611b0c2620f5eef39a73dfb83c3207d2946b
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/cluster/_hierarchy.cpython-311-x86_64-linux-gnu.so
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:27ef5f58760dcf36d5582d6465b226d544c27dd10d31be374096b61122326333
+size 249160
diff --git a/.venv/lib/python3.11/site-packages/scipy/cluster/_optimal_leaf_ordering.cpython-311-x86_64-linux-gnu.so b/.venv/lib/python3.11/site-packages/scipy/cluster/_optimal_leaf_ordering.cpython-311-x86_64-linux-gnu.so
new file mode 100644
index 0000000000000000000000000000000000000000..8dfe43cf8c94342faf30822d0f1657d889041845
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/cluster/_optimal_leaf_ordering.cpython-311-x86_64-linux-gnu.so
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6214d0140116fe646cc03f6111b4e37d8ece750f19128787779f09777960f0fa
+size 176168
diff --git a/.venv/lib/python3.11/site-packages/scipy/cluster/_vq.cpython-311-x86_64-linux-gnu.so b/.venv/lib/python3.11/site-packages/scipy/cluster/_vq.cpython-311-x86_64-linux-gnu.so
new file mode 100644
index 0000000000000000000000000000000000000000..d1be4feb3581c34da7ce90a9d09e5df4d42bd99e
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/cluster/_vq.cpython-311-x86_64-linux-gnu.so
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:873aa071ef24d84970943c724c060e9fd97b336bc4dec3d88bd5e4d218a4aba8
+size 117200
diff --git a/.venv/lib/python3.11/site-packages/scipy/cluster/hierarchy.py b/.venv/lib/python3.11/site-packages/scipy/cluster/hierarchy.py
new file mode 100644
index 0000000000000000000000000000000000000000..c94986afb9fdf0756de991fa3c5de86f994d6e52
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/cluster/hierarchy.py
@@ -0,0 +1,4338 @@
+"""
+Hierarchical clustering (:mod:`scipy.cluster.hierarchy`)
+========================================================
+
+.. currentmodule:: scipy.cluster.hierarchy
+
+These functions cut hierarchical clusterings into flat clusterings
+or find the roots of the forest formed by a cut by providing the flat
+cluster ids of each observation.
+
+.. autosummary::
+   :toctree: generated/
+
+   fcluster
+   fclusterdata
+   leaders
+
+These are routines for agglomerative clustering.
+
+.. autosummary::
+   :toctree: generated/
+
+   linkage
+   single
+   complete
+   average
+   weighted
+   centroid
+   median
+   ward
+
+These routines compute statistics on hierarchies.
+
+.. autosummary::
+   :toctree: generated/
+
+   cophenet
+   from_mlab_linkage
+   inconsistent
+   maxinconsts
+   maxdists
+   maxRstat
+   to_mlab_linkage
+
+Routines for visualizing flat clusters.
+
+.. autosummary::
+   :toctree: generated/
+
+   dendrogram
+
+These are data structures and routines for representing hierarchies as
+tree objects.
+
+.. autosummary::
+   :toctree: generated/
+
+   ClusterNode
+   leaves_list
+   to_tree
+   cut_tree
+   optimal_leaf_ordering
+
+These are predicates for checking the validity of linkage and
+inconsistency matrices as well as for checking isomorphism of two
+flat cluster assignments.
+
+.. autosummary::
+   :toctree: generated/
+
+   is_valid_im
+   is_valid_linkage
+   is_isomorphic
+   is_monotonic
+   correspond
+   num_obs_linkage
+
+Utility routines for plotting:
+
+.. autosummary::
+   :toctree: generated/
+
+   set_link_color_palette
+
+Utility classes:
+
+.. autosummary::
+   :toctree: generated/
+
+   DisjointSet -- data structure for incremental connectivity queries
+
+"""
+# Copyright (C) Damian Eads, 2007-2008. New BSD License.
+
+# hierarchy.py (derived from cluster.py, http://scipy-cluster.googlecode.com)
+#
+# Author: Damian Eads
+# Date:   September 22, 2007
+#
+# Copyright (c) 2007, 2008, Damian Eads
+#
+# 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 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.
+#   - Neither the name of the author nor the names of its
+#     contributors may be used to endorse or promote products derived
+#     from this software without specific prior written permission.
+#
+# 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
+# OWNER 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 warnings
+import bisect
+from collections import deque
+
+import numpy as np
+from . import _hierarchy, _optimal_leaf_ordering
+import scipy.spatial.distance as distance
+from scipy._lib._array_api import (_asarray, array_namespace, is_dask,
+                                   is_lazy_array, xp_capabilities, xp_copy)
+from scipy._lib._disjoint_set import DisjointSet
+import scipy._lib.array_api_extra as xpx
+
+
+_LINKAGE_METHODS = {'single': 0, 'complete': 1, 'average': 2, 'centroid': 3,
+                    'median': 4, 'ward': 5, 'weighted': 6}
+_EUCLIDEAN_METHODS = ('centroid', 'median', 'ward')
+
+__all__ = ['ClusterNode', 'DisjointSet', 'average', 'centroid', 'complete',
+           'cophenet', 'correspond', 'cut_tree', 'dendrogram', 'fcluster',
+           'fclusterdata', 'from_mlab_linkage', 'inconsistent',
+           'is_isomorphic', 'is_monotonic', 'is_valid_im', 'is_valid_linkage',
+           'leaders', 'leaves_list', 'linkage', 'maxRstat', 'maxdists',
+           'maxinconsts', 'median', 'num_obs_linkage', 'optimal_leaf_ordering',
+           'set_link_color_palette', 'single', 'to_mlab_linkage', 'to_tree',
+           'ward', 'weighted']
+
+
+class ClusterWarning(UserWarning):
+    pass
+
+
+def _warning(s):
+    warnings.warn(f'scipy.cluster: {s}', ClusterWarning, stacklevel=3)
+
+
+def int_floor(arr, xp):
+    # array_api_strict is strict about not allowing `int()` on a float array.
+    # That's typically not needed, here it is - so explicitly convert
+    return int(xp.asarray(arr, dtype=xp.int64))
+
+
+lazy_cython = xp_capabilities(
+    cpu_only=True, reason="Cython code",
+    warnings=[("dask.array", "merges chunks")])
+
+
+@lazy_cython
+def single(y):
+    """
+    Perform single/min/nearest linkage on the condensed distance matrix ``y``.
+
+    Parameters
+    ----------
+    y : ndarray
+        The upper triangular of the distance matrix. The result of
+        ``pdist`` is returned in this form.
+
+    Returns
+    -------
+    Z : ndarray
+        The linkage matrix.
+
+    See Also
+    --------
+    linkage : for advanced creation of hierarchical clusterings.
+    scipy.spatial.distance.pdist : pairwise distance metrics
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import single, fcluster
+    >>> from scipy.spatial.distance import pdist
+
+    First, we need a toy dataset to play with::
+
+        x x    x x
+        x        x
+
+        x        x
+        x x    x x
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    Then, we get a condensed distance matrix from this dataset:
+
+    >>> y = pdist(X)
+
+    Finally, we can perform the clustering:
+
+    >>> Z = single(y)
+    >>> Z
+    array([[ 0.,  1.,  1.,  2.],
+           [ 2., 12.,  1.,  3.],
+           [ 3.,  4.,  1.,  2.],
+           [ 5., 14.,  1.,  3.],
+           [ 6.,  7.,  1.,  2.],
+           [ 8., 16.,  1.,  3.],
+           [ 9., 10.,  1.,  2.],
+           [11., 18.,  1.,  3.],
+           [13., 15.,  2.,  6.],
+           [17., 20.,  2.,  9.],
+           [19., 21.,  2., 12.]])
+
+    The linkage matrix ``Z`` represents a dendrogram - see
+    `scipy.cluster.hierarchy.linkage` for a detailed explanation of its
+    contents.
+
+    We can use `scipy.cluster.hierarchy.fcluster` to see to which cluster
+    each initial point would belong given a distance threshold:
+
+    >>> fcluster(Z, 0.9, criterion='distance')
+    array([ 7,  8,  9, 10, 11, 12,  4,  5,  6,  1,  2,  3], dtype=int32)
+    >>> fcluster(Z, 1, criterion='distance')
+    array([3, 3, 3, 4, 4, 4, 2, 2, 2, 1, 1, 1], dtype=int32)
+    >>> fcluster(Z, 2, criterion='distance')
+    array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], dtype=int32)
+
+    Also, `scipy.cluster.hierarchy.dendrogram` can be used to generate a
+    plot of the dendrogram.
+    """
+    return linkage(y, method='single', metric='euclidean')
+
+
+@lazy_cython
+def complete(y):
+    """
+    Perform complete/max/farthest point linkage on a condensed distance matrix.
+
+    Parameters
+    ----------
+    y : ndarray
+        The upper triangular of the distance matrix. The result of
+        ``pdist`` is returned in this form.
+
+    Returns
+    -------
+    Z : ndarray
+        A linkage matrix containing the hierarchical clustering. See
+        the `linkage` function documentation for more information
+        on its structure.
+
+    See Also
+    --------
+    linkage : for advanced creation of hierarchical clusterings.
+    scipy.spatial.distance.pdist : pairwise distance metrics
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import complete, fcluster
+    >>> from scipy.spatial.distance import pdist
+
+    First, we need a toy dataset to play with::
+
+        x x    x x
+        x        x
+
+        x        x
+        x x    x x
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    Then, we get a condensed distance matrix from this dataset:
+
+    >>> y = pdist(X)
+
+    Finally, we can perform the clustering:
+
+    >>> Z = complete(y)
+    >>> Z
+    array([[ 0.        ,  1.        ,  1.        ,  2.        ],
+           [ 3.        ,  4.        ,  1.        ,  2.        ],
+           [ 6.        ,  7.        ,  1.        ,  2.        ],
+           [ 9.        , 10.        ,  1.        ,  2.        ],
+           [ 2.        , 12.        ,  1.41421356,  3.        ],
+           [ 5.        , 13.        ,  1.41421356,  3.        ],
+           [ 8.        , 14.        ,  1.41421356,  3.        ],
+           [11.        , 15.        ,  1.41421356,  3.        ],
+           [16.        , 17.        ,  4.12310563,  6.        ],
+           [18.        , 19.        ,  4.12310563,  6.        ],
+           [20.        , 21.        ,  5.65685425, 12.        ]])
+
+    The linkage matrix ``Z`` represents a dendrogram - see
+    `scipy.cluster.hierarchy.linkage` for a detailed explanation of its
+    contents.
+
+    We can use `scipy.cluster.hierarchy.fcluster` to see to which cluster
+    each initial point would belong given a distance threshold:
+
+    >>> fcluster(Z, 0.9, criterion='distance')
+    array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12], dtype=int32)
+    >>> fcluster(Z, 1.5, criterion='distance')
+    array([1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4], dtype=int32)
+    >>> fcluster(Z, 4.5, criterion='distance')
+    array([1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2], dtype=int32)
+    >>> fcluster(Z, 6, criterion='distance')
+    array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], dtype=int32)
+
+    Also, `scipy.cluster.hierarchy.dendrogram` can be used to generate a
+    plot of the dendrogram.
+    """
+    return linkage(y, method='complete', metric='euclidean')
+
+
+@lazy_cython
+def average(y):
+    """
+    Perform average/UPGMA linkage on a condensed distance matrix.
+
+    Parameters
+    ----------
+    y : ndarray
+        The upper triangular of the distance matrix. The result of
+        ``pdist`` is returned in this form.
+
+    Returns
+    -------
+    Z : ndarray
+        A linkage matrix containing the hierarchical clustering. See
+        `linkage` for more information on its structure.
+
+    See Also
+    --------
+    linkage : for advanced creation of hierarchical clusterings.
+    scipy.spatial.distance.pdist : pairwise distance metrics
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import average, fcluster
+    >>> from scipy.spatial.distance import pdist
+
+    First, we need a toy dataset to play with::
+
+        x x    x x
+        x        x
+
+        x        x
+        x x    x x
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    Then, we get a condensed distance matrix from this dataset:
+
+    >>> y = pdist(X)
+
+    Finally, we can perform the clustering:
+
+    >>> Z = average(y)
+    >>> Z
+    array([[ 0.        ,  1.        ,  1.        ,  2.        ],
+           [ 3.        ,  4.        ,  1.        ,  2.        ],
+           [ 6.        ,  7.        ,  1.        ,  2.        ],
+           [ 9.        , 10.        ,  1.        ,  2.        ],
+           [ 2.        , 12.        ,  1.20710678,  3.        ],
+           [ 5.        , 13.        ,  1.20710678,  3.        ],
+           [ 8.        , 14.        ,  1.20710678,  3.        ],
+           [11.        , 15.        ,  1.20710678,  3.        ],
+           [16.        , 17.        ,  3.39675184,  6.        ],
+           [18.        , 19.        ,  3.39675184,  6.        ],
+           [20.        , 21.        ,  4.09206523, 12.        ]])
+
+    The linkage matrix ``Z`` represents a dendrogram - see
+    `scipy.cluster.hierarchy.linkage` for a detailed explanation of its
+    contents.
+
+    We can use `scipy.cluster.hierarchy.fcluster` to see to which cluster
+    each initial point would belong given a distance threshold:
+
+    >>> fcluster(Z, 0.9, criterion='distance')
+    array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12], dtype=int32)
+    >>> fcluster(Z, 1.5, criterion='distance')
+    array([1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4], dtype=int32)
+    >>> fcluster(Z, 4, criterion='distance')
+    array([1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2], dtype=int32)
+    >>> fcluster(Z, 6, criterion='distance')
+    array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], dtype=int32)
+
+    Also, `scipy.cluster.hierarchy.dendrogram` can be used to generate a
+    plot of the dendrogram.
+
+    """
+    return linkage(y, method='average', metric='euclidean')
+
+
+@lazy_cython
+def weighted(y):
+    """
+    Perform weighted/WPGMA linkage on the condensed distance matrix.
+
+    See `linkage` for more information on the return
+    structure and algorithm.
+
+    Parameters
+    ----------
+    y : ndarray
+        The upper triangular of the distance matrix. The result of
+        ``pdist`` is returned in this form.
+
+    Returns
+    -------
+    Z : ndarray
+        A linkage matrix containing the hierarchical clustering. See
+        `linkage` for more information on its structure.
+
+    See Also
+    --------
+    linkage : for advanced creation of hierarchical clusterings.
+    scipy.spatial.distance.pdist : pairwise distance metrics
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import weighted, fcluster
+    >>> from scipy.spatial.distance import pdist
+
+    First, we need a toy dataset to play with::
+
+        x x    x x
+        x        x
+
+        x        x
+        x x    x x
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    Then, we get a condensed distance matrix from this dataset:
+
+    >>> y = pdist(X)
+
+    Finally, we can perform the clustering:
+
+    >>> Z = weighted(y)
+    >>> Z
+    array([[ 0.        ,  1.        ,  1.        ,  2.        ],
+           [ 6.        ,  7.        ,  1.        ,  2.        ],
+           [ 3.        ,  4.        ,  1.        ,  2.        ],
+           [ 9.        , 11.        ,  1.        ,  2.        ],
+           [ 2.        , 12.        ,  1.20710678,  3.        ],
+           [ 8.        , 13.        ,  1.20710678,  3.        ],
+           [ 5.        , 14.        ,  1.20710678,  3.        ],
+           [10.        , 15.        ,  1.20710678,  3.        ],
+           [18.        , 19.        ,  3.05595762,  6.        ],
+           [16.        , 17.        ,  3.32379407,  6.        ],
+           [20.        , 21.        ,  4.06357713, 12.        ]])
+
+    The linkage matrix ``Z`` represents a dendrogram - see
+    `scipy.cluster.hierarchy.linkage` for a detailed explanation of its
+    contents.
+
+    We can use `scipy.cluster.hierarchy.fcluster` to see to which cluster
+    each initial point would belong given a distance threshold:
+
+    >>> fcluster(Z, 0.9, criterion='distance')
+    array([ 7,  8,  9,  1,  2,  3, 10, 11, 12,  4,  6,  5], dtype=int32)
+    >>> fcluster(Z, 1.5, criterion='distance')
+    array([3, 3, 3, 1, 1, 1, 4, 4, 4, 2, 2, 2], dtype=int32)
+    >>> fcluster(Z, 4, criterion='distance')
+    array([2, 2, 2, 1, 1, 1, 2, 2, 2, 1, 1, 1], dtype=int32)
+    >>> fcluster(Z, 6, criterion='distance')
+    array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], dtype=int32)
+
+    Also, `scipy.cluster.hierarchy.dendrogram` can be used to generate a
+    plot of the dendrogram.
+
+    """
+    return linkage(y, method='weighted', metric='euclidean')
+
+
+@lazy_cython
+def centroid(y):
+    """
+    Perform centroid/UPGMC linkage.
+
+    See `linkage` for more information on the input matrix,
+    return structure, and algorithm.
+
+    The following are common calling conventions:
+
+    1. ``Z = centroid(y)``
+
+       Performs centroid/UPGMC linkage on the condensed distance
+       matrix ``y``.
+
+    2. ``Z = centroid(X)``
+
+       Performs centroid/UPGMC linkage on the observation matrix ``X``
+       using Euclidean distance as the distance metric.
+
+    Parameters
+    ----------
+    y : ndarray
+        A condensed distance matrix. A condensed
+        distance matrix is a flat array containing the upper
+        triangular of the distance matrix. This is the form that
+        ``pdist`` returns. Alternatively, a collection of
+        m observation vectors in n dimensions may be passed as
+        an m by n array.
+
+    Returns
+    -------
+    Z : ndarray
+        A linkage matrix containing the hierarchical clustering. See
+        the `linkage` function documentation for more information
+        on its structure.
+
+    See Also
+    --------
+    linkage : for advanced creation of hierarchical clusterings.
+    scipy.spatial.distance.pdist : pairwise distance metrics
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import centroid, fcluster
+    >>> from scipy.spatial.distance import pdist
+
+    First, we need a toy dataset to play with::
+
+        x x    x x
+        x        x
+
+        x        x
+        x x    x x
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    Then, we get a condensed distance matrix from this dataset:
+
+    >>> y = pdist(X)
+
+    Finally, we can perform the clustering:
+
+    >>> Z = centroid(y)
+    >>> Z
+    array([[ 0.        ,  1.        ,  1.        ,  2.        ],
+           [ 3.        ,  4.        ,  1.        ,  2.        ],
+           [ 9.        , 10.        ,  1.        ,  2.        ],
+           [ 6.        ,  7.        ,  1.        ,  2.        ],
+           [ 2.        , 12.        ,  1.11803399,  3.        ],
+           [ 5.        , 13.        ,  1.11803399,  3.        ],
+           [ 8.        , 15.        ,  1.11803399,  3.        ],
+           [11.        , 14.        ,  1.11803399,  3.        ],
+           [18.        , 19.        ,  3.33333333,  6.        ],
+           [16.        , 17.        ,  3.33333333,  6.        ],
+           [20.        , 21.        ,  3.33333333, 12.        ]]) # may vary
+
+    The linkage matrix ``Z`` represents a dendrogram - see
+    `scipy.cluster.hierarchy.linkage` for a detailed explanation of its
+    contents.
+
+    We can use `scipy.cluster.hierarchy.fcluster` to see to which cluster
+    each initial point would belong given a distance threshold:
+
+    >>> fcluster(Z, 0.9, criterion='distance')
+    array([ 7,  8,  9, 10, 11, 12,  1,  2,  3,  4,  5,  6], dtype=int32) # may vary
+    >>> fcluster(Z, 1.1, criterion='distance')
+    array([5, 5, 6, 7, 7, 8, 1, 1, 2, 3, 3, 4], dtype=int32) # may vary
+    >>> fcluster(Z, 2, criterion='distance')
+    array([3, 3, 3, 4, 4, 4, 1, 1, 1, 2, 2, 2], dtype=int32) # may vary
+    >>> fcluster(Z, 4, criterion='distance')
+    array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], dtype=int32)
+
+    Also, `scipy.cluster.hierarchy.dendrogram` can be used to generate a
+    plot of the dendrogram.
+
+    """
+    return linkage(y, method='centroid', metric='euclidean')
+
+
+@lazy_cython
+def median(y):
+    """
+    Perform median/WPGMC linkage.
+
+    See `linkage` for more information on the return structure
+    and algorithm.
+
+     The following are common calling conventions:
+
+     1. ``Z = median(y)``
+
+        Performs median/WPGMC linkage on the condensed distance matrix
+        ``y``.  See ``linkage`` for more information on the return
+        structure and algorithm.
+
+     2. ``Z = median(X)``
+
+        Performs median/WPGMC linkage on the observation matrix ``X``
+        using Euclidean distance as the distance metric. See `linkage`
+        for more information on the return structure and algorithm.
+
+    Parameters
+    ----------
+    y : ndarray
+        A condensed distance matrix. A condensed
+        distance matrix is a flat array containing the upper
+        triangular of the distance matrix. This is the form that
+        ``pdist`` returns.  Alternatively, a collection of
+        m observation vectors in n dimensions may be passed as
+        an m by n array.
+
+    Returns
+    -------
+    Z : ndarray
+        The hierarchical clustering encoded as a linkage matrix.
+
+    See Also
+    --------
+    linkage : for advanced creation of hierarchical clusterings.
+    scipy.spatial.distance.pdist : pairwise distance metrics
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import median, fcluster
+    >>> from scipy.spatial.distance import pdist
+
+    First, we need a toy dataset to play with::
+
+        x x    x x
+        x        x
+
+        x        x
+        x x    x x
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    Then, we get a condensed distance matrix from this dataset:
+
+    >>> y = pdist(X)
+
+    Finally, we can perform the clustering:
+
+    >>> Z = median(y)
+    >>> Z
+    array([[ 0.        ,  1.        ,  1.        ,  2.        ],
+           [ 3.        ,  4.        ,  1.        ,  2.        ],
+           [ 9.        , 10.        ,  1.        ,  2.        ],
+           [ 6.        ,  7.        ,  1.        ,  2.        ],
+           [ 2.        , 12.        ,  1.11803399,  3.        ],
+           [ 5.        , 13.        ,  1.11803399,  3.        ],
+           [ 8.        , 15.        ,  1.11803399,  3.        ],
+           [11.        , 14.        ,  1.11803399,  3.        ],
+           [18.        , 19.        ,  3.        ,  6.        ],
+           [16.        , 17.        ,  3.5       ,  6.        ],
+           [20.        , 21.        ,  3.25      , 12.        ]])
+
+    The linkage matrix ``Z`` represents a dendrogram - see
+    `scipy.cluster.hierarchy.linkage` for a detailed explanation of its
+    contents.
+
+    We can use `scipy.cluster.hierarchy.fcluster` to see to which cluster
+    each initial point would belong given a distance threshold:
+
+    >>> fcluster(Z, 0.9, criterion='distance')
+    array([ 7,  8,  9, 10, 11, 12,  1,  2,  3,  4,  5,  6], dtype=int32)
+    >>> fcluster(Z, 1.1, criterion='distance')
+    array([5, 5, 6, 7, 7, 8, 1, 1, 2, 3, 3, 4], dtype=int32)
+    >>> fcluster(Z, 2, criterion='distance')
+    array([3, 3, 3, 4, 4, 4, 1, 1, 1, 2, 2, 2], dtype=int32)
+    >>> fcluster(Z, 4, criterion='distance')
+    array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], dtype=int32)
+
+    Also, `scipy.cluster.hierarchy.dendrogram` can be used to generate a
+    plot of the dendrogram.
+
+    """
+    return linkage(y, method='median', metric='euclidean')
+
+
+@lazy_cython
+def ward(y):
+    """
+    Perform Ward's linkage on a condensed distance matrix.
+
+    See `linkage` for more information on the return structure
+    and algorithm.
+
+    The following are common calling conventions:
+
+    1. ``Z = ward(y)``
+       Performs Ward's linkage on the condensed distance matrix ``y``.
+
+    2. ``Z = ward(X)``
+       Performs Ward's linkage on the observation matrix ``X`` using
+       Euclidean distance as the distance metric.
+
+    Parameters
+    ----------
+    y : ndarray
+        A condensed distance matrix. A condensed
+        distance matrix is a flat array containing the upper
+        triangular of the distance matrix. This is the form that
+        ``pdist`` returns.  Alternatively, a collection of
+        m observation vectors in n dimensions may be passed as
+        an m by n array.
+
+    Returns
+    -------
+    Z : ndarray
+        The hierarchical clustering encoded as a linkage matrix. See
+        `linkage` for more information on the return structure and
+        algorithm.
+
+    See Also
+    --------
+    linkage : for advanced creation of hierarchical clusterings.
+    scipy.spatial.distance.pdist : pairwise distance metrics
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import ward, fcluster
+    >>> from scipy.spatial.distance import pdist
+
+    First, we need a toy dataset to play with::
+
+        x x    x x
+        x        x
+
+        x        x
+        x x    x x
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    Then, we get a condensed distance matrix from this dataset:
+
+    >>> y = pdist(X)
+
+    Finally, we can perform the clustering:
+
+    >>> Z = ward(y)
+    >>> Z
+    array([[ 0.        ,  1.        ,  1.        ,  2.        ],
+           [ 3.        ,  4.        ,  1.        ,  2.        ],
+           [ 6.        ,  7.        ,  1.        ,  2.        ],
+           [ 9.        , 10.        ,  1.        ,  2.        ],
+           [ 2.        , 12.        ,  1.29099445,  3.        ],
+           [ 5.        , 13.        ,  1.29099445,  3.        ],
+           [ 8.        , 14.        ,  1.29099445,  3.        ],
+           [11.        , 15.        ,  1.29099445,  3.        ],
+           [16.        , 17.        ,  5.77350269,  6.        ],
+           [18.        , 19.        ,  5.77350269,  6.        ],
+           [20.        , 21.        ,  8.16496581, 12.        ]])
+
+    The linkage matrix ``Z`` represents a dendrogram - see
+    `scipy.cluster.hierarchy.linkage` for a detailed explanation of its
+    contents.
+
+    We can use `scipy.cluster.hierarchy.fcluster` to see to which cluster
+    each initial point would belong given a distance threshold:
+
+    >>> fcluster(Z, 0.9, criterion='distance')
+    array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12], dtype=int32)
+    >>> fcluster(Z, 1.1, criterion='distance')
+    array([1, 1, 2, 3, 3, 4, 5, 5, 6, 7, 7, 8], dtype=int32)
+    >>> fcluster(Z, 3, criterion='distance')
+    array([1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4], dtype=int32)
+    >>> fcluster(Z, 9, criterion='distance')
+    array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], dtype=int32)
+
+    Also, `scipy.cluster.hierarchy.dendrogram` can be used to generate a
+    plot of the dendrogram.
+
+    """
+    return linkage(y, method='ward', metric='euclidean')
+
+
+@lazy_cython
+def linkage(y, method='single', metric='euclidean', optimal_ordering=False):
+    """
+    Perform hierarchical/agglomerative clustering.
+
+    The input y may be either a 1-D condensed distance matrix
+    or a 2-D array of observation vectors.
+
+    If y is a 1-D condensed distance matrix,
+    then y must be a :math:`\\binom{n}{2}` sized
+    vector, where n is the number of original observations paired
+    in the distance matrix. The behavior of this function is very
+    similar to the MATLAB linkage function.
+
+    A :math:`(n-1)` by 4 matrix ``Z`` is returned. At the
+    :math:`i`-th iteration, clusters with indices ``Z[i, 0]`` and
+    ``Z[i, 1]`` are combined to form cluster :math:`n + i`. A
+    cluster with an index less than :math:`n` corresponds to one of
+    the :math:`n` original observations. The distance between
+    clusters ``Z[i, 0]`` and ``Z[i, 1]`` is given by ``Z[i, 2]``. The
+    fourth value ``Z[i, 3]`` represents the number of original
+    observations in the newly formed cluster.
+
+    The following linkage methods are used to compute the distance
+    :math:`d(s, t)` between two clusters :math:`s` and
+    :math:`t`. The algorithm begins with a forest of clusters that
+    have yet to be used in the hierarchy being formed. When two
+    clusters :math:`s` and :math:`t` from this forest are combined
+    into a single cluster :math:`u`, :math:`s` and :math:`t` are
+    removed from the forest, and :math:`u` is added to the
+    forest. When only one cluster remains in the forest, the algorithm
+    stops, and this cluster becomes the root.
+
+    A distance matrix is maintained at each iteration. The ``d[i,j]``
+    entry corresponds to the distance between cluster :math:`i` and
+    :math:`j` in the original forest.
+
+    At each iteration, the algorithm must update the distance matrix
+    to reflect the distance of the newly formed cluster u with the
+    remaining clusters in the forest.
+
+    Suppose there are :math:`|u|` original observations
+    :math:`u[0], \\ldots, u[|u|-1]` in cluster :math:`u` and
+    :math:`|v|` original objects :math:`v[0], \\ldots, v[|v|-1]` in
+    cluster :math:`v`. Recall, :math:`s` and :math:`t` are
+    combined to form cluster :math:`u`. Let :math:`v` be any
+    remaining cluster in the forest that is not :math:`u`.
+
+    The following are methods for calculating the distance between the
+    newly formed cluster :math:`u` and each :math:`v`.
+
+      * method='single' assigns
+
+        .. math::
+           d(u,v) = \\min(dist(u[i],v[j]))
+
+        for all points :math:`i` in cluster :math:`u` and
+        :math:`j` in cluster :math:`v`. This is also known as the
+        Nearest Point Algorithm.
+
+      * method='complete' assigns
+
+        .. math::
+           d(u, v) = \\max(dist(u[i],v[j]))
+
+        for all points :math:`i` in cluster u and :math:`j` in
+        cluster :math:`v`. This is also known by the Farthest Point
+        Algorithm or Voor Hees Algorithm.
+
+      * method='average' assigns
+
+        .. math::
+           d(u,v) = \\sum_{ij} \\frac{d(u[i], v[j])}
+                                   {(|u|*|v|)}
+
+        for all points :math:`i` and :math:`j` where :math:`|u|`
+        and :math:`|v|` are the cardinalities of clusters :math:`u`
+        and :math:`v`, respectively. This is also called the UPGMA
+        algorithm.
+
+      * method='weighted' assigns
+
+        .. math::
+           d(u,v) = (dist(s,v) + dist(t,v))/2
+
+        where cluster u was formed with cluster s and t and v
+        is a remaining cluster in the forest (also called WPGMA).
+
+      * method='centroid' assigns
+
+        .. math::
+           dist(s,t) = ||c_s-c_t||_2
+
+        where :math:`c_s` and :math:`c_t` are the centroids of
+        clusters :math:`s` and :math:`t`, respectively. When two
+        clusters :math:`s` and :math:`t` are combined into a new
+        cluster :math:`u`, the new centroid is computed over all the
+        original objects in clusters :math:`s` and :math:`t`. The
+        distance then becomes the Euclidean distance between the
+        centroid of :math:`u` and the centroid of a remaining cluster
+        :math:`v` in the forest. This is also known as the UPGMC
+        algorithm.
+
+      * method='median' assigns :math:`d(s,t)` like the ``centroid``
+        method. When two clusters :math:`s` and :math:`t` are combined
+        into a new cluster :math:`u`, the average of centroids s and t
+        give the new centroid :math:`u`. This is also known as the
+        WPGMC algorithm.
+
+      * method='ward' uses the Ward variance minimization algorithm.
+        The new entry :math:`d(u,v)` is computed as follows,
+
+        .. math::
+
+           d(u,v) = \\sqrt{\\frac{|v|+|s|}
+                               {T}d(v,s)^2
+                        + \\frac{|v|+|t|}
+                               {T}d(v,t)^2
+                        - \\frac{|v|}
+                               {T}d(s,t)^2}
+
+        where :math:`u` is the newly joined cluster consisting of
+        clusters :math:`s` and :math:`t`, :math:`v` is an unused
+        cluster in the forest, :math:`T=|v|+|s|+|t|`, and
+        :math:`|*|` is the cardinality of its argument. This is also
+        known as the incremental algorithm.
+
+    Warning: When the minimum distance pair in the forest is chosen, there
+    may be two or more pairs with the same minimum distance. This
+    implementation may choose a different minimum than the MATLAB
+    version.
+
+    Parameters
+    ----------
+    y : ndarray
+        A condensed distance matrix. A condensed distance matrix
+        is a flat array containing the upper triangular of the distance matrix.
+        This is the form that ``pdist`` returns. Alternatively, a collection of
+        :math:`m` observation vectors in :math:`n` dimensions may be passed as
+        an :math:`m` by :math:`n` array. All elements of the condensed distance
+        matrix must be finite, i.e., no NaNs or infs.
+    method : str, optional
+        The linkage algorithm to use. See the ``Linkage Methods`` section below
+        for full descriptions.
+    metric : str or function, optional
+        The distance metric to use in the case that y is a collection of
+        observation vectors; ignored otherwise. See the ``pdist``
+        function for a list of valid distance metrics. A custom distance
+        function can also be used.
+    optimal_ordering : bool, optional
+        If True, the linkage matrix will be reordered so that the distance
+        between successive leaves is minimal. This results in a more intuitive
+        tree structure when the data are visualized. defaults to False, because
+        this algorithm can be slow, particularly on large datasets [2]_. See
+        also the `optimal_leaf_ordering` function.
+
+        .. versionadded:: 1.0.0
+
+    Returns
+    -------
+    Z : ndarray
+        The hierarchical clustering encoded as a linkage matrix.
+
+    Notes
+    -----
+    1. For method 'single', an optimized algorithm based on minimum spanning
+       tree is implemented. It has time complexity :math:`O(n^2)`.
+       For methods 'complete', 'average', 'weighted' and 'ward', an algorithm
+       called nearest-neighbors chain is implemented. It also has time
+       complexity :math:`O(n^2)`.
+       For other methods, a naive algorithm is implemented with :math:`O(n^3)`
+       time complexity.
+       All algorithms use :math:`O(n^2)` memory.
+       Refer to [1]_ for details about the algorithms.
+    2. Methods 'centroid', 'median', and 'ward' are correctly defined only if
+       Euclidean pairwise metric is used. If `y` is passed as precomputed
+       pairwise distances, then it is the user's responsibility to assure that
+       these distances are in fact Euclidean, otherwise the produced result
+       will be incorrect.
+
+    See Also
+    --------
+    scipy.spatial.distance.pdist : pairwise distance metrics
+
+    References
+    ----------
+    .. [1] Daniel Mullner, "Modern hierarchical, agglomerative clustering
+           algorithms", :arXiv:`1109.2378v1`.
+    .. [2] Ziv Bar-Joseph, David K. Gifford, Tommi S. Jaakkola, "Fast optimal
+           leaf ordering for hierarchical clustering", 2001. Bioinformatics
+           :doi:`10.1093/bioinformatics/17.suppl_1.S22`
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import dendrogram, linkage
+    >>> from matplotlib import pyplot as plt
+    >>> X = [[i] for i in [2, 8, 0, 4, 1, 9, 9, 0]]
+
+    >>> Z = linkage(X, 'ward')
+    >>> fig = plt.figure(figsize=(25, 10))
+    >>> dn = dendrogram(Z)
+
+    >>> Z = linkage(X, 'single')
+    >>> fig = plt.figure(figsize=(25, 10))
+    >>> dn = dendrogram(Z)
+    >>> plt.show()
+    """
+    xp = array_namespace(y)
+    y = _asarray(y, order='C', dtype=xp.float64, xp=xp)
+    lazy = is_lazy_array(y)
+
+    if method not in _LINKAGE_METHODS:
+        raise ValueError(f"Invalid method: {method}")
+
+    if method in _EUCLIDEAN_METHODS and metric != 'euclidean' and y.ndim == 2:
+        msg = f"`method={method}` requires the distance metric to be Euclidean"
+        raise ValueError(msg)
+
+    if y.ndim == 1:
+        distance.is_valid_y(y, throw=True, name='y')
+    elif y.ndim == 2:
+        if (not lazy and y.shape[0] == y.shape[1]
+            and xp.all(xpx.isclose(xp.linalg.diagonal(y), 0))
+            and xp.all(y >= 0) and xp.all(xpx.isclose(y, y.T))):
+            warnings.warn('The symmetric non-negative hollow observation '
+                          'matrix looks suspiciously like an uncondensed '
+                          'distance matrix',
+                          ClusterWarning, stacklevel=2)
+        y = distance.pdist(y, metric)
+    else:
+        raise ValueError("`y` must be 1 or 2 dimensional.")
+
+    if not lazy and not xp.all(xp.isfinite(y)):
+        raise ValueError("The condensed distance matrix must contain only "
+                         "finite values.")
+
+    n = distance.num_obs_y(y)
+    method_code = _LINKAGE_METHODS[method]
+
+    def cy_linkage(y, validate):
+        if validate and not np.all(np.isfinite(y)):
+            raise ValueError("The condensed distance matrix must contain only "
+                            "finite values.")            
+
+        if method == 'single':
+            return _hierarchy.mst_single_linkage(y, n)
+        elif method in ('complete', 'average', 'weighted', 'ward'):
+            return _hierarchy.nn_chain(y, n, method_code)
+        else:
+            return _hierarchy.fast_linkage(y, n, method_code)
+
+    result = xpx.lazy_apply(cy_linkage, y, validate=lazy,
+                            shape=(n - 1, 4), dtype=xp.float64,
+                            as_numpy=True, xp=xp)
+
+    if optimal_ordering:
+        return optimal_leaf_ordering(result, y)
+    else:
+        return result
+
+
+class ClusterNode:
+    """
+    A tree node class for representing a cluster.
+
+    Leaf nodes correspond to original observations, while non-leaf nodes
+    correspond to non-singleton clusters.
+
+    The `to_tree` function converts a matrix returned by the linkage
+    function into an easy-to-use tree representation.
+
+    All parameter names are also attributes.
+
+    Parameters
+    ----------
+    id : int
+        The node id.
+    left : ClusterNode instance, optional
+        The left child tree node.
+    right : ClusterNode instance, optional
+        The right child tree node.
+    dist : float, optional
+        Distance for this cluster in the linkage matrix.
+    count : int, optional
+        The number of samples in this cluster.
+
+    See Also
+    --------
+    to_tree : for converting a linkage matrix ``Z`` into a tree object.
+
+    """
+
+    def __init__(self, id, left=None, right=None, dist=0.0, count=1):
+        if id < 0:
+            raise ValueError('The id must be non-negative.')
+        if dist < 0:
+            raise ValueError('The distance must be non-negative.')
+        if (left is None and right is not None) or \
+           (left is not None and right is None):
+            raise ValueError('Only full or proper binary trees are permitted.'
+                             '  This node has one child.')
+        if count < 1:
+            raise ValueError('A cluster must contain at least one original '
+                             'observation.')
+        self.id = id
+        self.left = left
+        self.right = right
+        self.dist = dist
+        if self.left is None:
+            self.count = count
+        else:
+            self.count = left.count + right.count
+
+    def __lt__(self, node):
+        if not isinstance(node, ClusterNode):
+            raise ValueError("Can't compare ClusterNode "
+                             f"to type {type(node)}")
+        return self.dist < node.dist
+
+    def __gt__(self, node):
+        if not isinstance(node, ClusterNode):
+            raise ValueError("Can't compare ClusterNode "
+                             f"to type {type(node)}")
+        return self.dist > node.dist
+
+    def __eq__(self, node):
+        if not isinstance(node, ClusterNode):
+            raise ValueError("Can't compare ClusterNode "
+                             f"to type {type(node)}")
+        return self.dist == node.dist
+
+    def get_id(self):
+        """
+        The identifier of the target node.
+
+        For ``0 <= i < n``, `i` corresponds to original observation i.
+        For ``n <= i < 2n-1``, `i` corresponds to non-singleton cluster formed
+        at iteration ``i-n``.
+
+        Returns
+        -------
+        id : int
+            The identifier of the target node.
+
+        """
+        return self.id
+
+    def get_count(self):
+        """
+        The number of leaf nodes (original observations) belonging to
+        the cluster node nd. If the target node is a leaf, 1 is
+        returned.
+
+        Returns
+        -------
+        get_count : int
+            The number of leaf nodes below the target node.
+
+        """
+        return self.count
+
+    def get_left(self):
+        """
+        Return a reference to the left child tree object.
+
+        Returns
+        -------
+        left : ClusterNode
+            The left child of the target node. If the node is a leaf,
+            None is returned.
+
+        """
+        return self.left
+
+    def get_right(self):
+        """
+        Return a reference to the right child tree object.
+
+        Returns
+        -------
+        right : ClusterNode
+            The left child of the target node. If the node is a leaf,
+            None is returned.
+
+        """
+        return self.right
+
+    def is_leaf(self):
+        """
+        Return True if the target node is a leaf.
+
+        Returns
+        -------
+        leafness : bool
+            True if the target node is a leaf node.
+
+        """
+        return self.left is None
+
+    def pre_order(self, func=(lambda x: x.id)):
+        """
+        Perform pre-order traversal without recursive function calls.
+
+        When a leaf node is first encountered, ``func`` is called with
+        the leaf node as its argument, and its result is appended to
+        the list.
+
+        For example, the statement::
+
+           ids = root.pre_order(lambda x: x.id)
+
+        returns a list of the node ids corresponding to the leaf nodes
+        of the tree as they appear from left to right.
+
+        Parameters
+        ----------
+        func : function
+            Applied to each leaf ClusterNode object in the pre-order traversal.
+            Given the ``i``-th leaf node in the pre-order traversal ``n[i]``,
+            the result of ``func(n[i])`` is stored in ``L[i]``. If not
+            provided, the index of the original observation to which the node
+            corresponds is used.
+
+        Returns
+        -------
+        L : list
+            The pre-order traversal.
+
+        """
+        # Do a preorder traversal, caching the result. To avoid having to do
+        # recursion, we'll store the previous index we've visited in a vector.
+        n = self.count
+
+        curNode = [None] * (2 * n)
+        lvisited = set()
+        rvisited = set()
+        curNode[0] = self
+        k = 0
+        preorder = []
+        while k >= 0:
+            nd = curNode[k]
+            ndid = nd.id
+            if nd.is_leaf():
+                preorder.append(func(nd))
+                k = k - 1
+            else:
+                if ndid not in lvisited:
+                    curNode[k + 1] = nd.left
+                    lvisited.add(ndid)
+                    k = k + 1
+                elif ndid not in rvisited:
+                    curNode[k + 1] = nd.right
+                    rvisited.add(ndid)
+                    k = k + 1
+                # If we've visited the left and right of this non-leaf
+                # node already, go up in the tree.
+                else:
+                    k = k - 1
+
+        return preorder
+
+
+_cnode_bare = ClusterNode(0)
+_cnode_type = type(ClusterNode)
+
+
+def _order_cluster_tree(Z):
+    """
+    Return clustering nodes in bottom-up order by distance.
+
+    Parameters
+    ----------
+    Z : scipy.cluster.linkage array
+        The linkage matrix.
+
+    Returns
+    -------
+    nodes : list
+        A list of ClusterNode objects.
+    """
+    q = deque()
+    tree = to_tree(Z)
+    q.append(tree)
+    nodes = []
+
+    while q:
+        node = q.popleft()
+        if not node.is_leaf():
+            bisect.insort_left(nodes, node)
+            q.append(node.get_right())
+            q.append(node.get_left())
+    return nodes
+
+
+@xp_capabilities(np_only=True, reason="non-standard indexing")
+def cut_tree(Z, n_clusters=None, height=None):
+    """
+    Given a linkage matrix Z, return the cut tree.
+
+    Parameters
+    ----------
+    Z : scipy.cluster.linkage array
+        The linkage matrix.
+    n_clusters : array_like, optional
+        Number of clusters in the tree at the cut point.
+    height : array_like, optional
+        The height at which to cut the tree. Only possible for ultrametric
+        trees.
+
+    Returns
+    -------
+    cutree : array
+        An array indicating group membership at each agglomeration step. I.e.,
+        for a full cut tree, in the first column each data point is in its own
+        cluster. At the next step, two nodes are merged. Finally, all
+        singleton and non-singleton clusters are in one group. If `n_clusters`
+        or `height` are given, the columns correspond to the columns of
+        `n_clusters` or `height`.
+
+    Examples
+    --------
+    >>> from scipy import cluster
+    >>> import numpy as np
+    >>> from numpy.random import default_rng
+    >>> rng = default_rng()
+    >>> X = rng.random((50, 4))
+    >>> Z = cluster.hierarchy.ward(X)
+    >>> cutree = cluster.hierarchy.cut_tree(Z, n_clusters=[5, 10])
+    >>> cutree[:10]
+    array([[0, 0],
+           [1, 1],
+           [2, 2],
+           [3, 3],
+           [3, 4],
+           [2, 2],
+           [0, 0],
+           [1, 5],
+           [3, 6],
+           [4, 7]])  # random
+
+    """
+    xp = array_namespace(Z)
+    nobs = num_obs_linkage(Z)
+    nodes = _order_cluster_tree(Z)
+
+    if height is not None and n_clusters is not None:
+        raise ValueError("At least one of either height or n_clusters "
+                         "must be None")
+    elif height is None and n_clusters is None:  # return the full cut tree
+        cols_idx = xp.arange(nobs)
+    elif height is not None:
+        height = xp.asarray(height)
+        heights = xp.asarray([x.dist for x in nodes])
+        cols_idx = xp.searchsorted(heights, height)
+    else:
+        n_clusters = xp.asarray(n_clusters)
+        cols_idx = nobs - xp.searchsorted(xp.arange(nobs), n_clusters)
+
+    try:
+        n_cols = len(cols_idx)
+    except TypeError:  # scalar
+        n_cols = 1
+        cols_idx = xp.asarray([cols_idx])
+
+    groups = xp.zeros((n_cols, nobs), dtype=xp.int64)
+    last_group = xp.arange(nobs)
+    if 0 in cols_idx:
+        groups[0] = last_group
+
+    for i, node in enumerate(nodes):
+        idx = node.pre_order()
+        this_group = xp_copy(last_group, xp=xp)
+        # TODO ARRAY_API complex indexing not supported
+        this_group[idx] = xp.min(last_group[idx])
+        this_group[this_group > xp.max(last_group[idx])] -= 1
+        if i + 1 in cols_idx:
+            groups[np.nonzero(i + 1 == cols_idx)[0]] = this_group
+        last_group = this_group
+
+    return groups.T
+
+
+@xp_capabilities(jax_jit=False, allow_dask_compute=True)
+def to_tree(Z, rd=False):
+    """
+    Convert a linkage matrix into an easy-to-use tree object.
+
+    The reference to the root `ClusterNode` object is returned (by default).
+
+    Each `ClusterNode` object has a ``left``, ``right``, ``dist``, ``id``,
+    and ``count`` attribute. The left and right attributes point to
+    ClusterNode objects that were combined to generate the cluster.
+    If both are None then the `ClusterNode` object is a leaf node, its count
+    must be 1, and its distance is meaningless but set to 0.
+
+    *Note: This function is provided for the convenience of the library
+    user. ClusterNodes are not used as input to any of the functions in this
+    library.*
+
+    Parameters
+    ----------
+    Z : ndarray
+        The linkage matrix in proper form (see the `linkage`
+        function documentation).
+    rd : bool, optional
+        When False (default), a reference to the root `ClusterNode` object is
+        returned.  Otherwise, a tuple ``(r, d)`` is returned. ``r`` is a
+        reference to the root node while ``d`` is a list of `ClusterNode`
+        objects - one per original entry in the linkage matrix plus entries
+        for all clustering steps. If a cluster id is
+        less than the number of samples ``n`` in the data that the linkage
+        matrix describes, then it corresponds to a singleton cluster (leaf
+        node).
+        See `linkage` for more information on the assignment of cluster ids
+        to clusters.
+
+    Returns
+    -------
+    tree : ClusterNode or tuple (ClusterNode, list of ClusterNode)
+        If ``rd`` is False, a `ClusterNode`.
+        If ``rd`` is True, a list of length ``2*n - 1``, with ``n`` the number
+        of samples.  See the description of `rd` above for more details.
+
+    See Also
+    --------
+    linkage, is_valid_linkage, ClusterNode
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.cluster import hierarchy
+    >>> rng = np.random.default_rng()
+    >>> x = rng.random((5, 2))
+    >>> Z = hierarchy.linkage(x)
+    >>> hierarchy.to_tree(Z)
+    >> rootnode, nodelist = hierarchy.to_tree(Z, rd=True)
+    >>> rootnode
+    >> len(nodelist)
+    9
+
+    """
+    xp = array_namespace(Z)
+    Z = _asarray(Z, order='C', xp=xp)
+    _is_valid_linkage(Z, throw=True, name='Z', materialize=True, xp=xp)
+
+    # Number of original objects is equal to the number of rows plus 1.
+    n = Z.shape[0] + 1
+
+    # Create a list full of None's to store the node objects
+    d = [None] * (n * 2 - 1)
+
+    # Create the nodes corresponding to the n original objects.
+    for i in range(0, n):
+        d[i] = ClusterNode(i)
+
+    nd = None
+
+    for i in range(Z.shape[0]):
+        row = Z[i, :]
+
+        fi = int_floor(row[0], xp)
+        fj = int_floor(row[1], xp)
+        if fi > i + n:
+            raise ValueError('Corrupt matrix Z. Index to derivative cluster '
+                              f'is used before it is formed. See row {fi}, '
+                              'column 0')
+        if fj > i + n:
+            raise ValueError('Corrupt matrix Z. Index to derivative cluster '
+                              f'is used before it is formed. See row {fj}, '
+                              'column 1')
+
+        nd = ClusterNode(i + n, d[fi], d[fj], row[2])
+        #                ^ id   ^ left ^ right ^ dist
+        if row[3] != nd.count:
+            raise ValueError(f'Corrupt matrix Z. The count Z[{i},3] is '
+                              'incorrect.')
+        d[n + i] = nd
+
+    if rd:
+        return (nd, d)
+    else:
+        return nd
+
+
+@lazy_cython
+def optimal_leaf_ordering(Z, y, metric='euclidean'):
+    """
+    Given a linkage matrix Z and distance, reorder the cut tree.
+
+    Parameters
+    ----------
+    Z : ndarray
+        The hierarchical clustering encoded as a linkage matrix. See
+        `linkage` for more information on the return structure and
+        algorithm.
+    y : ndarray
+        The condensed distance matrix from which Z was generated.
+        Alternatively, a collection of m observation vectors in n
+        dimensions may be passed as an m by n array.
+    metric : str or function, optional
+        The distance metric to use in the case that y is a collection of
+        observation vectors; ignored otherwise. See the ``pdist``
+        function for a list of valid distance metrics. A custom distance
+        function can also be used.
+
+    Returns
+    -------
+    Z_ordered : ndarray
+        A copy of the linkage matrix Z, reordered to minimize the distance
+        between adjacent leaves.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.cluster import hierarchy
+    >>> rng = np.random.default_rng()
+    >>> X = rng.standard_normal((10, 10))
+    >>> Z = hierarchy.ward(X)
+    >>> hierarchy.leaves_list(Z)
+    array([0, 3, 1, 9, 2, 5, 7, 4, 6, 8], dtype=int32)
+    >>> hierarchy.leaves_list(hierarchy.optimal_leaf_ordering(Z, X))
+    array([3, 0, 2, 5, 7, 4, 8, 6, 9, 1], dtype=int32)
+
+    """
+    xp = array_namespace(Z, y)
+    Z = _asarray(Z, order='C', xp=xp)
+    y = _asarray(y, order='C', dtype=xp.float64, xp=xp)
+    lazy = is_lazy_array(Z)
+    _is_valid_linkage(Z, throw=True, name='Z', xp=xp)
+
+    if y.ndim == 1:
+        distance.is_valid_y(y, throw=True, name='y')
+    elif y.ndim == 2:
+        if (not lazy and y.shape[0] == y.shape[1]
+            and xp.all(xpx.isclose(xp.linalg.diagonal(y), 0))
+            and xp.all(y >= 0) and xp.all(xpx.isclose(y, y.T))):
+            warnings.warn('The symmetric non-negative hollow observation '
+                          'matrix looks suspiciously like an uncondensed '
+                          'distance matrix',
+                          ClusterWarning, stacklevel=2)
+        y = distance.pdist(y, metric)
+    else:
+        raise ValueError("`y` must be 1 or 2 dimensional.")
+    if not lazy and not xp.all(xp.isfinite(y)):
+        raise ValueError("The condensed distance matrix must contain only "
+                         "finite values.")
+
+    # The function name is prominently visible on the user-facing Dask dashboard;
+    # make sure it is meaningful.
+    def cy_optimal_leaf_ordering(Z, y, validate):
+        if validate:
+            _is_valid_linkage(Z, throw=True, name='Z', xp=np)
+            if not np.all(np.isfinite(y)):
+                raise ValueError("The condensed distance matrix must contain only "
+                                 "finite values.")
+        return _optimal_leaf_ordering.optimal_leaf_ordering(Z, y)
+
+    return xpx.lazy_apply(cy_optimal_leaf_ordering, Z, y, validate=lazy,
+                          shape=Z.shape, dtype=Z.dtype, as_numpy=True, xp=xp)
+
+
+@lazy_cython
+def cophenet(Z, Y=None):
+    """
+    Calculate the cophenetic distances between each observation in
+    the hierarchical clustering defined by the linkage ``Z``.
+
+    Suppose ``p`` and ``q`` are original observations in
+    disjoint clusters ``s`` and ``t``, respectively and
+    ``s`` and ``t`` are joined by a direct parent cluster
+    ``u``. The cophenetic distance between observations
+    ``i`` and ``j`` is simply the distance between
+    clusters ``s`` and ``t``.
+
+    Parameters
+    ----------
+    Z : ndarray
+        The hierarchical clustering encoded as an array
+        (see `linkage` function).
+    Y : ndarray (optional)
+        Calculates the cophenetic correlation coefficient ``c`` of a
+        hierarchical clustering defined by the linkage matrix `Z`
+        of a set of :math:`n` observations in :math:`m`
+        dimensions. `Y` is the condensed distance matrix from which
+        `Z` was generated.
+
+    Returns
+    -------
+    c : ndarray
+        The cophentic correlation distance (if ``Y`` is passed).
+    d : ndarray
+        The cophenetic distance matrix in condensed form. The
+        :math:`ij` th entry is the cophenetic distance between
+        original observations :math:`i` and :math:`j`.
+
+    See Also
+    --------
+    linkage :
+        for a description of what a linkage matrix is.
+    scipy.spatial.distance.squareform :
+        transforming condensed matrices into square ones.
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import single, cophenet
+    >>> from scipy.spatial.distance import pdist, squareform
+
+    Given a dataset ``X`` and a linkage matrix ``Z``, the cophenetic distance
+    between two points of ``X`` is the distance between the largest two
+    distinct clusters that each of the points:
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    ``X`` corresponds to this dataset ::
+
+        x x    x x
+        x        x
+
+        x        x
+        x x    x x
+
+    >>> Z = single(pdist(X))
+    >>> Z
+    array([[ 0.,  1.,  1.,  2.],
+           [ 2., 12.,  1.,  3.],
+           [ 3.,  4.,  1.,  2.],
+           [ 5., 14.,  1.,  3.],
+           [ 6.,  7.,  1.,  2.],
+           [ 8., 16.,  1.,  3.],
+           [ 9., 10.,  1.,  2.],
+           [11., 18.,  1.,  3.],
+           [13., 15.,  2.,  6.],
+           [17., 20.,  2.,  9.],
+           [19., 21.,  2., 12.]])
+    >>> cophenet(Z)
+    array([1., 1., 2., 2., 2., 2., 2., 2., 2., 2., 2., 1., 2., 2., 2., 2., 2.,
+           2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 1., 1., 2., 2.,
+           2., 2., 2., 2., 1., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.,
+           1., 1., 2., 2., 2., 1., 2., 2., 2., 2., 2., 2., 1., 1., 1.])
+
+    The output of the `scipy.cluster.hierarchy.cophenet` method is
+    represented in condensed form. We can use
+    `scipy.spatial.distance.squareform` to see the output as a
+    regular matrix (where each element ``ij`` denotes the cophenetic distance
+    between each ``i``, ``j`` pair of points in ``X``):
+
+    >>> squareform(cophenet(Z))
+    array([[0., 1., 1., 2., 2., 2., 2., 2., 2., 2., 2., 2.],
+           [1., 0., 1., 2., 2., 2., 2., 2., 2., 2., 2., 2.],
+           [1., 1., 0., 2., 2., 2., 2., 2., 2., 2., 2., 2.],
+           [2., 2., 2., 0., 1., 1., 2., 2., 2., 2., 2., 2.],
+           [2., 2., 2., 1., 0., 1., 2., 2., 2., 2., 2., 2.],
+           [2., 2., 2., 1., 1., 0., 2., 2., 2., 2., 2., 2.],
+           [2., 2., 2., 2., 2., 2., 0., 1., 1., 2., 2., 2.],
+           [2., 2., 2., 2., 2., 2., 1., 0., 1., 2., 2., 2.],
+           [2., 2., 2., 2., 2., 2., 1., 1., 0., 2., 2., 2.],
+           [2., 2., 2., 2., 2., 2., 2., 2., 2., 0., 1., 1.],
+           [2., 2., 2., 2., 2., 2., 2., 2., 2., 1., 0., 1.],
+           [2., 2., 2., 2., 2., 2., 2., 2., 2., 1., 1., 0.]])
+
+    In this example, the cophenetic distance between points on ``X`` that are
+    very close (i.e., in the same corner) is 1. For other pairs of points is 2,
+    because the points will be located in clusters at different
+    corners - thus, the distance between these clusters will be larger.
+
+    """
+    xp = array_namespace(Z, Y)
+    # Ensure float64 C-contiguous array. Cython code doesn't deal with striding.
+    Z = _asarray(Z, order='C', dtype=xp.float64, xp=xp)
+    _is_valid_linkage(Z, throw=True, name='Z', xp=xp)
+
+    def cy_cophenet(Z, validate):
+        if validate:
+            _is_valid_linkage(Z, throw=True, name='Z', xp=np)
+        n = Z.shape[0] + 1
+        zz = np.zeros((n * (n-1)) // 2, dtype=np.float64)
+        _hierarchy.cophenetic_distances(Z, zz, n)
+        return zz
+    
+    n = Z.shape[0] + 1
+    zz = xpx.lazy_apply(cy_cophenet, Z, validate=is_lazy_array(Z),
+                        shape=((n * (n-1)) // 2, ), dtype=xp.float64,
+                        as_numpy=True, xp=xp)
+                        
+    if Y is None:
+        return zz
+
+    Y = _asarray(Y, order='C', xp=xp)
+    distance.is_valid_y(Y, throw=True, name='Y')
+
+    z = xp.mean(zz)
+    y = xp.mean(Y)
+    Yy = Y - y
+    Zz = zz - z
+    numerator = (Yy * Zz)
+    denomA = Yy**2
+    denomB = Zz**2
+    c = xp.sum(numerator) / xp.sqrt(xp.sum(denomA) * xp.sum(denomB))
+    return (c, zz)
+
+
+@lazy_cython
+def inconsistent(Z, d=2):
+    r"""
+    Calculate inconsistency statistics on a linkage matrix.
+
+    Parameters
+    ----------
+    Z : ndarray
+        The :math:`(n-1)` by 4 matrix encoding the linkage (hierarchical
+        clustering).  See `linkage` documentation for more information on its
+        form.
+    d : int, optional
+        The number of links up to `d` levels below each non-singleton cluster.
+
+    Returns
+    -------
+    R : ndarray
+        A :math:`(n-1)` by 4 matrix where the ``i``'th row contains the link
+        statistics for the non-singleton cluster ``i``. The link statistics are
+        computed over the link heights for links :math:`d` levels below the
+        cluster ``i``. ``R[i,0]`` and ``R[i,1]`` are the mean and standard
+        deviation of the link heights, respectively; ``R[i,2]`` is the number
+        of links included in the calculation; and ``R[i,3]`` is the
+        inconsistency coefficient,
+
+        .. math:: \frac{\mathtt{Z[i,2]} - \mathtt{R[i,0]}} {R[i,1]}
+
+    Notes
+    -----
+    This function behaves similarly to the MATLAB(TM) ``inconsistent``
+    function.
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import inconsistent, linkage
+    >>> from matplotlib import pyplot as plt
+    >>> X = [[i] for i in [2, 8, 0, 4, 1, 9, 9, 0]]
+    >>> Z = linkage(X, 'ward')
+    >>> print(Z)
+    [[ 5.          6.          0.          2.        ]
+     [ 2.          7.          0.          2.        ]
+     [ 0.          4.          1.          2.        ]
+     [ 1.          8.          1.15470054  3.        ]
+     [ 9.         10.          2.12132034  4.        ]
+     [ 3.         12.          4.11096096  5.        ]
+     [11.         13.         14.07183949  8.        ]]
+    >>> inconsistent(Z)
+    array([[ 0.        ,  0.        ,  1.        ,  0.        ],
+           [ 0.        ,  0.        ,  1.        ,  0.        ],
+           [ 1.        ,  0.        ,  1.        ,  0.        ],
+           [ 0.57735027,  0.81649658,  2.        ,  0.70710678],
+           [ 1.04044011,  1.06123822,  3.        ,  1.01850858],
+           [ 3.11614065,  1.40688837,  2.        ,  0.70710678],
+           [ 6.44583366,  6.76770586,  3.        ,  1.12682288]])
+
+    """
+    xp = array_namespace(Z)
+    Z = _asarray(Z, order='C', dtype=xp.float64, xp=xp)
+    _is_valid_linkage(Z, throw=True, name='Z', xp=xp)
+
+    if d != np.floor(d) or d < 0:
+        raise ValueError('The second argument d must be a nonnegative '
+                         'integer value.')
+
+    def cy_inconsistent(Z, d, validate):
+        if validate:
+            _is_valid_linkage(Z, throw=True, name='Z', xp=np)
+        R = np.zeros((Z.shape[0], 4), dtype=np.float64)
+        n = Z.shape[0] + 1
+        _hierarchy.inconsistent(Z, R, n, d)
+        return R
+
+    return xpx.lazy_apply(cy_inconsistent, Z, d=int(d), validate=is_lazy_array(Z),
+                          shape=(Z.shape[0], 4), dtype=xp.float64,
+                          as_numpy=True, xp=xp)
+
+
+@lazy_cython
+def from_mlab_linkage(Z):
+    """
+    Convert a linkage matrix generated by MATLAB(TM) to a new
+    linkage matrix compatible with this module.
+
+    The conversion does two things:
+
+     * the indices are converted from ``1..N`` to ``0..(N-1)`` form,
+       and
+
+     * a fourth column ``Z[:,3]`` is added where ``Z[i,3]`` represents the
+       number of original observations (leaves) in the non-singleton
+       cluster ``i``.
+
+    This function is useful when loading in linkages from legacy data
+    files generated by MATLAB.
+
+    Parameters
+    ----------
+    Z : ndarray
+        A linkage matrix generated by MATLAB(TM).
+
+    Returns
+    -------
+    ZS : ndarray
+        A linkage matrix compatible with ``scipy.cluster.hierarchy``.
+
+    See Also
+    --------
+    linkage : for a description of what a linkage matrix is.
+    to_mlab_linkage : transform from SciPy to MATLAB format.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.cluster.hierarchy import ward, from_mlab_linkage
+
+    Given a linkage matrix in MATLAB format ``mZ``, we can use
+    `scipy.cluster.hierarchy.from_mlab_linkage` to import
+    it into SciPy format:
+
+    >>> mZ = np.array([[1, 2, 1], [4, 5, 1], [7, 8, 1],
+    ...                [10, 11, 1], [3, 13, 1.29099445],
+    ...                [6, 14, 1.29099445],
+    ...                [9, 15, 1.29099445],
+    ...                [12, 16, 1.29099445],
+    ...                [17, 18, 5.77350269],
+    ...                [19, 20, 5.77350269],
+    ...                [21, 22,  8.16496581]])
+
+    >>> Z = from_mlab_linkage(mZ)
+    >>> Z
+    array([[  0.        ,   1.        ,   1.        ,   2.        ],
+           [  3.        ,   4.        ,   1.        ,   2.        ],
+           [  6.        ,   7.        ,   1.        ,   2.        ],
+           [  9.        ,  10.        ,   1.        ,   2.        ],
+           [  2.        ,  12.        ,   1.29099445,   3.        ],
+           [  5.        ,  13.        ,   1.29099445,   3.        ],
+           [  8.        ,  14.        ,   1.29099445,   3.        ],
+           [ 11.        ,  15.        ,   1.29099445,   3.        ],
+           [ 16.        ,  17.        ,   5.77350269,   6.        ],
+           [ 18.        ,  19.        ,   5.77350269,   6.        ],
+           [ 20.        ,  21.        ,   8.16496581,  12.        ]])
+
+    As expected, the linkage matrix ``Z`` returned includes an
+    additional column counting the number of original samples in
+    each cluster. Also, all cluster indices are reduced by 1
+    (MATLAB format uses 1-indexing, whereas SciPy uses 0-indexing).
+
+    """
+    xp = array_namespace(Z)
+    Z = _asarray(Z, dtype=xp.float64, order='C', xp=xp)
+
+    # If it's empty, return it.
+    if Z.shape in ((), (0, )):
+        return xp_copy(Z, xp=xp)
+
+    if Z.ndim != 2:
+        raise ValueError("The linkage array must be rectangular.")
+
+    # If it contains no rows, return it.
+    n = Z.shape[0]
+    if n == 0:
+        return xp_copy(Z, xp=xp)
+
+    lazy = is_lazy_array(Z)
+
+    if not lazy and xp.min(Z[:, :2]) != 1.0 and xp.max(Z[:, :2]) != 2 * n:
+        raise ValueError('The format of the indices is not 1..N')
+
+    res = xp.empty((Z.shape[0], Z.shape[1] + 1), dtype=Z.dtype)
+    res = xpx.at(res)[:, :2].set(Z[:, :2] - 1.0)
+    res = xpx.at(res)[:, 2:-1].set(Z[:, 2:])
+
+    def cy_from_mlab_linkage(Zpart, validate):
+        n = Zpart.shape[0]
+        if validate and np.min(Zpart[:, :2]) != 0.0 and np.max(Zpart[:, :2]) != 2 * n:
+            raise ValueError('The format of the indices is not 1..N')
+
+        if not Zpart.flags.writeable:
+            Zpart = Zpart.copy()  # xp=jax.numpy
+
+        CS = np.zeros((n,))
+        _hierarchy.calculate_cluster_sizes(Zpart, CS, n + 1)
+        return CS
+
+    CS = xpx.lazy_apply(cy_from_mlab_linkage, res[:, :-1], validate=lazy,
+                        shape=(res.shape[0],), dtype=xp.float64,
+                        as_numpy=True, xp=xp)
+
+    return xpx.at(res)[:, -1].set(CS)
+
+
+@xp_capabilities()
+def to_mlab_linkage(Z):
+    """
+    Convert a linkage matrix to a MATLAB(TM) compatible one.
+
+    Converts a linkage matrix ``Z`` generated by the linkage function
+    of this module to a MATLAB(TM) compatible one. The return linkage
+    matrix has the last column removed and the cluster indices are
+    converted to ``1..N`` indexing.
+
+    Parameters
+    ----------
+    Z : ndarray
+        A linkage matrix generated by ``scipy.cluster.hierarchy``.
+
+    Returns
+    -------
+    to_mlab_linkage : ndarray
+        A linkage matrix compatible with MATLAB(TM)'s hierarchical
+        clustering functions.
+
+        The return linkage matrix has the last column removed
+        and the cluster indices are converted to ``1..N`` indexing.
+
+    See Also
+    --------
+    linkage : for a description of what a linkage matrix is.
+    from_mlab_linkage : transform from Matlab to SciPy format.
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import ward, to_mlab_linkage
+    >>> from scipy.spatial.distance import pdist
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    >>> Z = ward(pdist(X))
+    >>> Z
+    array([[ 0.        ,  1.        ,  1.        ,  2.        ],
+           [ 3.        ,  4.        ,  1.        ,  2.        ],
+           [ 6.        ,  7.        ,  1.        ,  2.        ],
+           [ 9.        , 10.        ,  1.        ,  2.        ],
+           [ 2.        , 12.        ,  1.29099445,  3.        ],
+           [ 5.        , 13.        ,  1.29099445,  3.        ],
+           [ 8.        , 14.        ,  1.29099445,  3.        ],
+           [11.        , 15.        ,  1.29099445,  3.        ],
+           [16.        , 17.        ,  5.77350269,  6.        ],
+           [18.        , 19.        ,  5.77350269,  6.        ],
+           [20.        , 21.        ,  8.16496581, 12.        ]])
+
+    After a linkage matrix ``Z`` has been created, we can use
+    `scipy.cluster.hierarchy.to_mlab_linkage` to convert it
+    into MATLAB format:
+
+    >>> mZ = to_mlab_linkage(Z)
+    >>> mZ
+    array([[  1.        ,   2.        ,   1.        ],
+           [  4.        ,   5.        ,   1.        ],
+           [  7.        ,   8.        ,   1.        ],
+           [ 10.        ,  11.        ,   1.        ],
+           [  3.        ,  13.        ,   1.29099445],
+           [  6.        ,  14.        ,   1.29099445],
+           [  9.        ,  15.        ,   1.29099445],
+           [ 12.        ,  16.        ,   1.29099445],
+           [ 17.        ,  18.        ,   5.77350269],
+           [ 19.        ,  20.        ,   5.77350269],
+           [ 21.        ,  22.        ,   8.16496581]])
+
+    The new linkage matrix ``mZ`` uses 1-indexing for all the
+    clusters (instead of 0-indexing). Also, the last column of
+    the original linkage matrix has been dropped.
+
+    """
+    xp = array_namespace(Z)
+    Z = _asarray(Z, dtype=xp.float64, xp=xp)
+    if Z.shape in ((), (0, )):
+        return xp_copy(Z, xp=xp)
+    _is_valid_linkage(Z, throw=True, name='Z', xp=xp)
+
+    return xp.concat((Z[:, :2] + 1.0, Z[:, 2:3]), axis=1)
+
+
+@xp_capabilities()
+def is_monotonic(Z):
+    """
+    Return True if the linkage passed is monotonic.
+
+    The linkage is monotonic if for every cluster :math:`s` and :math:`t`
+    joined, the distance between them is no less than the distance
+    between any previously joined clusters.
+
+    Parameters
+    ----------
+    Z : ndarray
+        The linkage matrix to check for monotonicity.
+
+    Returns
+    -------
+    b : bool
+        A boolean indicating whether the linkage is monotonic.
+
+    See Also
+    --------
+    linkage : for a description of what a linkage matrix is.
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import median, ward, is_monotonic
+    >>> from scipy.spatial.distance import pdist
+
+    By definition, some hierarchical clustering algorithms - such as
+    `scipy.cluster.hierarchy.ward` - produce monotonic assignments of
+    samples to clusters; however, this is not always true for other
+    hierarchical methods - e.g. `scipy.cluster.hierarchy.median`.
+
+    Given a linkage matrix ``Z`` (as the result of a hierarchical clustering
+    method) we can test programmatically whether it has the monotonicity
+    property or not, using `scipy.cluster.hierarchy.is_monotonic`:
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    >>> Z = ward(pdist(X))
+    >>> Z
+    array([[ 0.        ,  1.        ,  1.        ,  2.        ],
+           [ 3.        ,  4.        ,  1.        ,  2.        ],
+           [ 6.        ,  7.        ,  1.        ,  2.        ],
+           [ 9.        , 10.        ,  1.        ,  2.        ],
+           [ 2.        , 12.        ,  1.29099445,  3.        ],
+           [ 5.        , 13.        ,  1.29099445,  3.        ],
+           [ 8.        , 14.        ,  1.29099445,  3.        ],
+           [11.        , 15.        ,  1.29099445,  3.        ],
+           [16.        , 17.        ,  5.77350269,  6.        ],
+           [18.        , 19.        ,  5.77350269,  6.        ],
+           [20.        , 21.        ,  8.16496581, 12.        ]])
+    >>> is_monotonic(Z)
+    True
+
+    >>> Z = median(pdist(X))
+    >>> Z
+    array([[ 0.        ,  1.        ,  1.        ,  2.        ],
+           [ 3.        ,  4.        ,  1.        ,  2.        ],
+           [ 9.        , 10.        ,  1.        ,  2.        ],
+           [ 6.        ,  7.        ,  1.        ,  2.        ],
+           [ 2.        , 12.        ,  1.11803399,  3.        ],
+           [ 5.        , 13.        ,  1.11803399,  3.        ],
+           [ 8.        , 15.        ,  1.11803399,  3.        ],
+           [11.        , 14.        ,  1.11803399,  3.        ],
+           [18.        , 19.        ,  3.        ,  6.        ],
+           [16.        , 17.        ,  3.5       ,  6.        ],
+           [20.        , 21.        ,  3.25      , 12.        ]])
+    >>> is_monotonic(Z)
+    False
+
+    Note that this method is equivalent to just verifying that the distances
+    in the third column of the linkage matrix appear in a monotonically
+    increasing order.
+
+    """
+    xp = array_namespace(Z)
+    Z = _asarray(Z, xp=xp)
+    _is_valid_linkage(Z, throw=True, name='Z', xp=xp)
+
+    # We expect the i'th value to be greater than its successor.
+    return xp.all(Z[1:, 2] >= Z[:-1, 2])
+
+
+@xp_capabilities(warnings=[("dask.array", "see notes"), ("jax.numpy", "see notes")])
+def is_valid_im(R, warning=False, throw=False, name=None):
+    """Return True if the inconsistency matrix passed is valid.
+
+    It must be a :math:`n` by 4 array of doubles. The standard
+    deviations ``R[:,1]`` must be nonnegative. The link counts
+    ``R[:,2]`` must be positive and no greater than :math:`n-1`.
+
+    Parameters
+    ----------
+    R : ndarray
+        The inconsistency matrix to check for validity.
+    warning : bool, optional
+        When True, issues a Python warning if the linkage
+        matrix passed is invalid.
+    throw : bool, optional
+        When True, throws a Python exception if the linkage
+        matrix passed is invalid.
+    name : str, optional
+        This string refers to the variable name of the invalid
+        linkage matrix.
+
+    Returns
+    -------
+    b : bool
+        True if the inconsistency matrix is valid; False otherwise.
+
+    Notes
+    -----
+    *Array API support (experimental):* If the input is a lazy Array (e.g. Dask
+    or JAX), the return value may be a 0-dimensional bool Array. When warning=True
+    or throw=True, calling this function materializes the array.
+
+    See Also
+    --------
+    linkage : for a description of what a linkage matrix is.
+    inconsistent : for the creation of a inconsistency matrix.
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import ward, inconsistent, is_valid_im
+    >>> from scipy.spatial.distance import pdist
+
+    Given a data set ``X``, we can apply a clustering method to obtain a
+    linkage matrix ``Z``. `scipy.cluster.hierarchy.inconsistent` can
+    be also used to obtain the inconsistency matrix ``R`` associated to
+    this clustering process:
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    >>> Z = ward(pdist(X))
+    >>> R = inconsistent(Z)
+    >>> Z
+    array([[ 0.        ,  1.        ,  1.        ,  2.        ],
+           [ 3.        ,  4.        ,  1.        ,  2.        ],
+           [ 6.        ,  7.        ,  1.        ,  2.        ],
+           [ 9.        , 10.        ,  1.        ,  2.        ],
+           [ 2.        , 12.        ,  1.29099445,  3.        ],
+           [ 5.        , 13.        ,  1.29099445,  3.        ],
+           [ 8.        , 14.        ,  1.29099445,  3.        ],
+           [11.        , 15.        ,  1.29099445,  3.        ],
+           [16.        , 17.        ,  5.77350269,  6.        ],
+           [18.        , 19.        ,  5.77350269,  6.        ],
+           [20.        , 21.        ,  8.16496581, 12.        ]])
+    >>> R
+    array([[1.        , 0.        , 1.        , 0.        ],
+           [1.        , 0.        , 1.        , 0.        ],
+           [1.        , 0.        , 1.        , 0.        ],
+           [1.        , 0.        , 1.        , 0.        ],
+           [1.14549722, 0.20576415, 2.        , 0.70710678],
+           [1.14549722, 0.20576415, 2.        , 0.70710678],
+           [1.14549722, 0.20576415, 2.        , 0.70710678],
+           [1.14549722, 0.20576415, 2.        , 0.70710678],
+           [2.78516386, 2.58797734, 3.        , 1.15470054],
+           [2.78516386, 2.58797734, 3.        , 1.15470054],
+           [6.57065706, 1.38071187, 3.        , 1.15470054]])
+
+    Now we can use `scipy.cluster.hierarchy.is_valid_im` to verify that
+    ``R`` is correct:
+
+    >>> is_valid_im(R)
+    True
+
+    However, if ``R`` is wrongly constructed (e.g., one of the standard
+    deviations is set to a negative value), then the check will fail:
+
+    >>> R[-1,1] = R[-1,1] * -1
+    >>> is_valid_im(R)
+    False
+
+    """
+    xp = array_namespace(R)
+    R = _asarray(R, xp=xp)
+    return _is_valid_im(R, warning=warning, throw=throw, name=name,
+                        materialize=True, xp=xp)
+
+
+def _is_valid_im(R, warning=False, throw=False, name=None, materialize=False, *, xp):
+    """Variant of `is_valid_im` to be called internally by other scipy functions,
+    which by default does not materialize lazy input arrays (Dask, JAX, etc.) when
+    warning=True or throw=True.
+    """
+    name_str = f"{name!r} " if name else ''
+    try:
+        if R.dtype != xp.float64:
+            raise TypeError(f'Inconsistency matrix {name_str}must contain doubles '
+                            '(double).')
+        if len(R.shape) != 2:
+            raise ValueError(f'Inconsistency matrix {name_str}must have shape=2 (i.e. '
+                             'be two-dimensional).')
+        if R.shape[1] != 4:
+            raise ValueError(f'Inconsistency matrix {name_str}'
+                             'must have 4 columns.')
+        if R.shape[0] < 1:
+            raise ValueError(f'Inconsistency matrix {name_str}'
+                             'must have at least one row.')
+    except (TypeError, ValueError) as e:
+        if throw:
+            raise
+        if warning:
+            _warning(str(e))
+        return False
+
+    return _lazy_valid_checks(
+        (xp.any(R[:, 0] < 0),
+         f'Inconsistency matrix {name_str} contains negative link height means.'),
+        (xp.any(R[:, 1] < 0),
+         f'Inconsistency matrix {name_str} contains negative link height standard '
+         'deviations.'),
+        (xp.any(R[:, 2] < 0),
+         f'Inconsistency matrix {name_str} contains negative link counts.'),
+        throw=throw, warning=warning, materialize=materialize, xp=xp
+    )
+
+
+@xp_capabilities(warnings=[("dask.array", "see notes"), ("jax.numpy", "see notes")])
+def is_valid_linkage(Z, warning=False, throw=False, name=None):
+    """
+    Check the validity of a linkage matrix.
+
+    A linkage matrix is valid if it is a 2-D array (type double)
+    with :math:`n` rows and 4 columns. The first two columns must contain
+    indices between 0 and :math:`2n-1`. For a given row ``i``, the following
+    two expressions have to hold:
+
+    .. math::
+
+        0 \\leq \\mathtt{Z[i,0]} \\leq i+n-1
+        0 \\leq Z[i,1] \\leq i+n-1
+
+    I.e., a cluster cannot join another cluster unless the cluster being joined
+    has been generated.
+
+    The fourth column of `Z` represents the number of original observations
+    in a cluster, so a valid ``Z[i, 3]`` value may not exceed the number of
+    original observations.
+
+    Parameters
+    ----------
+    Z : array_like
+        Linkage matrix.
+    warning : bool, optional
+        When True, issues a Python warning if the linkage
+        matrix passed is invalid.
+    throw : bool, optional
+        When True, throws a Python exception if the linkage
+        matrix passed is invalid.
+    name : str, optional
+        This string refers to the variable name of the invalid
+        linkage matrix.
+
+    Returns
+    -------
+    b : bool
+        True if the inconsistency matrix is valid; False otherwise.
+
+    Notes
+    -----
+    *Array API support (experimental):* If the input is a lazy Array (e.g. Dask
+    or JAX), the return value may be a 0-dimensional bool Array. When warning=True
+    or throw=True, calling this function materializes the array.
+
+    See Also
+    --------
+    linkage: for a description of what a linkage matrix is.
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import ward, is_valid_linkage
+    >>> from scipy.spatial.distance import pdist
+
+    All linkage matrices generated by the clustering methods in this module
+    will be valid (i.e., they will have the appropriate dimensions and the two
+    required expressions will hold for all the rows).
+
+    We can check this using `scipy.cluster.hierarchy.is_valid_linkage`:
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    >>> Z = ward(pdist(X))
+    >>> Z
+    array([[ 0.        ,  1.        ,  1.        ,  2.        ],
+           [ 3.        ,  4.        ,  1.        ,  2.        ],
+           [ 6.        ,  7.        ,  1.        ,  2.        ],
+           [ 9.        , 10.        ,  1.        ,  2.        ],
+           [ 2.        , 12.        ,  1.29099445,  3.        ],
+           [ 5.        , 13.        ,  1.29099445,  3.        ],
+           [ 8.        , 14.        ,  1.29099445,  3.        ],
+           [11.        , 15.        ,  1.29099445,  3.        ],
+           [16.        , 17.        ,  5.77350269,  6.        ],
+           [18.        , 19.        ,  5.77350269,  6.        ],
+           [20.        , 21.        ,  8.16496581, 12.        ]])
+    >>> is_valid_linkage(Z)
+    True
+
+    However, if we create a linkage matrix in a wrong way - or if we modify
+    a valid one in a way that any of the required expressions don't hold
+    anymore, then the check will fail:
+
+    >>> Z[3][1] = 20    # the cluster number 20 is not defined at this point
+    >>> is_valid_linkage(Z)
+    False
+
+    """
+    xp = array_namespace(Z)
+    Z = _asarray(Z, xp=xp)
+    return _is_valid_linkage(Z, warning=warning, throw=throw,
+                             name=name, materialize=True, xp=xp)
+
+
+def _is_valid_linkage(Z, warning=False, throw=False, name=None,
+                      materialize=False, *, xp):
+    """Variant of `is_valid_linkage` to be called internally by other scipy functions,
+    which by default does not materialize lazy input arrays (Dask, JAX, etc.) when
+    warning=True or throw=True.
+    """
+    name_str = f"{name!r} " if name else ''
+    try:
+        if Z.dtype != xp.float64:
+            raise TypeError(f'Linkage matrix {name_str}must contain doubles.')
+        if len(Z.shape) != 2:
+            raise ValueError(f'Linkage matrix {name_str}must have shape=2 (i.e. be'
+                             ' two-dimensional).')
+        if Z.shape[1] != 4:
+            raise ValueError(f'Linkage matrix {name_str}must have 4 columns.')
+        if Z.shape[0] == 0:
+            raise ValueError('Linkage must be computed on at least two '
+                             'observations.')
+    except (TypeError, ValueError) as e:
+        if throw:
+            raise
+        if warning:
+            _warning(str(e))
+        return False
+
+    n = Z.shape[0]
+    if n < 2:
+        return True
+
+    return _lazy_valid_checks(
+        (xp.any(Z[:, :2] < 0),
+         f'Linkage {name_str}contains negative indices.'),
+        (xp.any(Z[:, 2] < 0),
+         f'Linkage {name_str}contains negative distances.'),
+        (xp.any(Z[:, 3] < 0),
+         f'Linkage {name_str}contains negative counts.'),
+        (xp.any(Z[:, 3] > n + 1),
+         f'Linkage {name_str}contains excessive observations in a cluster'),
+        (xp.any(xp.max(Z[:, :2], axis=1) >= xp.arange(n + 1, 2 * n + 1, dtype=Z.dtype)),
+         f'Linkage {name_str}uses non-singleton cluster before it is formed.'),
+        (xpx.nunique(Z[:, :2]) < n * 2,
+         f'Linkage {name_str}uses the same cluster more than once.'),
+        throw=throw, warning=warning, materialize=materialize, xp=xp
+    )
+
+
+def _lazy_valid_checks(*args, throw=False, warning=False, materialize=False, xp):
+    """Validate a set of conditions on the contents of possibly lazy arrays.
+
+    Parameters
+    ----------
+    args : tuples of (Array, str)
+        The first element of each tuple must be a 0-dimensional Array
+        that evaluates to bool; the second element must be the message to convey
+        if the  first element evaluates to True.
+    throw: bool
+        Set to True to `raise ValueError(args[i][1])` if `args[i][0]` is True.
+    warning: bool
+        Set to True to issue a warning with message `args[i][1]` if `args[i][0]`
+        is True.
+    materialize: bool
+        Set to True to force materialization of lazy arrays when throw=True or
+        warning=True. If the inputs are lazy and materialize=False, ignore the
+        `throw` and `warning` flags.
+    xp: module
+        Array API namespace
+
+    Returns
+    -------
+    If xp is an eager backend (e.g. numpy) and all conditions are False, return True.
+    If throw is True, raise. Otherwise, return False.
+
+    If xp is a lazy backend (e.g. Dask or JAX), return a 0-dimensional bool Array.
+    """
+    conds = xp.concat([xp.reshape(cond, (1, )) for cond, _ in args])
+
+    lazy = is_lazy_array(conds)
+    if not throw and not warning or (lazy and not materialize):
+        out = ~xp.any(conds)
+        return out if lazy else bool(out)
+
+    if is_dask(xp):
+        # Only materialize the graph once, instead of once per check
+        conds = conds.compute()
+
+    # Don't call np.asarray(conds), as it would be blocked by the device transfer
+    # guard on CuPy and PyTorch and the densification guard on Sparse, whereas
+    # bool() will not.
+    conds = [bool(cond) for cond in conds]
+
+    for cond, (_, msg) in zip(conds, args):
+        if throw and cond:
+            raise ValueError(msg)
+        elif warning and cond:
+            warnings.warn(msg, ClusterWarning, stacklevel=3)
+
+    return not any(conds)
+
+
+@xp_capabilities()
+def num_obs_linkage(Z):
+    """
+    Return the number of original observations of the linkage matrix passed.
+
+    Parameters
+    ----------
+    Z : ndarray
+        The linkage matrix on which to perform the operation.
+
+    Returns
+    -------
+    n : int
+        The number of original observations in the linkage.
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import ward, num_obs_linkage
+    >>> from scipy.spatial.distance import pdist
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    >>> Z = ward(pdist(X))
+
+    ``Z`` is a linkage matrix obtained after using the Ward clustering method
+    with ``X``, a dataset with 12 data points.
+
+    >>> num_obs_linkage(Z)
+    12
+
+    """
+    xp = array_namespace(Z)
+    Z = _asarray(Z, xp=xp)
+    _is_valid_linkage(Z, throw=True, name='Z', xp=xp)
+    return Z.shape[0] + 1
+
+
+@xp_capabilities()
+def correspond(Z, Y):
+    """
+    Check for correspondence between linkage and condensed distance matrices.
+
+    They must have the same number of original observations for
+    the check to succeed.
+
+    This function is useful as a sanity check in algorithms that make
+    extensive use of linkage and distance matrices that must
+    correspond to the same set of original observations.
+
+    Parameters
+    ----------
+    Z : array_like
+        The linkage matrix to check for correspondence.
+    Y : array_like
+        The condensed distance matrix to check for correspondence.
+
+    Returns
+    -------
+    b : bool
+        A boolean indicating whether the linkage matrix and distance
+        matrix could possibly correspond to one another.
+
+    See Also
+    --------
+    linkage : for a description of what a linkage matrix is.
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import ward, correspond
+    >>> from scipy.spatial.distance import pdist
+
+    This method can be used to check if a given linkage matrix ``Z`` has been
+    obtained from the application of a cluster method over a dataset ``X``:
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+    >>> X_condensed = pdist(X)
+    >>> Z = ward(X_condensed)
+
+    Here, we can compare ``Z`` and ``X`` (in condensed form):
+
+    >>> correspond(Z, X_condensed)
+    True
+
+    """
+    xp = array_namespace(Z, Y)
+    Z = _asarray(Z, xp=xp)
+    Y = _asarray(Y, xp=xp)
+    _is_valid_linkage(Z, throw=True, xp=xp)
+    distance.is_valid_y(Y, throw=True)
+    return distance.num_obs_y(Y) == num_obs_linkage(Z)
+
+
+@xp_capabilities(cpu_only=True, reason="Cython code",
+                 jax_jit=False, allow_dask_compute=True)
+def fcluster(Z, t, criterion='inconsistent', depth=2, R=None, monocrit=None):
+    """
+    Form flat clusters from the hierarchical clustering defined by
+    the given linkage matrix.
+
+    Parameters
+    ----------
+    Z : ndarray
+        The hierarchical clustering encoded with the matrix returned
+        by the `linkage` function.
+    t : scalar
+        For criteria 'inconsistent', 'distance' or 'monocrit',
+         this is the threshold to apply when forming flat clusters.
+        For 'maxclust' or 'maxclust_monocrit' criteria,
+         this would be max number of clusters requested.
+    criterion : str, optional
+        The criterion to use in forming flat clusters. This can
+        be any of the following values:
+
+          ``inconsistent`` :
+              If a cluster node and all its
+              descendants have an inconsistent value less than or equal
+              to `t`, then all its leaf descendants belong to the
+              same flat cluster. When no non-singleton cluster meets
+              this criterion, every node is assigned to its own
+              cluster. (Default)
+
+          ``distance`` :
+              Forms flat clusters so that the original
+              observations in each flat cluster have no greater a
+              cophenetic distance than `t`.
+
+          ``maxclust`` :
+              Finds a minimum threshold ``r`` so that
+              the cophenetic distance between any two original
+              observations in the same flat cluster is no more than
+              ``r`` and no more than `t` flat clusters are formed.
+
+          ``monocrit`` :
+              Forms a flat cluster from a cluster node c
+              with index i when ``monocrit[j] <= t``.
+
+              For example, to threshold on the maximum mean distance
+              as computed in the inconsistency matrix R with a
+              threshold of 0.8 do::
+
+                  MR = maxRstat(Z, R, 3)
+                  fcluster(Z, t=0.8, criterion='monocrit', monocrit=MR)
+
+          ``maxclust_monocrit`` :
+              Forms a flat cluster from a
+              non-singleton cluster node ``c`` when ``monocrit[i] <=
+              r`` for all cluster indices ``i`` below and including
+              ``c``. ``r`` is minimized such that no more than ``t``
+              flat clusters are formed. monocrit must be
+              monotonic. For example, to minimize the threshold t on
+              maximum inconsistency values so that no more than 3 flat
+              clusters are formed, do::
+
+                  MI = maxinconsts(Z, R)
+                  fcluster(Z, t=3, criterion='maxclust_monocrit', monocrit=MI)
+    depth : int, optional
+        The maximum depth to perform the inconsistency calculation.
+        It has no meaning for the other criteria. Default is 2.
+    R : ndarray, optional
+        The inconsistency matrix to use for the ``'inconsistent'``
+        criterion. This matrix is computed if not provided.
+    monocrit : ndarray, optional
+        An array of length n-1. `monocrit[i]` is the
+        statistics upon which non-singleton i is thresholded. The
+        monocrit vector must be monotonic, i.e., given a node c with
+        index i, for all node indices j corresponding to nodes
+        below c, ``monocrit[i] >= monocrit[j]``.
+
+    Returns
+    -------
+    fcluster : ndarray
+        An array of length ``n``. ``T[i]`` is the flat cluster number to
+        which original observation ``i`` belongs.
+
+    See Also
+    --------
+    linkage : for information about hierarchical clustering methods work.
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import ward, fcluster
+    >>> from scipy.spatial.distance import pdist
+
+    All cluster linkage methods - e.g., `scipy.cluster.hierarchy.ward`
+    generate a linkage matrix ``Z`` as their output:
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    >>> Z = ward(pdist(X))
+
+    >>> Z
+    array([[ 0.        ,  1.        ,  1.        ,  2.        ],
+           [ 3.        ,  4.        ,  1.        ,  2.        ],
+           [ 6.        ,  7.        ,  1.        ,  2.        ],
+           [ 9.        , 10.        ,  1.        ,  2.        ],
+           [ 2.        , 12.        ,  1.29099445,  3.        ],
+           [ 5.        , 13.        ,  1.29099445,  3.        ],
+           [ 8.        , 14.        ,  1.29099445,  3.        ],
+           [11.        , 15.        ,  1.29099445,  3.        ],
+           [16.        , 17.        ,  5.77350269,  6.        ],
+           [18.        , 19.        ,  5.77350269,  6.        ],
+           [20.        , 21.        ,  8.16496581, 12.        ]])
+
+    This matrix represents a dendrogram, where the first and second elements
+    are the two clusters merged at each step, the third element is the
+    distance between these clusters, and the fourth element is the size of
+    the new cluster - the number of original data points included.
+
+    `scipy.cluster.hierarchy.fcluster` can be used to flatten the
+    dendrogram, obtaining as a result an assignation of the original data
+    points to single clusters.
+
+    This assignation mostly depends on a distance threshold ``t`` - the maximum
+    inter-cluster distance allowed:
+
+    >>> fcluster(Z, t=0.9, criterion='distance')
+    array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12], dtype=int32)
+
+    >>> fcluster(Z, t=1.1, criterion='distance')
+    array([1, 1, 2, 3, 3, 4, 5, 5, 6, 7, 7, 8], dtype=int32)
+
+    >>> fcluster(Z, t=3, criterion='distance')
+    array([1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4], dtype=int32)
+
+    >>> fcluster(Z, t=9, criterion='distance')
+    array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], dtype=int32)
+
+    In the first case, the threshold ``t`` is too small to allow any two
+    samples in the data to form a cluster, so 12 different clusters are
+    returned.
+
+    In the second case, the threshold is large enough to allow the first
+    4 points to be merged with their nearest neighbors. So, here, only 8
+    clusters are returned.
+
+    The third case, with a much higher threshold, allows for up to 8 data
+    points to be connected - so 4 clusters are returned here.
+
+    Lastly, the threshold of the fourth case is large enough to allow for
+    all data points to be merged together - so a single cluster is returned.
+
+    """
+    xp = array_namespace(Z)
+    Z = _asarray(Z, order='C', dtype=xp.float64, xp=xp)
+    _is_valid_linkage(Z, throw=True, name='Z', materialize=True, xp=xp)
+
+    n = Z.shape[0] + 1
+    T = np.zeros((n,), dtype='i')
+
+    if monocrit is not None:
+        monocrit = np.asarray(monocrit, order='C', dtype=np.float64)
+
+    Z = np.asarray(Z)
+    monocrit = np.asarray(monocrit)
+    if criterion == 'inconsistent':
+        if R is None:
+            R = inconsistent(Z, depth)
+        else:
+            R = _asarray(R, order='C', dtype=xp.float64, xp=xp)
+            _is_valid_im(R, throw=True, name='R', materialize=True, xp=xp)
+            # Since the C code does not support striding using strides.
+            # The dimensions are used instead.
+            R = np.asarray(R)
+        _hierarchy.cluster_in(Z, R, T, float(t), int(n))
+    elif criterion == 'distance':
+        _hierarchy.cluster_dist(Z, T, float(t), int(n))
+    elif criterion == 'maxclust':
+        _hierarchy.cluster_maxclust_dist(Z, T, int(n), t)
+    elif criterion == 'monocrit':
+        _hierarchy.cluster_monocrit(Z, monocrit, T, float(t), int(n))
+    elif criterion == 'maxclust_monocrit':
+        _hierarchy.cluster_maxclust_monocrit(Z, monocrit, T, int(n), int(t))
+    else:
+        raise ValueError(f'Invalid cluster formation criterion: {str(criterion)}')
+    return xp.asarray(T)
+
+
+@xp_capabilities(cpu_only=True, reason="Cython code",
+                 jax_jit=False, allow_dask_compute=True)
+def fclusterdata(X, t, criterion='inconsistent',
+                 metric='euclidean', depth=2, method='single', R=None):
+    """
+    Cluster observation data using a given metric.
+
+    Clusters the original observations in the n-by-m data
+    matrix X (n observations in m dimensions), using the euclidean
+    distance metric to calculate distances between original observations,
+    performs hierarchical clustering using the single linkage algorithm,
+    and forms flat clusters using the inconsistency method with `t` as the
+    cut-off threshold.
+
+    A 1-D array ``T`` of length ``n`` is returned. ``T[i]`` is
+    the index of the flat cluster to which the original observation ``i``
+    belongs.
+
+    Parameters
+    ----------
+    X : (N, M) ndarray
+        N by M data matrix with N observations in M dimensions.
+    t : scalar
+        For criteria 'inconsistent', 'distance' or 'monocrit',
+         this is the threshold to apply when forming flat clusters.
+        For 'maxclust' or 'maxclust_monocrit' criteria,
+         this would be max number of clusters requested.
+    criterion : str, optional
+        Specifies the criterion for forming flat clusters. Valid
+        values are 'inconsistent' (default), 'distance', or 'maxclust'
+        cluster formation algorithms. See `fcluster` for descriptions.
+    metric : str or function, optional
+        The distance metric for calculating pairwise distances. See
+        ``distance.pdist`` for descriptions and linkage to verify
+        compatibility with the linkage method.
+    depth : int, optional
+        The maximum depth for the inconsistency calculation. See
+        `inconsistent` for more information.
+    method : str, optional
+        The linkage method to use (single, complete, average,
+        weighted, median centroid, ward). See `linkage` for more
+        information. Default is "single".
+    R : ndarray, optional
+        The inconsistency matrix. It will be computed if necessary
+        if it is not passed.
+
+    Returns
+    -------
+    fclusterdata : ndarray
+        A vector of length n. T[i] is the flat cluster number to
+        which original observation i belongs.
+
+    See Also
+    --------
+    scipy.spatial.distance.pdist : pairwise distance metrics
+
+    Notes
+    -----
+    This function is similar to the MATLAB function ``clusterdata``.
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import fclusterdata
+
+    This is a convenience method that abstracts all the steps to perform in a
+    typical SciPy's hierarchical clustering workflow.
+
+    * Transform the input data into a condensed matrix with
+      `scipy.spatial.distance.pdist`.
+
+    * Apply a clustering method.
+
+    * Obtain flat clusters at a user defined distance threshold ``t`` using
+      `scipy.cluster.hierarchy.fcluster`.
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    >>> fclusterdata(X, t=1)
+    array([3, 3, 3, 4, 4, 4, 2, 2, 2, 1, 1, 1], dtype=int32)
+
+    The output here (for the dataset ``X``, distance threshold ``t``, and the
+    default settings) is four clusters with three data points each.
+
+    """
+    xp = array_namespace(X)
+    X = _asarray(X, order='C', dtype=xp.float64, xp=xp)
+
+    if X.ndim != 2:
+        raise TypeError('The observation matrix X must be an n by m array.')
+
+    Y = distance.pdist(X, metric=metric)
+    Z = linkage(Y, method=method)
+    if R is None:
+        R = inconsistent(Z, d=depth)
+    else:
+        R = _asarray(R, order='C', xp=xp)
+    T = fcluster(Z, criterion=criterion, depth=depth, R=R, t=t)
+    return T
+
+
+@lazy_cython
+def leaves_list(Z):
+    """
+    Return a list of leaf node ids.
+
+    The return corresponds to the observation vector index as it appears
+    in the tree from left to right. Z is a linkage matrix.
+
+    Parameters
+    ----------
+    Z : ndarray
+        The hierarchical clustering encoded as a matrix.  `Z` is
+        a linkage matrix.  See `linkage` for more information.
+
+    Returns
+    -------
+    leaves_list : ndarray
+        The list of leaf node ids.
+
+    See Also
+    --------
+    dendrogram : for information about dendrogram structure.
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import ward, dendrogram, leaves_list
+    >>> from scipy.spatial.distance import pdist
+    >>> from matplotlib import pyplot as plt
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    >>> Z = ward(pdist(X))
+
+    The linkage matrix ``Z`` represents a dendrogram, that is, a tree that
+    encodes the structure of the clustering performed.
+    `scipy.cluster.hierarchy.leaves_list` shows the mapping between
+    indices in the ``X`` dataset and leaves in the dendrogram:
+
+    >>> leaves_list(Z)
+    array([ 2,  0,  1,  5,  3,  4,  8,  6,  7, 11,  9, 10], dtype=int32)
+
+    >>> fig = plt.figure(figsize=(25, 10))
+    >>> dn = dendrogram(Z)
+    >>> plt.show()
+
+    """
+    xp = array_namespace(Z)
+    Z = _asarray(Z, order='C', xp=xp)
+    _is_valid_linkage(Z, throw=True, name='Z', xp=xp)
+
+    def cy_leaves_list(Z, validate):
+        if validate:
+            _is_valid_linkage(Z, throw=True, name='Z', xp=np)
+        n = Z.shape[0] + 1
+        ML = np.zeros((n,), dtype=np.int32)
+        _hierarchy.prelist(Z, ML, n)
+        return ML
+
+    n = Z.shape[0] + 1
+    return xpx.lazy_apply(cy_leaves_list, Z, validate=is_lazy_array(Z),
+                          shape=(n, ), dtype=xp.int32,
+                          as_numpy=True, xp=xp)
+
+
+# Maps number of leaves to text size.
+#
+# p <= 20, size="12"
+# 20 < p <= 30, size="10"
+# 30 < p <= 50, size="8"
+# 50 < p <= np.inf, size="6"
+
+_dtextsizes = {20: 12, 30: 10, 50: 8, 85: 6, np.inf: 5}
+_drotation = {20: 0, 40: 45, np.inf: 90}
+_dtextsortedkeys = list(_dtextsizes.keys())
+_dtextsortedkeys.sort()
+_drotationsortedkeys = list(_drotation.keys())
+_drotationsortedkeys.sort()
+
+
+def _remove_dups(L):
+    """
+    Remove duplicates AND preserve the original order of the elements.
+
+    The set class is not guaranteed to do this.
+    """
+    seen_before = set()
+    L2 = []
+    for i in L:
+        if i not in seen_before:
+            seen_before.add(i)
+            L2.append(i)
+    return L2
+
+
+def _get_tick_text_size(p):
+    for k in _dtextsortedkeys:
+        if p <= k:
+            return _dtextsizes[k]
+
+
+def _get_tick_rotation(p):
+    for k in _drotationsortedkeys:
+        if p <= k:
+            return _drotation[k]
+
+
+def _plot_dendrogram(icoords, dcoords, ivl, p, n, mh, orientation,
+                     no_labels, color_list, leaf_font_size=None,
+                     leaf_rotation=None, contraction_marks=None,
+                     ax=None, above_threshold_color='C0'):
+    # Import matplotlib here so that it's not imported unless dendrograms
+    # are plotted. Raise an informative error if importing fails.
+    try:
+        # if an axis is provided, don't use pylab at all
+        if ax is None:
+            import matplotlib.pylab
+        import matplotlib.patches
+        import matplotlib.collections
+    except ImportError as e:
+        raise ImportError("You must install the matplotlib library to plot "
+                          "the dendrogram. Use no_plot=True to calculate the "
+                          "dendrogram without plotting.") from e
+
+    if ax is None:
+        ax = matplotlib.pylab.gca()
+        # if we're using pylab, we want to trigger a draw at the end
+        trigger_redraw = True
+    else:
+        trigger_redraw = False
+
+    # Independent variable plot width
+    ivw = len(ivl) * 10
+    # Dependent variable plot height
+    dvw = mh + mh * 0.05
+
+    iv_ticks = np.arange(5, len(ivl) * 10 + 5, 10)
+    if orientation in ('top', 'bottom'):
+        if orientation == 'top':
+            ax.set_ylim([0, dvw])
+            ax.set_xlim([0, ivw])
+        else:
+            ax.set_ylim([dvw, 0])
+            ax.set_xlim([0, ivw])
+
+        xlines = icoords
+        ylines = dcoords
+        if no_labels:
+            ax.set_xticks([])
+            ax.set_xticklabels([])
+        else:
+            ax.set_xticks(iv_ticks)
+
+            if orientation == 'top':
+                ax.xaxis.set_ticks_position('bottom')
+            else:
+                ax.xaxis.set_ticks_position('top')
+
+            # Make the tick marks invisible because they cover up the links
+            for line in ax.get_xticklines():
+                line.set_visible(False)
+
+            leaf_rot = (float(_get_tick_rotation(len(ivl)))
+                        if (leaf_rotation is None) else leaf_rotation)
+            leaf_font = (float(_get_tick_text_size(len(ivl)))
+                         if (leaf_font_size is None) else leaf_font_size)
+            ax.set_xticklabels(ivl, rotation=leaf_rot, size=leaf_font)
+
+    elif orientation in ('left', 'right'):
+        if orientation == 'left':
+            ax.set_xlim([dvw, 0])
+            ax.set_ylim([0, ivw])
+        else:
+            ax.set_xlim([0, dvw])
+            ax.set_ylim([0, ivw])
+
+        xlines = dcoords
+        ylines = icoords
+        if no_labels:
+            ax.set_yticks([])
+            ax.set_yticklabels([])
+        else:
+            ax.set_yticks(iv_ticks)
+
+            if orientation == 'left':
+                ax.yaxis.set_ticks_position('right')
+            else:
+                ax.yaxis.set_ticks_position('left')
+
+            # Make the tick marks invisible because they cover up the links
+            for line in ax.get_yticklines():
+                line.set_visible(False)
+
+            leaf_font = (float(_get_tick_text_size(len(ivl)))
+                         if (leaf_font_size is None) else leaf_font_size)
+
+            if leaf_rotation is not None:
+                ax.set_yticklabels(ivl, rotation=leaf_rotation, size=leaf_font)
+            else:
+                ax.set_yticklabels(ivl, size=leaf_font)
+
+    # Let's use collections instead. This way there is a separate legend item
+    # for each tree grouping, rather than stupidly one for each line segment.
+    colors_used = _remove_dups(color_list)
+    color_to_lines = {}
+    for color in colors_used:
+        color_to_lines[color] = []
+    for (xline, yline, color) in zip(xlines, ylines, color_list):
+        color_to_lines[color].append(list(zip(xline, yline)))
+
+    colors_to_collections = {}
+    # Construct the collections.
+    for color in colors_used:
+        coll = matplotlib.collections.LineCollection(color_to_lines[color],
+                                                     colors=(color,))
+        colors_to_collections[color] = coll
+
+    # Add all the groupings below the color threshold.
+    for color in colors_used:
+        if color != above_threshold_color:
+            ax.add_collection(colors_to_collections[color])
+    # If there's a grouping of links above the color threshold, it goes last.
+    if above_threshold_color in colors_to_collections:
+        ax.add_collection(colors_to_collections[above_threshold_color])
+
+    if contraction_marks is not None:
+        Ellipse = matplotlib.patches.Ellipse
+        for (x, y) in contraction_marks:
+            if orientation in ('left', 'right'):
+                e = Ellipse((y, x), width=dvw / 100, height=1.0)
+            else:
+                e = Ellipse((x, y), width=1.0, height=dvw / 100)
+            ax.add_artist(e)
+            e.set_clip_box(ax.bbox)
+            e.set_alpha(0.5)
+            e.set_facecolor('k')
+
+    if trigger_redraw:
+        matplotlib.pylab.draw_if_interactive()
+
+
+# C0  is used for above threshold color
+_link_line_colors_default = ('C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9')
+_link_line_colors = list(_link_line_colors_default)
+
+
+@xp_capabilities(out_of_scope=True)
+def set_link_color_palette(palette):
+    """
+    Set list of matplotlib color codes for use by dendrogram.
+
+    Note that this palette is global (i.e., setting it once changes the colors
+    for all subsequent calls to `dendrogram`) and that it affects only the
+    the colors below ``color_threshold``.
+
+    Note that `dendrogram` also accepts a custom coloring function through its
+    ``link_color_func`` keyword, which is more flexible and non-global.
+
+    Parameters
+    ----------
+    palette : list of str or None
+        A list of matplotlib color codes.  The order of the color codes is the
+        order in which the colors are cycled through when color thresholding in
+        the dendrogram.
+
+        If ``None``, resets the palette to its default (which are matplotlib
+        default colors C1 to C9).
+
+    Returns
+    -------
+    None
+
+    See Also
+    --------
+    dendrogram
+
+    Notes
+    -----
+    Ability to reset the palette with ``None`` added in SciPy 0.17.0.
+
+    Thread safety: using this function in a multi-threaded fashion may
+    result in `dendrogram` producing plots with unexpected colors.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.cluster import hierarchy
+    >>> ytdist = np.array([662., 877., 255., 412., 996., 295., 468., 268.,
+    ...                    400., 754., 564., 138., 219., 869., 669.])
+    >>> Z = hierarchy.linkage(ytdist, 'single')
+    >>> dn = hierarchy.dendrogram(Z, no_plot=True)
+    >>> dn['color_list']
+    ['C1', 'C0', 'C0', 'C0', 'C0']
+    >>> hierarchy.set_link_color_palette(['c', 'm', 'y', 'k'])
+    >>> dn = hierarchy.dendrogram(Z, no_plot=True, above_threshold_color='b')
+    >>> dn['color_list']
+    ['c', 'b', 'b', 'b', 'b']
+    >>> dn = hierarchy.dendrogram(Z, no_plot=True, color_threshold=267,
+    ...                           above_threshold_color='k')
+    >>> dn['color_list']
+    ['c', 'm', 'm', 'k', 'k']
+
+    Now, reset the color palette to its default:
+
+    >>> hierarchy.set_link_color_palette(None)
+
+    """
+    if palette is None:
+        # reset to its default
+        palette = _link_line_colors_default
+    elif not isinstance(palette, list | tuple):
+        raise TypeError("palette must be a list or tuple")
+    _ptypes = [isinstance(p, str) for p in palette]
+
+    if False in _ptypes:
+        raise TypeError("all palette list elements must be color strings")
+
+    global _link_line_colors
+    _link_line_colors = palette
+
+
+@xp_capabilities(cpu_only=True, jax_jit=False, allow_dask_compute=True)
+def dendrogram(Z, p=30, truncate_mode=None, color_threshold=None,
+               get_leaves=True, orientation='top', labels=None,
+               count_sort=False, distance_sort=False, show_leaf_counts=True,
+               no_plot=False, no_labels=False, leaf_font_size=None,
+               leaf_rotation=None, leaf_label_func=None,
+               show_contracted=False, link_color_func=None, ax=None,
+               above_threshold_color='C0'):
+    """
+    Plot the hierarchical clustering as a dendrogram.
+
+    The dendrogram illustrates how each cluster is
+    composed by drawing a U-shaped link between a non-singleton
+    cluster and its children. The top of the U-link indicates a
+    cluster merge. The two legs of the U-link indicate which clusters
+    were merged. The length of the two legs of the U-link represents
+    the distance between the child clusters. It is also the
+    cophenetic distance between original observations in the two
+    children clusters.
+
+    Parameters
+    ----------
+    Z : ndarray
+        The linkage matrix encoding the hierarchical clustering to
+        render as a dendrogram. See the ``linkage`` function for more
+        information on the format of ``Z``.
+    p : int, optional
+        The ``p`` parameter for ``truncate_mode``.
+    truncate_mode : str, optional
+        The dendrogram can be hard to read when the original
+        observation matrix from which the linkage is derived is
+        large. Truncation is used to condense the dendrogram. There
+        are several modes:
+
+        ``None``
+          No truncation is performed (default).
+          Note: ``'none'`` is an alias for ``None`` that's kept for
+          backward compatibility.
+
+        ``'lastp'``
+          The last ``p`` non-singleton clusters formed in the linkage are the
+          only non-leaf nodes in the linkage; they correspond to rows
+          ``Z[n-p-2:end]`` in ``Z``. All other non-singleton clusters are
+          contracted into leaf nodes.
+
+        ``'level'``
+          No more than ``p`` levels of the dendrogram tree are displayed.
+          A "level" includes all nodes with ``p`` merges from the final merge.
+
+          Note: ``'mtica'`` is an alias for ``'level'`` that's kept for
+          backward compatibility.
+
+    color_threshold : double, optional
+        For brevity, let :math:`t` be the ``color_threshold``.
+        Colors all the descendent links below a cluster node
+        :math:`k` the same color if :math:`k` is the first node below
+        the cut threshold :math:`t`. All links connecting nodes with
+        distances greater than or equal to the threshold are colored
+        with de default matplotlib color ``'C0'``. If :math:`t` is less
+        than or equal to zero, all nodes are colored ``'C0'``.
+        If ``color_threshold`` is None or 'default',
+        corresponding with MATLAB(TM) behavior, the threshold is set to
+        ``0.7*max(Z[:,2])``.
+
+    get_leaves : bool, optional
+        Includes a list ``R['leaves']=H`` in the result
+        dictionary. For each :math:`i`, ``H[i] == j``, cluster node
+        ``j`` appears in position ``i`` in the left-to-right traversal
+        of the leaves, where :math:`j < 2n-1` and :math:`i < n`.
+    orientation : str, optional
+        The direction to plot the dendrogram, which can be any
+        of the following strings:
+
+        ``'top'``
+          Plots the root at the top, and plot descendent links going downwards.
+          (default).
+
+        ``'bottom'``
+          Plots the root at the bottom, and plot descendent links going
+          upwards.
+
+        ``'left'``
+          Plots the root at the left, and plot descendent links going right.
+
+        ``'right'``
+          Plots the root at the right, and plot descendent links going left.
+
+    labels : ndarray, optional
+        By default, ``labels`` is None so the index of the original observation
+        is used to label the leaf nodes.  Otherwise, this is an :math:`n`-sized
+        sequence, with ``n == Z.shape[0] + 1``. The ``labels[i]`` value is the
+        text to put under the :math:`i` th leaf node only if it corresponds to
+        an original observation and not a non-singleton cluster.
+    count_sort : str or bool, optional
+        For each node n, the order (visually, from left-to-right) n's
+        two descendent links are plotted is determined by this
+        parameter, which can be any of the following values:
+
+        ``False``
+          Nothing is done.
+
+        ``'ascending'`` or ``True``
+          The child with the minimum number of original objects in its cluster
+          is plotted first.
+
+        ``'descending'``
+          The child with the maximum number of original objects in its cluster
+          is plotted first.
+
+        Note, ``distance_sort`` and ``count_sort`` cannot both be True.
+    distance_sort : str or bool, optional
+        For each node n, the order (visually, from left-to-right) n's
+        two descendent links are plotted is determined by this
+        parameter, which can be any of the following values:
+
+        ``False``
+          Nothing is done.
+
+        ``'ascending'`` or ``True``
+          The child with the minimum distance between its direct descendents is
+          plotted first.
+
+        ``'descending'``
+          The child with the maximum distance between its direct descendents is
+          plotted first.
+
+        Note ``distance_sort`` and ``count_sort`` cannot both be True.
+    show_leaf_counts : bool, optional
+         When True, leaf nodes representing :math:`k>1` original
+         observation are labeled with the number of observations they
+         contain in parentheses.
+    no_plot : bool, optional
+        When True, the final rendering is not performed. This is
+        useful if only the data structures computed for the rendering
+        are needed or if matplotlib is not available.
+    no_labels : bool, optional
+        When True, no labels appear next to the leaf nodes in the
+        rendering of the dendrogram.
+    leaf_rotation : double, optional
+        Specifies the angle (in degrees) to rotate the leaf
+        labels. When unspecified, the rotation is based on the number of
+        nodes in the dendrogram (default is 0).
+    leaf_font_size : int, optional
+        Specifies the font size (in points) of the leaf labels. When
+        unspecified, the size based on the number of nodes in the
+        dendrogram.
+    leaf_label_func : lambda or function, optional
+        When ``leaf_label_func`` is a callable function, for each
+        leaf with cluster index :math:`k < 2n-1`. The function
+        is expected to return a string with the label for the
+        leaf.
+
+        Indices :math:`k < n` correspond to original observations
+        while indices :math:`k \\geq n` correspond to non-singleton
+        clusters.
+
+        For example, to label singletons with their node id and
+        non-singletons with their id, count, and inconsistency
+        coefficient, simply do::
+
+            # First define the leaf label function.
+            def llf(id):
+                if id < n:
+                    return str(id)
+                else:
+                    return '[%d %d %1.2f]' % (id, count, R[n-id,3])
+
+            # The text for the leaf nodes is going to be big so force
+            # a rotation of 90 degrees.
+            dendrogram(Z, leaf_label_func=llf, leaf_rotation=90)
+
+            # leaf_label_func can also be used together with ``truncate_mode``,
+            # in which case you will get your leaves labeled after truncation:
+            dendrogram(Z, leaf_label_func=llf, leaf_rotation=90,
+                       truncate_mode='level', p=2)
+
+    show_contracted : bool, optional
+        When True the heights of non-singleton nodes contracted
+        into a leaf node are plotted as crosses along the link
+        connecting that leaf node.  This really is only useful when
+        truncation is used (see ``truncate_mode`` parameter).
+    link_color_func : callable, optional
+        If given, `link_color_function` is called with each non-singleton id
+        corresponding to each U-shaped link it will paint. The function is
+        expected to return the color to paint the link, encoded as a matplotlib
+        color string code. For example::
+
+            dendrogram(Z, link_color_func=lambda k: colors[k])
+
+        colors the direct links below each untruncated non-singleton node
+        ``k`` using ``colors[k]``.
+    ax : matplotlib Axes instance, optional
+        If None and `no_plot` is not True, the dendrogram will be plotted
+        on the current axes.  Otherwise if `no_plot` is not True the
+        dendrogram will be plotted on the given ``Axes`` instance. This can be
+        useful if the dendrogram is part of a more complex figure.
+    above_threshold_color : str, optional
+        This matplotlib color string sets the color of the links above the
+        color_threshold. The default is ``'C0'``.
+
+    Returns
+    -------
+    R : dict
+        A dictionary of data structures computed to render the
+        dendrogram. Its has the following keys:
+
+        ``'color_list'``
+          A list of color names. The k'th element represents the color of the
+          k'th link.
+
+        ``'icoord'`` and ``'dcoord'``
+          Each of them is a list of lists. Let ``icoord = [I1, I2, ..., Ip]``
+          where ``Ik = [xk1, xk2, xk3, xk4]`` and ``dcoord = [D1, D2, ..., Dp]``
+          where ``Dk = [yk1, yk2, yk3, yk4]``, then the k'th link painted is
+          ``(xk1, yk1)`` - ``(xk2, yk2)`` - ``(xk3, yk3)`` - ``(xk4, yk4)``.
+
+        ``'ivl'``
+          A list of labels corresponding to the leaf nodes.
+
+        ``'leaves'``
+          For each i, ``H[i] == j``, cluster node ``j`` appears in position
+          ``i`` in the left-to-right traversal of the leaves, where
+          :math:`j < 2n-1` and :math:`i < n`. If ``j`` is less than ``n``, the
+          ``i``-th leaf node corresponds to an original observation.
+          Otherwise, it corresponds to a non-singleton cluster.
+
+        ``'leaves_color_list'``
+          A list of color names. The k'th element represents the color of the
+          k'th leaf.
+
+    See Also
+    --------
+    linkage, set_link_color_palette
+
+    Notes
+    -----
+    It is expected that the distances in ``Z[:,2]`` be monotonic, otherwise
+    crossings appear in the dendrogram.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.cluster import hierarchy
+    >>> import matplotlib.pyplot as plt
+
+    A very basic example:
+
+    >>> ytdist = np.array([662., 877., 255., 412., 996., 295., 468., 268.,
+    ...                    400., 754., 564., 138., 219., 869., 669.])
+    >>> Z = hierarchy.linkage(ytdist, 'single')
+    >>> plt.figure()
+    >>> dn = hierarchy.dendrogram(Z)
+
+    Now, plot in given axes, improve the color scheme and use both vertical and
+    horizontal orientations:
+
+    >>> hierarchy.set_link_color_palette(['m', 'c', 'y', 'k'])
+    >>> fig, axes = plt.subplots(1, 2, figsize=(8, 3))
+    >>> dn1 = hierarchy.dendrogram(Z, ax=axes[0], above_threshold_color='y',
+    ...                            orientation='top')
+    >>> dn2 = hierarchy.dendrogram(Z, ax=axes[1],
+    ...                            above_threshold_color='#bcbddc',
+    ...                            orientation='right')
+    >>> hierarchy.set_link_color_palette(None)  # reset to default after use
+    >>> plt.show()
+
+    """
+    # This feature was thought about but never implemented (still useful?):
+    #
+    #         ... = dendrogram(..., leaves_order=None)
+    #
+    #         Plots the leaves in the order specified by a vector of
+    #         original observation indices. If the vector contains duplicates
+    #         or results in a crossing, an exception will be thrown. Passing
+    #         None orders leaf nodes based on the order they appear in the
+    #         pre-order traversal.
+    xp = array_namespace(Z)
+    Z = _asarray(Z, order='C', xp=xp)
+
+    if orientation not in ["top", "left", "bottom", "right"]:
+        raise ValueError("orientation must be one of 'top', 'left', "
+                         "'bottom', or 'right'")
+
+    if labels is not None:
+        try:
+            len_labels = len(labels)
+        except (TypeError, AttributeError):
+            len_labels = labels.shape[0]
+        if Z.shape[0] + 1 != len_labels:
+            raise ValueError("Dimensions of Z and labels must be consistent.")
+
+    _is_valid_linkage(Z, throw=True, name='Z', materialize=True, xp=xp)
+    Zs = Z.shape
+    n = Zs[0] + 1
+    if isinstance(p, int | float):
+        p = int(p)
+    else:
+        raise TypeError('The second argument must be a number')
+
+    if truncate_mode not in ('lastp', 'mtica', 'level', 'none', None):
+        # 'mtica' is kept working for backwards compat.
+        raise ValueError('Invalid truncation mode.')
+
+    if truncate_mode == 'lastp':
+        if p > n or p == 0:
+            p = n
+
+    if truncate_mode == 'mtica':
+        # 'mtica' is an alias
+        truncate_mode = 'level'
+
+    if truncate_mode == 'level':
+        if p <= 0:
+            p = np.inf
+
+    if get_leaves:
+        lvs = []
+    else:
+        lvs = None
+
+    icoord_list = []
+    dcoord_list = []
+    color_list = []
+    current_color = [0]
+    currently_below_threshold = [False]
+    ivl = []  # list of leaves
+
+    if color_threshold is None or (isinstance(color_threshold, str) and
+                                   color_threshold == 'default'):
+        color_threshold = xp.max(Z[:, 2]) * 0.7
+
+    R = {'icoord': icoord_list, 'dcoord': dcoord_list, 'ivl': ivl,
+         'leaves': lvs, 'color_list': color_list}
+
+    # Empty list will be filled in _dendrogram_calculate_info
+    contraction_marks = [] if show_contracted else None
+
+    _dendrogram_calculate_info(
+        Z=Z, p=p,
+        truncate_mode=truncate_mode,
+        color_threshold=color_threshold,
+        get_leaves=get_leaves,
+        orientation=orientation,
+        labels=labels,
+        count_sort=count_sort,
+        distance_sort=distance_sort,
+        show_leaf_counts=show_leaf_counts,
+        i=2*n - 2,
+        iv=0.0,
+        ivl=ivl,
+        n=n,
+        icoord_list=icoord_list,
+        dcoord_list=dcoord_list,
+        lvs=lvs,
+        current_color=current_color,
+        color_list=color_list,
+        currently_below_threshold=currently_below_threshold,
+        leaf_label_func=leaf_label_func,
+        contraction_marks=contraction_marks,
+        link_color_func=link_color_func,
+        above_threshold_color=above_threshold_color)
+
+    if not no_plot:
+        mh = xp.max(Z[:, 2])
+        _plot_dendrogram(icoord_list, dcoord_list, ivl, p, n, mh, orientation,
+                         no_labels, color_list,
+                         leaf_font_size=leaf_font_size,
+                         leaf_rotation=leaf_rotation,
+                         contraction_marks=contraction_marks,
+                         ax=ax,
+                         above_threshold_color=above_threshold_color)
+
+    R["leaves_color_list"] = _get_leaves_color_list(R)
+
+    return R
+
+
+def _get_leaves_color_list(R):
+    leaves_color_list = [None] * len(R['leaves'])
+    for link_x, link_y, link_color in zip(R['icoord'],
+                                          R['dcoord'],
+                                          R['color_list']):
+        for (xi, yi) in zip(link_x, link_y):
+            if yi == 0.0 and (xi % 5 == 0 and xi % 2 == 1):
+                # if yi is 0.0 and xi is divisible by 5 and odd,
+                # the point is a leaf
+                # xi of leaves are      5, 15, 25, 35, ... (see `iv_ticks`)
+                # index of leaves are   0,  1,  2,  3, ... as below
+                leaf_index = (int(xi) - 5) // 10
+                # each leaf has a same color of its link.
+                leaves_color_list[leaf_index] = link_color
+    return leaves_color_list
+
+
+def _append_singleton_leaf_node(Z, p, n, level, lvs, ivl, leaf_label_func,
+                                i, labels):
+    # If the leaf id structure is not None and is a list then the caller
+    # to dendrogram has indicated that cluster id's corresponding to the
+    # leaf nodes should be recorded.
+
+    if lvs is not None:
+        lvs.append(int(i))
+
+    # If leaf node labels are to be displayed...
+    if ivl is not None:
+        # If a leaf_label_func has been provided, the label comes from the
+        # string returned from the leaf_label_func, which is a function
+        # passed to dendrogram.
+        if leaf_label_func:
+            ivl.append(leaf_label_func(int(i)))
+        else:
+            # Otherwise, if the dendrogram caller has passed a labels list
+            # for the leaf nodes, use it.
+            if labels is not None:
+                ivl.append(labels[int(i - n)])
+            else:
+                # Otherwise, use the id as the label for the leaf.x
+                ivl.append(str(int(i)))
+
+
+def _append_nonsingleton_leaf_node(Z, p, n, level, lvs, ivl, leaf_label_func,
+                                   i, labels, show_leaf_counts):
+    # If the leaf id structure is not None and is a list then the caller
+    # to dendrogram has indicated that cluster id's corresponding to the
+    # leaf nodes should be recorded.
+
+    if lvs is not None:
+        lvs.append(int(i))
+    if ivl is not None:
+        if leaf_label_func:
+            ivl.append(leaf_label_func(int(i)))
+        else:
+            if show_leaf_counts:
+                ivl.append("(" + str(np.asarray(Z[i - n, 3], dtype=np.int64)) + ")")
+            else:
+                ivl.append("")
+
+
+def _append_contraction_marks(Z, iv, i, n, contraction_marks, xp):
+    _append_contraction_marks_sub(Z, iv, int_floor(Z[i - n, 0], xp),
+                                  n, contraction_marks, xp)
+    _append_contraction_marks_sub(Z, iv, int_floor(Z[i - n, 1], xp),
+                                  n, contraction_marks, xp)
+
+
+def _append_contraction_marks_sub(Z, iv, i, n, contraction_marks, xp):
+    if i >= n:
+        contraction_marks.append((iv, Z[i - n, 2]))
+        _append_contraction_marks_sub(Z, iv, int_floor(Z[i - n, 0], xp),
+                                      n, contraction_marks, xp)
+        _append_contraction_marks_sub(Z, iv, int_floor(Z[i - n, 1], xp),
+                                      n, contraction_marks, xp)
+
+
+def _dendrogram_calculate_info(Z, p, truncate_mode,
+                               color_threshold=np.inf, get_leaves=True,
+                               orientation='top', labels=None,
+                               count_sort=False, distance_sort=False,
+                               show_leaf_counts=False, i=-1, iv=0.0,
+                               ivl=None, n=0, icoord_list=None, dcoord_list=None,
+                               lvs=None, mhr=False,
+                               current_color=None, color_list=None,
+                               currently_below_threshold=None,
+                               leaf_label_func=None, level=0,
+                               contraction_marks=None,
+                               link_color_func=None,
+                               above_threshold_color='C0'):
+    """
+    Calculate the endpoints of the links as well as the labels for the
+    the dendrogram rooted at the node with index i. iv is the independent
+    variable value to plot the left-most leaf node below the root node i
+    (if orientation='top', this would be the left-most x value where the
+    plotting of this root node i and its descendents should begin).
+
+    ivl is a list to store the labels of the leaf nodes. The leaf_label_func
+    is called whenever ivl != None, labels == None, and
+    leaf_label_func != None. When ivl != None and labels != None, the
+    labels list is used only for labeling the leaf nodes. When
+    ivl == None, no labels are generated for leaf nodes.
+
+    When get_leaves==True, a list of leaves is built as they are visited
+    in the dendrogram.
+
+    Returns a tuple with l being the independent variable coordinate that
+    corresponds to the midpoint of cluster to the left of cluster i if
+    i is non-singleton, otherwise the independent coordinate of the leaf
+    node if i is a leaf node.
+
+    Returns
+    -------
+    A tuple (left, w, h, md), where:
+        * left is the independent variable coordinate of the center of the
+          the U of the subtree
+
+        * w is the amount of space used for the subtree (in independent
+          variable units)
+
+        * h is the height of the subtree in dependent variable units
+
+        * md is the ``max(Z[*,2]``) for all nodes ``*`` below and including
+          the target node.
+
+    """
+    xp = array_namespace(Z)
+    if n == 0:
+        raise ValueError("Invalid singleton cluster count n.")
+
+    if i == -1:
+        raise ValueError("Invalid root cluster index i.")
+
+    if truncate_mode == 'lastp':
+        # If the node is a leaf node but corresponds to a non-singleton
+        # cluster, its label is either the empty string or the number of
+        # original observations belonging to cluster i.
+        if 2*n - p > i >= n:
+            d = Z[i - n, 2]
+            _append_nonsingleton_leaf_node(Z, p, n, level, lvs, ivl,
+                                           leaf_label_func, i, labels,
+                                           show_leaf_counts)
+            if contraction_marks is not None:
+                _append_contraction_marks(Z, iv + 5.0, i, n, contraction_marks, xp)
+            return (iv + 5.0, 10.0, 0.0, d)
+        elif i < n:
+            _append_singleton_leaf_node(Z, p, n, level, lvs, ivl,
+                                        leaf_label_func, i, labels)
+            return (iv + 5.0, 10.0, 0.0, 0.0)
+    elif truncate_mode == 'level':
+        if i > n and level > p:
+            d = Z[i - n, 2]
+            _append_nonsingleton_leaf_node(Z, p, n, level, lvs, ivl,
+                                           leaf_label_func, i, labels,
+                                           show_leaf_counts)
+            if contraction_marks is not None:
+                _append_contraction_marks(Z, iv + 5.0, i, n, contraction_marks, xp)
+            return (iv + 5.0, 10.0, 0.0, d)
+        elif i < n:
+            _append_singleton_leaf_node(Z, p, n, level, lvs, ivl,
+                                        leaf_label_func, i, labels)
+            return (iv + 5.0, 10.0, 0.0, 0.0)
+
+    # Otherwise, only truncate if we have a leaf node.
+    #
+    # Only place leaves if they correspond to original observations.
+    if i < n:
+        _append_singleton_leaf_node(Z, p, n, level, lvs, ivl,
+                                    leaf_label_func, i, labels)
+        return (iv + 5.0, 10.0, 0.0, 0.0)
+
+    # !!! Otherwise, we don't have a leaf node, so work on plotting a
+    # non-leaf node.
+    # Actual indices of a and b
+    aa = int_floor(Z[i - n, 0], xp)
+    ab = int_floor(Z[i - n, 1], xp)
+    if aa >= n:
+        # The number of singletons below cluster a
+        na = Z[aa - n, 3]
+        # The distance between a's two direct children.
+        da = Z[aa - n, 2]
+    else:
+        na = 1
+        da = 0.0
+    if ab >= n:
+        nb = Z[ab - n, 3]
+        db = Z[ab - n, 2]
+    else:
+        nb = 1
+        db = 0.0
+
+    if count_sort == 'ascending' or count_sort is True:
+        # If a has a count greater than b, it and its descendents should
+        # be drawn to the right. Otherwise, to the left.
+        if na > nb:
+            # The cluster index to draw to the left (ua) will be ab
+            # and the one to draw to the right (ub) will be aa
+            ua = ab
+            ub = aa
+        else:
+            ua = aa
+            ub = ab
+    elif count_sort == 'descending':
+        # If a has a count less than or equal to b, it and its
+        # descendents should be drawn to the left. Otherwise, to
+        # the right.
+        if na > nb:
+            ua = aa
+            ub = ab
+        else:
+            ua = ab
+            ub = aa
+    elif distance_sort == 'ascending' or distance_sort is True:
+        # If a has a distance greater than b, it and its descendents should
+        # be drawn to the right. Otherwise, to the left.
+        if da > db:
+            ua = ab
+            ub = aa
+        else:
+            ua = aa
+            ub = ab
+    elif distance_sort == 'descending':
+        # If a has a distance less than or equal to b, it and its
+        # descendents should be drawn to the left. Otherwise, to
+        # the right.
+        if da > db:
+            ua = aa
+            ub = ab
+        else:
+            ua = ab
+            ub = aa
+    else:
+        ua = aa
+        ub = ab
+
+    # Updated iv variable and the amount of space used.
+    (uiva, uwa, uah, uamd) = \
+        _dendrogram_calculate_info(
+            Z=Z, p=p,
+            truncate_mode=truncate_mode,
+            color_threshold=color_threshold,
+            get_leaves=get_leaves,
+            orientation=orientation,
+            labels=labels,
+            count_sort=count_sort,
+            distance_sort=distance_sort,
+            show_leaf_counts=show_leaf_counts,
+            i=ua, iv=iv, ivl=ivl, n=n,
+            icoord_list=icoord_list,
+            dcoord_list=dcoord_list, lvs=lvs,
+            current_color=current_color,
+            color_list=color_list,
+            currently_below_threshold=currently_below_threshold,
+            leaf_label_func=leaf_label_func,
+            level=level + 1, contraction_marks=contraction_marks,
+            link_color_func=link_color_func,
+            above_threshold_color=above_threshold_color)
+
+    h = Z[i - n, 2]
+    if h >= color_threshold or color_threshold <= 0:
+        c = above_threshold_color
+
+        if currently_below_threshold[0]:
+            current_color[0] = (current_color[0] + 1) % len(_link_line_colors)
+        currently_below_threshold[0] = False
+    else:
+        currently_below_threshold[0] = True
+        c = _link_line_colors[current_color[0]]
+
+    (uivb, uwb, ubh, ubmd) = \
+        _dendrogram_calculate_info(
+            Z=Z, p=p,
+            truncate_mode=truncate_mode,
+            color_threshold=color_threshold,
+            get_leaves=get_leaves,
+            orientation=orientation,
+            labels=labels,
+            count_sort=count_sort,
+            distance_sort=distance_sort,
+            show_leaf_counts=show_leaf_counts,
+            i=ub, iv=iv + uwa, ivl=ivl, n=n,
+            icoord_list=icoord_list,
+            dcoord_list=dcoord_list, lvs=lvs,
+            current_color=current_color,
+            color_list=color_list,
+            currently_below_threshold=currently_below_threshold,
+            leaf_label_func=leaf_label_func,
+            level=level + 1, contraction_marks=contraction_marks,
+            link_color_func=link_color_func,
+            above_threshold_color=above_threshold_color)
+
+    max_dist = max(uamd, ubmd, h)
+
+    icoord_list.append([uiva, uiva, uivb, uivb])
+    dcoord_list.append([uah, h, h, ubh])
+    if link_color_func is not None:
+        v = link_color_func(int(i))
+        if not isinstance(v, str):
+            raise TypeError("link_color_func must return a matplotlib "
+                            "color string!")
+        color_list.append(v)
+    else:
+        color_list.append(c)
+
+    return (((uiva + uivb) / 2), uwa + uwb, h, max_dist)
+
+
+@xp_capabilities()
+def is_isomorphic(T1, T2):
+    """
+    Determine if two different cluster assignments are equivalent.
+
+    Parameters
+    ----------
+    T1 : array_like
+        An assignment of singleton cluster ids to flat cluster ids.
+    T2 : array_like
+        An assignment of singleton cluster ids to flat cluster ids.
+
+    Returns
+    -------
+    b : bool
+        Whether the flat cluster assignments `T1` and `T2` are
+        equivalent.
+
+    See Also
+    --------
+    linkage : for a description of what a linkage matrix is.
+    fcluster : for the creation of flat cluster assignments.
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import fcluster, is_isomorphic
+    >>> from scipy.cluster.hierarchy import single, complete
+    >>> from scipy.spatial.distance import pdist
+
+    Two flat cluster assignments can be isomorphic if they represent the same
+    cluster assignment, with different labels.
+
+    For example, we can use the `scipy.cluster.hierarchy.single` method
+    and flatten the output to four clusters:
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    >>> Z = single(pdist(X))
+    >>> T = fcluster(Z, 1, criterion='distance')
+    >>> T
+    array([3, 3, 3, 4, 4, 4, 2, 2, 2, 1, 1, 1], dtype=int32)
+
+    We can then do the same using the
+    `scipy.cluster.hierarchy.complete`: method:
+
+    >>> Z = complete(pdist(X))
+    >>> T_ = fcluster(Z, 1.5, criterion='distance')
+    >>> T_
+    array([1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4], dtype=int32)
+
+    As we can see, in both cases we obtain four clusters and all the data
+    points are distributed in the same way - the only thing that changes
+    are the flat cluster labels (3 => 1, 4 =>2, 2 =>3 and 4 =>1), so both
+    cluster assignments are isomorphic:
+
+    >>> is_isomorphic(T, T_)
+    True
+
+    """
+    xp = array_namespace(T1, T2)
+    T1 = _asarray(T1, xp=xp)
+    T2 = _asarray(T2, xp=xp)
+
+    if T1.ndim != 1:
+        raise ValueError('T1 must be one-dimensional.')
+    if T2.ndim != 1:
+        raise ValueError('T2 must be one-dimensional.')
+    if T1.shape != T2.shape:
+        raise ValueError('T1 and T2 must have the same number of elements.')
+
+    # For each pair of (i, j) indices, test that
+    # T1[i] == T1[j] <--> T2[i] == T2[j]
+    # In other words, if the same symbol appears multiple times in T1,
+    # then a potentially different symbol must also be repeated in the same
+    # positions in T2, and vice versa.
+
+    # O(n*log(n)) algorithm.
+    # It is also possible to write a O(n) algorithm on top of unique_all(),
+    # but in practice it's much slower even for large clusters.
+    idx = xp.argsort(T1)
+    T1 = xp.take(T1, idx)
+    T2 = xp.take(T2, idx)
+    changes1 = T1 != xp.roll(T1, -1)
+    changes2 = T2 != xp.roll(T2, -1)
+    return xp.all(changes1 == changes2)
+
+
+@lazy_cython
+def maxdists(Z):
+    """
+    Return the maximum distance between any non-singleton cluster.
+
+    Parameters
+    ----------
+    Z : ndarray
+        The hierarchical clustering encoded as a matrix. See
+        ``linkage`` for more information.
+
+    Returns
+    -------
+    maxdists : ndarray
+        A ``(n-1)`` sized numpy array of doubles; ``MD[i]`` represents
+        the maximum distance between any cluster (including
+        singletons) below and including the node with index i. More
+        specifically, ``MD[i] = Z[Q(i)-n, 2].max()`` where ``Q(i)`` is the
+        set of all node indices below and including node i.
+
+    See Also
+    --------
+    linkage : for a description of what a linkage matrix is.
+    is_monotonic : for testing for monotonicity of a linkage matrix.
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import median, maxdists
+    >>> from scipy.spatial.distance import pdist
+
+    Given a linkage matrix ``Z``, `scipy.cluster.hierarchy.maxdists`
+    computes for each new cluster generated (i.e., for each row of the linkage
+    matrix) what is the maximum distance between any two child clusters.
+
+    Due to the nature of hierarchical clustering, in many cases this is going
+    to be just the distance between the two child clusters that were merged
+    to form the current one - that is, Z[:,2].
+
+    However, for non-monotonic cluster assignments such as
+    `scipy.cluster.hierarchy.median` clustering this is not always the
+    case: There may be cluster formations were the distance between the two
+    clusters merged is smaller than the distance between their children.
+
+    We can see this in an example:
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    >>> Z = median(pdist(X))
+    >>> Z
+    array([[ 0.        ,  1.        ,  1.        ,  2.        ],
+           [ 3.        ,  4.        ,  1.        ,  2.        ],
+           [ 9.        , 10.        ,  1.        ,  2.        ],
+           [ 6.        ,  7.        ,  1.        ,  2.        ],
+           [ 2.        , 12.        ,  1.11803399,  3.        ],
+           [ 5.        , 13.        ,  1.11803399,  3.        ],
+           [ 8.        , 15.        ,  1.11803399,  3.        ],
+           [11.        , 14.        ,  1.11803399,  3.        ],
+           [18.        , 19.        ,  3.        ,  6.        ],
+           [16.        , 17.        ,  3.5       ,  6.        ],
+           [20.        , 21.        ,  3.25      , 12.        ]])
+    >>> maxdists(Z)
+    array([1.        , 1.        , 1.        , 1.        , 1.11803399,
+           1.11803399, 1.11803399, 1.11803399, 3.        , 3.5       ,
+           3.5       ])
+
+    Note that while the distance between the two clusters merged when creating the
+    last cluster is 3.25, there are two children (clusters 16 and 17) whose distance
+    is larger (3.5). Thus, `scipy.cluster.hierarchy.maxdists` returns 3.5 in
+    this case.
+
+    """
+    xp = array_namespace(Z)
+    Z = _asarray(Z, order='C', dtype=xp.float64, xp=xp)
+    _is_valid_linkage(Z, throw=True, name='Z', xp=xp)
+
+    def cy_maxdists(Z, validate):
+        if validate:
+            _is_valid_linkage(Z, throw=True, name='Z', xp=np)
+        MD = np.zeros((Z.shape[0],))
+        _hierarchy.get_max_dist_for_each_cluster(Z, MD, Z.shape[0] + 1)
+        return MD
+
+    return xpx.lazy_apply(cy_maxdists, Z, validate=is_lazy_array(Z),
+                          shape=(Z.shape[0], ), dtype=xp.float64,
+                          as_numpy=True, xp=xp)
+
+
+@lazy_cython
+def maxinconsts(Z, R):
+    """
+    Return the maximum inconsistency coefficient for each
+    non-singleton cluster and its children.
+
+    Parameters
+    ----------
+    Z : ndarray
+        The hierarchical clustering encoded as a matrix. See
+        `linkage` for more information.
+    R : ndarray
+        The inconsistency matrix.
+
+    Returns
+    -------
+    MI : ndarray
+        A monotonic ``(n-1)``-sized numpy array of doubles.
+
+    See Also
+    --------
+    linkage : for a description of what a linkage matrix is.
+    inconsistent : for the creation of a inconsistency matrix.
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import median, inconsistent, maxinconsts
+    >>> from scipy.spatial.distance import pdist
+
+    Given a data set ``X``, we can apply a clustering method to obtain a
+    linkage matrix ``Z``. `scipy.cluster.hierarchy.inconsistent` can
+    be also used to obtain the inconsistency matrix ``R`` associated to
+    this clustering process:
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    >>> Z = median(pdist(X))
+    >>> R = inconsistent(Z)
+    >>> Z
+    array([[ 0.        ,  1.        ,  1.        ,  2.        ],
+           [ 3.        ,  4.        ,  1.        ,  2.        ],
+           [ 9.        , 10.        ,  1.        ,  2.        ],
+           [ 6.        ,  7.        ,  1.        ,  2.        ],
+           [ 2.        , 12.        ,  1.11803399,  3.        ],
+           [ 5.        , 13.        ,  1.11803399,  3.        ],
+           [ 8.        , 15.        ,  1.11803399,  3.        ],
+           [11.        , 14.        ,  1.11803399,  3.        ],
+           [18.        , 19.        ,  3.        ,  6.        ],
+           [16.        , 17.        ,  3.5       ,  6.        ],
+           [20.        , 21.        ,  3.25      , 12.        ]])
+    >>> R
+    array([[1.        , 0.        , 1.        , 0.        ],
+           [1.        , 0.        , 1.        , 0.        ],
+           [1.        , 0.        , 1.        , 0.        ],
+           [1.        , 0.        , 1.        , 0.        ],
+           [1.05901699, 0.08346263, 2.        , 0.70710678],
+           [1.05901699, 0.08346263, 2.        , 0.70710678],
+           [1.05901699, 0.08346263, 2.        , 0.70710678],
+           [1.05901699, 0.08346263, 2.        , 0.70710678],
+           [1.74535599, 1.08655358, 3.        , 1.15470054],
+           [1.91202266, 1.37522872, 3.        , 1.15470054],
+           [3.25      , 0.25      , 3.        , 0.        ]])
+
+    Here, `scipy.cluster.hierarchy.maxinconsts` can be used to compute
+    the maximum value of the inconsistency statistic (the last column of
+    ``R``) for each non-singleton cluster and its children:
+
+    >>> maxinconsts(Z, R)
+    array([0.        , 0.        , 0.        , 0.        , 0.70710678,
+           0.70710678, 0.70710678, 0.70710678, 1.15470054, 1.15470054,
+           1.15470054])
+
+    """
+    xp = array_namespace(Z, R)
+    Z = _asarray(Z, order='C', dtype=xp.float64, xp=xp)
+    R = _asarray(R, order='C', dtype=xp.float64, xp=xp)
+    _is_valid_linkage(Z, throw=True, name='Z', xp=xp)
+    _is_valid_im(R, throw=True, name='R', xp=xp)
+
+    if Z.shape[0] != R.shape[0]:
+        raise ValueError("The inconsistency matrix and linkage matrix each "
+                         "have a different number of rows.")
+    
+    def cy_maxinconsts(Z, R, validate):
+        if validate:
+            _is_valid_linkage(Z, throw=True, name='Z', xp=np)
+            _is_valid_im(R, throw=True, name='R', xp=np)
+        n = Z.shape[0] + 1
+        MI = np.zeros((n - 1,))
+        _hierarchy.get_max_Rfield_for_each_cluster(Z, R, MI, n, 3)
+        return MI
+
+    return xpx.lazy_apply(cy_maxinconsts, Z, R, validate=is_lazy_array(Z),
+                          shape=(Z.shape[0], ), dtype=xp.float64,
+                          as_numpy=True, xp=xp)
+
+
+@lazy_cython
+def maxRstat(Z, R, i):
+    """
+    Return the maximum statistic for each non-singleton cluster and its
+    children.
+
+    Parameters
+    ----------
+    Z : array_like
+        The hierarchical clustering encoded as a matrix. See `linkage` for more
+        information.
+    R : array_like
+        The inconsistency matrix.
+    i : int
+        The column of `R` to use as the statistic.
+
+    Returns
+    -------
+    MR : ndarray
+        Calculates the maximum statistic for the i'th column of the
+        inconsistency matrix `R` for each non-singleton cluster
+        node. ``MR[j]`` is the maximum over ``R[Q(j)-n, i]``, where
+        ``Q(j)`` the set of all node ids corresponding to nodes below
+        and including ``j``.
+
+    See Also
+    --------
+    linkage : for a description of what a linkage matrix is.
+    inconsistent : for the creation of a inconsistency matrix.
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import median, inconsistent, maxRstat
+    >>> from scipy.spatial.distance import pdist
+
+    Given a data set ``X``, we can apply a clustering method to obtain a
+    linkage matrix ``Z``. `scipy.cluster.hierarchy.inconsistent` can
+    be also used to obtain the inconsistency matrix ``R`` associated to
+    this clustering process:
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    >>> Z = median(pdist(X))
+    >>> R = inconsistent(Z)
+    >>> R
+    array([[1.        , 0.        , 1.        , 0.        ],
+           [1.        , 0.        , 1.        , 0.        ],
+           [1.        , 0.        , 1.        , 0.        ],
+           [1.        , 0.        , 1.        , 0.        ],
+           [1.05901699, 0.08346263, 2.        , 0.70710678],
+           [1.05901699, 0.08346263, 2.        , 0.70710678],
+           [1.05901699, 0.08346263, 2.        , 0.70710678],
+           [1.05901699, 0.08346263, 2.        , 0.70710678],
+           [1.74535599, 1.08655358, 3.        , 1.15470054],
+           [1.91202266, 1.37522872, 3.        , 1.15470054],
+           [3.25      , 0.25      , 3.        , 0.        ]])
+
+    `scipy.cluster.hierarchy.maxRstat` can be used to compute
+    the maximum value of each column of ``R``, for each non-singleton
+    cluster and its children:
+
+    >>> maxRstat(Z, R, 0)
+    array([1.        , 1.        , 1.        , 1.        , 1.05901699,
+           1.05901699, 1.05901699, 1.05901699, 1.74535599, 1.91202266,
+           3.25      ])
+    >>> maxRstat(Z, R, 1)
+    array([0.        , 0.        , 0.        , 0.        , 0.08346263,
+           0.08346263, 0.08346263, 0.08346263, 1.08655358, 1.37522872,
+           1.37522872])
+    >>> maxRstat(Z, R, 3)
+    array([0.        , 0.        , 0.        , 0.        , 0.70710678,
+           0.70710678, 0.70710678, 0.70710678, 1.15470054, 1.15470054,
+           1.15470054])
+
+    """
+    xp = array_namespace(Z, R)
+    Z = _asarray(Z, order='C', dtype=xp.float64, xp=xp)
+    R = _asarray(R, order='C', dtype=xp.float64, xp=xp)
+    _is_valid_linkage(Z, throw=True, name='Z', xp=xp)
+    _is_valid_im(R, throw=True, name='R', xp=xp)
+
+    if not isinstance(i, int):
+        raise TypeError('The third argument must be an integer.')
+
+    if i < 0 or i > 3:
+        raise ValueError('i must be an integer between 0 and 3 inclusive.')
+
+    if Z.shape[0] != R.shape[0]:
+        raise ValueError("The inconsistency matrix and linkage matrix each "
+                         "have a different number of rows.")
+
+    def cy_maxRstat(Z, R, i, validate):
+        if validate:
+            _is_valid_linkage(Z, throw=True, name='Z', xp=np)
+            _is_valid_im(R, throw=True, name='R', xp=np)
+        MR = np.zeros((Z.shape[0],))
+        n = Z.shape[0] + 1
+        _hierarchy.get_max_Rfield_for_each_cluster(Z, R, MR, n, i)
+        return MR
+
+    return xpx.lazy_apply(cy_maxRstat, Z, R, i=i, validate=is_lazy_array(Z),
+                          shape=(Z.shape[0], ), dtype=xp.float64,
+                          as_numpy=True, xp=xp)
+
+
+# Data-dependent output shape makes it impossible to use jax.jit
+@xp_capabilities(cpu_only=True, reason="Cython code", jax_jit=False,
+                 warnings=[("dask.array", "merges chunks")])
+def leaders(Z, T):
+    """
+    Return the root nodes in a hierarchical clustering.
+
+    Returns the root nodes in a hierarchical clustering corresponding
+    to a cut defined by a flat cluster assignment vector ``T``. See
+    the ``fcluster`` function for more information on the format of ``T``.
+
+    For each flat cluster :math:`j` of the :math:`k` flat clusters
+    represented in the n-sized flat cluster assignment vector ``T``,
+    this function finds the lowest cluster node :math:`i` in the linkage
+    tree Z, such that:
+
+      * leaf descendants belong only to flat cluster j
+        (i.e., ``T[p]==j`` for all :math:`p` in :math:`S(i)`, where
+        :math:`S(i)` is the set of leaf ids of descendant leaf nodes
+        with cluster node :math:`i`)
+
+      * there does not exist a leaf that is not a descendant with
+        :math:`i` that also belongs to cluster :math:`j`
+        (i.e., ``T[q]!=j`` for all :math:`q` not in :math:`S(i)`). If
+        this condition is violated, ``T`` is not a valid cluster
+        assignment vector, and an exception will be thrown.
+
+    Parameters
+    ----------
+    Z : ndarray
+        The hierarchical clustering encoded as a matrix. See
+        `linkage` for more information.
+    T : ndarray
+        The flat cluster assignment vector.
+
+    Returns
+    -------
+    L : ndarray
+        The leader linkage node id's stored as a k-element 1-D array,
+        where ``k`` is the number of flat clusters found in ``T``.
+
+        ``L[j]=i`` is the linkage cluster node id that is the
+        leader of flat cluster with id M[j]. If ``i < n``, ``i``
+        corresponds to an original observation, otherwise it
+        corresponds to a non-singleton cluster.
+    M : ndarray
+        The leader linkage node id's stored as a k-element 1-D array, where
+        ``k`` is the number of flat clusters found in ``T``. This allows the
+        set of flat cluster ids to be any arbitrary set of ``k`` integers.
+
+        For example: if ``L[3]=2`` and ``M[3]=8``, the flat cluster with
+        id 8's leader is linkage node 2.
+
+    See Also
+    --------
+    fcluster : for the creation of flat cluster assignments.
+
+    Examples
+    --------
+    >>> from scipy.cluster.hierarchy import ward, fcluster, leaders
+    >>> from scipy.spatial.distance import pdist
+
+    Given a linkage matrix ``Z`` - obtained after apply a clustering method
+    to a dataset ``X`` - and a flat cluster assignment array ``T``:
+
+    >>> X = [[0, 0], [0, 1], [1, 0],
+    ...      [0, 4], [0, 3], [1, 4],
+    ...      [4, 0], [3, 0], [4, 1],
+    ...      [4, 4], [3, 4], [4, 3]]
+
+    >>> Z = ward(pdist(X))
+    >>> Z
+    array([[ 0.        ,  1.        ,  1.        ,  2.        ],
+           [ 3.        ,  4.        ,  1.        ,  2.        ],
+           [ 6.        ,  7.        ,  1.        ,  2.        ],
+           [ 9.        , 10.        ,  1.        ,  2.        ],
+           [ 2.        , 12.        ,  1.29099445,  3.        ],
+           [ 5.        , 13.        ,  1.29099445,  3.        ],
+           [ 8.        , 14.        ,  1.29099445,  3.        ],
+           [11.        , 15.        ,  1.29099445,  3.        ],
+           [16.        , 17.        ,  5.77350269,  6.        ],
+           [18.        , 19.        ,  5.77350269,  6.        ],
+           [20.        , 21.        ,  8.16496581, 12.        ]])
+
+    >>> T = fcluster(Z, 3, criterion='distance')
+    >>> T
+    array([1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4], dtype=int32)
+
+    `scipy.cluster.hierarchy.leaders` returns the indices of the nodes
+    in the dendrogram that are the leaders of each flat cluster:
+
+    >>> L, M = leaders(Z, T)
+    >>> L
+    array([16, 17, 18, 19], dtype=int32)
+
+    (remember that indices 0-11 point to the 12 data points in ``X``,
+    whereas indices 12-22 point to the 11 rows of ``Z``)
+
+    `scipy.cluster.hierarchy.leaders` also returns the indices of
+    the flat clusters in ``T``:
+
+    >>> M
+    array([1, 2, 3, 4], dtype=int32)
+
+    Notes
+    -----
+    *Array API support (experimental):* This function returns arrays
+    with data-dependent shape. In JAX, at the moment of writing this makes it
+    impossible to execute it inside `@jax.jit`.
+    """
+    xp = array_namespace(Z, T)
+    Z = _asarray(Z, order='C', dtype=xp.float64, xp=xp)
+    T = _asarray(T, order='C', xp=xp)
+    _is_valid_linkage(Z, throw=True, name='Z', xp=xp)
+
+    if T.dtype != xp.int32:
+        raise TypeError('T must be a 1-D array of dtype int32.')
+
+    if T.shape[0] != Z.shape[0] + 1:
+        raise ValueError('Mismatch: len(T)!=Z.shape[0] + 1.')
+
+    n_obs = Z.shape[0] + 1
+
+    def cy_leaders(Z, T, validate):
+        if validate:
+            _is_valid_linkage(Z, throw=True, name='Z', xp=np)
+        n_clusters = int(xpx.nunique(T))
+        L = np.zeros(n_clusters, dtype=np.int32)
+        M = np.zeros(n_clusters, dtype=np.int32)
+        s = _hierarchy.leaders(Z, T, L, M, n_clusters, n_obs)
+        if s >= 0:
+            raise ValueError('T is not a valid assignment vector. Error found '
+                             f'when examining linkage node {s} (< 2n-1).')
+        return L, M
+
+    return xpx.lazy_apply(cy_leaders, Z, T, validate=is_lazy_array(Z),
+                          shape=((None,), (None, )), dtype=(xp.int32, xp.int32),
+                          as_numpy=True, xp=xp)
diff --git a/.venv/lib/python3.11/site-packages/scipy/cluster/tests/__init__.py b/.venv/lib/python3.11/site-packages/scipy/cluster/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/.venv/lib/python3.11/site-packages/scipy/cluster/tests/hierarchy_test_data.py b/.venv/lib/python3.11/site-packages/scipy/cluster/tests/hierarchy_test_data.py
new file mode 100644
index 0000000000000000000000000000000000000000..7d874ca5eb7141a44559307d1c28dd412171396f
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/cluster/tests/hierarchy_test_data.py
@@ -0,0 +1,145 @@
+from numpy import array
+
+
+Q_X = array([[5.26563660e-01, 3.14160190e-01, 8.00656370e-02],
+             [7.50205180e-01, 4.60299830e-01, 8.98696460e-01],
+             [6.65461230e-01, 6.94011420e-01, 9.10465700e-01],
+             [9.64047590e-01, 1.43082200e-03, 7.39874220e-01],
+             [1.08159060e-01, 5.53028790e-01, 6.63804780e-02],
+             [9.31359130e-01, 8.25424910e-01, 9.52315440e-01],
+             [6.78086960e-01, 3.41903970e-01, 5.61481950e-01],
+             [9.82730940e-01, 7.04605210e-01, 8.70978630e-02],
+             [6.14691610e-01, 4.69989230e-02, 6.02406450e-01],
+             [5.80161260e-01, 9.17354970e-01, 5.88163850e-01],
+             [1.38246310e+00, 1.96358160e+00, 1.94437880e+00],
+             [2.10675860e+00, 1.67148730e+00, 1.34854480e+00],
+             [1.39880070e+00, 1.66142050e+00, 1.32224550e+00],
+             [1.71410460e+00, 1.49176380e+00, 1.45432170e+00],
+             [1.54102340e+00, 1.84374950e+00, 1.64658950e+00],
+             [2.08512480e+00, 1.84524350e+00, 2.17340850e+00],
+             [1.30748740e+00, 1.53801650e+00, 2.16007740e+00],
+             [1.41447700e+00, 1.99329070e+00, 1.99107420e+00],
+             [1.61943490e+00, 1.47703280e+00, 1.89788160e+00],
+             [1.59880600e+00, 1.54988980e+00, 1.57563350e+00],
+             [3.37247380e+00, 2.69635310e+00, 3.39981700e+00],
+             [3.13705120e+00, 3.36528090e+00, 3.06089070e+00],
+             [3.29413250e+00, 3.19619500e+00, 2.90700170e+00],
+             [2.65510510e+00, 3.06785900e+00, 2.97198540e+00],
+             [3.30941040e+00, 2.59283970e+00, 2.57714110e+00],
+             [2.59557220e+00, 3.33477370e+00, 3.08793190e+00],
+             [2.58206180e+00, 3.41615670e+00, 3.26441990e+00],
+             [2.71127000e+00, 2.77032450e+00, 2.63466500e+00],
+             [2.79617850e+00, 3.25473720e+00, 3.41801560e+00],
+             [2.64741750e+00, 2.54538040e+00, 3.25354110e+00]])
+
+ytdist = array([662., 877., 255., 412., 996., 295., 468., 268., 400., 754.,
+                564., 138., 219., 869., 669.])
+
+linkage_ytdist_single = array([[2., 5., 138., 2.],
+                               [3., 4., 219., 2.],
+                               [0., 7., 255., 3.],
+                               [1., 8., 268., 4.],
+                               [6., 9., 295., 6.]])
+
+linkage_ytdist_complete = array([[2., 5., 138., 2.],
+                                 [3., 4., 219., 2.],
+                                 [1., 6., 400., 3.],
+                                 [0., 7., 412., 3.],
+                                 [8., 9., 996., 6.]])
+
+linkage_ytdist_average = array([[2., 5., 138., 2.],
+                                [3., 4., 219., 2.],
+                                [0., 7., 333.5, 3.],
+                                [1., 6., 347.5, 3.],
+                                [8., 9., 680.77777778, 6.]])
+
+linkage_ytdist_weighted = array([[2., 5., 138., 2.],
+                                 [3., 4., 219., 2.],
+                                 [0., 7., 333.5, 3.],
+                                 [1., 6., 347.5, 3.],
+                                 [8., 9., 670.125, 6.]])
+
+# the optimal leaf ordering of linkage_ytdist_single
+linkage_ytdist_single_olo = array([[5., 2., 138., 2.],
+                                   [4., 3., 219., 2.],
+                                   [7., 0., 255., 3.],
+                                   [1., 8., 268., 4.],
+                                   [6., 9., 295., 6.]])
+
+X = array([[1.43054825, -7.5693489],
+           [6.95887839, 6.82293382],
+           [2.87137846, -9.68248579],
+           [7.87974764, -6.05485803],
+           [8.24018364, -6.09495602],
+           [7.39020262, 8.54004355]])
+ 
+linkage_X_centroid = array([[3., 4., 0.36265956, 2.],
+                            [1., 5., 1.77045373, 2.],
+                            [0., 2., 2.55760419, 2.],
+                            [6., 8., 6.43614494, 4.],
+                            [7., 9., 15.17363237, 6.]])
+
+linkage_X_median = array([[3., 4., 0.36265956, 2.],
+                          [1., 5., 1.77045373, 2.],
+                          [0., 2., 2.55760419, 2.],
+                          [6., 8., 6.43614494, 4.],
+                          [7., 9., 15.17363237, 6.]])
+
+linkage_X_ward = array([[3., 4., 0.36265956, 2.],
+                        [1., 5., 1.77045373, 2.],
+                        [0., 2., 2.55760419, 2.],
+                        [6., 8., 9.10208346, 4.],
+                        [7., 9., 24.7784379, 6.]])
+
+# the optimal leaf ordering of linkage_X_ward
+linkage_X_ward_olo = array([[4., 3., 0.36265956, 2.],
+                            [5., 1., 1.77045373, 2.],
+                            [2., 0., 2.55760419, 2.],
+                            [6., 8., 9.10208346, 4.],
+                            [7., 9., 24.7784379, 6.]])
+
+inconsistent_ytdist = {
+    1: array([[138., 0., 1., 0.],
+              [219., 0., 1., 0.],
+              [255., 0., 1., 0.],
+              [268., 0., 1., 0.],
+              [295., 0., 1., 0.]]),
+    2: array([[138., 0., 1., 0.],
+              [219., 0., 1., 0.],
+              [237., 25.45584412, 2., 0.70710678],
+              [261.5, 9.19238816, 2., 0.70710678],
+              [233.66666667, 83.9424406, 3., 0.7306594]]),
+    3: array([[138., 0., 1., 0.],
+              [219., 0., 1., 0.],
+              [237., 25.45584412, 2., 0.70710678],
+              [247.33333333, 25.38372182, 3., 0.81417007],
+              [239., 69.36377537, 4., 0.80733783]]),
+    4: array([[138., 0., 1., 0.],
+              [219., 0., 1., 0.],
+              [237., 25.45584412, 2., 0.70710678],
+              [247.33333333, 25.38372182, 3., 0.81417007],
+              [235., 60.73302232, 5., 0.98793042]])}
+
+fcluster_inconsistent = {
+    0.8: array([6, 2, 2, 4, 6, 2, 3, 7, 3, 5, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 1,
+                1, 1, 1, 1, 1, 1, 1, 1, 1]),
+    1.0: array([6, 2, 2, 4, 6, 2, 3, 7, 3, 5, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 1,
+                1, 1, 1, 1, 1, 1, 1, 1, 1]),
+    2.0: array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                1, 1, 1, 1, 1, 1, 1, 1, 1])}
+
+fcluster_distance = {
+    0.6: array([4, 4, 4, 4, 4, 4, 4, 5, 4, 4, 6, 6, 6, 6, 6, 7, 6, 6, 6, 6, 3,
+                1, 1, 1, 2, 1, 1, 1, 1, 1]),
+    1.0: array([2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1,
+                1, 1, 1, 1, 1, 1, 1, 1, 1]),
+    2.0: array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                1, 1, 1, 1, 1, 1, 1, 1, 1])}
+
+fcluster_maxclust = {
+    8.0: array([5, 5, 5, 5, 5, 5, 5, 6, 5, 5, 7, 7, 7, 7, 7, 8, 7, 7, 7, 7, 4,
+                1, 1, 1, 3, 1, 1, 1, 1, 2]),
+    4.0: array([3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 2,
+                1, 1, 1, 1, 1, 1, 1, 1, 1]),
+    1.0: array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                1, 1, 1, 1, 1, 1, 1, 1, 1])}
diff --git a/.venv/lib/python3.11/site-packages/scipy/cluster/tests/test_disjoint_set.py b/.venv/lib/python3.11/site-packages/scipy/cluster/tests/test_disjoint_set.py
new file mode 100644
index 0000000000000000000000000000000000000000..a73512d35eef168f625a1942a87d248e73a71aa2
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/cluster/tests/test_disjoint_set.py
@@ -0,0 +1,202 @@
+import pytest
+from pytest import raises as assert_raises
+import numpy as np
+from scipy.cluster.hierarchy import DisjointSet
+import string
+
+
+def generate_random_token():
+    k = len(string.ascii_letters)
+    tokens = list(np.arange(k, dtype=int))
+    tokens += list(np.arange(k, dtype=float))
+    tokens += list(string.ascii_letters)
+    tokens += [None for i in range(k)]
+    tokens = np.array(tokens, dtype=object)
+    rng = np.random.RandomState(seed=0)
+
+    while 1:
+        size = rng.randint(1, 3)
+        element = rng.choice(tokens, size)
+        if size == 1:
+            yield element[0]
+        else:
+            yield tuple(element)
+
+
+def get_elements(n):
+    # dict is deterministic without difficulty of comparing numpy ints
+    elements = {}
+    for element in generate_random_token():
+        if element not in elements:
+            elements[element] = len(elements)
+            if len(elements) >= n:
+                break
+    return list(elements.keys())
+
+
+def test_init():
+    n = 10
+    elements = get_elements(n)
+    dis = DisjointSet(elements)
+    assert dis.n_subsets == n
+    assert list(dis) == elements
+
+
+def test_len():
+    n = 10
+    elements = get_elements(n)
+    dis = DisjointSet(elements)
+    assert len(dis) == n
+
+    dis.add("dummy")
+    assert len(dis) == n + 1
+
+
+@pytest.mark.parametrize("n", [10, 100])
+def test_contains(n):
+    elements = get_elements(n)
+    dis = DisjointSet(elements)
+    for x in elements:
+        assert x in dis
+
+    assert "dummy" not in dis
+
+
+@pytest.mark.parametrize("n", [10, 100])
+def test_add(n):
+    elements = get_elements(n)
+    dis1 = DisjointSet(elements)
+
+    dis2 = DisjointSet()
+    for i, x in enumerate(elements):
+        dis2.add(x)
+        assert len(dis2) == i + 1
+
+        # test idempotency by adding element again
+        dis2.add(x)
+        assert len(dis2) == i + 1
+
+    assert list(dis1) == list(dis2)
+
+
+def test_element_not_present():
+    elements = get_elements(n=10)
+    dis = DisjointSet(elements)
+
+    with assert_raises(KeyError):
+        dis["dummy"]
+
+    with assert_raises(KeyError):
+        dis.merge(elements[0], "dummy")
+
+    with assert_raises(KeyError):
+        dis.connected(elements[0], "dummy")
+
+
+@pytest.mark.parametrize("direction", ["forwards", "backwards"])
+@pytest.mark.parametrize("n", [10, 100])
+def test_linear_union_sequence(n, direction):
+    elements = get_elements(n)
+    dis = DisjointSet(elements)
+    assert elements == list(dis)
+
+    indices = list(range(n - 1))
+    if direction == "backwards":
+        indices = indices[::-1]
+
+    for it, i in enumerate(indices):
+        assert not dis.connected(elements[i], elements[i + 1])
+        assert dis.merge(elements[i], elements[i + 1])
+        assert dis.connected(elements[i], elements[i + 1])
+        assert dis.n_subsets == n - 1 - it
+
+    roots = [dis[i] for i in elements]
+    if direction == "forwards":
+        assert all(elements[0] == r for r in roots)
+    else:
+        assert all(elements[-2] == r for r in roots)
+    assert not dis.merge(elements[0], elements[-1])
+
+
+@pytest.mark.parametrize("n", [10, 100])
+def test_self_unions(n):
+    elements = get_elements(n)
+    dis = DisjointSet(elements)
+
+    for x in elements:
+        assert dis.connected(x, x)
+        assert not dis.merge(x, x)
+        assert dis.connected(x, x)
+    assert dis.n_subsets == len(elements)
+
+    assert elements == list(dis)
+    roots = [dis[x] for x in elements]
+    assert elements == roots
+
+
+@pytest.mark.parametrize("order", ["ab", "ba"])
+@pytest.mark.parametrize("n", [10, 100])
+def test_equal_size_ordering(n, order):
+    elements = get_elements(n)
+    dis = DisjointSet(elements)
+
+    rng = np.random.RandomState(seed=0)
+    indices = np.arange(n)
+    rng.shuffle(indices)
+
+    for i in range(0, len(indices), 2):
+        a, b = elements[indices[i]], elements[indices[i + 1]]
+        if order == "ab":
+            assert dis.merge(a, b)
+        else:
+            assert dis.merge(b, a)
+
+        expected = elements[min(indices[i], indices[i + 1])]
+        assert dis[a] == expected
+        assert dis[b] == expected
+
+
+@pytest.mark.parametrize("kmax", [5, 10])
+def test_binary_tree(kmax):
+    n = 2**kmax
+    elements = get_elements(n)
+    dis = DisjointSet(elements)
+    rng = np.random.RandomState(seed=0)
+
+    for k in 2**np.arange(kmax):
+        for i in range(0, n, 2 * k):
+            r1, r2 = rng.randint(0, k, size=2)
+            a, b = elements[i + r1], elements[i + k + r2]
+            assert not dis.connected(a, b)
+            assert dis.merge(a, b)
+            assert dis.connected(a, b)
+
+        assert elements == list(dis)
+        roots = [dis[i] for i in elements]
+        expected_indices = np.arange(n) - np.arange(n) % (2 * k)
+        expected = [elements[i] for i in expected_indices]
+        assert roots == expected
+
+
+@pytest.mark.parametrize("n", [10, 100])
+def test_subsets(n):
+    elements = get_elements(n)
+    dis = DisjointSet(elements)
+
+    rng = np.random.RandomState(seed=0)
+    for i, j in rng.randint(0, n, (n, 2)):
+        x = elements[i]
+        y = elements[j]
+
+        expected = {element for element in dis if {dis[element]} == {dis[x]}}
+        assert dis.subset_size(x) == len(dis.subset(x))
+        assert expected == dis.subset(x)
+
+        expected = {dis[element]: set() for element in dis}
+        for element in dis:
+            expected[dis[element]].add(element)
+        expected = list(expected.values())
+        assert expected == dis.subsets()
+
+        dis.merge(x, y)
+        assert dis.subset(x) == dis.subset(y)
diff --git a/.venv/lib/python3.11/site-packages/scipy/cluster/tests/test_hierarchy.py b/.venv/lib/python3.11/site-packages/scipy/cluster/tests/test_hierarchy.py
new file mode 100644
index 0000000000000000000000000000000000000000..e634bb0d9ee2c889236df3e1e4ecb7b6009c449b
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/cluster/tests/test_hierarchy.py
@@ -0,0 +1,1237 @@
+#
+# Author: Damian Eads
+# Date: April 17, 2008
+#
+# Copyright (C) 2008 Damian Eads
+#
+# 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 numpy.testing import assert_allclose, assert_equal, assert_array_equal, assert_
+import pytest
+from pytest import raises as assert_raises
+
+from scipy.cluster.hierarchy import (
+    ClusterWarning, linkage, from_mlab_linkage, to_mlab_linkage,
+    num_obs_linkage, inconsistent, cophenet, fclusterdata, fcluster,
+    is_isomorphic, single, ward, leaders,
+    correspond, is_monotonic, maxdists, maxinconsts, maxRstat,
+    is_valid_linkage, is_valid_im, to_tree, leaves_list, dendrogram,
+    set_link_color_palette, cut_tree, optimal_leaf_ordering,
+    _order_cluster_tree, _hierarchy, _EUCLIDEAN_METHODS, _LINKAGE_METHODS)
+from scipy.cluster._hierarchy import Heap
+from scipy.spatial.distance import pdist
+from scipy._lib._array_api import (eager_warns, make_xp_test_case,
+                                   xp_assert_close, xp_assert_equal)
+import scipy._lib.array_api_extra as xpx
+
+from threading import Lock
+
+from . import hierarchy_test_data
+
+class eager:
+    # Bypass xpx.testing.lazy_xp_function when calling
+    # these functions from this namespace
+    is_valid_im = is_valid_im
+    is_valid_linkage = is_valid_linkage
+
+
+# Matplotlib is not a scipy dependency but is optionally used in dendrogram, so
+# check if it's available
+try:
+    import matplotlib
+    # and set the backend to be Agg (no gui)
+    matplotlib.use('Agg')
+    # before importing pyplot
+    import matplotlib.pyplot as plt
+    have_matplotlib = True
+except Exception:
+    have_matplotlib = False
+
+skip_xp_backends = pytest.mark.skip_xp_backends
+
+
+@make_xp_test_case(linkage)
+class TestLinkage:
+
+    @skip_xp_backends("jax.numpy", reason="Can't raise inside jax.pure_callback")
+    def test_linkage_non_finite_elements_in_distance_matrix(self, xp):
+        # Tests linkage(Y) where Y contains a non-finite element (e.g. NaN or Inf).
+        # Exception expected.
+        y = xp.asarray([xp.nan] + [0.0]*5)
+        assert_raises(ValueError, linkage, y)
+
+    def test_linkage_empty_distance_matrix(self, xp):
+        # Tests linkage(Y) where Y is a 0x4 linkage matrix. Exception expected.
+        y = xp.zeros((0,))
+        assert_raises(ValueError, linkage, y)
+
+    def test_linkage_tdist(self, xp):
+        for method in ['single', 'complete', 'average', 'weighted']:
+            self.check_linkage_tdist(method, xp)
+
+    def check_linkage_tdist(self, method, xp):
+        # Tests linkage(Y, method) on the tdist data set.
+        Z = linkage(xp.asarray(hierarchy_test_data.ytdist), method)
+        expectedZ = getattr(hierarchy_test_data, 'linkage_ytdist_' + method)
+        xp_assert_close(Z, xp.asarray(expectedZ), atol=1e-10)
+
+    def test_linkage_X(self, xp):
+        for method in ['centroid', 'median', 'ward']:
+            self.check_linkage_q(method, xp)
+
+    def check_linkage_q(self, method, xp):
+        # Tests linkage(Y, method) on the Q data set.
+        Z = linkage(xp.asarray(hierarchy_test_data.X), method)
+        expectedZ = getattr(hierarchy_test_data, 'linkage_X_' + method)
+        xp_assert_close(Z, xp.asarray(expectedZ), atol=1e-06)
+
+        X = xp.asarray(hierarchy_test_data.X)
+        y = pdist(X, metric="euclidean")
+        Z = linkage(y, method)
+        xp_assert_close(Z, xp.asarray(expectedZ), atol=1e-06)
+
+    def test_compare_with_trivial(self, xp):
+        rng = np.random.RandomState(0)
+        n = 20
+        X = rng.rand(n, 2)
+        d = pdist(X)
+
+        for method, code in _LINKAGE_METHODS.items():
+            Z_trivial = _hierarchy.linkage(d, n, code)
+            Z = linkage(xp.asarray(d), method)
+            xp_assert_close(Z, xp.asarray(Z_trivial), rtol=1e-14, atol=1e-15)
+
+    def test_optimal_leaf_ordering(self, xp):
+        Z = linkage(xp.asarray(hierarchy_test_data.ytdist), optimal_ordering=True)
+        expectedZ = getattr(hierarchy_test_data, 'linkage_ytdist_single_olo')
+        xp_assert_close(Z, xp.asarray(expectedZ), atol=1e-10)
+
+    @pytest.mark.parametrize("method,expect", [
+        ('single',   [[0, 1, 1.41421356, 2],
+                      [2, 3, 1.41421356, 3]]),
+        ('complete', [[0, 1, 1.41421356, 2],
+                      [2, 3, 2.82842712, 3]]),
+        ('average',  [[0, 1, 1.41421356, 2],
+                      [2, 3, 2.12132034, 3]]),
+        ('weighted', [[0, 1, 1.41421356, 2],
+                      [2, 3, 2.12132034, 3]]),
+        ('centroid', [[0, 1, 1.41421356, 2],
+                      [2, 3, 2.12132034, 3]]),
+        ('median',   [[0, 1, 1.41421356, 2],
+                      [2, 3, 2.12132034, 3]]),
+        ('ward',     [[0, 1, 1.41421356, 2],
+                      [2, 3, 2.44948974, 3]]),
+    ])
+    def test_linkage_ties(self, method, expect, xp):
+        X = xp.asarray([[-1, -1], [0, 0], [1, 1]])
+        Z = linkage(X, method=method)
+        expect = xp.asarray(expect, dtype=xp.float64)
+        xp_assert_close(Z, expect, atol=1e-06)
+
+    def test_unsupported_uncondensed_distance_matrix_linkage_warning(self, xp):
+        X = xp.asarray([[0, 1], [1, 0]])
+        with eager_warns(ClusterWarning, xp=xp):
+            linkage(X)
+
+    @pytest.mark.parametrize("method", _EUCLIDEAN_METHODS)
+    def test_euclidean_linkage_value_error(self, method, xp):
+        X = xp.asarray([[1, 1], [1, 1]])
+        with pytest.raises(ValueError):
+            linkage(X, method=method, metric='cityblock')
+
+    def test_2x2_linkage(self, xp):
+        Z1 = linkage(xp.asarray([1]), method='single', metric='euclidean')
+        Z2 = linkage(xp.asarray([[0, 1], [0, 0]]), method='single', metric='euclidean')
+        xp_assert_close(Z1, Z2, rtol=1e-15)
+
+    @skip_xp_backends("jax.numpy", reason="Can't raise inside jax.pure_callback")
+    def test_centroid_neg_distance(self, xp):
+        # gh-21011
+        values = xp.asarray([0, 0, -1])
+        with pytest.raises(ValueError):
+            # This is just checking that this doesn't crash
+            linkage(values, method='centroid')
+
+
+@make_xp_test_case(inconsistent)
+class TestInconsistent:
+
+    def test_inconsistent_tdist(self, xp):
+        for depth in hierarchy_test_data.inconsistent_ytdist:
+            self.check_inconsistent_tdist(depth, xp)
+
+    def check_inconsistent_tdist(self, depth, xp):
+        Z = xp.asarray(hierarchy_test_data.linkage_ytdist_single)
+        xp_assert_close(inconsistent(Z, depth),
+                        xp.asarray(hierarchy_test_data.inconsistent_ytdist[depth]))
+
+
+@make_xp_test_case(cophenet)
+class TestCopheneticDistance:
+
+    def test_linkage_cophenet_tdist_Z(self, xp):
+        # Tests cophenet(Z) on tdist data set.
+        expectedM = xp.asarray([268, 295, 255, 255, 295, 295, 268, 268, 295, 295,
+                                295, 138, 219, 295, 295])
+        Z = xp.asarray(hierarchy_test_data.linkage_ytdist_single)
+        M = cophenet(Z)
+        xp_assert_close(M, xp.asarray(expectedM, dtype=xp.float64), atol=1e-10)
+
+    def test_linkage_cophenet_tdist_Z_Y(self, xp):
+        # Tests cophenet(Z, Y) on tdist data set.
+        Z = xp.asarray(hierarchy_test_data.linkage_ytdist_single)
+        (c, M) = cophenet(Z, xp.asarray(hierarchy_test_data.ytdist))
+        expectedM = xp.asarray([268, 295, 255, 255, 295, 295, 268, 268, 295, 295,
+                                295, 138, 219, 295, 295], dtype=xp.float64)
+        expectedc = xp.asarray(0.639931296433393415057366837573, dtype=xp.float64)[()]
+        xp_assert_close(c, expectedc, atol=1e-10)
+        xp_assert_close(M, expectedM, atol=1e-10)
+
+    @skip_xp_backends("jax.numpy", reason="Can't raise inside jax.pure_callback")
+    def test_gh_22183(self, xp):
+        # check for lack of segfault
+        # (out of bounds memory access)
+        # and correct interception of
+        # invalid linkage matrix
+        arr=[[0.0, 1.0, 1.0, 2.0],
+             [2.0, 12.0, 1.0, 3.0],
+             [3.0, 4.0, 1.0, 2.0],
+             [5.0, 14.0, 1.0, 3.0],
+             [6.0, 7.0, 1.0, 2.0],
+             [8.0, 16.0, 1.0, 3.0],
+             [9.0, 10.0, 1.0, 2.0],
+             [11.0, 18.0, 1.0, 3.0],
+             [13.0, 15.0, 2.0, 6.0],
+             [17.0, 20.0, 2.0, 32.0],
+             [19.0, 21.0, 2.0, 12.0]]
+        with pytest.raises(ValueError, match="excessive observations"):
+            cophenet(xp.asarray(arr))
+
+
+@make_xp_test_case(from_mlab_linkage, to_mlab_linkage)
+class TestMLabLinkageConversion:
+
+    def test_mlab_linkage_conversion_empty(self, xp):
+        # Tests from/to_mlab_linkage on empty linkage array.
+        X = xp.asarray([], dtype=xp.float64)
+        xp_assert_equal(from_mlab_linkage(X), X)
+        xp_assert_equal(to_mlab_linkage(X), X)
+
+    def test_mlab_linkage_conversion_single_row(self, xp):
+        # Tests from/to_mlab_linkage on linkage array with single row.
+        Z = xp.asarray([[0., 1., 3., 2.]])
+        Zm = xp.asarray([[1, 2, 3]])
+        xp_assert_close(from_mlab_linkage(Zm), xp.asarray(Z, dtype=xp.float64),
+                        rtol=1e-15)
+        xp_assert_close(to_mlab_linkage(Z), xp.asarray(Zm, dtype=xp.float64),
+                        rtol=1e-15)
+
+    def test_mlab_linkage_conversion_multiple_rows(self, xp):
+        # Tests from/to_mlab_linkage on linkage array with multiple rows.
+        Zm = xp.asarray([[3, 6, 138], [4, 5, 219],
+                         [1, 8, 255], [2, 9, 268], [7, 10, 295]])
+        Z = xp.asarray([[2., 5., 138., 2.],
+                        [3., 4., 219., 2.],
+                        [0., 7., 255., 3.],
+                        [1., 8., 268., 4.],
+                        [6., 9., 295., 6.]],
+                       dtype=xp.float64)
+        xp_assert_close(from_mlab_linkage(Zm), Z, rtol=1e-15)
+        xp_assert_close(to_mlab_linkage(Z), xp.asarray(Zm, dtype=xp.float64),
+                        rtol=1e-15)
+
+
+@make_xp_test_case(fclusterdata)
+class TestFclusterData:
+
+    @make_xp_test_case(is_isomorphic)
+    @pytest.mark.parametrize("criterion,t",
+        [("inconsistent", t) for t in hierarchy_test_data.fcluster_inconsistent]
+        + [("distance", t) for t in hierarchy_test_data.fcluster_distance]
+        + [("maxclust", t) for t in hierarchy_test_data.fcluster_maxclust]
+    )
+    def test_fclusterdata(self, t, criterion, xp):
+        # Tests fclusterdata(X, criterion=criterion, t=t) on a random 3-cluster data set
+        expectedT = xp.asarray(getattr(hierarchy_test_data, 'fcluster_' + criterion)[t])
+        X = xp.asarray(hierarchy_test_data.Q_X)
+        T = fclusterdata(X, criterion=criterion, t=t)
+        assert is_isomorphic(T, expectedT)
+
+
+@make_xp_test_case(fcluster)
+class TestFcluster:
+
+    @make_xp_test_case(single, is_isomorphic)
+    @pytest.mark.parametrize("criterion,t",
+        [("inconsistent", t) for t in hierarchy_test_data.fcluster_inconsistent]
+        + [("distance", t) for t in hierarchy_test_data.fcluster_distance]
+        + [("maxclust", t) for t in hierarchy_test_data.fcluster_maxclust]
+    )
+    def test_fcluster(self, t, criterion, xp):
+        # Tests fcluster(Z, criterion=criterion, t=t) on a random 3-cluster data set.
+        expectedT = xp.asarray(getattr(hierarchy_test_data, 'fcluster_' + criterion)[t])
+        Z = single(xp.asarray(hierarchy_test_data.Q_X))
+        T = fcluster(Z, criterion=criterion, t=t)
+        assert_(is_isomorphic(T, expectedT))
+
+    @make_xp_test_case(single, is_isomorphic, maxdists)
+    @pytest.mark.parametrize("t", hierarchy_test_data.fcluster_distance)
+    def test_fcluster_monocrit(self, t, xp):
+        expectedT = xp.asarray(hierarchy_test_data.fcluster_distance[t])
+        Z = single(xp.asarray(hierarchy_test_data.Q_X))
+        T = fcluster(Z, t, criterion='monocrit', monocrit=maxdists(Z))
+        assert_(is_isomorphic(T, expectedT))
+
+    @make_xp_test_case(single, is_isomorphic, maxdists)
+    @pytest.mark.parametrize("t", hierarchy_test_data.fcluster_maxclust)
+    def test_fcluster_maxclust_monocrit(self, t, xp):
+        expectedT = xp.asarray(hierarchy_test_data.fcluster_maxclust[t])
+        Z = single(xp.asarray(hierarchy_test_data.Q_X))
+        T = fcluster(Z, t, criterion='maxclust_monocrit', monocrit=maxdists(Z))
+        assert_(is_isomorphic(T, expectedT))
+
+    @make_xp_test_case(single)
+    def test_fcluster_maxclust_gh_12651(self, xp):
+        y = xp.asarray([[1], [4], [5]])
+        Z = single(y)
+        assert_array_equal(fcluster(Z, t=1, criterion="maxclust"),
+                           xp.asarray([1, 1, 1]))
+        assert_array_equal(fcluster(Z, t=2, criterion="maxclust"),
+                           xp.asarray([2, 1, 1]))
+        assert_array_equal(fcluster(Z, t=3, criterion="maxclust"),
+                           xp.asarray([1, 2, 3]))
+        assert_array_equal(fcluster(Z, t=5, criterion="maxclust"),
+                           xp.asarray([1, 2, 3]))
+
+
+@make_xp_test_case(leaders)
+class TestLeaders:
+
+    def test_leaders_single(self, xp):
+        # Tests leaders using a flat clustering generated by single linkage.
+        X = hierarchy_test_data.Q_X
+        Y = pdist(X)
+        Z = linkage(Y)
+        T = fcluster(Z, criterion='maxclust', t=3)
+        Z = xp.asarray(Z)
+        T = xp.asarray(T, dtype=xp.int32)
+        L = leaders(Z, T)
+        expect = xp.asarray([53, 55, 56, 2, 3, 1], dtype=xp.int32)
+        xp_assert_close(xp.concat(L), expect, rtol=1e-15)
+
+
+@make_xp_test_case(is_isomorphic)
+class TestIsIsomorphic:
+
+    def test_array_like(self):
+        assert is_isomorphic([1, 1, 1], [2, 2, 2])
+        assert is_isomorphic([], [])
+
+    def test_is_isomorphic_1(self, xp):
+        # Tests is_isomorphic on test case #1 (one flat cluster, different labellings)
+        a = xp.asarray([1, 1, 1])
+        b = xp.asarray([2, 2, 2])
+        assert is_isomorphic(a, b)
+        assert is_isomorphic(b, a)
+
+    def test_is_isomorphic_2(self, xp):
+        # Tests is_isomorphic on test case #2 (two flat clusters, different labelings)
+        a = xp.asarray([1, 7, 1])
+        b = xp.asarray([2, 3, 2])
+        assert is_isomorphic(a, b)
+        assert is_isomorphic(b, a)
+
+    def test_is_isomorphic_3(self, xp):
+        # Tests is_isomorphic on test case #3 (no flat clusters)
+        a = xp.asarray([])
+        b = xp.asarray([])
+        assert is_isomorphic(a, b)
+
+    def test_is_isomorphic_4A(self, xp):
+        # Tests is_isomorphic on test case #4A
+        # (3 flat clusters, different labelings, isomorphic)
+        a = xp.asarray([1, 2, 3])
+        b = xp.asarray([1, 3, 2])
+        assert is_isomorphic(a, b)
+        assert is_isomorphic(b, a)
+
+    def test_is_isomorphic_4B(self, xp):
+        # Tests is_isomorphic on test case #4B
+        # (3 flat clusters, different labelings, nonisomorphic)
+        a = xp.asarray([1, 2, 3, 3])
+        b = xp.asarray([1, 3, 2, 3])
+        assert not is_isomorphic(a, b)
+        assert not is_isomorphic(b, a)
+
+    def test_is_isomorphic_4C(self, xp):
+        # Tests is_isomorphic on test case #4C
+        # (3 flat clusters, different labelings, isomorphic)
+        a = xp.asarray([7, 2, 3])
+        b = xp.asarray([6, 3, 2])
+        assert is_isomorphic(a, b)
+        assert is_isomorphic(b, a)
+
+    @pytest.mark.parametrize("nclusters", [2, 3, 5])
+    def test_is_isomorphic_5(self, nclusters, xp):
+        # Tests is_isomorphic on test case #5 (1000 observations, 2/3/5 random
+        # clusters, random permutation of the labeling).
+        self.is_isomorphic_randperm(1000, nclusters, xp=xp)
+
+    @pytest.mark.parametrize("nclusters", [2, 3, 5])
+    def test_is_isomorphic_6(self, nclusters, xp):
+        # Tests is_isomorphic on test case #5A (1000 observations, 2/3/5 random
+        # clusters, random permutation of the labeling, slightly
+        # nonisomorphic.)
+        self.is_isomorphic_randperm(1000, nclusters, True, 5, xp=xp)
+
+    def test_is_isomorphic_7(self, xp):
+        # Regression test for gh-6271
+        a = xp.asarray([1, 2, 3])
+        b = xp.asarray([1, 1, 1])
+        assert not is_isomorphic(a, b)
+
+    def is_isomorphic_randperm(self, nobs, nclusters, noniso=False, nerrors=0, *, xp):
+        rng = np.random.default_rng()
+        for _ in range(3):
+            a = rng.integers(0, nclusters, size=nobs)
+            p = rng.permutation(nclusters)
+            b = p.take(a.astype(np.intp))
+            if noniso:
+                q = rng.permutation(nobs)
+                b[q[0:nerrors]] += 1
+                b[q[0:nerrors]] %= nclusters
+            a = xp.asarray(a)
+            b = xp.asarray(b)
+            assert is_isomorphic(a, b) == (not noniso)
+            assert is_isomorphic(b, a) == (not noniso)
+
+
+@make_xp_test_case(is_valid_linkage)
+class TestIsValidLinkage:
+
+    @pytest.mark.parametrize("nrow, ncol, valid", [(2, 5, False), (2, 3, False),
+                                                  (1, 4, True), (2, 4, True)])
+    def test_is_valid_linkage_various_size(self, nrow, ncol, valid, xp):
+        # Tests is_valid_linkage(Z) with linkage matrices of various sizes
+        Z = xp.asarray([[0, 1, 3.0, 2, 5],
+                        [3, 2, 4.0, 3, 3]], dtype=xp.float64)
+        Z = Z[:nrow, :ncol]
+        xp_assert_equal(is_valid_linkage(Z), valid, check_namespace=False)
+        if not valid:
+            assert_raises(ValueError, is_valid_linkage, Z, throw=True)
+
+    def test_is_valid_linkage_int_type(self, xp):
+        # Tests is_valid_linkage(Z) with integer type.
+        Z = xp.asarray([[0, 1, 3.0, 2],
+                        [3, 2, 4.0, 3]], dtype=xp.int64)
+        xp_assert_equal(is_valid_linkage(Z), False, check_namespace=False)
+        assert_raises(TypeError, is_valid_linkage, Z, throw=True)
+
+    def test_is_valid_linkage_empty(self, xp):
+        # Tests is_valid_linkage(Z) with empty linkage.
+        Z = xp.zeros((0, 4), dtype=xp.float64)
+        xp_assert_equal(is_valid_linkage(Z), False, check_namespace=False)
+        assert_raises(ValueError, is_valid_linkage, Z, throw=True)
+
+    def test_is_valid_linkage_4_and_up(self, xp):
+        # Tests is_valid_linkage(Z) on linkage on observation sets between
+        # sizes 4 and 15 (step size 3).
+        for i in range(4, 15, 3):
+            y = np.random.rand(i*(i-1)//2)
+            Z = xp.asarray(linkage(y))
+            y = xp.asarray(y)
+            xp_assert_equal(is_valid_linkage(Z), True, check_namespace=False)
+
+    def test_is_valid_linkage_4_and_up_neg_index_left(self, xp):
+        # Tests is_valid_linkage(Z) on linkage on observation sets between
+        # sizes 4 and 15 (step size 3) with negative indices (left).
+        for i in range(4, 15, 3):
+            y = np.random.rand(i*(i-1)//2)
+            Z = xp.asarray(linkage(y))
+            y = xp.asarray(y)
+            Z = xpx.at(Z)[i//2, 0].set(-2)
+            xp_assert_equal(is_valid_linkage(Z), False, check_namespace=False)
+            with pytest.raises(ValueError):
+                eager.is_valid_linkage(Z, throw=True)
+
+    def test_is_valid_linkage_4_and_up_neg_index_right(self, xp):
+        # Tests is_valid_linkage(Z) on linkage on observation sets between
+        # sizes 4 and 15 (step size 3) with negative indices (right).
+        for i in range(4, 15, 3):
+            y = np.random.rand(i*(i-1)//2)
+            Z = xp.asarray(linkage(y))
+            y = xp.asarray(y)
+            Z = xpx.at(Z)[i//2, 1].set(-2)
+            xp_assert_equal(is_valid_linkage(Z), False, check_namespace=False)
+            with pytest.raises(ValueError):
+                eager.is_valid_linkage(Z, throw=True)
+
+    def test_is_valid_linkage_4_and_up_neg_dist(self, xp):
+        # Tests is_valid_linkage(Z) on linkage on observation sets between
+        # sizes 4 and 15 (step size 3) with negative distances.
+        for i in range(4, 15, 3):
+            y = np.random.rand(i*(i-1)//2)
+            Z = xp.asarray(linkage(y))
+            y = xp.asarray(y)
+            Z = xpx.at(Z)[i//2, 2].set(-0.5)
+            xp_assert_equal(is_valid_linkage(Z), False, check_namespace=False)
+            with pytest.raises(ValueError):
+                eager.is_valid_linkage(Z, throw=True)
+
+    def test_is_valid_linkage_4_and_up_neg_counts(self, xp):
+        # Tests is_valid_linkage(Z) on linkage on observation sets between
+        # sizes 4 and 15 (step size 3) with negative counts.
+        for i in range(4, 15, 3):
+            y = np.random.rand(i*(i-1)//2)
+            Z = xp.asarray(linkage(y))
+            y = xp.asarray(y)
+            Z = xpx.at(Z)[i//2, 3].set(-2)
+            xp_assert_equal(is_valid_linkage(Z), False, check_namespace=False)
+            with pytest.raises(ValueError):
+                eager.is_valid_linkage(Z, throw=True)
+
+
+@make_xp_test_case(is_valid_im)
+class TestIsValidInconsistent:
+
+    def test_is_valid_im_int_type(self, xp):
+        # Tests is_valid_im(R) with integer type.
+        R = xp.asarray([[0, 1, 3.0, 2],
+                        [3, 2, 4.0, 3]], dtype=xp.int64)
+        xp_assert_equal(is_valid_im(R), False, check_namespace=False)
+        assert_raises(TypeError, is_valid_im, R, throw=True)
+
+    @pytest.mark.parametrize("nrow, ncol, valid", [(2, 5, False), (2, 3, False),
+                                                  (1, 4, True), (2, 4, True)])
+    def test_is_valid_im_various_size(self, nrow, ncol, valid, xp):
+        # Tests is_valid_im(R) with linkage matrices of various sizes
+        R = xp.asarray([[0, 1, 3.0, 2, 5],
+                        [3, 2, 4.0, 3, 3]], dtype=xp.float64)
+        R = R[:nrow, :ncol]
+        xp_assert_equal(is_valid_im(R), valid, check_namespace=False)
+        if not valid:
+            assert_raises(ValueError, is_valid_im, R, throw=True)
+
+    def test_is_valid_im_empty(self, xp):
+        # Tests is_valid_im(R) with empty inconsistency matrix.
+        R = xp.zeros((0, 4), dtype=xp.float64)
+        xp_assert_equal(is_valid_im(R), False, check_namespace=False)
+        assert_raises(ValueError, is_valid_im, R, throw=True)
+
+    def test_is_valid_im_4_and_up(self, xp):
+        # Tests is_valid_im(R) on im on observation sets between sizes 4 and 15
+        # (step size 3).
+        for i in range(4, 15, 3):
+            y = np.random.rand(i*(i-1)//2)
+            Z = linkage(y)
+            R = inconsistent(Z)
+            R = xp.asarray(R)
+            xp_assert_equal(is_valid_im(R), True, check_namespace=False)
+
+    def test_is_valid_im_4_and_up_neg_index_left(self, xp):
+        # Tests is_valid_im(R) on im on observation sets between sizes 4 and 15
+        # (step size 3) with negative link height means.
+        for i in range(4, 15, 3):
+            y = np.random.rand(i*(i-1)//2)
+            Z = linkage(y)
+            R = inconsistent(Z)
+            R = xpx.at(R)[i//2 , 0].set(-2.0)
+            R = xp.asarray(R)
+            xp_assert_equal(is_valid_im(R), False, check_namespace=False)
+            with pytest.raises(ValueError):
+                eager.is_valid_im(R, throw=True)
+
+    def test_is_valid_im_4_and_up_neg_index_right(self, xp):
+        # Tests is_valid_im(R) on im on observation sets between sizes 4 and 15
+        # (step size 3) with negative link height standard deviations.
+        for i in range(4, 15, 3):
+            y = np.random.rand(i*(i-1)//2)
+            Z = linkage(y)
+            R = inconsistent(Z)
+            R = xpx.at(R)[i//2 , 1].set(-2.0)
+            R = xp.asarray(R)
+            xp_assert_equal(is_valid_im(R), False, check_namespace=False)
+            with pytest.raises(ValueError):
+                eager.is_valid_im(R, throw=True)
+
+    def test_is_valid_im_4_and_up_neg_dist(self, xp):
+        # Tests is_valid_im(R) on im on observation sets between sizes 4 and 15
+        # (step size 3) with negative link counts.
+        for i in range(4, 15, 3):
+            y = np.random.rand(i*(i-1)//2)
+            Z = linkage(y)
+            R = inconsistent(Z)
+            R = xpx.at(R)[i//2, 2].set(-0.5)
+            R = xp.asarray(R)
+            xp_assert_equal(is_valid_im(R), False, check_namespace=False)
+            with pytest.raises(ValueError):
+                eager.is_valid_im(R, throw=True)
+
+
+class TestNumObsLinkage:
+
+    def test_num_obs_linkage_empty(self, xp):
+        # Tests num_obs_linkage(Z) with empty linkage.
+        Z = xp.zeros((0, 4), dtype=xp.float64)
+        assert_raises(ValueError, num_obs_linkage, Z)
+
+    def test_num_obs_linkage_1x4(self, xp):
+        # Tests num_obs_linkage(Z) on linkage over 2 observations.
+        Z = xp.asarray([[0, 1, 3.0, 2]], dtype=xp.float64)
+        assert num_obs_linkage(Z) == 2
+
+    def test_num_obs_linkage_2x4(self, xp):
+        # Tests num_obs_linkage(Z) on linkage over 3 observations.
+        Z = xp.asarray([[0, 1, 3.0, 2],
+                        [3, 2, 4.0, 3]], dtype=xp.float64)
+        assert num_obs_linkage(Z) == 3
+
+    def test_num_obs_linkage_4_and_up(self, xp):
+        # Tests num_obs_linkage(Z) on linkage on observation sets between sizes
+        # 4 and 15 (step size 3).
+        for i in range(4, 15, 3):
+            y = np.random.rand(i*(i-1)//2)
+            Z = xp.asarray(linkage(y))
+            assert num_obs_linkage(Z) == i
+
+    def test_num_obs_linkage_multi_matrix(self, xp):
+        # Tests num_obs_linkage with observation matrices of multiple sizes.
+        for n in range(2, 10):
+            X = np.random.rand(n, 4)
+            Y = pdist(X)
+            Z = xp.asarray(linkage(Y))
+            assert num_obs_linkage(Z) == n
+
+
+@make_xp_test_case(leaves_list, to_tree)
+class TestLeavesList:
+
+    def test_leaves_list_1x4(self, xp):
+        # Tests leaves_list(Z) on a 1x4 linkage.
+        Z = xp.asarray([[0, 1, 3.0, 2]], dtype=xp.float64)
+        to_tree(Z)
+        assert_allclose(leaves_list(Z), [0, 1], rtol=1e-15)
+
+    def test_leaves_list_2x4(self, xp):
+        # Tests leaves_list(Z) on a 2x4 linkage.
+        Z = xp.asarray([[0, 1, 3.0, 2],
+                        [3, 2, 4.0, 3]], dtype=xp.float64)
+        to_tree(Z)
+        assert_allclose(leaves_list(Z), [0, 1, 2], rtol=1e-15)
+
+    @pytest.mark.parametrize("method",
+        ['single', 'complete', 'average', 'weighted', 'centroid', 'median', 'ward'])
+    def test_leaves_list_Q(self, method, xp):
+        # Tests leaves_list(Z) on the Q data set
+        X = hierarchy_test_data.Q_X
+        Z = xp.asarray(linkage(X, method))
+        node = to_tree(Z)
+        assert_allclose(node.pre_order(), leaves_list(Z), rtol=1e-15)
+
+    def test_Q_subtree_pre_order(self, xp):
+        # Tests that pre_order() works when called on sub-trees.
+        X = hierarchy_test_data.Q_X
+        Z = xp.asarray(linkage(X, 'single'))
+        node = to_tree(Z)
+        assert_allclose(node.pre_order(),
+                        (node.get_left().pre_order() + node.get_right().pre_order()),
+                        rtol=1e-15)
+
+
+@make_xp_test_case(correspond)
+class TestCorrespond:
+
+    def test_correspond_empty(self, xp):
+        # Tests correspond(Z, y) with empty linkage and condensed distance matrix.
+        y = xp.zeros((0,), dtype=xp.float64)
+        Z = xp.zeros((0,4), dtype=xp.float64)
+        assert_raises(ValueError, correspond, Z, y)
+
+    def test_correspond_2_and_up(self, xp):
+        # Tests correspond(Z, y) on linkage and CDMs over observation sets of
+        # different sizes.
+        for i in range(2, 4):
+            y = np.random.rand(i*(i-1)//2)
+            Z = xp.asarray(linkage(y))
+            y = xp.asarray(y)
+            assert_(correspond(Z, y))
+        for i in range(4, 15, 3):
+            y = np.random.rand(i*(i-1)//2)
+            Z = xp.asarray(linkage(y))
+            y = xp.asarray(y)
+            assert_(correspond(Z, y))
+
+    def test_correspond_4_and_up(self, xp):
+        # Tests correspond(Z, y) on linkage and CDMs over observation sets of
+        # different sizes. Correspondence should be false.
+        for (i, j) in (list(zip(list(range(2, 4)), list(range(3, 5)))) +
+                       list(zip(list(range(3, 5)), list(range(2, 4))))):
+            y = np.random.rand(i*(i-1)//2)
+            y2 = np.random.rand(j*(j-1)//2)
+            Z = xp.asarray(linkage(y))
+            Z2 = xp.asarray(linkage(y2))
+            y = xp.asarray(y)
+            y2 = xp.asarray(y2)
+            assert not correspond(Z, y2)
+            assert not correspond(Z2, y)
+
+    def test_correspond_4_and_up_2(self, xp):
+        # Tests correspond(Z, y) on linkage and CDMs over observation sets of
+        # different sizes. Correspondence should be false.
+        for (i, j) in (list(zip(list(range(2, 7)), list(range(16, 21)))) +
+                       list(zip(list(range(2, 7)), list(range(16, 21))))):
+            y = np.random.rand(i*(i-1)//2)
+            y2 = np.random.rand(j*(j-1)//2)
+            Z = xp.asarray(linkage(y))
+            Z2 = xp.asarray(linkage(y2))
+            y = xp.asarray(y)
+            y2 = xp.asarray(y2)
+            assert not correspond(Z, y2)
+            assert not correspond(Z2, y)
+
+
+@make_xp_test_case(is_monotonic)
+class TestIsMonotonic:
+
+    def test_is_monotonic_empty(self, xp):
+        # Tests is_monotonic(Z) on an empty linkage.
+        Z = xp.zeros((0, 4), dtype=xp.float64)
+        assert_raises(ValueError, is_monotonic, Z)
+
+    def test_is_monotonic_1x4(self, xp):
+        # Tests is_monotonic(Z) on 1x4 linkage. Expecting True.
+        Z = xp.asarray([[0, 1, 0.3, 2]], dtype=xp.float64)
+        assert is_monotonic(Z)
+
+    def test_is_monotonic_2x4_T(self, xp):
+        # Tests is_monotonic(Z) on 2x4 linkage. Expecting True.
+        Z = xp.asarray([[0, 1, 0.3, 2],
+                        [2, 3, 0.4, 3]], dtype=xp.float64)
+        assert is_monotonic(Z)
+
+    def test_is_monotonic_2x4_F(self, xp):
+        # Tests is_monotonic(Z) on 2x4 linkage. Expecting False.
+        Z = xp.asarray([[0, 1, 0.4, 2],
+                        [2, 3, 0.3, 3]], dtype=xp.float64)
+        assert not is_monotonic(Z)
+
+    def test_is_monotonic_3x4_T(self, xp):
+        # Tests is_monotonic(Z) on 3x4 linkage. Expecting True.
+        Z = xp.asarray([[0, 1, 0.3, 2],
+                        [2, 3, 0.4, 2],
+                        [4, 5, 0.6, 4]], dtype=xp.float64)
+        assert is_monotonic(Z)
+
+    def test_is_monotonic_3x4_F1(self, xp):
+        # Tests is_monotonic(Z) on 3x4 linkage (case 1). Expecting False.
+        Z = xp.asarray([[0, 1, 0.3, 2],
+                        [2, 3, 0.2, 2],
+                        [4, 5, 0.6, 4]], dtype=xp.float64)
+        assert not is_monotonic(Z)
+
+    def test_is_monotonic_3x4_F2(self, xp):
+        # Tests is_monotonic(Z) on 3x4 linkage (case 2). Expecting False.
+        Z = xp.asarray([[0, 1, 0.8, 2],
+                        [2, 3, 0.4, 2],
+                        [4, 5, 0.6, 4]], dtype=xp.float64)
+        assert not is_monotonic(Z)
+
+    def test_is_monotonic_3x4_F3(self, xp):
+        # Tests is_monotonic(Z) on 3x4 linkage (case 3). Expecting False
+        Z = xp.asarray([[0, 1, 0.3, 2],
+                        [2, 3, 0.4, 2],
+                        [4, 5, 0.2, 4]], dtype=xp.float64)
+        assert not is_monotonic(Z)
+
+    def test_is_monotonic_tdist_linkage1(self, xp):
+        # Tests is_monotonic(Z) on clustering generated by single linkage on
+        # tdist data set. Expecting True.
+        Z = xp.asarray(linkage(hierarchy_test_data.ytdist, 'single'))
+        assert is_monotonic(Z)
+
+    def test_is_monotonic_tdist_linkage2(self, xp):
+        # Tests is_monotonic(Z) on clustering generated by single linkage on
+        # tdist data set. Perturbing. Expecting False.
+        Z = xp.asarray(linkage(hierarchy_test_data.ytdist, 'single'))
+        Z = xpx.at(Z)[2, 2].set(0.0)
+        assert not is_monotonic(Z)
+
+    def test_is_monotonic_Q_linkage(self, xp):
+        # Tests is_monotonic(Z) on clustering generated by single linkage on
+        # Q data set. Expecting True.
+        X = hierarchy_test_data.Q_X
+        Z = xp.asarray(linkage(X, 'single'))
+        assert is_monotonic(Z)
+
+
+@make_xp_test_case(maxdists)
+class TestMaxDists:
+
+    def test_maxdists_empty_linkage(self, xp):
+        # Tests maxdists(Z) on empty linkage. Expecting exception.
+        Z = xp.zeros((0, 4), dtype=xp.float64)
+        assert_raises(ValueError, maxdists, Z)
+
+    def test_maxdists_one_cluster_linkage(self, xp):
+        # Tests maxdists(Z) on linkage with one cluster.
+        Z = xp.asarray([[0, 1, 0.3, 4]], dtype=xp.float64)
+        MD = maxdists(Z)
+        expectedMD = calculate_maximum_distances(Z, xp)
+        xp_assert_close(MD, expectedMD, atol=1e-15)
+
+    @pytest.mark.parametrize(
+        "method", ['single', 'complete', 'ward', 'centroid', 'median'])
+    def test_maxdists_Q_linkage(self, method, xp):
+        # Tests maxdists(Z) on the Q data set
+        X = hierarchy_test_data.Q_X
+        Z = xp.asarray(linkage(X, method))
+        MD = maxdists(Z)
+        expectedMD = calculate_maximum_distances(Z, xp)
+        xp_assert_close(MD, expectedMD, atol=1e-15)
+
+
+@make_xp_test_case(maxinconsts)
+class TestMaxInconsts:
+
+    def test_maxinconsts_empty_linkage(self, xp):
+        # Tests maxinconsts(Z, R) on empty linkage. Expecting exception.
+        Z = xp.zeros((0, 4), dtype=xp.float64)
+        R = xp.zeros((0, 4), dtype=xp.float64)
+        assert_raises(ValueError, maxinconsts, Z, R)
+
+    def test_maxinconsts_difrow_linkage(self, xp):
+        # Tests maxinconsts(Z, R) on linkage and inconsistency matrices with
+        # different numbers of clusters. Expecting exception.
+        Z = xp.asarray([[0, 1, 0.3, 4]], dtype=xp.float64)
+        R = np.random.rand(2, 4)
+        R = xp.asarray(R)
+        assert_raises(ValueError, maxinconsts, Z, R)
+
+    def test_maxinconsts_one_cluster_linkage(self, xp):
+        # Tests maxinconsts(Z, R) on linkage with one cluster.
+        Z = xp.asarray([[0, 1, 0.3, 4]], dtype=xp.float64)
+        R = xp.asarray([[0, 0, 0, 0.3]], dtype=xp.float64)
+        MD = maxinconsts(Z, R)
+        expectedMD = calculate_maximum_inconsistencies(Z, R, xp=xp)
+        xp_assert_close(MD, expectedMD, atol=1e-15)
+
+    @pytest.mark.parametrize(
+        "method", ['single', 'complete', 'ward', 'centroid', 'median'])
+    def test_maxinconsts_Q_linkage(self, method, xp):
+        # Tests maxinconsts(Z, R) on the Q data set
+        X = hierarchy_test_data.Q_X
+        Z = linkage(X, method)
+        R = xp.asarray(inconsistent(Z))
+        Z = xp.asarray(Z)
+        MD = maxinconsts(Z, R)
+        expectedMD = calculate_maximum_inconsistencies(Z, R, xp=xp)
+        xp_assert_close(MD, expectedMD, atol=1e-15)
+
+
+@make_xp_test_case(maxRstat)
+class TestMaxRStat:
+
+    def test_maxRstat_invalid_index(self, xp):
+        # Tests maxRstat(Z, R, i). Expecting exception.
+        Z = xp.asarray([[0, 1, 0.3, 4]], dtype=xp.float64)
+        R = xp.asarray([[0, 0, 0, 0.3]], dtype=xp.float64)
+        with pytest.raises(TypeError):
+            maxRstat(Z, R, 3.3)
+        with pytest.raises(ValueError):
+            maxRstat(Z, R, -1)
+        with pytest.raises(ValueError):
+            maxRstat(Z, R, 4)
+
+    @pytest.mark.parametrize("i", range(4))
+    def test_maxRstat_empty_linkage(self, i, xp):
+        # Tests maxRstat(Z, R, i) on empty linkage. Expecting exception.
+        Z = xp.zeros((0, 4), dtype=xp.float64)
+        R = xp.zeros((0, 4), dtype=xp.float64)
+        assert_raises(ValueError, maxRstat, Z, R, i)
+
+    @pytest.mark.parametrize("i", range(4))
+    def test_maxRstat_difrow_linkage(self, i, xp):
+        # Tests maxRstat(Z, R, i) on linkage and inconsistency matrices with
+        # different numbers of clusters. Expecting exception.
+        Z = xp.asarray([[0, 1, 0.3, 4]], dtype=xp.float64)
+        R = np.random.rand(2, 4)
+        R = xp.asarray(R)
+        assert_raises(ValueError, maxRstat, Z, R, i)
+
+    def test_maxRstat_one_cluster_linkage(self, xp):
+        # Tests maxRstat(Z, R, i) on linkage with one cluster.
+        Z = xp.asarray([[0, 1, 0.3, 4]], dtype=xp.float64)
+        R = xp.asarray([[0, 0, 0, 0.3]], dtype=xp.float64)
+        MD = maxRstat(Z, R, 1)
+        expectedMD = calculate_maximum_inconsistencies(Z, R, 1, xp)
+        xp_assert_close(MD, expectedMD, atol=1e-15)
+
+    @pytest.mark.parametrize(
+        "method", ['single', 'complete', 'ward', 'centroid', 'median'])
+    def test_maxRstat_Q_linkage(self, method, xp):
+        # Tests maxRstat(Z, R, 1) on the Q data set
+        X = hierarchy_test_data.Q_X
+        Z = linkage(X, method)
+        R = xp.asarray(inconsistent(Z))
+        Z = xp.asarray(Z)
+        MD = maxRstat(Z, R, 1)
+        expectedMD = calculate_maximum_inconsistencies(Z, R, 1, xp)
+        xp_assert_close(MD, expectedMD, atol=1e-15)
+
+
+@make_xp_test_case(dendrogram)
+class TestDendrogram:
+
+    def test_dendrogram_single_linkage_tdist(self, xp):
+        # Tests dendrogram calculation on single linkage of the tdist data set.
+        Z = xp.asarray(linkage(hierarchy_test_data.ytdist, 'single'))
+        R = dendrogram(Z, no_plot=True)
+        leaves = R["leaves"]
+        assert_equal(leaves, [2, 5, 1, 0, 3, 4])
+
+    def test_valid_orientation(self, xp):
+        Z = xp.asarray(linkage(hierarchy_test_data.ytdist, 'single'))
+        assert_raises(ValueError, dendrogram, Z, orientation="foo")
+
+    def test_labels_as_array_or_list(self, xp):
+        # test for gh-12418
+        Z = xp.asarray(linkage(hierarchy_test_data.ytdist, 'single'))
+        labels = [1, 3, 2, 6, 4, 5]
+        result1 = dendrogram(Z, labels=xp.asarray(labels), no_plot=True)
+        result2 = dendrogram(Z, labels=labels, no_plot=True)
+        assert result1 == result2
+
+    @pytest.mark.skipif(not have_matplotlib, reason="no matplotlib")
+    def test_valid_label_size(self, xp):
+        link = xp.asarray([
+            [0, 1, 1.0, 4],
+            [2, 3, 1.0, 5],
+            [4, 5, 2.0, 6],
+        ])
+        plt.figure()
+        with pytest.raises(ValueError) as exc_info:
+            dendrogram(link, labels=list(range(100)))
+        assert "Dimensions of Z and labels must be consistent."\
+               in str(exc_info.value)
+
+        with pytest.raises(
+                ValueError,
+                match="Dimensions of Z and labels must be consistent."):
+            dendrogram(link, labels=[])
+
+        plt.close()
+
+    @skip_xp_backends('torch',
+         reason='MPL 3.9.2 & torch DeprecationWarning from __array_wrap__'
+                ' and NumPy 2.0'
+    )
+    @skip_xp_backends('dask.array',
+         reason='dask.array has bad interaction with matplotlib'
+    )
+    @pytest.mark.skipif(not have_matplotlib, reason="no matplotlib")
+    @pytest.mark.parametrize("orientation", ['top', 'bottom', 'left', 'right'])
+    def test_dendrogram_plot(self, orientation, xp):
+        # Tests dendrogram plotting.
+        Z = xp.asarray(linkage(hierarchy_test_data.ytdist, 'single'))
+        expected = {'color_list': ['C1', 'C0', 'C0', 'C0', 'C0'],
+                    'dcoord': [[0.0, 138.0, 138.0, 0.0],
+                               [0.0, 219.0, 219.0, 0.0],
+                               [0.0, 255.0, 255.0, 219.0],
+                               [0.0, 268.0, 268.0, 255.0],
+                               [138.0, 295.0, 295.0, 268.0]],
+                    'icoord': [[5.0, 5.0, 15.0, 15.0],
+                               [45.0, 45.0, 55.0, 55.0],
+                               [35.0, 35.0, 50.0, 50.0],
+                               [25.0, 25.0, 42.5, 42.5],
+                               [10.0, 10.0, 33.75, 33.75]],
+                    'ivl': ['2', '5', '1', '0', '3', '4'],
+                    'leaves': [2, 5, 1, 0, 3, 4],
+                    'leaves_color_list': ['C1', 'C1', 'C0', 'C0', 'C0', 'C0'],
+                    }
+
+        fig = plt.figure()
+        ax = fig.add_subplot(221)
+
+        # test that dendrogram accepts ax keyword
+        R1 = dendrogram(Z, ax=ax, orientation=orientation)
+        R1['dcoord'] = np.asarray(R1['dcoord'])
+        assert_equal(R1, expected)
+
+        # test that dendrogram accepts and handle the leaf_font_size and
+        # leaf_rotation keywords
+        dendrogram(Z, ax=ax, orientation=orientation,
+                   leaf_font_size=20, leaf_rotation=90)
+        testlabel = (
+            ax.get_xticklabels()[0]
+            if orientation in ['top', 'bottom']
+            else ax.get_yticklabels()[0]
+        )
+        assert_equal(testlabel.get_rotation(), 90)
+        assert_equal(testlabel.get_size(), 20)
+        dendrogram(Z, ax=ax, orientation=orientation,
+                   leaf_rotation=90)
+        testlabel = (
+            ax.get_xticklabels()[0]
+            if orientation in ['top', 'bottom']
+            else ax.get_yticklabels()[0]
+        )
+        assert_equal(testlabel.get_rotation(), 90)
+        dendrogram(Z, ax=ax, orientation=orientation,
+                   leaf_font_size=20)
+        testlabel = (
+            ax.get_xticklabels()[0]
+            if orientation in ['top', 'bottom']
+            else ax.get_yticklabels()[0]
+        )
+        assert_equal(testlabel.get_size(), 20)
+        plt.close()
+
+        # test plotting to gca (will import pylab)
+        R2 = dendrogram(Z, orientation=orientation)
+        plt.close()
+        R2['dcoord'] = np.asarray(R2['dcoord'])
+        assert_equal(R2, expected)
+
+    @skip_xp_backends('torch',
+          reason='MPL 3.9.2 & torch DeprecationWarning from __array_wrap__'
+                 ' and NumPy 2.0'
+     )
+    @skip_xp_backends('dask.array',
+         reason='dask.array has bad interaction with matplotlib'
+    )
+    @pytest.mark.skipif(not have_matplotlib, reason="no matplotlib")
+    def test_dendrogram_truncate_mode(self, xp):
+        Z = xp.asarray(linkage(hierarchy_test_data.ytdist, 'single'))
+
+        R = dendrogram(Z, 2, 'lastp', show_contracted=True)
+        plt.close()
+        R['dcoord'] = np.asarray(R['dcoord'])
+        assert_equal(R, {'color_list': ['C0'],
+                         'dcoord': [[0.0, 295.0, 295.0, 0.0]],
+                         'icoord': [[5.0, 5.0, 15.0, 15.0]],
+                         'ivl': ['(2)', '(4)'],
+                         'leaves': [6, 9],
+                         'leaves_color_list': ['C0', 'C0'],
+                         })
+
+        R = dendrogram(Z, 2, 'mtica', show_contracted=True)
+        plt.close()
+        R['dcoord'] = np.asarray(R['dcoord'])
+        assert_equal(R, {'color_list': ['C1', 'C0', 'C0', 'C0'],
+                         'dcoord': [[0.0, 138.0, 138.0, 0.0],
+                                    [0.0, 255.0, 255.0, 0.0],
+                                    [0.0, 268.0, 268.0, 255.0],
+                                    [138.0, 295.0, 295.0, 268.0]],
+                         'icoord': [[5.0, 5.0, 15.0, 15.0],
+                                    [35.0, 35.0, 45.0, 45.0],
+                                    [25.0, 25.0, 40.0, 40.0],
+                                    [10.0, 10.0, 32.5, 32.5]],
+                         'ivl': ['2', '5', '1', '0', '(2)'],
+                         'leaves': [2, 5, 1, 0, 7],
+                         'leaves_color_list': ['C1', 'C1', 'C0', 'C0', 'C0'],
+                         })
+
+    @pytest.fixture
+    def dendrogram_lock(self):
+        return Lock()
+
+    def test_dendrogram_colors(self, xp, dendrogram_lock):
+        # Tests dendrogram plots with alternate colors
+        Z = xp.asarray(linkage(hierarchy_test_data.ytdist, 'single'))
+
+        with dendrogram_lock:
+            # Global color palette might be changed concurrently
+            set_link_color_palette(['c', 'm', 'y', 'k'])
+            R = dendrogram(Z, no_plot=True,
+                           above_threshold_color='g', color_threshold=250)
+            set_link_color_palette(['g', 'r', 'c', 'm', 'y', 'k'])
+
+            color_list = R['color_list']
+            assert_equal(color_list, ['c', 'm', 'g', 'g', 'g'])
+
+            # reset color palette (global list)
+            set_link_color_palette(None)
+
+    def test_dendrogram_leaf_colors_zero_dist(self, xp):
+        # tests that the colors of leafs are correct for tree
+        # with two identical points
+        X = np.asarray([[1, 0, 0],
+                        [0, 0, 1],
+                        [0, 2, 0],
+                        [0, 0, 1],
+                        [0, 1, 0],
+                        [0, 1, 0]])
+        Z = xp.asarray(linkage(X, "single"))
+        d = dendrogram(Z, no_plot=True)
+        exp_colors = ['C0', 'C1', 'C1', 'C0', 'C2', 'C2']
+        colors = d["leaves_color_list"]
+        assert_equal(colors, exp_colors)
+
+    def test_dendrogram_leaf_colors(self, xp):
+        # tests that the colors are correct for a tree
+        # with two near points ((0, 0, 1.1) and (0, 0, 1))
+        X = np.asarray([[1, 0, 0],
+                        [0, 0, 1.1],
+                        [0, 2, 0],
+                        [0, 0, 1],
+                        [0, 1, 0],
+                        [0, 1, 0]])
+        Z = xp.asarray(linkage(X, "single"))
+        d = dendrogram(Z, no_plot=True)
+        exp_colors = ['C0', 'C1', 'C1', 'C0', 'C2', 'C2']
+        colors = d["leaves_color_list"]
+        assert_equal(colors, exp_colors)
+
+
+def calculate_maximum_distances(Z, xp):
+    # Used for testing correctness of maxdists.
+    n = Z.shape[0] + 1
+    B = xp.zeros((n-1,), dtype=Z.dtype)
+    for i in range(0, n - 1):
+        q = xp.zeros((3,))
+        left = Z[i, 0]
+        right = Z[i, 1]
+        if left >= n:
+            b_left = B[xp.asarray(left, dtype=xp.int64) - n]
+            q = xpx.at(q, 0).set(b_left)
+        if right >= n:
+            b_right = B[xp.asarray(right, dtype=xp.int64) - n]
+            q = xpx.at(q, 1).set(b_right)
+        q = xpx.at(q, 2).set(Z[i, 2])
+        B = xpx.at(B, i).set(xp.max(q))
+    return B
+
+
+def calculate_maximum_inconsistencies(Z, R, k=3, xp=np):
+    # Used for testing correctness of maxinconsts.
+    n = Z.shape[0] + 1
+    dtype = xp.result_type(Z, R)
+    B = xp.zeros((n-1,), dtype=dtype)
+    for i in range(0, n - 1):
+        q = xp.zeros((3,))
+        left = Z[i, 0]
+        right = Z[i, 1]
+        if left >= n:
+            b_left = B[xp.asarray(left, dtype=xp.int64) - n]
+            q = xpx.at(q, 0).set(b_left)
+        if right >= n:
+            b_right = B[xp.asarray(right, dtype=xp.int64) - n]
+            q = xpx.at(q, 1).set(b_right)
+        q = xpx.at(q, 2).set(R[i, k])
+        B = xpx.at(B, i).set(xp.max(q))
+    return B
+
+
+@make_xp_test_case(to_tree)
+def test_node_compare(xp):
+    np.random.seed(23)
+    nobs = 50
+    X = np.random.randn(nobs, 4)
+    Z = xp.asarray(ward(X))
+    tree = to_tree(Z)
+    assert_(tree > tree.get_left())
+    assert_(tree.get_right() > tree.get_left())
+    assert_(tree.get_right() == tree.get_right())
+    assert_(tree.get_right() != tree.get_left())
+
+
+@make_xp_test_case(cut_tree)
+def test_cut_tree(xp):
+    np.random.seed(23)
+    nobs = 50
+    X = np.random.randn(nobs, 4)
+    Z = xp.asarray(ward(X))
+    cutree = cut_tree(Z)
+
+    # cutree.dtype varies between int32 and int64 over platforms
+    xp_assert_close(cutree[:, 0], xp.arange(nobs), rtol=1e-15, check_dtype=False)
+    xp_assert_close(cutree[:, -1], xp.zeros(nobs), rtol=1e-15, check_dtype=False)
+    assert_equal(np.asarray(cutree).max(0), np.arange(nobs - 1, -1, -1))
+
+    xp_assert_close(cutree[:, [-5]], cut_tree(Z, n_clusters=5), rtol=1e-15)
+    xp_assert_close(cutree[:, [-5, -10]], cut_tree(Z, n_clusters=[5, 10]), rtol=1e-15)
+    xp_assert_close(cutree[:, [-10, -5]], cut_tree(Z, n_clusters=[10, 5]), rtol=1e-15)
+
+    nodes = _order_cluster_tree(Z)
+    heights = xp.asarray([node.dist for node in nodes])
+
+    xp_assert_close(cutree[:, np.searchsorted(heights, [5])],
+                    cut_tree(Z, height=5), rtol=1e-15)
+    xp_assert_close(cutree[:, np.searchsorted(heights, [5, 10])],
+                    cut_tree(Z, height=[5, 10]), rtol=1e-15)
+    xp_assert_close(cutree[:, np.searchsorted(heights, [10, 5])],
+                    cut_tree(Z, height=[10, 5]), rtol=1e-15)
+
+
+@make_xp_test_case(optimal_leaf_ordering)
+def test_optimal_leaf_ordering(xp):
+    # test with the distance vector y
+    Z = optimal_leaf_ordering(xp.asarray(linkage(hierarchy_test_data.ytdist)),
+                              xp.asarray(hierarchy_test_data.ytdist))
+    expectedZ = hierarchy_test_data.linkage_ytdist_single_olo
+    xp_assert_close(Z, xp.asarray(expectedZ), atol=1e-10)
+
+    # test with the observation matrix X
+    Z = optimal_leaf_ordering(xp.asarray(linkage(hierarchy_test_data.X, 'ward')),
+                              xp.asarray(hierarchy_test_data.X))
+    expectedZ = hierarchy_test_data.linkage_X_ward_olo
+    xp_assert_close(Z, xp.asarray(expectedZ), atol=1e-06)
+
+
+@skip_xp_backends(np_only=True, reason='`Heap` only supports NumPy backend')
+def test_Heap(xp):
+    values = xp.asarray([2, -1, 0, -1.5, 3])
+    heap = Heap(values)
+
+    pair = heap.get_min()
+    assert_equal(pair['key'], 3)
+    assert_equal(pair['value'], -1.5)
+
+    heap.remove_min()
+    pair = heap.get_min()
+    assert_equal(pair['key'], 1)
+    assert_equal(pair['value'], -1)
+
+    heap.change_value(1, 2.5)
+    pair = heap.get_min()
+    assert_equal(pair['key'], 2)
+    assert_equal(pair['value'], 0)
+
+    heap.remove_min()
+    heap.remove_min()
+
+    heap.change_value(1, 10)
+    pair = heap.get_min()
+    assert_equal(pair['key'], 4)
+    assert_equal(pair['value'], 3)
+
+    heap.remove_min()
+    pair = heap.get_min()
+    assert_equal(pair['key'], 1)
+    assert_equal(pair['value'], 10)
diff --git a/.venv/lib/python3.11/site-packages/scipy/cluster/tests/test_vq.py b/.venv/lib/python3.11/site-packages/scipy/cluster/tests/test_vq.py
new file mode 100644
index 0000000000000000000000000000000000000000..cd285d96ed0badb8aa42d7c68ae04998b717c4a3
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/cluster/tests/test_vq.py
@@ -0,0 +1,437 @@
+import math
+import sys
+import warnings
+from copy import deepcopy
+from threading import Lock
+
+import numpy as np
+from numpy.testing import assert_array_equal
+import pytest
+from pytest import raises as assert_raises
+
+from scipy.cluster.vq import (kmeans, kmeans2, py_vq, vq, whiten,
+                              ClusterError, _krandinit)
+from scipy.cluster import _vq
+from scipy.sparse._sputils import matrix
+
+from scipy._lib import array_api_extra as xpx
+from scipy._lib._array_api import (
+    SCIPY_ARRAY_API, eager_warns, is_lazy_array, make_xp_test_case,
+    xp_copy, xp_assert_close, xp_assert_equal
+)
+
+xfail_xp_backends = pytest.mark.xfail_xp_backends
+skip_xp_backends = pytest.mark.skip_xp_backends
+
+TESTDATA_2D = np.array([
+    -2.2, 1.17, -1.63, 1.69, -2.04, 4.38, -3.09, 0.95, -1.7, 4.79, -1.68, 0.68,
+    -2.26, 3.34, -2.29, 2.55, -1.72, -0.72, -1.99, 2.34, -2.75, 3.43, -2.45,
+    2.41, -4.26, 3.65, -1.57, 1.87, -1.96, 4.03, -3.01, 3.86, -2.53, 1.28,
+    -4.0, 3.95, -1.62, 1.25, -3.42, 3.17, -1.17, 0.12, -3.03, -0.27, -2.07,
+    -0.55, -1.17, 1.34, -2.82, 3.08, -2.44, 0.24, -1.71, 2.48, -5.23, 4.29,
+    -2.08, 3.69, -1.89, 3.62, -2.09, 0.26, -0.92, 1.07, -2.25, 0.88, -2.25,
+    2.02, -4.31, 3.86, -2.03, 3.42, -2.76, 0.3, -2.48, -0.29, -3.42, 3.21,
+    -2.3, 1.73, -2.84, 0.69, -1.81, 2.48, -5.24, 4.52, -2.8, 1.31, -1.67,
+    -2.34, -1.18, 2.17, -2.17, 2.82, -1.85, 2.25, -2.45, 1.86, -6.79, 3.94,
+    -2.33, 1.89, -1.55, 2.08, -1.36, 0.93, -2.51, 2.74, -2.39, 3.92, -3.33,
+    2.99, -2.06, -0.9, -2.83, 3.35, -2.59, 3.05, -2.36, 1.85, -1.69, 1.8,
+    -1.39, 0.66, -2.06, 0.38, -1.47, 0.44, -4.68, 3.77, -5.58, 3.44, -2.29,
+    2.24, -1.04, -0.38, -1.85, 4.23, -2.88, 0.73, -2.59, 1.39, -1.34, 1.75,
+    -1.95, 1.3, -2.45, 3.09, -1.99, 3.41, -5.55, 5.21, -1.73, 2.52, -2.17,
+    0.85, -2.06, 0.49, -2.54, 2.07, -2.03, 1.3, -3.23, 3.09, -1.55, 1.44,
+    -0.81, 1.1, -2.99, 2.92, -1.59, 2.18, -2.45, -0.73, -3.12, -1.3, -2.83,
+    0.2, -2.77, 3.24, -1.98, 1.6, -4.59, 3.39, -4.85, 3.75, -2.25, 1.71, -3.28,
+    3.38, -1.74, 0.88, -2.41, 1.92, -2.24, 1.19, -2.48, 1.06, -1.68, -0.62,
+    -1.3, 0.39, -1.78, 2.35, -3.54, 2.44, -1.32, 0.66, -2.38, 2.76, -2.35,
+    3.95, -1.86, 4.32, -2.01, -1.23, -1.79, 2.76, -2.13, -0.13, -5.25, 3.84,
+    -2.24, 1.59, -4.85, 2.96, -2.41, 0.01, -0.43, 0.13, -3.92, 2.91, -1.75,
+    -0.53, -1.69, 1.69, -1.09, 0.15, -2.11, 2.17, -1.53, 1.22, -2.1, -0.86,
+    -2.56, 2.28, -3.02, 3.33, -1.12, 3.86, -2.18, -1.19, -3.03, 0.79, -0.83,
+    0.97, -3.19, 1.45, -1.34, 1.28, -2.52, 4.22, -4.53, 3.22, -1.97, 1.75,
+    -2.36, 3.19, -0.83, 1.53, -1.59, 1.86, -2.17, 2.3, -1.63, 2.71, -2.03,
+    3.75, -2.57, -0.6, -1.47, 1.33, -1.95, 0.7, -1.65, 1.27, -1.42, 1.09, -3.0,
+    3.87, -2.51, 3.06, -2.6, 0.74, -1.08, -0.03, -2.44, 1.31, -2.65, 2.99,
+    -1.84, 1.65, -4.76, 3.75, -2.07, 3.98, -2.4, 2.67, -2.21, 1.49, -1.21,
+    1.22, -5.29, 2.38, -2.85, 2.28, -5.6, 3.78, -2.7, 0.8, -1.81, 3.5, -3.75,
+    4.17, -1.29, 2.99, -5.92, 3.43, -1.83, 1.23, -1.24, -1.04, -2.56, 2.37,
+    -3.26, 0.39, -4.63, 2.51, -4.52, 3.04, -1.7, 0.36, -1.41, 0.04, -2.1, 1.0,
+    -1.87, 3.78, -4.32, 3.59, -2.24, 1.38, -1.99, -0.22, -1.87, 1.95, -0.84,
+    2.17, -5.38, 3.56, -1.27, 2.9, -1.79, 3.31, -5.47, 3.85, -1.44, 3.69,
+    -2.02, 0.37, -1.29, 0.33, -2.34, 2.56, -1.74, -1.27, -1.97, 1.22, -2.51,
+    -0.16, -1.64, -0.96, -2.99, 1.4, -1.53, 3.31, -2.24, 0.45, -2.46, 1.71,
+    -2.88, 1.56, -1.63, 1.46, -1.41, 0.68, -1.96, 2.76, -1.61,
+    2.11]).reshape((200, 2))
+
+
+# Global data
+X = np.array([[3.0, 3], [4, 3], [4, 2],
+              [9, 2], [5, 1], [6, 2], [9, 4],
+              [5, 2], [5, 4], [7, 4], [6, 5]])
+
+CODET1 = np.array([[3.0000, 3.0000],
+                   [6.2000, 4.0000],
+                   [5.8000, 1.8000]])
+
+CODET2 = np.array([[11.0/3, 8.0/3],
+                   [6.7500, 4.2500],
+                   [6.2500, 1.7500]])
+
+LABEL1 = np.array([0, 1, 2, 2, 2, 2, 1, 2, 1, 1, 1])
+
+
+@make_xp_test_case(whiten)
+class TestWhiten:
+
+    def test_whiten(self, xp):
+        desired = xp.asarray([[5.08738849, 2.97091878],
+                            [3.19909255, 0.69660580],
+                            [4.51041982, 0.02640918],
+                            [4.38567074, 0.95120889],
+                            [2.32191480, 1.63195503]])
+
+        obs = xp.asarray([[0.98744510, 0.82766775],
+                          [0.62093317, 0.19406729],
+                          [0.87545741, 0.00735733],
+                          [0.85124403, 0.26499712],
+                          [0.45067590, 0.45464607]])
+        xp_assert_close(whiten(obs), desired, rtol=1e-5)
+
+    def test_whiten_zero_std(self, xp):
+        desired = xp.asarray([[0., 1.0, 2.86666544],
+                              [0., 1.0, 1.32460034],
+                              [0., 1.0, 3.74382172]])
+
+        obs = xp.asarray([[0., 1., 0.74109533],
+                          [0., 1., 0.34243798],
+                          [0., 1., 0.96785929]])
+
+        with eager_warns(RuntimeWarning, match="Some columns have standard...", xp=xp):
+            actual = whiten(obs)
+        xp_assert_close(actual, desired, rtol=1e-5)
+
+    @pytest.mark.filterwarnings("ignore:invalid value encountered:RuntimeWarning:dask")
+    @pytest.mark.parametrize("bad_value", [math.nan, math.inf, -math.inf])
+    def test_whiten_not_finite(self, bad_value, xp):
+        obs = xp.asarray([[0.98744510, bad_value],
+                          [0.62093317, 0.19406729],
+                          [0.87545741, 0.00735733],
+                          [0.85124403, 0.26499712],
+                          [0.45067590, 0.45464607]])
+
+        if is_lazy_array(obs):
+            desired = xp.asarray([[5.08738849, math.nan],
+                                  [3.19909255, math.nan],
+                                  [4.51041982, math.nan],
+                                  [4.38567074, math.nan],
+                                  [2.32191480, math.nan]])
+            xp_assert_close(whiten(obs), desired, rtol=1e-5)
+        else:
+            assert_raises(ValueError, whiten, obs)
+
+    @pytest.mark.skipif(SCIPY_ARRAY_API,
+                        reason='`np.matrix` unsupported in array API mode')
+    def test_whiten_not_finite_matrix(self):
+        for bad_value in np.nan, np.inf, -np.inf:
+            obs = matrix([[0.98744510, bad_value],
+                          [0.62093317, 0.19406729],
+                          [0.87545741, 0.00735733],
+                          [0.85124403, 0.26499712],
+                          [0.45067590, 0.45464607]])
+            assert_raises(ValueError, whiten, obs)
+
+
+@make_xp_test_case(vq)
+class TestVq:
+
+    def test_py_vq(self, xp):
+        initc = np.concatenate([[X[0]], [X[1]], [X[2]]])
+        # label1.dtype varies between int32 and int64 over platforms
+        label1 = py_vq(xp.asarray(X), xp.asarray(initc))[0]
+        xp_assert_equal(label1, xp.asarray(LABEL1, dtype=xp.int64),
+                        check_dtype=False)
+
+    @pytest.mark.skipif(SCIPY_ARRAY_API,
+                        reason='`np.matrix` unsupported in array API mode')
+    def test_py_vq_matrix(self):
+        initc = np.concatenate([[X[0]], [X[1]], [X[2]]])
+        # label1.dtype varies between int32 and int64 over platforms
+        label1 = py_vq(matrix(X), matrix(initc))[0]
+        assert_array_equal(label1, LABEL1)
+
+    def test_vq(self, xp):
+        initc = np.concatenate([[X[0]], [X[1]], [X[2]]])
+        label1, _ = _vq.vq(X, initc)
+        assert_array_equal(label1, LABEL1)
+        _, _ = vq(xp.asarray(X), xp.asarray(initc))
+
+    @pytest.mark.skipif(SCIPY_ARRAY_API,
+                        reason='`np.matrix` unsupported in array API mode')
+    def test_vq_matrix(self):
+        initc = np.concatenate([[X[0]], [X[1]], [X[2]]])
+        label1, _ = _vq.vq(matrix(X), matrix(initc))
+        assert_array_equal(label1, LABEL1)
+        _, _ = vq(matrix(X), matrix(initc))
+
+    def test_vq_1d(self, xp):
+        # Test special rank 1 vq algo, python implementation.
+        data = X[:, 0]
+        initc = data[:3]
+        a, b = _vq.vq(data, initc)
+        data = xp.asarray(data)
+        initc = xp.asarray(initc)
+        ta, tb = py_vq(data[:, np.newaxis], initc[:, np.newaxis])
+        # ta.dtype varies between int32 and int64 over platforms
+        xp_assert_equal(ta, xp.asarray(a, dtype=xp.int64), check_dtype=False)
+        xp_assert_equal(tb, xp.asarray(b))
+
+    def test__vq_sametype(self):
+        a = np.asarray([1.0, 2.0])
+        b = a.astype(np.float32)
+        assert_raises(TypeError, _vq.vq, a, b)
+
+    def test__vq_invalid_type(self):
+        a = np.asarray([1, 2], dtype=int)
+        assert_raises(TypeError, _vq.vq, a, a)
+
+    def test_vq_large_nfeat(self, xp):
+        X = np.random.rand(20, 20)
+        code_book = np.random.rand(3, 20)
+
+        codes0, dis0 = _vq.vq(X, code_book)
+        codes1, dis1 = py_vq(
+            xp.asarray(X), xp.asarray(code_book)
+        )
+        xp_assert_close(dis1, xp.asarray(dis0), rtol=1e-5)
+        # codes1.dtype varies between int32 and int64 over platforms
+        xp_assert_equal(codes1, xp.asarray(codes0, dtype=xp.int64), check_dtype=False)
+
+        X = X.astype(np.float32)
+        code_book = code_book.astype(np.float32)
+
+        codes0, dis0 = _vq.vq(X, code_book)
+        codes1, dis1 = py_vq(
+            xp.asarray(X), xp.asarray(code_book)
+        )
+        xp_assert_close(dis1, xp.asarray(dis0, dtype=xp.float64), rtol=1e-5)
+        # codes1.dtype varies between int32 and int64 over platforms
+        xp_assert_equal(codes1, xp.asarray(codes0, dtype=xp.int64), check_dtype=False)
+
+    def test_vq_large_features(self, xp):
+        X = np.random.rand(10, 5) * 1000000
+        code_book = np.random.rand(2, 5) * 1000000
+
+        codes0, dis0 = _vq.vq(X, code_book)
+        codes1, dis1 = py_vq(
+            xp.asarray(X), xp.asarray(code_book)
+        )
+        xp_assert_close(dis1, xp.asarray(dis0), rtol=1e-5)
+        # codes1.dtype varies between int32 and int64 over platforms
+        xp_assert_equal(codes1, xp.asarray(codes0, dtype=xp.int64), check_dtype=False)
+
+
+# Whole class skipped on GPU for now;
+# once pdist/cdist are hooked up for CuPy, more tests will work
+@make_xp_test_case(kmeans, kmeans2)
+class TestKMeans:
+
+    def test_large_features(self, xp):
+        # Generate a data set with large values, and run kmeans on it to
+        # (regression for 1077).
+        d = 300
+        n = 100
+
+        m1 = np.random.randn(d)
+        m2 = np.random.randn(d)
+        x = 10000 * np.random.randn(n, d) - 20000 * m1
+        y = 10000 * np.random.randn(n, d) + 20000 * m2
+
+        data = np.empty((x.shape[0] + y.shape[0], d), np.float64)
+        data[:x.shape[0]] = x
+        data[x.shape[0]:] = y
+
+        # use `seed` to ensure backwards compatibility after SPEC7
+        kmeans(xp.asarray(data), 2, seed=1)
+
+    def test_kmeans_simple(self, xp):
+        rng = np.random.default_rng(54321)
+        initc = np.concatenate([[X[0]], [X[1]], [X[2]]])
+        code1 = kmeans(xp.asarray(X), xp.asarray(initc), iter=1, rng=rng)[0]
+        xp_assert_close(code1, xp.asarray(CODET2))
+
+    @pytest.mark.skipif(SCIPY_ARRAY_API,
+                        reason='`np.matrix` unsupported in array API mode')
+    def test_kmeans_simple_matrix(self):
+        rng = np.random.default_rng(54321)
+        initc = np.concatenate([[X[0]], [X[1]], [X[2]]])
+        code1 = kmeans(matrix(X), matrix(initc), iter=1, rng=rng)[0]
+        xp_assert_close(code1, CODET2)
+
+    def test_kmeans_lost_cluster(self, xp):
+        # This will cause kmeans to have a cluster with no points.
+        data = xp.asarray(TESTDATA_2D)
+        initk = xp.asarray([[-1.8127404, -0.67128041],
+                            [2.04621601, 0.07401111],
+                            [-2.31149087, -0.05160469]])
+
+        kmeans(data, initk)
+        with warnings.catch_warnings():
+            warnings.filterwarnings(
+                "ignore",
+                ("One of the clusters is empty. Re-run kmeans with a different "
+                 "initialization"),
+                UserWarning,
+            )
+            kmeans2(data, initk, missing='warn')
+
+        assert_raises(ClusterError, kmeans2, data, initk, missing='raise')
+
+    def test_kmeans2_simple(self, xp):
+        rng = np.random.default_rng(12345678)
+        initc = xp.asarray(np.concatenate([[X[0]], [X[1]], [X[2]]]))
+        arrays = [xp.asarray] if SCIPY_ARRAY_API else [np.asarray, matrix]
+        for tp in arrays:
+            code1 = kmeans2(tp(X), tp(initc), iter=1, rng=rng)[0]
+            code2 = kmeans2(tp(X), tp(initc), iter=2, rng=rng)[0]
+
+            xp_assert_close(code1, xp.asarray(CODET1))
+            xp_assert_close(code2, xp.asarray(CODET2))
+
+    @pytest.mark.skipif(SCIPY_ARRAY_API,
+                        reason='`np.matrix` unsupported in array API mode')
+    def test_kmeans2_simple_matrix(self):
+        rng = np.random.default_rng(12345678)
+        initc = np.concatenate([[X[0]], [X[1]], [X[2]]])
+        code1 = kmeans2(matrix(X), matrix(initc), iter=1, rng=rng)[0]
+        code2 = kmeans2(matrix(X), matrix(initc), iter=2, rng=rng)[0]
+
+        xp_assert_close(code1, CODET1)
+        xp_assert_close(code2, CODET2)
+
+    def test_kmeans2_rank1(self, xp):
+        data = xp.asarray(TESTDATA_2D)
+        data1 = data[:, 0]
+
+        initc = data1[:3]
+        code = xp_copy(initc, xp=xp)
+
+        # use `seed` to ensure backwards compatibility after SPEC7
+        kmeans2(data1, code, iter=1, seed=1)[0]
+        kmeans2(data1, code, iter=2)[0]
+
+    def test_kmeans2_rank1_2(self, xp):
+        data = xp.asarray(TESTDATA_2D)
+        data1 = data[:, 0]
+        kmeans2(data1, 2, iter=1)
+
+    def test_kmeans2_high_dim(self, xp):
+        # test kmeans2 when the number of dimensions exceeds the number
+        # of input points
+        data = xp.asarray(TESTDATA_2D)
+        data = xp.reshape(data, (20, 20))[:10, :]
+        kmeans2(data, 2)
+
+    def test_kmeans2_init(self, xp):
+        rng = np.random.default_rng(12345678)
+        data = xp.asarray(TESTDATA_2D)
+        k = 3
+
+        kmeans2(data, k, minit='points', rng=rng)
+        kmeans2(data[:, 1], k, minit='points', rng=rng)  # special case (1-D)
+
+        kmeans2(data, k, minit='++', rng=rng)
+        kmeans2(data[:, 1], k, minit='++', rng=rng)  # special case (1-D)
+
+        # minit='random' can give warnings, filter those
+        with warnings.catch_warnings():
+            warnings.filterwarnings("ignore", "One of the clusters is empty. Re-run.")
+            kmeans2(data, k, minit='random', rng=rng)
+            kmeans2(data[:, 1], k, minit='random', rng=rng)  # special case (1-D)
+
+    @pytest.fixture
+    def krand_lock(self):
+        return Lock()
+
+    @xfail_xp_backends('dask.array', reason="Wrong answer")
+    @pytest.mark.skipif(sys.platform == 'win32',
+                        reason='Fails with MemoryError in Wine.')
+    def test_krandinit(self, xp, krand_lock):
+        data = xp.asarray(TESTDATA_2D)
+        datas = [xp.reshape(data, (200, 2)),
+                 xp.reshape(data, (20, 20))[:10, :]]
+        k = int(1e6)
+        with krand_lock:
+            for data in datas:
+                rng = np.random.default_rng(1234)
+                init = _krandinit(data, k, rng, xp)
+                orig_cov = xpx.cov(data.T, xp=xp)
+                init_cov = xpx.cov(init.T, xp=xp)
+                xp_assert_close(orig_cov, init_cov, atol=1.1e-2)
+
+    def test_kmeans2_empty(self, xp):
+        # Regression test for gh-1032.
+        assert_raises(ValueError, kmeans2, xp.asarray([]), 2)
+
+    def test_kmeans_0k(self, xp):
+        # Regression test for gh-1073: fail when k arg is 0.
+        assert_raises(ValueError, kmeans, xp.asarray(X), 0)
+        assert_raises(ValueError, kmeans2, xp.asarray(X), 0)
+        assert_raises(ValueError, kmeans2, xp.asarray(X), xp.asarray([]))
+
+    def test_kmeans_large_thres(self, xp):
+        # Regression test for gh-1774
+        x = xp.asarray([1, 2, 3, 4, 10], dtype=xp.float64)
+        res = kmeans(x, 1, thresh=1e16)
+        xp_assert_close(res[0], xp.asarray([4.], dtype=xp.float64))
+        xp_assert_close(res[1], xp.asarray(2.3999999999999999, dtype=xp.float64)[()])
+
+    def test_kmeans2_kpp_low_dim(self, xp):
+        # Regression test for gh-11462
+        rng = np.random.default_rng(2358792345678234568)
+        prev_res = xp.asarray([[-1.95266667, 0.898],
+                               [-3.153375, 3.3945]], dtype=xp.float64)
+        res, _ = kmeans2(xp.asarray(TESTDATA_2D), 2, minit='++', rng=rng)
+        xp_assert_close(res, prev_res)
+
+    def test_kmeans2_kpp_high_dim(self, xp):
+        # Regression test for gh-11462
+        rng = np.random.default_rng(23587923456834568)
+        n_dim = 100
+        size = 10
+        centers = np.vstack([5 * np.ones(n_dim),
+                             -5 * np.ones(n_dim)])
+
+        data = np.vstack([
+            rng.multivariate_normal(centers[0], np.eye(n_dim), size=size),
+            rng.multivariate_normal(centers[1], np.eye(n_dim), size=size)
+        ])
+
+        data = xp.asarray(data)
+        res, _ = kmeans2(data, 2, minit='++', rng=rng)
+        xp_assert_equal(xp.sign(res), xp.sign(xp.asarray(centers)))
+
+    def test_kmeans_diff_convergence(self, xp):
+        # Regression test for gh-8727
+        obs = xp.asarray([-3, -1, 0, 1, 1, 8], dtype=xp.float64)
+        res = kmeans(obs, xp.asarray([-3., 0.99]))
+        xp_assert_close(res[0], xp.asarray([-0.4,  8.], dtype=xp.float64))
+        xp_assert_close(res[1], xp.asarray(1.0666666666666667, dtype=xp.float64)[()])
+
+    def test_kmeans_and_kmeans2_random_seed(self, xp):
+
+        seed_list = [
+            1234, np.random.RandomState(1234), np.random.default_rng(1234)
+        ]
+
+        for seed in seed_list:
+            seed1 = deepcopy(seed)
+            seed2 = deepcopy(seed)
+            data = xp.asarray(TESTDATA_2D)
+            # test for kmeans
+            res1, _ = kmeans(data, 2, seed=seed1)
+            res2, _ = kmeans(data, 2, seed=seed2)
+            xp_assert_close(res1, res2)  # should be same results
+            # test for kmeans2
+            for minit in ["random", "points", "++"]:
+                res1, _ = kmeans2(data, 2, minit=minit, seed=seed1)
+                res2, _ = kmeans2(data, 2, minit=minit, seed=seed2)
+                xp_assert_close(res1, res2)  # should be same results
diff --git a/.venv/lib/python3.11/site-packages/scipy/cluster/vq.py b/.venv/lib/python3.11/site-packages/scipy/cluster/vq.py
new file mode 100644
index 0000000000000000000000000000000000000000..65debbbde3ab3913be052cf56e3ffbae98dbe378
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/cluster/vq.py
@@ -0,0 +1,832 @@
+"""
+K-means clustering and vector quantization (:mod:`scipy.cluster.vq`)
+====================================================================
+
+Provides routines for k-means clustering, generating code books
+from k-means models and quantizing vectors by comparing them with
+centroids in a code book.
+
+.. autosummary::
+   :toctree: generated/
+
+   whiten -- Normalize a group of observations so each feature has unit variance
+   vq -- Calculate code book membership of a set of observation vectors
+   kmeans -- Perform k-means on a set of observation vectors forming k clusters
+   kmeans2 -- A different implementation of k-means with more methods
+           -- for initializing centroids
+
+Background information
+----------------------
+The k-means algorithm takes as input the number of clusters to
+generate, k, and a set of observation vectors to cluster. It
+returns a set of centroids, one for each of the k clusters. An
+observation vector is classified with the cluster number or
+centroid index of the centroid closest to it.
+
+A vector v belongs to cluster i if it is closer to centroid i than
+any other centroid. If v belongs to i, we say centroid i is the
+dominating centroid of v. The k-means algorithm tries to
+minimize distortion, which is defined as the sum of the squared distances
+between each observation vector and its dominating centroid.
+The minimization is achieved by iteratively reclassifying
+the observations into clusters and recalculating the centroids until
+a configuration is reached in which the centroids are stable. One can
+also define a maximum number of iterations.
+
+Since vector quantization is a natural application for k-means,
+information theory terminology is often used. The centroid index
+or cluster index is also referred to as a "code" and the table
+mapping codes to centroids and, vice versa, is often referred to as a
+"code book". The result of k-means, a set of centroids, can be
+used to quantize vectors. Quantization aims to find an encoding of
+vectors that reduces the expected distortion.
+
+All routines expect obs to be an M by N array, where the rows are
+the observation vectors. The codebook is a k by N array, where the
+ith row is the centroid of code word i. The observation vectors
+and centroids have the same feature dimension.
+
+As an example, suppose we wish to compress a 24-bit color image
+(each pixel is represented by one byte for red, one for blue, and
+one for green) before sending it over the web. By using a smaller
+8-bit encoding, we can reduce the amount of data by two
+thirds. Ideally, the colors for each of the 256 possible 8-bit
+encoding values should be chosen to minimize distortion of the
+color. Running k-means with k=256 generates a code book of 256
+codes, which fills up all possible 8-bit sequences. Instead of
+sending a 3-byte value for each pixel, the 8-bit centroid index
+(or code word) of the dominating centroid is transmitted. The code
+book is also sent over the wire so each 8-bit code can be
+translated back to a 24-bit pixel value representation. If the
+image of interest was of an ocean, we would expect many 24-bit
+blues to be represented by 8-bit codes. If it was an image of a
+human face, more flesh-tone colors would be represented in the
+code book.
+
+"""
+import warnings
+import numpy as np
+from collections import deque
+from scipy._lib._array_api import (_asarray, array_namespace, is_lazy_array,
+                                   xp_capabilities, xp_copy, xp_size)
+from scipy._lib._util import (check_random_state, rng_integers,
+                              _transition_to_rng)
+from scipy._lib import array_api_extra as xpx
+from scipy.spatial.distance import cdist
+
+from . import _vq
+
+__docformat__ = 'restructuredtext'
+
+__all__ = ['whiten', 'vq', 'kmeans', 'kmeans2']
+
+
+class ClusterError(Exception):
+    pass
+
+
+@xp_capabilities()
+def whiten(obs, check_finite=None):
+    """
+    Normalize a group of observations on a per feature basis.
+
+    Before running k-means, it is beneficial to rescale each feature
+    dimension of the observation set by its standard deviation (i.e. "whiten"
+    it - as in "white noise" where each frequency has equal power).
+    Each feature is divided by its standard deviation across all observations
+    to give it unit variance.
+
+    Parameters
+    ----------
+    obs : ndarray
+        Each row of the array is an observation.  The
+        columns are the features seen during each observation::
+
+            #        f0  f1  f2
+            obs = [[ 1., 1., 1.],  #o0
+                   [ 2., 2., 2.],  #o1
+                   [ 3., 3., 3.],  #o2
+                   [ 4., 4., 4.]]  #o3
+
+    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 for eager backends and False for lazy ones.
+
+    Returns
+    -------
+    result : ndarray
+        Contains the values in `obs` scaled by the standard deviation
+        of each column.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.cluster.vq import whiten
+    >>> features  = np.array([[1.9, 2.3, 1.7],
+    ...                       [1.5, 2.5, 2.2],
+    ...                       [0.8, 0.6, 1.7,]])
+    >>> whiten(features)
+    array([[ 4.17944278,  2.69811351,  7.21248917],
+           [ 3.29956009,  2.93273208,  9.33380951],
+           [ 1.75976538,  0.7038557 ,  7.21248917]])
+
+    """
+    xp = array_namespace(obs)
+    if check_finite is None:
+        check_finite = not is_lazy_array(obs)
+    obs = _asarray(obs, check_finite=check_finite, xp=xp)
+    std_dev = xp.std(obs, axis=0)
+    zero_std_mask = std_dev == 0
+    std_dev = xpx.at(std_dev, zero_std_mask).set(1.0)
+    if check_finite and xp.any(zero_std_mask):
+        warnings.warn("Some columns have standard deviation zero. "
+                      "The values of these columns will not change.",
+                      RuntimeWarning, stacklevel=2)
+    return obs / std_dev
+
+
+@xp_capabilities(cpu_only=True, reason="uses spatial.distance.cdist",
+                 jax_jit=False, allow_dask_compute=True)
+def vq(obs, code_book, check_finite=True):
+    """
+    Assign codes from a code book to observations.
+
+    Assigns a code from a code book to each observation. Each
+    observation vector in the 'M' by 'N' `obs` array is compared with the
+    centroids in the code book and assigned the code of the closest
+    centroid.
+
+    The features in `obs` should have unit variance, which can be
+    achieved by passing them through the whiten function. The code
+    book can be created with the k-means algorithm or a different
+    encoding algorithm.
+
+    Parameters
+    ----------
+    obs : ndarray
+        Each row of the 'M' x 'N' array is an observation. The columns are
+        the "features" seen during each observation. The features must be
+        whitened first using the whiten function or something equivalent.
+    code_book : ndarray
+        The code book is usually generated using the k-means algorithm.
+        Each row of the array holds a different code, and the columns are
+        the features of the code::
+
+            #              f0  f1  f2  f3
+            code_book = [[ 1., 2., 3., 4.],  #c0
+                         [ 1., 2., 3., 4.],  #c1
+                         [ 1., 2., 3., 4.]]  #c2
+
+    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
+
+    Returns
+    -------
+    code : ndarray
+        A length M array holding the code book index for each observation.
+    dist : ndarray
+        The distortion (distance) between the observation and its nearest
+        code.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.cluster.vq import vq
+    >>> code_book = np.array([[1., 1., 1.],
+    ...                       [2., 2., 2.]])
+    >>> features  = np.array([[1.9, 2.3, 1.7],
+    ...                       [1.5, 2.5, 2.2],
+    ...                       [0.8, 0.6, 1.7]])
+    >>> vq(features, code_book)
+    (array([1, 1, 0], dtype=int32), array([0.43588989, 0.73484692, 0.83066239]))
+
+    """
+    xp = array_namespace(obs, code_book)
+    obs = _asarray(obs, xp=xp, check_finite=check_finite)
+    code_book = _asarray(code_book, xp=xp, check_finite=check_finite)
+    ct = xp.result_type(obs, code_book)
+
+    if xp.isdtype(ct, kind='real floating'):
+        c_obs = xp.astype(obs, ct, copy=False)
+        c_code_book = xp.astype(code_book, ct, copy=False)
+        c_obs = np.asarray(c_obs)
+        c_code_book = np.asarray(c_code_book)
+        result = _vq.vq(c_obs, c_code_book)
+        return xp.asarray(result[0]), xp.asarray(result[1])
+    return py_vq(obs, code_book, check_finite=False)
+
+
+def py_vq(obs, code_book, check_finite=True):
+    """ Python version of vq algorithm.
+
+    The algorithm computes the Euclidean distance between each
+    observation and every frame in the code_book.
+
+    Parameters
+    ----------
+    obs : ndarray
+        Expects a rank 2 array. Each row is one observation.
+    code_book : ndarray
+        Code book to use. Same format than obs. Should have same number of
+        features (e.g., columns) than obs.
+    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
+
+    Returns
+    -------
+    code : ndarray
+        code[i] gives the label of the ith obversation; its code is
+        code_book[code[i]].
+    mind_dist : ndarray
+        min_dist[i] gives the distance between the ith observation and its
+        corresponding code.
+
+    Notes
+    -----
+    This function is slower than the C version but works for
+    all input types. If the inputs have the wrong types for the
+    C versions of the function, this one is called as a last resort.
+
+    It is about 20 times slower than the C version.
+
+    """
+    xp = array_namespace(obs, code_book)
+    obs = _asarray(obs, xp=xp, check_finite=check_finite)
+    code_book = _asarray(code_book, xp=xp, check_finite=check_finite)
+
+    if obs.ndim != code_book.ndim:
+        raise ValueError("Observation and code_book should have the same rank")
+
+    if obs.ndim == 1:
+        obs = obs[:, xp.newaxis]
+        code_book = code_book[:, xp.newaxis]
+
+    # Once `cdist` has array API support, this `xp.asarray` call can be removed
+    dist = xp.asarray(cdist(obs, code_book))
+    code = xp.argmin(dist, axis=1)
+    min_dist = xp.min(dist, axis=1)
+    return code, min_dist
+
+
+def _kmeans(obs, guess, thresh=1e-5, xp=None):
+    """ "raw" version of k-means.
+
+    Returns
+    -------
+    code_book
+        The lowest distortion codebook found.
+    avg_dist
+        The average distance a observation is from a code in the book.
+        Lower means the code_book matches the data better.
+
+    See Also
+    --------
+    kmeans : wrapper around k-means
+
+    Examples
+    --------
+    Note: not whitened in this example.
+
+    >>> import numpy as np
+    >>> from scipy.cluster.vq import _kmeans
+    >>> features  = np.array([[ 1.9,2.3],
+    ...                       [ 1.5,2.5],
+    ...                       [ 0.8,0.6],
+    ...                       [ 0.4,1.8],
+    ...                       [ 1.0,1.0]])
+    >>> book = np.array((features[0],features[2]))
+    >>> _kmeans(features,book)
+    (array([[ 1.7       ,  2.4       ],
+           [ 0.73333333,  1.13333333]]), 0.40563916697728591)
+
+    """
+    xp = np if xp is None else xp
+    code_book = guess
+    diff = xp.inf
+    prev_avg_dists = deque([diff], maxlen=2)
+
+    np_obs = np.asarray(obs)
+    while diff > thresh:
+        # compute membership and distances between obs and code_book
+        obs_code, distort = vq(obs, code_book, check_finite=False)
+        prev_avg_dists.append(xp.mean(distort, axis=-1))
+        # recalc code_book as centroids of associated obs
+        obs_code = np.asarray(obs_code)
+        code_book, has_members = _vq.update_cluster_means(np_obs, obs_code,
+                                                          code_book.shape[0])
+        code_book = code_book[has_members]
+        code_book = xp.asarray(code_book)
+        diff = xp.abs(prev_avg_dists[0] - prev_avg_dists[1])
+
+    return code_book, prev_avg_dists[1]
+
+
+@xp_capabilities(cpu_only=True, jax_jit=False, allow_dask_compute=True)
+@_transition_to_rng("seed")
+def kmeans(obs, k_or_guess, iter=20, thresh=1e-5, check_finite=True,
+           *, rng=None):
+    """
+    Performs k-means on a set of observation vectors forming k clusters.
+
+    The k-means algorithm adjusts the classification of the observations
+    into clusters and updates the cluster centroids until the position of
+    the centroids is stable over successive iterations. In this
+    implementation of the algorithm, the stability of the centroids is
+    determined by comparing the absolute value of the change in the average
+    Euclidean distance between the observations and their corresponding
+    centroids against a threshold. This yields
+    a code book mapping centroids to codes and vice versa.
+
+    Parameters
+    ----------
+    obs : ndarray
+       Each row of the M by N array is an observation vector. The
+       columns are the features seen during each observation.
+       The features must be whitened first with the `whiten` function.
+
+    k_or_guess : int or ndarray
+       The number of centroids to generate. A code is assigned to
+       each centroid, which is also the row index of the centroid
+       in the code_book matrix generated.
+
+       The initial k centroids are chosen by randomly selecting
+       observations from the observation matrix. Alternatively,
+       passing a k by N array specifies the initial k centroids.
+
+    iter : int, optional
+       The number of times to run k-means, returning the codebook
+       with the lowest distortion. This argument is ignored if
+       initial centroids are specified with an array for the
+       ``k_or_guess`` parameter. This parameter does not represent the
+       number of iterations of the k-means algorithm.
+
+    thresh : float, optional
+       Terminates the k-means algorithm if the change in
+       distortion since the last k-means iteration is less than
+       or equal to threshold.
+
+    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
+    rng : `numpy.random.Generator`, optional
+        Pseudorandom number generator state. When `rng` is None, a new
+        `numpy.random.Generator` is created using entropy from the
+        operating system. Types other than `numpy.random.Generator` are
+        passed to `numpy.random.default_rng` to instantiate a ``Generator``.
+
+    Returns
+    -------
+    codebook : ndarray
+       A k by N array of k centroids. The ith centroid
+       codebook[i] is represented with the code i. The centroids
+       and codes generated represent the lowest distortion seen,
+       not necessarily the globally minimal distortion.
+       Note that the number of centroids is not necessarily the same as the
+       ``k_or_guess`` parameter, because centroids assigned to no observations
+       are removed during iterations.
+
+    distortion : float
+       The mean (non-squared) Euclidean distance between the observations
+       passed and the centroids generated. Note the difference to the standard
+       definition of distortion in the context of the k-means algorithm, which
+       is the sum of the squared distances.
+
+    See Also
+    --------
+    kmeans2 : a different implementation of k-means clustering
+       with more methods for generating initial centroids but without
+       using a distortion change threshold as a stopping criterion.
+
+    whiten : must be called prior to passing an observation matrix
+       to kmeans.
+
+    Notes
+    -----
+    For more functionalities or optimal performance, you can use
+    `sklearn.cluster.KMeans `_.
+    `This `_
+    is a benchmark result of several implementations.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.cluster.vq import vq, kmeans, whiten
+    >>> import matplotlib.pyplot as plt
+    >>> features  = np.array([[ 1.9,2.3],
+    ...                       [ 1.5,2.5],
+    ...                       [ 0.8,0.6],
+    ...                       [ 0.4,1.8],
+    ...                       [ 0.1,0.1],
+    ...                       [ 0.2,1.8],
+    ...                       [ 2.0,0.5],
+    ...                       [ 0.3,1.5],
+    ...                       [ 1.0,1.0]])
+    >>> whitened = whiten(features)
+    >>> book = np.array((whitened[0],whitened[2]))
+    >>> kmeans(whitened,book)
+    (array([[ 2.3110306 ,  2.86287398],    # random
+           [ 0.93218041,  1.24398691]]), 0.85684700941625547)
+
+    >>> codes = 3
+    >>> kmeans(whitened,codes)
+    (array([[ 2.3110306 ,  2.86287398],    # random
+           [ 1.32544402,  0.65607529],
+           [ 0.40782893,  2.02786907]]), 0.5196582527686241)
+
+    >>> # Create 50 datapoints in two clusters a and b
+    >>> pts = 50
+    >>> rng = np.random.default_rng()
+    >>> a = rng.multivariate_normal([0, 0], [[4, 1], [1, 4]], size=pts)
+    >>> b = rng.multivariate_normal([30, 10],
+    ...                             [[10, 2], [2, 1]],
+    ...                             size=pts)
+    >>> features = np.concatenate((a, b))
+    >>> # Whiten data
+    >>> whitened = whiten(features)
+    >>> # Find 2 clusters in the data
+    >>> codebook, distortion = kmeans(whitened, 2)
+    >>> # Plot whitened data and cluster centers in red
+    >>> plt.scatter(whitened[:, 0], whitened[:, 1])
+    >>> plt.scatter(codebook[:, 0], codebook[:, 1], c='r')
+    >>> plt.show()
+
+    """
+    if isinstance(k_or_guess, int):
+        xp = array_namespace(obs)
+    else:
+        xp = array_namespace(obs, k_or_guess)
+    obs = _asarray(obs, xp=xp, check_finite=check_finite)
+    guess = _asarray(k_or_guess, xp=xp, check_finite=check_finite)
+    if iter < 1:
+        raise ValueError(f"iter must be at least 1, got {iter}")
+
+    # Determine whether a count (scalar) or an initial guess (array) was passed.
+    if xp_size(guess) != 1:
+        if xp_size(guess) < 1:
+            raise ValueError(f"Asked for 0 clusters. Initial book was {guess}")
+        return _kmeans(obs, guess, thresh=thresh, xp=xp)
+
+    # k_or_guess is a scalar, now verify that it's an integer
+    k = int(guess)
+    if k != guess:
+        raise ValueError("If k_or_guess is a scalar, it must be an integer.")
+    if k < 1:
+        raise ValueError(f"Asked for {k} clusters.")
+
+    rng = check_random_state(rng)
+
+    # initialize best distance value to a large value
+    best_dist = xp.inf
+    for i in range(iter):
+        # the initial code book is randomly selected from observations
+        guess = _kpoints(obs, k, rng, xp)
+        book, dist = _kmeans(obs, guess, thresh=thresh, xp=xp)
+        if dist < best_dist:
+            best_book = book
+            best_dist = dist
+    return best_book, best_dist
+
+
+def _kpoints(data, k, rng, xp):
+    """Pick k points at random in data (one row = one observation).
+
+    Parameters
+    ----------
+    data : ndarray
+        Expect a rank 1 or 2 array. Rank 1 are assumed to describe one
+        dimensional data, rank 2 multidimensional data, in which case one
+        row is one observation.
+    k : int
+        Number of samples to generate.
+    rng : `numpy.random.Generator` or `numpy.random.RandomState`
+        Random number generator.
+
+    Returns
+    -------
+    x : ndarray
+        A 'k' by 'N' containing the initial centroids
+
+    """
+    idx = rng.choice(data.shape[0], size=int(k), replace=False)
+    # convert to array with default integer dtype (avoids numpy#25607)
+    idx = xp.asarray(idx, dtype=xp.asarray([1]).dtype)
+    return xp.take(data, idx, axis=0)
+
+
+def _krandinit(data, k, rng, xp):
+    """Returns k samples of a random variable whose parameters depend on data.
+
+    More precisely, it returns k observations sampled from a Gaussian random
+    variable whose mean and covariances are the ones estimated from the data.
+
+    Parameters
+    ----------
+    data : ndarray
+        Expect a rank 1 or 2 array. Rank 1 is assumed to describe 1-D
+        data, rank 2 multidimensional data, in which case one
+        row is one observation.
+    k : int
+        Number of samples to generate.
+    rng : `numpy.random.Generator` or `numpy.random.RandomState`
+        Random number generator.
+
+    Returns
+    -------
+    x : ndarray
+        A 'k' by 'N' containing the initial centroids
+
+    """
+    mu = xp.mean(data, axis=0)
+    k = np.asarray(k)
+
+    if data.ndim == 1:
+        _cov = xpx.cov(data, xp=xp)
+        x = rng.standard_normal(size=k)
+        x = xp.asarray(x)
+        x *= xp.sqrt(_cov)
+    elif data.shape[1] > data.shape[0]:
+        # initialize when the covariance matrix is rank deficient
+        _, s, vh = xp.linalg.svd(data - mu, full_matrices=False)
+        x = rng.standard_normal(size=(k, xp_size(s)))
+        x = xp.asarray(x)
+        sVh = s[:, None] * vh / xp.sqrt(data.shape[0] - xp.asarray(1.))
+        x = x @ sVh
+    else:
+        _cov = xpx.atleast_nd(xpx.cov(data.T, xp=xp), ndim=2, xp=xp)
+
+        # k rows, d cols (one row = one obs)
+        # Generate k sample of a random variable ~ Gaussian(mu, cov)
+        x = rng.standard_normal(size=(k, xp_size(mu)))
+        x = xp.asarray(x)
+        x = x @ xp.linalg.cholesky(_cov).T
+
+    x += mu
+    return x
+
+
+def _kpp(data, k, rng, xp):
+    """ Picks k points in the data based on the kmeans++ method.
+
+    Parameters
+    ----------
+    data : ndarray
+        Expect a rank 1 or 2 array. Rank 1 is assumed to describe 1-D
+        data, rank 2 multidimensional data, in which case one
+        row is one observation.
+    k : int
+        Number of samples to generate.
+    rng : `numpy.random.Generator` or `numpy.random.RandomState`
+        Random number generator.
+
+    Returns
+    -------
+    init : ndarray
+        A 'k' by 'N' containing the initial centroids.
+
+    References
+    ----------
+    .. [1] D. Arthur and S. Vassilvitskii, "k-means++: the advantages of
+       careful seeding", Proceedings of the Eighteenth Annual ACM-SIAM Symposium
+       on Discrete Algorithms, 2007.
+    """
+
+    ndim = len(data.shape)
+    if ndim == 1:
+        data = data[:, None]
+
+    dims = data.shape[1]
+
+    init = xp.empty((int(k), dims))
+
+    for i in range(k):
+        if i == 0:
+            data_idx = rng_integers(rng, data.shape[0])
+        else:
+            D2 = cdist(init[:i,:], data, metric='sqeuclidean').min(axis=0)
+            probs = D2/D2.sum()
+            cumprobs = probs.cumsum()
+            r = rng.uniform()
+            cumprobs = np.asarray(cumprobs)
+            data_idx = int(np.searchsorted(cumprobs, r))
+
+        init = xpx.at(init)[i, :].set(data[data_idx, :])
+
+    if ndim == 1:
+        init = init[:, 0]
+    return init
+
+
+_valid_init_meth = {'random': _krandinit, 'points': _kpoints, '++': _kpp}
+
+
+def _missing_warn():
+    """Print a warning when called."""
+    warnings.warn("One of the clusters is empty. "
+                  "Re-run kmeans with a different initialization.",
+                  stacklevel=3)
+
+
+def _missing_raise():
+    """Raise a ClusterError when called."""
+    raise ClusterError("One of the clusters is empty. "
+                       "Re-run kmeans with a different initialization.")
+
+
+_valid_miss_meth = {'warn': _missing_warn, 'raise': _missing_raise}
+
+
+@xp_capabilities(cpu_only=True, jax_jit=False, allow_dask_compute=True)
+@_transition_to_rng("seed")
+def kmeans2(data, k, iter=10, thresh=1e-5, minit='random',
+            missing='warn', check_finite=True, *, rng=None):
+    """
+    Classify a set of observations into k clusters using the k-means algorithm.
+
+    The algorithm attempts to minimize the Euclidean distance between
+    observations and centroids. Several initialization methods are
+    included.
+
+    Parameters
+    ----------
+    data : ndarray
+        A 'M' by 'N' array of 'M' observations in 'N' dimensions or a length
+        'M' array of 'M' 1-D observations.
+    k : int or ndarray
+        The number of clusters to form as well as the number of
+        centroids to generate. If `minit` initialization string is
+        'matrix', or if a ndarray is given instead, it is
+        interpreted as initial cluster to use instead.
+    iter : int, optional
+        Number of iterations of the k-means algorithm to run. Note
+        that this differs in meaning from the iters parameter to
+        the kmeans function.
+    thresh : float, optional
+        (not used yet)
+    minit : str, optional
+        Method for initialization. Available methods are 'random',
+        'points', '++' and 'matrix':
+
+        'random': generate k centroids from a Gaussian with mean and
+        variance estimated from the data.
+
+        'points': choose k observations (rows) at random from data for
+        the initial centroids.
+
+        '++': choose k observations accordingly to the kmeans++ method
+        (careful seeding)
+
+        'matrix': interpret the k parameter as a k by M (or length k
+        array for 1-D data) array of initial centroids.
+    missing : str, optional
+        Method to deal with empty clusters. Available methods are
+        'warn' and 'raise':
+
+        'warn': give a warning and continue.
+
+        'raise': raise an ClusterError and terminate the algorithm.
+    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
+    rng : `numpy.random.Generator`, optional
+        Pseudorandom number generator state. When `rng` is None, a new
+        `numpy.random.Generator` is created using entropy from the
+        operating system. Types other than `numpy.random.Generator` are
+        passed to `numpy.random.default_rng` to instantiate a ``Generator``.
+
+    Returns
+    -------
+    centroid : ndarray
+        A 'k' by 'N' array of centroids found at the last iteration of
+        k-means.
+    label : ndarray
+        label[i] is the code or index of the centroid the
+        ith observation is closest to.
+
+    See Also
+    --------
+    kmeans
+
+    References
+    ----------
+    .. [1] D. Arthur and S. Vassilvitskii, "k-means++: the advantages of
+       careful seeding", Proceedings of the Eighteenth Annual ACM-SIAM Symposium
+       on Discrete Algorithms, 2007.
+
+    Examples
+    --------
+    >>> from scipy.cluster.vq import kmeans2
+    >>> import matplotlib.pyplot as plt
+    >>> import numpy as np
+
+    Create z, an array with shape (100, 2) containing a mixture of samples
+    from three multivariate normal distributions.
+
+    >>> rng = np.random.default_rng()
+    >>> a = rng.multivariate_normal([0, 6], [[2, 1], [1, 1.5]], size=45)
+    >>> b = rng.multivariate_normal([2, 0], [[1, -1], [-1, 3]], size=30)
+    >>> c = rng.multivariate_normal([6, 4], [[5, 0], [0, 1.2]], size=25)
+    >>> z = np.concatenate((a, b, c))
+    >>> rng.shuffle(z)
+
+    Compute three clusters.
+
+    >>> centroid, label = kmeans2(z, 3, minit='points')
+    >>> centroid
+    array([[ 2.22274463, -0.61666946],  # may vary
+           [ 0.54069047,  5.86541444],
+           [ 6.73846769,  4.01991898]])
+
+    How many points are in each cluster?
+
+    >>> counts = np.bincount(label)
+    >>> counts
+    array([29, 51, 20])  # may vary
+
+    Plot the clusters.
+
+    >>> w0 = z[label == 0]
+    >>> w1 = z[label == 1]
+    >>> w2 = z[label == 2]
+    >>> plt.plot(w0[:, 0], w0[:, 1], 'o', alpha=0.5, label='cluster 0')
+    >>> plt.plot(w1[:, 0], w1[:, 1], 'd', alpha=0.5, label='cluster 1')
+    >>> plt.plot(w2[:, 0], w2[:, 1], 's', alpha=0.5, label='cluster 2')
+    >>> plt.plot(centroid[:, 0], centroid[:, 1], 'k*', label='centroids')
+    >>> plt.axis('equal')
+    >>> plt.legend(shadow=True)
+    >>> plt.show()
+
+    """
+    if int(iter) < 1:
+        raise ValueError(f"Invalid iter ({iter}), must be a positive integer.")
+    try:
+        miss_meth = _valid_miss_meth[missing]
+    except KeyError as e:
+        raise ValueError(f"Unknown missing method {missing!r}") from e
+
+    if isinstance(k, int):
+        xp = array_namespace(data)
+    else:
+        xp = array_namespace(data, k)
+    data = _asarray(data, xp=xp, check_finite=check_finite)
+    code_book = xp_copy(k, xp=xp)
+    if data.ndim == 1:
+        d = 1
+    elif data.ndim == 2:
+        d = data.shape[1]
+    else:
+        raise ValueError("Input of rank > 2 is not supported.")
+
+    if xp_size(data) < 1 or xp_size(code_book) < 1:
+        raise ValueError("Empty input is not supported.")
+
+    # If k is not a single value, it should be compatible with data's shape
+    if minit == 'matrix' or xp_size(code_book) > 1:
+        if data.ndim != code_book.ndim:
+            raise ValueError("k array doesn't match data rank")
+        nc = code_book.shape[0]
+        if data.ndim > 1 and code_book.shape[1] != d:
+            raise ValueError("k array doesn't match data dimension")
+    else:
+        nc = int(code_book)
+
+        if nc < 1:
+            raise ValueError(
+                f"Cannot ask kmeans2 for {nc} clusters (k was {code_book})"
+            )
+        elif nc != code_book:
+            warnings.warn("k was not an integer, was converted.", stacklevel=2)
+
+        try:
+            init_meth = _valid_init_meth[minit]
+        except KeyError as e:
+            raise ValueError(f"Unknown init method {minit!r}") from e
+        else:
+            rng = check_random_state(rng)
+            code_book = init_meth(data, code_book, rng, xp)
+
+    data = np.asarray(data)
+    code_book = np.asarray(code_book)
+    for _ in range(iter):
+        # Compute the nearest neighbor for each obs using the current code book
+        label = vq(data, code_book, check_finite=check_finite)[0]
+        # Update the code book by computing centroids
+        new_code_book, has_members = _vq.update_cluster_means(data, label, nc)
+        if not has_members.all():
+            miss_meth()
+            # Set the empty clusters to their previous positions
+            new_code_book[~has_members] = code_book[~has_members]
+        code_book = new_code_book
+
+    return xp.asarray(code_book), xp.asarray(label)
diff --git a/.venv/lib/python3.11/site-packages/scipy/conftest.py b/.venv/lib/python3.11/site-packages/scipy/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..63676695bf6388f931acfc4374d770c0cac0e902
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/conftest.py
@@ -0,0 +1,708 @@
+# Pytest customization
+import json
+import multiprocessing
+import os
+import sys
+import warnings
+import tempfile
+from contextlib import contextmanager
+from typing import Literal
+
+import numpy as np
+import pytest
+try:
+    import hypothesis
+    hypothesis_available = True
+except ImportError:
+    hypothesis_available = False
+
+from scipy._lib._fpumode import get_fpu_mode
+from scipy._lib._array_api import (
+    SCIPY_ARRAY_API, SCIPY_DEVICE, array_namespace, default_xp,
+    is_cupy, is_dask, is_jax, is_torch,
+)
+from scipy._lib._testutils import FPUModeChangeWarning
+from scipy._lib.array_api_extra.testing import patch_lazy_xp_functions
+from scipy._lib import _pep440
+
+try:
+    from scipy_doctest.conftest import dt_config
+    HAVE_SCPDT = True
+except ModuleNotFoundError:
+    HAVE_SCPDT = False
+
+try:
+    import pytest_run_parallel  # noqa:F401
+    PARALLEL_RUN_AVAILABLE = True
+except Exception:
+    PARALLEL_RUN_AVAILABLE = False
+
+
+def pytest_configure(config):
+    """
+    Add pytest markers to avoid PytestUnknownMarkWarning
+
+    This needs to contain all markers that are SciPy-specific, as well as
+    dummy fallbacks for markers defined in optional test packages.
+
+    Note that we need both the registration here *and* in `pytest.ini`.
+    """
+    config.addinivalue_line("markers",
+        "slow: Tests that are very slow.")
+    config.addinivalue_line("markers",
+        "xslow: mark test as extremely slow (not run unless explicitly requested)")
+    config.addinivalue_line("markers",
+        "xfail_on_32bit: mark test as failing on 32-bit platforms")
+    config.addinivalue_line("markers",
+        "array_api_backends: test iterates on all array API backends")
+    config.addinivalue_line("markers",
+        ("skip_xp_backends(backends, reason=None, np_only=False, cpu_only=False, " +
+         "eager_only=False, exceptions=None): mark the desired skip configuration " +
+         "for the `skip_xp_backends` fixture"))
+    config.addinivalue_line("markers",
+        ("xfail_xp_backends(backends, reason=None, np_only=False, cpu_only=False, " +
+         "eager_only=False, exceptions=None): mark the desired xfail configuration " +
+         "for the `xfail_xp_backends` fixture"))
+
+    try:
+        import pytest_timeout  # noqa:F401
+    except Exception:
+        config.addinivalue_line(
+            "markers", 'timeout: mark a test for a non-default timeout')
+    try:
+        # This is a more reliable test of whether pytest_fail_slow is installed
+        # When I uninstalled it, `import pytest_fail_slow` didn't fail!
+        from pytest_fail_slow import parse_duration  # type: ignore[import-not-found] # noqa:F401,E501
+    except Exception:
+        config.addinivalue_line(
+            "markers", 'fail_slow: mark a test for a non-default timeout failure')
+
+    if not PARALLEL_RUN_AVAILABLE:
+        config.addinivalue_line(
+            'markers',
+            'parallel_threads_limit(n): run the given test function in parallel '
+            'using `n` threads.')
+        config.addinivalue_line(
+            "markers",
+            "thread_unsafe: mark the test function as single-threaded",
+        )
+        config.addinivalue_line(
+            "markers",
+            "iterations(n): run the given test function `n` times in each thread",
+        )
+
+    if os.name == 'posix' and sys.version_info < (3, 14):
+        # On POSIX, Python 3.13 and older uses the 'fork' context by
+        # default. Calling fork() from multiple threads leads to
+        # deadlocks. This has been changed in 3.14 to 'forkserver'.
+        multiprocessing.set_start_method('forkserver', force=True)
+
+
+def pytest_runtest_setup(item):
+    mark = item.get_closest_marker("xslow")
+    if mark is not None:
+        try:
+            v = int(os.environ.get('SCIPY_XSLOW', '0'))
+        except ValueError:
+            v = False
+        if not v:
+            pytest.skip("very slow test; "
+                        "set environment variable SCIPY_XSLOW=1 to run it")
+    mark = item.get_closest_marker("xfail_on_32bit")
+    if mark is not None and np.intp(0).itemsize < 8:
+        pytest.xfail(f'Fails on our 32-bit test platform(s): {mark.args[0]}')
+
+    # Older versions of threadpoolctl have an issue that may lead to this
+    # warning being emitted, see gh-14441
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore", pytest.PytestUnraisableExceptionWarning)
+
+        try:
+            from threadpoolctl import threadpool_limits
+
+            HAS_THREADPOOLCTL = True
+        except Exception:  # observed in gh-14441: (ImportError, AttributeError)
+            # Optional dependency only. All exceptions are caught, for robustness
+            HAS_THREADPOOLCTL = False
+
+        if HAS_THREADPOOLCTL:
+            # Set the number of openmp threads based on the number of workers
+            # xdist is using to prevent oversubscription. Simplified version of what
+            # sklearn does (it can rely on threadpoolctl and its builtin OpenMP helper
+            # functions)
+            try:
+                xdist_worker_count = int(os.environ['PYTEST_XDIST_WORKER_COUNT'])
+            except KeyError:
+                # raises when pytest-xdist is not installed
+                return
+
+            if not os.getenv('OMP_NUM_THREADS'):
+                max_openmp_threads = os.cpu_count() // 2  # use nr of physical cores
+                threads_per_worker = max(max_openmp_threads // xdist_worker_count, 1)
+                try:
+                    threadpool_limits(threads_per_worker, user_api='blas')
+                except Exception:
+                    # May raise AttributeError for older versions of OpenBLAS.
+                    # Catch any error for robustness.
+                    return
+
+
+@pytest.fixture(scope="function", autouse=True)
+def check_fpu_mode(request):
+    """
+    Check FPU mode was not changed during the test.
+    """
+    old_mode = get_fpu_mode()
+    yield
+    new_mode = get_fpu_mode()
+
+    if old_mode != new_mode:
+        warnings.warn(f"FPU mode changed from {old_mode:#x} to {new_mode:#x} during "
+                      "the test",
+                      category=FPUModeChangeWarning, stacklevel=0)
+
+
+if not PARALLEL_RUN_AVAILABLE:
+    @pytest.fixture
+    def num_parallel_threads():
+        return 1
+
+
+# Array API backend handling
+xp_known_backends = {'numpy', 'array_api_strict', 'torch', 'cupy', 'jax.numpy',
+                     'dask.array'}
+xp_available_backends = [
+    pytest.param(np, id='numpy', marks=pytest.mark.array_api_backends)
+]
+xp_skip_cpu_only_backends = set()
+xp_skip_eager_only_backends = set()
+
+if SCIPY_ARRAY_API:
+    # fill the dict of backends with available libraries
+    try:
+        import array_api_strict
+        xp_available_backends.append(
+            pytest.param(array_api_strict, id='array_api_strict',
+                         marks=pytest.mark.array_api_backends))
+        if _pep440.parse(array_api_strict.__version__) < _pep440.Version('2.3'):
+            raise ImportError("array-api-strict must be >= version 2.3")
+        array_api_strict.set_array_api_strict_flags(
+            api_version='2024.12'
+        )
+    except ImportError:
+        pass
+
+    try:
+        import torch  # type: ignore[import-not-found]
+        xp_available_backends.append(
+            pytest.param(torch, id='torch',
+            marks=pytest.mark.array_api_backends))
+        torch.set_default_device(SCIPY_DEVICE)
+        if SCIPY_DEVICE != "cpu":
+            xp_skip_cpu_only_backends.add('torch')
+
+        # default to float64 unless explicitly requested
+        default = os.getenv('SCIPY_DEFAULT_DTYPE', default='float64')
+        if default == 'float64':
+            torch.set_default_dtype(torch.float64)
+        elif default != "float32":
+            raise ValueError(
+                "SCIPY_DEFAULT_DTYPE env var, if set, can only be either 'float64' "
+               f"or 'float32'. Got '{default}' instead."
+            )
+    except ImportError:
+        pass
+
+    try:
+        import cupy  # type: ignore[import-not-found]
+        # Note: cupy disregards SCIPY_DEVICE and always runs on cuda.
+        # It will fail to import if you don't have CUDA hardware and drivers.
+        xp_available_backends.append(
+            pytest.param(cupy, id='cupy',
+            marks=pytest.mark.array_api_backends))
+        xp_skip_cpu_only_backends.add('cupy')
+
+        # this is annoying in CuPy 13.x
+        warnings.filterwarnings(
+            'ignore', 'cupyx.jit.rawkernel is experimental', category=FutureWarning
+        )
+        from cupyx.scipy import signal
+        del signal
+    except ImportError:
+        pass
+
+    try:
+        import jax.numpy  # type: ignore[import-not-found]
+        
+        xp_available_backends.append(
+            pytest.param(jax.numpy, id='jax.numpy',
+            marks=[pytest.mark.array_api_backends,
+                   # Uses xpx.testing.patch_lazy_xp_functions to monkey-patch module
+                   pytest.mark.thread_unsafe]))
+
+        jax.config.update("jax_enable_x64", True)
+        jax.config.update("jax_default_device", jax.devices(SCIPY_DEVICE)[0])
+        if SCIPY_DEVICE != "cpu":
+            xp_skip_cpu_only_backends.add('jax.numpy')
+        # JAX can be eager or lazy (when wrapped in jax.jit). However it is
+        # recommended by upstream devs to assume it's always lazy.
+        xp_skip_eager_only_backends.add('jax.numpy')
+    except ImportError:
+        pass
+
+    try:
+        import dask.array as da
+
+        xp_available_backends.append(
+            pytest.param(da, id='dask.array',
+            marks=[pytest.mark.array_api_backends,
+                   # Uses xpx.testing.patch_lazy_xp_functions to monkey-patch module
+                   pytest.mark.thread_unsafe]))
+
+        # Dask can wrap around cupy. However, this is untested in scipy
+        # (and will almost surely not work as delegation will misbehave).
+
+        # Dask, strictly speaking, can be eager, in the sense that
+        # __array__, __bool__ etc. are implemented and do not raise.
+        # However, calling them triggers an extra computation of the whole graph
+        # until that point, which is highly destructive for performance.
+        xp_skip_eager_only_backends.add('dask.array')
+    except ImportError:
+        pass
+
+    xp_available_backend_ids = {p.id for p in xp_available_backends}
+    assert not xp_available_backend_ids - xp_known_backends
+
+    # by default, use all available backends
+    if (
+        isinstance(SCIPY_ARRAY_API, str)
+        and SCIPY_ARRAY_API.lower() not in ("1", "true", "all")
+    ):
+        SCIPY_ARRAY_API_ = set(json.loads(SCIPY_ARRAY_API))
+        if SCIPY_ARRAY_API_ != {'all'}:
+            if SCIPY_ARRAY_API_ - xp_available_backend_ids:
+                msg = ("'--array-api-backend' must be in "
+                       f"{xp_available_backend_ids}; got {SCIPY_ARRAY_API_}")
+                raise ValueError(msg)
+            # Only select a subset of backends
+            xp_available_backends = [
+                param for param in xp_available_backends
+                if param.id in SCIPY_ARRAY_API_
+            ]
+
+
+@pytest.fixture(params=xp_available_backends)
+def xp(request):
+    """Run the test that uses this fixture on each available array API library.
+
+    You can select all and only the tests that use the `xp` fixture by
+    passing `-m array_api_backends` to pytest.
+
+    You can select where individual tests run through the `@skip_xp_backends`,
+    `@xfail_xp_backends`, and `@skip_xp_invalid_arg` pytest markers.
+
+    Please read: https://docs.scipy.org/doc/scipy/dev/api-dev/array_api.html#adding-tests
+    """
+    # Read all @pytest.marks.skip_xp_backends markers that decorate to the test,
+    # if any, and raise pytest.skip() if the current xp is in the list.
+    skip_or_xfail_xp_backends(request, "skip")
+    # Read all @pytest.marks.xfail_xp_backends markers that decorate the test,
+    # if any, and raise pytest.xfail() if the current xp is in the list.
+    skip_or_xfail_xp_backends(request, "xfail")
+
+    xp = request.param
+    # Potentially wrap namespace with array_api_compat
+    xp = array_namespace(xp.empty(0))
+
+    if SCIPY_ARRAY_API:
+        # If xp==jax.numpy, wrap tested functions in jax.jit
+        # If xp==dask.array, wrap tested functions to test that graph is not computed
+        with patch_lazy_xp_functions(request=request, xp=request.param):
+            # Throughout all calls to assert_almost_equal, assert_array_almost_equal,
+            # and xp_assert_* functions, test that the array namespace is xp in both
+            # the expected and actual arrays. This is to detect the case where both
+            # arrays are erroneously just plain numpy while xp is something else.
+            with default_xp(xp):
+                yield xp
+    else:
+        yield xp
+
+
+skip_xp_invalid_arg = pytest.mark.skipif(SCIPY_ARRAY_API,
+    reason = ('Test involves masked arrays, object arrays, or other types '
+              'that are not valid input when `SCIPY_ARRAY_API` is used.'))
+
+
+def _backends_kwargs_from_request(request, skip_or_xfail):
+    """A helper for {skip,xfail}_xp_backends.
+
+    Return dict of {backend to skip/xfail: top reason to skip/xfail it}
+    """
+    markers = list(request.node.iter_markers(f'{skip_or_xfail}_xp_backends'))
+    reasons = {backend: [] for backend in xp_known_backends}
+
+    for marker in markers:
+        invalid_kwargs = set(marker.kwargs) - {
+            "cpu_only", "np_only", "eager_only", "reason", "exceptions"}
+        if invalid_kwargs:
+            raise TypeError(f"Invalid kwargs: {invalid_kwargs}")
+
+        exceptions = set(marker.kwargs.get('exceptions', []))
+        invalid_exceptions = exceptions - xp_known_backends
+        if (invalid_exceptions := list(exceptions - xp_known_backends)):
+            raise ValueError(f"Unknown backend(s): {invalid_exceptions}; "
+                             f"must be a subset of {list(xp_known_backends)}")
+
+        if marker.kwargs.get('np_only', False):
+            reason = marker.kwargs.get("reason") or "do not run with non-NumPy backends"
+            for backend, backend_reasons in reasons.items():
+                if backend != 'numpy' and backend not in exceptions:
+                    backend_reasons.append(reason)
+
+        elif marker.kwargs.get('cpu_only', False):
+            reason = marker.kwargs.get("reason") or (
+                "no array-agnostic implementation or delegation available "
+                "for this backend and device")
+            for backend in xp_skip_cpu_only_backends - exceptions:
+                reasons[backend].append(reason)
+
+        elif marker.kwargs.get('eager_only', False):
+            reason = marker.kwargs.get("reason") or (
+                "eager checks not executed on lazy backends")
+            for backend in xp_skip_eager_only_backends - exceptions:
+                reasons[backend].append(reason)
+
+        # add backends, if any
+        if len(marker.args) == 1:
+            backend = marker.args[0]
+            if backend not in xp_known_backends:
+                raise ValueError(f"Unknown backend: {backend}; "
+                                 f"must be one of {list(xp_known_backends)}")
+            reason = marker.kwargs.get("reason") or (
+                f"do not run with array API backend: {backend}")
+            # reason overrides the ones from cpu_only, np_only, and eager_only.
+            # This is regardless of order of appearence of the markers.
+            reasons[backend].insert(0, reason)
+
+            for kwarg in ("cpu_only", "np_only", "eager_only", "exceptions"):
+                if kwarg in marker.kwargs:
+                    raise ValueError(f"{kwarg} is mutually exclusive with {backend}")
+
+        elif len(marker.args) > 1:
+            raise ValueError(
+                f"Please specify only one backend per marker: {marker.args}"
+            )
+
+    return {backend: backend_reasons[0]
+            for backend, backend_reasons in reasons.items()
+            if backend_reasons}
+
+
+def skip_or_xfail_xp_backends(request: pytest.FixtureRequest,
+                              skip_or_xfail: Literal['skip', 'xfail']) -> None:
+    """
+    Helper of the `xp` fixture.
+    Skip or xfail based on the ``skip_xp_backends`` or ``xfail_xp_backends`` markers.
+
+    See the "Support for the array API standard" docs page for usage examples.
+
+    Usage
+    -----
+    ::
+        skip_xp_backends = pytest.mark.skip_xp_backends
+        xfail_xp_backends = pytest.mark.xfail_xp_backends
+        ...
+
+        @skip_xp_backends(backend, *, reason=None)
+        @skip_xp_backends(*, cpu_only=True, exceptions=(), reason=None)
+        @skip_xp_backends(*, eager_only=True, exceptions=(), reason=None)
+        @skip_xp_backends(*, np_only=True, exceptions=(), reason=None)
+
+        @xfail_xp_backends(backend, *, reason=None)
+        @xfail_xp_backends(*, cpu_only=True, exceptions=(), reason=None)
+        @xfail_xp_backends(*, eager_only=True, exceptions=(), reason=None)
+        @xfail_xp_backends(*, np_only=True, exceptions=(), reason=None)
+
+    Parameters
+    ----------
+    backend : str, optional
+        Backend to skip/xfail, e.g. ``"torch"``.
+        Mutually exclusive with ``cpu_only``, ``eager_only``, and ``np_only``.
+    cpu_only : bool, optional
+        When ``True``, the test is skipped/xfailed on non-CPU devices,
+        minus exceptions. Mutually exclusive with ``backend``.
+    eager_only : bool, optional
+        When ``True``, the test is skipped/xfailed for lazy backends, e.g. those
+        with major caveats when invoking ``__array__``, ``__bool__``, ``__float__``,
+        or ``__complex__``, minus exceptions. Mutually exclusive with ``backend``.
+    np_only : bool, optional
+        When ``True``, the test is skipped/xfailed for all backends other
+        than the default NumPy backend and the exceptions.
+        Mutually exclusive with ``backend``. Implies ``cpu_only`` and ``eager_only``.
+    reason : str, optional
+        A reason for the skip/xfail. If omitted, a default reason is used.
+    exceptions : list[str], optional
+        A list of exceptions for use with ``cpu_only``, ``eager_only``, or ``np_only``.
+        This should be provided when delegation is implemented for some,
+        but not all, non-CPU/non-NumPy backends.
+    """
+    if f"{skip_or_xfail}_xp_backends" not in request.keywords:
+        return
+
+    skip_xfail_reasons = _backends_kwargs_from_request(
+        request, skip_or_xfail=skip_or_xfail
+    )
+    xp = request.param
+    if xp.__name__ in skip_xfail_reasons:
+        reason = skip_xfail_reasons[xp.__name__]
+        assert reason  # Default reason applied above
+        skip_or_xfail = getattr(pytest, skip_or_xfail)
+        skip_or_xfail(reason=reason)
+
+
+@pytest.fixture
+def devices(xp):
+    """Fixture that returns a list of all devices for the backend, plus None.
+    Used to test input->output device propagation.
+
+    Usage
+    -----
+    from scipy._lib._array_api import xp_device
+
+    def test_device(xp, devices):
+        for d in devices:
+            x = xp.asarray(..., device=d)
+            y = f(x)
+            assert xp_device(y) == xp_device(x)
+    """
+    if is_cupy(xp):
+        # CuPy does not support devices other than the current one
+        # data-apis/array-api-compat#293
+        pytest.xfail(reason="data-apis/array-api-compat#293")
+    if is_dask(xp):
+        # Skip dummy DASK_DEVICE from array-api-compat, which does not propagate
+        return ["cpu", None]
+    if is_jax(xp):
+        # The .device attribute is not accessible inside jax.jit; the consequence
+        # (downstream of array-api-compat hacks) is that a non-default device in
+        # input is not guaranteed to propagate to the output even if the scipy code
+        # states `device=xp_device(arg)`` in all array creation functions.
+        # While this issue is specific to jax.jit, it would be unnecessarily
+        # verbose to skip the test for each jit-capable function and run it for
+        # those that only support eager mode.
+        pytest.xfail(reason="jax-ml/jax#26000")
+    if is_torch(xp):
+        devices = xp.__array_namespace_info__().devices()
+        # open an issue about this - cannot branch based on `any`/`all`?
+        return (device for device in devices if device.type != 'meta')
+
+    return xp.__array_namespace_info__().devices() + [None]
+
+
+if hypothesis_available:
+    # Following the approach of NumPy's conftest.py...
+    # Use a known and persistent tmpdir for hypothesis' caches, which
+    # can be automatically cleared by the OS or user.
+    hypothesis.configuration.set_hypothesis_home_dir(
+        os.path.join(tempfile.gettempdir(), ".hypothesis")
+    )
+
+    # We register two custom profiles for SciPy - for details see
+    # https://hypothesis.readthedocs.io/en/latest/settings.html
+    # The first is designed for our own CI runs; the latter also
+    # forces determinism and is designed for use via scipy.test()
+    hypothesis.settings.register_profile(
+        name="nondeterministic", deadline=None, print_blob=True,
+    )
+    hypothesis.settings.register_profile(
+        name="deterministic",
+        deadline=None, print_blob=True, database=None, derandomize=True,
+        suppress_health_check=list(hypothesis.HealthCheck),
+    )
+
+    # Profile is currently set by environment variable `SCIPY_HYPOTHESIS_PROFILE`
+    # In the future, it would be good to work the choice into `.spin/cmds.py`.
+    SCIPY_HYPOTHESIS_PROFILE = os.environ.get("SCIPY_HYPOTHESIS_PROFILE",
+                                              "deterministic")
+    hypothesis.settings.load_profile(SCIPY_HYPOTHESIS_PROFILE)
+
+
+############################################################################
+# doctesting stuff
+
+if HAVE_SCPDT:
+
+    # FIXME: populate the dict once
+    @contextmanager
+    def warnings_errors_and_rng(test=None):
+        """Temporarily turn (almost) all warnings to errors.
+
+        Filter out known warnings which we allow.
+        """
+        known_warnings = dict()
+
+        # these functions are known to emit "divide by zero" RuntimeWarnings
+        divide_by_zero = [
+            'scipy.linalg.norm', 'scipy.ndimage.center_of_mass',
+        ]
+        for name in divide_by_zero:
+            known_warnings[name] = dict(category=RuntimeWarning,
+                                        message='divide by zero')
+
+        # Deprecated stuff
+        deprecated = []
+        for name in deprecated:
+            known_warnings[name] = dict(category=DeprecationWarning)
+
+        from scipy import integrate
+        # the functions are known to emit IntegrationWarnings
+        integration_w = ['scipy.special.ellip_normal',
+                         'scipy.special.ellip_harm_2',
+        ]
+        for name in integration_w:
+            known_warnings[name] = dict(category=integrate.IntegrationWarning,
+                                        message='The occurrence of roundoff')
+
+        # scipy.stats deliberately emits UserWarnings sometimes
+        user_w = ['scipy.stats.anderson_ksamp', 'scipy.stats.kurtosistest',
+                  'scipy.stats.normaltest', 'scipy.sparse.linalg.norm']
+        for name in user_w:
+            known_warnings[name] = dict(category=UserWarning)
+
+        # additional one-off warnings to filter
+        dct = {
+            'scipy.sparse.linalg.norm':
+                dict(category=UserWarning, message="Exited at iteration"),
+            # tutorials
+            'linalg.rst':
+                dict(message='the matrix subclass is not',
+                     category=PendingDeprecationWarning),
+            'stats.rst':
+                dict(message='The maximum number of subdivisions',
+                     category=integrate.IntegrationWarning),
+        }
+        known_warnings.update(dct)
+
+        # these legitimately emit warnings in examples
+        legit = set('scipy.signal.normalize')
+
+        # Now, the meat of the matter: filter warnings,
+        # also control the random seed for each doctest.
+
+        # XXX: this matches the refguide-check behavior, but is a tad strange:
+        # makes sure that the seed the old-fashioned np.random* methods is
+        # *NOT* reproducible but the new-style `default_rng()` *IS* repoducible.
+        # Should these two be either both repro or both not repro?
+
+        from scipy._lib._util import _fixed_default_rng
+        import numpy as np
+        with _fixed_default_rng():
+            np.random.seed(None)
+            with warnings.catch_warnings():
+                if test and test.name in known_warnings:
+                    warnings.filterwarnings('ignore', **known_warnings[test.name])
+                    yield
+                elif test and test.name in legit:
+                    yield
+                else:
+                    warnings.simplefilter('error', Warning)
+                    warnings.filterwarnings('ignore', ".*odr.*", DeprecationWarning)
+                    yield
+
+    dt_config.user_context_mgr = warnings_errors_and_rng
+    dt_config.skiplist = set([
+        'scipy.linalg.LinAlgError',     # comes from numpy
+        'scipy.fftpack.fftshift',       # fftpack stuff is also from numpy
+        'scipy.fftpack.ifftshift',
+        'scipy.fftpack.fftfreq',
+        'scipy.special.sinc',           # sinc is from numpy
+        'scipy.optimize.show_options',  # does not have much to doctest
+        'scipy.signal.normalize',       # manipulates warnings (XXX temp skip)
+        'scipy.sparse.linalg.norm',     # XXX temp skip
+        # these below test things which inherit from np.ndarray
+        # cross-ref https://github.com/numpy/numpy/issues/28019
+        'scipy.io.matlab.MatlabObject.strides',
+        'scipy.io.matlab.MatlabObject.dtype',
+        'scipy.io.matlab.MatlabOpaque.dtype',
+        'scipy.io.matlab.MatlabOpaque.strides',
+        'scipy.io.matlab.MatlabFunction.strides',
+        'scipy.io.matlab.MatlabFunction.dtype'
+    ])
+
+    # these are affected by NumPy 2.0 scalar repr: rely on string comparison
+    if np.__version__ < "2":
+        dt_config.skiplist.update(set([
+            'scipy.io.hb_read',
+            'scipy.io.hb_write',
+            'scipy.sparse.csgraph.connected_components',
+            'scipy.sparse.csgraph.depth_first_order',
+            'scipy.sparse.csgraph.shortest_path',
+            'scipy.sparse.csgraph.floyd_warshall',
+            'scipy.sparse.csgraph.dijkstra',
+            'scipy.sparse.csgraph.bellman_ford',
+            'scipy.sparse.csgraph.johnson',
+            'scipy.sparse.csgraph.yen',
+            'scipy.sparse.csgraph.breadth_first_order',
+            'scipy.sparse.csgraph.reverse_cuthill_mckee',
+            'scipy.sparse.csgraph.structural_rank',
+            'scipy.sparse.csgraph.construct_dist_matrix',
+            'scipy.sparse.csgraph.reconstruct_path',
+            'scipy.ndimage.value_indices',
+            'scipy.stats.mstats.describe',
+    ]))
+
+    # help pytest collection a bit: these names are either private
+    # (distributions), or just do not need doctesting.
+    dt_config.pytest_extra_ignore = [
+        "scipy.stats.distributions",
+        "scipy.optimize.cython_optimize",
+        "scipy.test",
+        "scipy.show_config",
+        # equivalent to "pytest --ignore=path/to/file"
+        "scipy/special/_precompute",
+        "scipy/interpolate/_interpnd_info.py",
+        "scipy/interpolate/_rbfinterp_pythran.py",
+        "scipy/_build_utils/tempita.py",
+        "scipy/_lib/array_api_compat",
+        "scipy/_lib/highs",
+        "scipy/_lib/unuran",
+        "scipy/_lib/_gcutils.py",
+        "scipy/_lib/doccer.py",
+        "scipy/_lib/_uarray",
+        "scipy/linalg/_cython_signature_generator.py",
+        "scipy/linalg/_generate_pyx.py",
+        "scipy/linalg/_linalg_pythran.py",
+        "scipy/linalg/_matfuncs_sqrtm_triu.py",
+        "scipy/ndimage/utils/generate_label_testvectors.py",
+        "scipy/optimize/_group_columns.py",
+        "scipy/optimize/_max_len_seq_inner.py",
+        "scipy/signal/_max_len_seq_inner.py",
+        "scipy/sparse/_generate_sparsetools.py",
+        "scipy/special/_generate_pyx.py",
+        "scipy/stats/_stats_pythran.py",
+    ]
+
+    dt_config.pytest_extra_xfail = {
+        # name: reason
+        "ND_regular_grid.rst": "ReST parser limitation",
+        "extrapolation_examples.rst": "ReST parser limitation",
+        "sampling_pinv.rst": "__cinit__ unexpected argument",
+        "sampling_srou.rst": "nan in scalar_power",
+        "probability_distributions.rst": "integration warning",
+    }
+
+    # tutorials
+    dt_config.pseudocode = set(['integrate.nquad(func,'])
+    dt_config.local_resources = {
+        'io.rst': [
+            "octave_a.mat",
+            "octave_cells.mat",
+            "octave_struct.mat"
+        ]
+    }
+
+    dt_config.strict_check = True
+
+    # ignore Matplotlib's `ax.text`:
+    dt_config.stopwords.add('.text(')
+############################################################################
diff --git a/.venv/lib/python3.11/site-packages/scipy/constants/__init__.py b/.venv/lib/python3.11/site-packages/scipy/constants/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..fdf939b249c17256b622c2f2756a5f34c4a128cc
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/constants/__init__.py
@@ -0,0 +1,358 @@
+r"""
+==================================
+Constants (:mod:`scipy.constants`)
+==================================
+
+.. currentmodule:: scipy.constants
+
+Physical and mathematical constants and units.
+
+
+Mathematical constants
+======================
+
+================  =================================================================
+``pi``            Pi
+``golden``        Golden ratio
+``golden_ratio``  Golden ratio
+================  =================================================================
+
+
+Physical constants
+==================
+The following physical constants are available as attributes of `scipy.constants`.
+All units are `SI `_.
+
+===========================  ================================================================  ===============
+Attribute                    Quantity                                                          Units
+===========================  ================================================================  ===============
+``c``                        speed of light in vacuum                                          m s^-1
+``speed_of_light``           speed of light in vacuum                                          m s^-1
+``mu_0``                     the magnetic constant :math:`\mu_0`                               N A^-2
+``epsilon_0``                the electric constant (vacuum permittivity), :math:`\epsilon_0`   F m^-1
+``h``                        the Planck constant :math:`h`                                     J Hz^-1
+``Planck``                   the Planck constant :math:`h`                                     J Hz^-1
+``hbar``                     the reduced Planck constant, :math:`\hbar = h/(2\pi)`             J s
+``G``                        Newtonian constant of gravitation                                 m^3 kg^-1 s^-2
+``gravitational_constant``   Newtonian constant of gravitation                                 m^3 kg^-1 s^-2
+``g``                        standard acceleration of gravity                                  m s^-2
+``e``                        elementary charge                                                 C
+``elementary_charge``        elementary charge                                                 C
+``R``                        molar gas constant                                                J mol^-1 K^-1
+``gas_constant``             molar gas constant                                                J mol^-1 K^-1
+``alpha``                    fine-structure constant                                           (unitless)
+``fine_structure``           fine-structure constant                                           (unitless)
+``N_A``                      Avogadro constant                                                 mol^-1
+``Avogadro``                 Avogadro constant                                                 mol^-1
+``k``                        Boltzmann constant                                                J K^-1
+``Boltzmann``                Boltzmann constant                                                J K^-1
+``sigma``                    Stefan-Boltzmann constant :math:`\sigma`                          W m^-2 K^-4
+``Stefan_Boltzmann``         Stefan-Boltzmann constant :math:`\sigma`                          W m^-2 K^-4
+``Wien``                     Wien wavelength displacement law constant                         m K
+``Rydberg``                  Rydberg constant                                                  m^-1
+``m_e``                      electron mass                                                     kg
+``electron_mass``            electron mass                                                     kg
+``m_p``                      proton mass                                                       kg
+``proton_mass``              proton mass                                                       kg
+``m_n``                      neutron mass                                                      kg
+``neutron_mass``             neutron mass                                                      kg
+===========================  ================================================================  ===============
+
+
+Constants database
+------------------
+
+In addition to the above variables, :mod:`scipy.constants` also contains the
+2022 CODATA recommended values [CODATA2022]_ database containing more physical
+constants.
+
+.. autosummary::
+   :toctree: generated/
+
+   value      -- Value in physical_constants indexed by key
+   unit       -- Unit in physical_constants indexed by key
+   precision  -- Relative precision in physical_constants indexed by key
+   find       -- Return list of physical_constant keys with a given string
+   ConstantWarning -- Constant sought not in newest CODATA data set
+
+.. data:: physical_constants
+
+   Dictionary of physical constants, of the format
+   ``physical_constants[name] = (value, unit, uncertainty)``.
+   The CODATA database uses ellipses to indicate that a value is defined
+   (exactly) in terms of others but cannot be represented exactly with the
+   allocated number of digits. In these cases, SciPy calculates the derived
+   value and reports it to the full precision of a Python ``float``. Although 
+   ``physical_constants`` lists the uncertainty as ``0.0`` to indicate that
+   the CODATA value is exact, the value in ``physical_constants`` is still
+   subject to the truncation error inherent in double-precision representation.
+
+Available constants:
+
+======================================================================  ====
+%(constant_names)s
+======================================================================  ====
+
+
+Units
+=====
+
+SI prefixes
+-----------
+
+============  =================================================================
+``quetta``    :math:`10^{30}`
+``ronna``     :math:`10^{27}`
+``yotta``     :math:`10^{24}`
+``zetta``     :math:`10^{21}`
+``exa``       :math:`10^{18}`
+``peta``      :math:`10^{15}`
+``tera``      :math:`10^{12}`
+``giga``      :math:`10^{9}`
+``mega``      :math:`10^{6}`
+``kilo``      :math:`10^{3}`
+``hecto``     :math:`10^{2}`
+``deka``      :math:`10^{1}`
+``deci``      :math:`10^{-1}`
+``centi``     :math:`10^{-2}`
+``milli``     :math:`10^{-3}`
+``micro``     :math:`10^{-6}`
+``nano``      :math:`10^{-9}`
+``pico``      :math:`10^{-12}`
+``femto``     :math:`10^{-15}`
+``atto``      :math:`10^{-18}`
+``zepto``     :math:`10^{-21}`
+``yocto``     :math:`10^{-24}`
+``ronto``     :math:`10^{-27}`
+``quecto``    :math:`10^{-30}`
+============  =================================================================
+
+Binary prefixes
+---------------
+
+============  =================================================================
+``kibi``      :math:`2^{10}`
+``mebi``      :math:`2^{20}`
+``gibi``      :math:`2^{30}`
+``tebi``      :math:`2^{40}`
+``pebi``      :math:`2^{50}`
+``exbi``      :math:`2^{60}`
+``zebi``      :math:`2^{70}`
+``yobi``      :math:`2^{80}`
+============  =================================================================
+
+Mass
+----
+
+=================  ============================================================
+``gram``           :math:`10^{-3}` kg
+``metric_ton``     :math:`10^{3}` kg
+``grain``          one grain in kg
+``lb``             one pound (avoirdupous) in kg
+``pound``          one pound (avoirdupous) in kg
+``blob``           one inch version of a slug in kg (added in 1.0.0)
+``slinch``         one inch version of a slug in kg (added in 1.0.0)
+``slug``           one slug in kg (added in 1.0.0)
+``oz``             one ounce in kg
+``ounce``          one ounce in kg
+``stone``          one stone in kg
+``grain``          one grain in kg
+``long_ton``       one long ton in kg
+``short_ton``      one short ton in kg
+``troy_ounce``     one Troy ounce in kg
+``troy_pound``     one Troy pound in kg
+``carat``          one carat in kg
+``m_u``            atomic mass constant (in kg)
+``u``              atomic mass constant (in kg)
+``atomic_mass``    atomic mass constant (in kg)
+=================  ============================================================
+
+Angle
+-----
+
+=================  ============================================================
+``degree``         degree in radians
+``arcmin``         arc minute in radians
+``arcminute``      arc minute in radians
+``arcsec``         arc second in radians
+``arcsecond``      arc second in radians
+=================  ============================================================
+
+
+Time
+----
+
+=================  ============================================================
+``minute``         one minute in seconds
+``hour``           one hour in seconds
+``day``            one day in seconds
+``week``           one week in seconds
+``year``           one year (365 days) in seconds
+``Julian_year``    one Julian year (365.25 days) in seconds
+=================  ============================================================
+
+
+Length
+------
+
+=====================  ============================================================
+``inch``               one inch in meters
+``foot``               one foot in meters
+``yard``               one yard in meters
+``mile``               one mile in meters
+``mil``                one mil in meters
+``pt``                 one point in meters
+``point``              one point in meters
+``survey_foot``        one survey foot in meters
+``survey_mile``        one survey mile in meters
+``nautical_mile``      one nautical mile in meters
+``fermi``              one Fermi in meters
+``angstrom``           one Angstrom in meters
+``micron``             one micron in meters
+``au``                 one astronomical unit in meters
+``astronomical_unit``  one astronomical unit in meters
+``light_year``         one light year in meters
+``parsec``             one parsec in meters
+=====================  ============================================================
+
+Pressure
+--------
+
+=================  ============================================================
+``atm``            standard atmosphere in pascals
+``atmosphere``     standard atmosphere in pascals
+``bar``            one bar in pascals
+``torr``           one torr (mmHg) in pascals
+``mmHg``           one torr (mmHg) in pascals
+``psi``            one psi in pascals
+=================  ============================================================
+
+Area
+----
+
+=================  ============================================================
+``hectare``        one hectare in square meters
+``acre``           one acre in square meters
+=================  ============================================================
+
+
+Volume
+------
+
+===================    ========================================================
+``liter``              one liter in cubic meters
+``litre``              one liter in cubic meters
+``gallon``             one gallon (US) in cubic meters
+``gallon_US``          one gallon (US) in cubic meters
+``gallon_imp``         one gallon (UK) in cubic meters
+``fluid_ounce``        one fluid ounce (US) in cubic meters
+``fluid_ounce_US``     one fluid ounce (US) in cubic meters
+``fluid_ounce_imp``    one fluid ounce (UK) in cubic meters
+``bbl``                one barrel in cubic meters
+``barrel``             one barrel in cubic meters
+===================    ========================================================
+
+Speed
+-----
+
+==================    ==========================================================
+``kmh``               kilometers per hour in meters per second
+``mph``               miles per hour in meters per second
+``mach``              one Mach (approx., at 15 C, 1 atm) in meters per second
+``speed_of_sound``    one Mach (approx., at 15 C, 1 atm) in meters per second
+``knot``              one knot in meters per second
+==================    ==========================================================
+
+
+Temperature
+-----------
+
+=====================  =======================================================
+``zero_Celsius``       zero of Celsius scale in Kelvin
+``degree_Fahrenheit``  one Fahrenheit (only differences) in Kelvins
+=====================  =======================================================
+
+.. autosummary::
+   :toctree: generated/
+
+   convert_temperature
+
+Energy
+------
+
+====================  =======================================================
+``eV``                one electron volt in Joules
+``electron_volt``     one electron volt in Joules
+``calorie``           one calorie (thermochemical) in Joules
+``calorie_th``        one calorie (thermochemical) in Joules
+``calorie_IT``        one calorie (International Steam Table calorie, 1956) in Joules
+``erg``               one erg in Joules
+``Btu``               one British thermal unit (International Steam Table) in Joules
+``Btu_IT``            one British thermal unit (International Steam Table) in Joules
+``Btu_th``            one British thermal unit (thermochemical) in Joules
+``ton_TNT``           one ton of TNT in Joules
+====================  =======================================================
+
+Power
+-----
+
+====================  =======================================================
+``hp``                one horsepower in watts
+``horsepower``        one horsepower in watts
+====================  =======================================================
+
+Force
+-----
+
+====================  =======================================================
+``dyn``               one dyne in newtons
+``dyne``              one dyne in newtons
+``lbf``               one pound force in newtons
+``pound_force``       one pound force in newtons
+``kgf``               one kilogram force in newtons
+``kilogram_force``    one kilogram force in newtons
+====================  =======================================================
+
+Optics
+------
+
+.. autosummary::
+   :toctree: generated/
+
+   lambda2nu
+   nu2lambda
+
+References
+==========
+
+.. [CODATA2022] CODATA Recommended Values of the Fundamental
+   Physical Constants 2022.
+
+   https://physics.nist.gov/cuu/Constants/
+
+"""  # noqa: E501
+# Modules contributed by BasSw (wegwerp@gmail.com)
+from ._codata import *
+from ._constants import *
+from ._codata import _obsolete_constants, physical_constants
+
+# Deprecated namespaces, to be removed in v2.0.0
+from . import codata, constants
+
+_constant_names_list = [(_k.lower(), _k, _v)
+                        for _k, _v in physical_constants.items()
+                        if _k not in _obsolete_constants]
+_constant_names = "\n".join(["``{}``{}  {} {}".format(_x[1], " "*(66-len(_x[1])),
+                                                  _x[2][0], _x[2][1])
+                             for _x in sorted(_constant_names_list)])
+if __doc__:
+    __doc__ = __doc__ % dict(constant_names=_constant_names)
+
+del _constant_names
+del _constant_names_list
+
+__all__ = [s for s in dir() if not s.startswith('_')]
+
+from scipy._lib._testutils import PytestTester
+test = PytestTester(__name__)
+del PytestTester
diff --git a/.venv/lib/python3.11/site-packages/scipy/constants/_codata.py b/.venv/lib/python3.11/site-packages/scipy/constants/_codata.py
new file mode 100644
index 0000000000000000000000000000000000000000..3b7e9b3779d249b834505956a9c270564c41670f
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/constants/_codata.py
@@ -0,0 +1,2272 @@
+"""
+Fundamental Physical Constants
+------------------------------
+
+These constants are taken from CODATA Recommended Values of the Fundamental
+Physical Constants 2022.
+
+Object
+------
+physical_constants : dict
+    A dictionary containing physical constants. Keys are the names of physical
+    constants, values are tuples (value, units, precision).
+
+Functions
+---------
+value(key):
+    Returns the value of the physical constant(key).
+unit(key):
+    Returns the units of the physical constant(key).
+precision(key):
+    Returns the relative precision of the physical constant(key).
+find(sub):
+    Prints or returns list of keys containing the string sub, default is all.
+
+Source
+------
+The values of the constants provided at this site are recommended for
+international use by CODATA and are the latest available. Termed the "2018
+CODATA recommended values," they are generally recognized worldwide for use in
+all fields of science and technology. The values became available on 20 May
+2019 and replaced the 2014 CODATA set. Also available is an introduction to the
+constants for non-experts at
+
+https://physics.nist.gov/cuu/Constants/introduction.html
+
+References
+----------
+Theoretical and experimental publications relevant to the fundamental constants
+and closely related precision measurements published since the mid 1980s, but
+also including many older papers of particular interest, some of which date
+back to the 1800s. To search the bibliography, visit
+
+https://physics.nist.gov/cuu/Constants/
+
+"""
+
+# Compiled by Charles Harris, dated October 3, 2002
+# updated to 2002 values by BasSw, 2006
+# Updated to 2006 values by Vincent Davis June 2010
+# Updated to 2014 values by Joseph Booker, 2015
+# Updated to 2018 values by Jakob Jakobson, 2019
+# Updated to 2022 values by Jakob Jakobson, 2024
+
+import warnings
+import math
+
+from typing import Any
+from collections.abc import Callable
+
+from scipy._lib._array_api import xp_capabilities
+
+__all__ = ['physical_constants', 'value', 'unit', 'precision', 'find',
+           'ConstantWarning']
+
+"""
+Source:  https://physics.nist.gov/cuu/Constants/
+
+The values of the constants provided at this site are recommended for
+international use by CODATA and are the latest available. Termed the "2018
+CODATA recommended values," they are generally recognized worldwide for use in
+all fields of science and technology. The values became available on 20 May
+2019 and replaced the 2014 CODATA set.
+"""
+
+#
+# Source:  https://physics.nist.gov/cuu/Constants/
+#
+
+# Quantity                                             Value                 Uncertainty          Unit
+# ---------------------------------------------------- --------------------- -------------------- -------------
+txt2002 = """\
+Wien displacement law constant                         2.897 7685e-3         0.000 0051e-3         m K
+atomic unit of 1st hyperpolarizablity                  3.206 361 51e-53      0.000 000 28e-53      C^3 m^3 J^-2
+atomic unit of 2nd hyperpolarizablity                  6.235 3808e-65        0.000 0011e-65        C^4 m^4 J^-3
+atomic unit of electric dipole moment                  8.478 353 09e-30      0.000 000 73e-30      C m
+atomic unit of electric polarizablity                  1.648 777 274e-41     0.000 000 016e-41     C^2 m^2 J^-1
+atomic unit of electric quadrupole moment              4.486 551 24e-40      0.000 000 39e-40      C m^2
+atomic unit of magn. dipole moment                     1.854 801 90e-23      0.000 000 16e-23      J T^-1
+atomic unit of magn. flux density                      2.350 517 42e5        0.000 000 20e5        T
+deuteron magn. moment                                  0.433 073 482e-26     0.000 000 038e-26     J T^-1
+deuteron magn. moment to Bohr magneton ratio           0.466 975 4567e-3     0.000 000 0050e-3
+deuteron magn. moment to nuclear magneton ratio        0.857 438 2329        0.000 000 0092
+deuteron-electron magn. moment ratio                   -4.664 345 548e-4     0.000 000 050e-4
+deuteron-proton magn. moment ratio                     0.307 012 2084        0.000 000 0045
+deuteron-neutron magn. moment ratio                    -0.448 206 52         0.000 000 11
+electron gyromagn. ratio                               1.760 859 74e11       0.000 000 15e11       s^-1 T^-1
+electron gyromagn. ratio over 2 pi                     28 024.9532           0.0024                MHz T^-1
+electron magn. moment                                  -928.476 412e-26      0.000 080e-26         J T^-1
+electron magn. moment to Bohr magneton ratio           -1.001 159 652 1859   0.000 000 000 0038
+electron magn. moment to nuclear magneton ratio        -1838.281 971 07      0.000 000 85
+electron magn. moment anomaly                          1.159 652 1859e-3     0.000 000 0038e-3
+electron to shielded proton magn. moment ratio         -658.227 5956         0.000 0071
+electron to shielded helion magn. moment ratio         864.058 255           0.000 010
+electron-deuteron magn. moment ratio                   -2143.923 493         0.000 023
+electron-muon magn. moment ratio                       206.766 9894          0.000 0054
+electron-neutron magn. moment ratio                    960.920 50            0.000 23
+electron-proton magn. moment ratio                     -658.210 6862         0.000 0066
+magn. constant                                         12.566 370 614...e-7  (exact)               N A^-2
+magn. flux quantum                                     2.067 833 72e-15      0.000 000 18e-15      Wb
+muon magn. moment                                      -4.490 447 99e-26     0.000 000 40e-26      J T^-1
+muon magn. moment to Bohr magneton ratio               -4.841 970 45e-3      0.000 000 13e-3
+muon magn. moment to nuclear magneton ratio            -8.890 596 98         0.000 000 23
+muon-proton magn. moment ratio                         -3.183 345 118        0.000 000 089
+neutron gyromagn. ratio                                1.832 471 83e8        0.000 000 46e8        s^-1 T^-1
+neutron gyromagn. ratio over 2 pi                      29.164 6950           0.000 0073            MHz T^-1
+neutron magn. moment                                   -0.966 236 45e-26     0.000 000 24e-26      J T^-1
+neutron magn. moment to Bohr magneton ratio            -1.041 875 63e-3      0.000 000 25e-3
+neutron magn. moment to nuclear magneton ratio         -1.913 042 73         0.000 000 45
+neutron to shielded proton magn. moment ratio          -0.684 996 94         0.000 000 16
+neutron-electron magn. moment ratio                    1.040 668 82e-3       0.000 000 25e-3
+neutron-proton magn. moment ratio                      -0.684 979 34         0.000 000 16
+proton gyromagn. ratio                                 2.675 222 05e8        0.000 000 23e8        s^-1 T^-1
+proton gyromagn. ratio over 2 pi                       42.577 4813           0.000 0037            MHz T^-1
+proton magn. moment                                    1.410 606 71e-26      0.000 000 12e-26      J T^-1
+proton magn. moment to Bohr magneton ratio             1.521 032 206e-3      0.000 000 015e-3
+proton magn. moment to nuclear magneton ratio          2.792 847 351         0.000 000 028
+proton magn. shielding correction                      25.689e-6             0.015e-6
+proton-neutron magn. moment ratio                      -1.459 898 05         0.000 000 34
+shielded helion gyromagn. ratio                        2.037 894 70e8        0.000 000 18e8        s^-1 T^-1
+shielded helion gyromagn. ratio over 2 pi              32.434 1015           0.000 0028            MHz T^-1
+shielded helion magn. moment                           -1.074 553 024e-26    0.000 000 093e-26     J T^-1
+shielded helion magn. moment to Bohr magneton ratio    -1.158 671 474e-3     0.000 000 014e-3
+shielded helion magn. moment to nuclear magneton ratio -2.127 497 723        0.000 000 025
+shielded helion to proton magn. moment ratio           -0.761 766 562        0.000 000 012
+shielded helion to shielded proton magn. moment ratio  -0.761 786 1313       0.000 000 0033
+shielded helion gyromagn. ratio                        2.037 894 70e8        0.000 000 18e8        s^-1 T^-1
+shielded helion gyromagn. ratio over 2 pi              32.434 1015           0.000 0028            MHz T^-1
+shielded proton magn. moment                           1.410 570 47e-26      0.000 000 12e-26      J T^-1
+shielded proton magn. moment to Bohr magneton ratio    1.520 993 132e-3      0.000 000 016e-3
+shielded proton magn. moment to nuclear magneton ratio 2.792 775 604         0.000 000 030
+{220} lattice spacing of silicon                       192.015 5965e-12      0.000 0070e-12        m"""
+
+
+def exact2002(exact):
+    replace = {
+        'magn. constant': 4e-7 * math.pi,
+    }
+    return replace
+
+
+txt2006 = """\
+lattice spacing of silicon                             192.015 5762 e-12     0.000 0050 e-12       m
+alpha particle-electron mass ratio                     7294.299 5365         0.000 0031
+alpha particle mass                                    6.644 656 20 e-27     0.000 000 33 e-27     kg
+alpha particle mass energy equivalent                  5.971 919 17 e-10     0.000 000 30 e-10     J
+alpha particle mass energy equivalent in MeV           3727.379 109          0.000 093             MeV
+alpha particle mass in u                               4.001 506 179 127     0.000 000 000 062     u
+alpha particle molar mass                              4.001 506 179 127 e-3 0.000 000 000 062 e-3 kg mol^-1
+alpha particle-proton mass ratio                       3.972 599 689 51      0.000 000 000 41
+Angstrom star                                          1.000 014 98 e-10     0.000 000 90 e-10     m
+atomic mass constant                                   1.660 538 782 e-27    0.000 000 083 e-27    kg
+atomic mass constant energy equivalent                 1.492 417 830 e-10    0.000 000 074 e-10    J
+atomic mass constant energy equivalent in MeV          931.494 028           0.000 023             MeV
+atomic mass unit-electron volt relationship            931.494 028 e6        0.000 023 e6          eV
+atomic mass unit-hartree relationship                  3.423 177 7149 e7     0.000 000 0049 e7     E_h
+atomic mass unit-hertz relationship                    2.252 342 7369 e23    0.000 000 0032 e23    Hz
+atomic mass unit-inverse meter relationship            7.513 006 671 e14     0.000 000 011 e14     m^-1
+atomic mass unit-joule relationship                    1.492 417 830 e-10    0.000 000 074 e-10    J
+atomic mass unit-kelvin relationship                   1.080 9527 e13        0.000 0019 e13        K
+atomic mass unit-kilogram relationship                 1.660 538 782 e-27    0.000 000 083 e-27    kg
+atomic unit of 1st hyperpolarizability                 3.206 361 533 e-53    0.000 000 081 e-53    C^3 m^3 J^-2
+atomic unit of 2nd hyperpolarizability                 6.235 380 95 e-65     0.000 000 31 e-65     C^4 m^4 J^-3
+atomic unit of action                                  1.054 571 628 e-34    0.000 000 053 e-34    J s
+atomic unit of charge                                  1.602 176 487 e-19    0.000 000 040 e-19    C
+atomic unit of charge density                          1.081 202 300 e12     0.000 000 027 e12     C m^-3
+atomic unit of current                                 6.623 617 63 e-3      0.000 000 17 e-3      A
+atomic unit of electric dipole mom.                    8.478 352 81 e-30     0.000 000 21 e-30     C m
+atomic unit of electric field                          5.142 206 32 e11      0.000 000 13 e11      V m^-1
+atomic unit of electric field gradient                 9.717 361 66 e21      0.000 000 24 e21      V m^-2
+atomic unit of electric polarizability                 1.648 777 2536 e-41   0.000 000 0034 e-41   C^2 m^2 J^-1
+atomic unit of electric potential                      27.211 383 86         0.000 000 68          V
+atomic unit of electric quadrupole mom.                4.486 551 07 e-40     0.000 000 11 e-40     C m^2
+atomic unit of energy                                  4.359 743 94 e-18     0.000 000 22 e-18     J
+atomic unit of force                                   8.238 722 06 e-8      0.000 000 41 e-8      N
+atomic unit of length                                  0.529 177 208 59 e-10 0.000 000 000 36 e-10 m
+atomic unit of mag. dipole mom.                        1.854 801 830 e-23    0.000 000 046 e-23    J T^-1
+atomic unit of mag. flux density                       2.350 517 382 e5      0.000 000 059 e5      T
+atomic unit of magnetizability                         7.891 036 433 e-29    0.000 000 027 e-29    J T^-2
+atomic unit of mass                                    9.109 382 15 e-31     0.000 000 45 e-31     kg
+atomic unit of momentum                                1.992 851 565 e-24    0.000 000 099 e-24    kg m s^-1
+atomic unit of permittivity                            1.112 650 056... e-10 (exact)               F m^-1
+atomic unit of time                                    2.418 884 326 505 e-17 0.000 000 000 016 e-17 s
+atomic unit of velocity                                2.187 691 2541 e6     0.000 000 0015 e6     m s^-1
+Avogadro constant                                      6.022 141 79 e23      0.000 000 30 e23      mol^-1
+Bohr magneton                                          927.400 915 e-26      0.000 023 e-26        J T^-1
+Bohr magneton in eV/T                                  5.788 381 7555 e-5    0.000 000 0079 e-5    eV T^-1
+Bohr magneton in Hz/T                                  13.996 246 04 e9      0.000 000 35 e9       Hz T^-1
+Bohr magneton in inverse meters per tesla              46.686 4515           0.000 0012            m^-1 T^-1
+Bohr magneton in K/T                                   0.671 7131            0.000 0012            K T^-1
+Bohr radius                                            0.529 177 208 59 e-10 0.000 000 000 36 e-10 m
+Boltzmann constant                                     1.380 6504 e-23       0.000 0024 e-23       J K^-1
+Boltzmann constant in eV/K                             8.617 343 e-5         0.000 015 e-5         eV K^-1
+Boltzmann constant in Hz/K                             2.083 6644 e10        0.000 0036 e10        Hz K^-1
+Boltzmann constant in inverse meters per kelvin        69.503 56             0.000 12              m^-1 K^-1
+characteristic impedance of vacuum                     376.730 313 461...    (exact)               ohm
+classical electron radius                              2.817 940 2894 e-15   0.000 000 0058 e-15   m
+Compton wavelength                                     2.426 310 2175 e-12   0.000 000 0033 e-12   m
+Compton wavelength over 2 pi                           386.159 264 59 e-15   0.000 000 53 e-15     m
+conductance quantum                                    7.748 091 7004 e-5    0.000 000 0053 e-5    S
+conventional value of Josephson constant               483 597.9 e9          (exact)               Hz V^-1
+conventional value of von Klitzing constant            25 812.807            (exact)               ohm
+Cu x unit                                              1.002 076 99 e-13     0.000 000 28 e-13     m
+deuteron-electron mag. mom. ratio                      -4.664 345 537 e-4    0.000 000 039 e-4
+deuteron-electron mass ratio                           3670.482 9654         0.000 0016
+deuteron g factor                                      0.857 438 2308        0.000 000 0072
+deuteron mag. mom.                                     0.433 073 465 e-26    0.000 000 011 e-26    J T^-1
+deuteron mag. mom. to Bohr magneton ratio              0.466 975 4556 e-3    0.000 000 0039 e-3
+deuteron mag. mom. to nuclear magneton ratio           0.857 438 2308        0.000 000 0072
+deuteron mass                                          3.343 583 20 e-27     0.000 000 17 e-27     kg
+deuteron mass energy equivalent                        3.005 062 72 e-10     0.000 000 15 e-10     J
+deuteron mass energy equivalent in MeV                 1875.612 793          0.000 047             MeV
+deuteron mass in u                                     2.013 553 212 724     0.000 000 000 078     u
+deuteron molar mass                                    2.013 553 212 724 e-3 0.000 000 000 078 e-3 kg mol^-1
+deuteron-neutron mag. mom. ratio                       -0.448 206 52         0.000 000 11
+deuteron-proton mag. mom. ratio                        0.307 012 2070        0.000 000 0024
+deuteron-proton mass ratio                             1.999 007 501 08      0.000 000 000 22
+deuteron rms charge radius                             2.1402 e-15           0.0028 e-15           m
+electric constant                                      8.854 187 817... e-12 (exact)               F m^-1
+electron charge to mass quotient                       -1.758 820 150 e11    0.000 000 044 e11     C kg^-1
+electron-deuteron mag. mom. ratio                      -2143.923 498         0.000 018
+electron-deuteron mass ratio                           2.724 437 1093 e-4    0.000 000 0012 e-4
+electron g factor                                      -2.002 319 304 3622   0.000 000 000 0015
+electron gyromag. ratio                                1.760 859 770 e11     0.000 000 044 e11     s^-1 T^-1
+electron gyromag. ratio over 2 pi                      28 024.953 64         0.000 70              MHz T^-1
+electron mag. mom.                                     -928.476 377 e-26     0.000 023 e-26        J T^-1
+electron mag. mom. anomaly                             1.159 652 181 11 e-3  0.000 000 000 74 e-3
+electron mag. mom. to Bohr magneton ratio              -1.001 159 652 181 11 0.000 000 000 000 74
+electron mag. mom. to nuclear magneton ratio           -1838.281 970 92      0.000 000 80
+electron mass                                          9.109 382 15 e-31     0.000 000 45 e-31     kg
+electron mass energy equivalent                        8.187 104 38 e-14     0.000 000 41 e-14     J
+electron mass energy equivalent in MeV                 0.510 998 910         0.000 000 013         MeV
+electron mass in u                                     5.485 799 0943 e-4    0.000 000 0023 e-4    u
+electron molar mass                                    5.485 799 0943 e-7    0.000 000 0023 e-7    kg mol^-1
+electron-muon mag. mom. ratio                          206.766 9877          0.000 0052
+electron-muon mass ratio                               4.836 331 71 e-3      0.000 000 12 e-3
+electron-neutron mag. mom. ratio                       960.920 50            0.000 23
+electron-neutron mass ratio                            5.438 673 4459 e-4    0.000 000 0033 e-4
+electron-proton mag. mom. ratio                        -658.210 6848         0.000 0054
+electron-proton mass ratio                             5.446 170 2177 e-4    0.000 000 0024 e-4
+electron-tau mass ratio                                2.875 64 e-4          0.000 47 e-4
+electron to alpha particle mass ratio                  1.370 933 555 70 e-4  0.000 000 000 58 e-4
+electron to shielded helion mag. mom. ratio            864.058 257           0.000 010
+electron to shielded proton mag. mom. ratio            -658.227 5971         0.000 0072
+electron volt                                          1.602 176 487 e-19    0.000 000 040 e-19    J
+electron volt-atomic mass unit relationship            1.073 544 188 e-9     0.000 000 027 e-9     u
+electron volt-hartree relationship                     3.674 932 540 e-2     0.000 000 092 e-2     E_h
+electron volt-hertz relationship                       2.417 989 454 e14     0.000 000 060 e14     Hz
+electron volt-inverse meter relationship               8.065 544 65 e5       0.000 000 20 e5       m^-1
+electron volt-joule relationship                       1.602 176 487 e-19    0.000 000 040 e-19    J
+electron volt-kelvin relationship                      1.160 4505 e4         0.000 0020 e4         K
+electron volt-kilogram relationship                    1.782 661 758 e-36    0.000 000 044 e-36    kg
+elementary charge                                      1.602 176 487 e-19    0.000 000 040 e-19    C
+elementary charge over h                               2.417 989 454 e14     0.000 000 060 e14     A J^-1
+Faraday constant                                       96 485.3399           0.0024                C mol^-1
+Faraday constant for conventional electric current     96 485.3401           0.0048                C_90 mol^-1
+Fermi coupling constant                                1.166 37 e-5          0.000 01 e-5          GeV^-2
+fine-structure constant                                7.297 352 5376 e-3    0.000 000 0050 e-3
+first radiation constant                               3.741 771 18 e-16     0.000 000 19 e-16     W m^2
+first radiation constant for spectral radiance         1.191 042 759 e-16    0.000 000 059 e-16    W m^2 sr^-1
+hartree-atomic mass unit relationship                  2.921 262 2986 e-8    0.000 000 0042 e-8    u
+hartree-electron volt relationship                     27.211 383 86         0.000 000 68          eV
+Hartree energy                                         4.359 743 94 e-18     0.000 000 22 e-18     J
+Hartree energy in eV                                   27.211 383 86         0.000 000 68          eV
+hartree-hertz relationship                             6.579 683 920 722 e15 0.000 000 000 044 e15 Hz
+hartree-inverse meter relationship                     2.194 746 313 705 e7  0.000 000 000 015 e7  m^-1
+hartree-joule relationship                             4.359 743 94 e-18     0.000 000 22 e-18     J
+hartree-kelvin relationship                            3.157 7465 e5         0.000 0055 e5         K
+hartree-kilogram relationship                          4.850 869 34 e-35     0.000 000 24 e-35     kg
+helion-electron mass ratio                             5495.885 2765         0.000 0052
+helion mass                                            5.006 411 92 e-27     0.000 000 25 e-27     kg
+helion mass energy equivalent                          4.499 538 64 e-10     0.000 000 22 e-10     J
+helion mass energy equivalent in MeV                   2808.391 383          0.000 070             MeV
+helion mass in u                                       3.014 932 2473        0.000 000 0026        u
+helion molar mass                                      3.014 932 2473 e-3    0.000 000 0026 e-3    kg mol^-1
+helion-proton mass ratio                               2.993 152 6713        0.000 000 0026
+hertz-atomic mass unit relationship                    4.439 821 6294 e-24   0.000 000 0064 e-24   u
+hertz-electron volt relationship                       4.135 667 33 e-15     0.000 000 10 e-15     eV
+hertz-hartree relationship                             1.519 829 846 006 e-16 0.000 000 000010e-16 E_h
+hertz-inverse meter relationship                       3.335 640 951... e-9  (exact)               m^-1
+hertz-joule relationship                               6.626 068 96 e-34     0.000 000 33 e-34     J
+hertz-kelvin relationship                              4.799 2374 e-11       0.000 0084 e-11       K
+hertz-kilogram relationship                            7.372 496 00 e-51     0.000 000 37 e-51     kg
+inverse fine-structure constant                        137.035 999 679       0.000 000 094
+inverse meter-atomic mass unit relationship            1.331 025 0394 e-15   0.000 000 0019 e-15   u
+inverse meter-electron volt relationship               1.239 841 875 e-6     0.000 000 031 e-6     eV
+inverse meter-hartree relationship                     4.556 335 252 760 e-8 0.000 000 000 030 e-8 E_h
+inverse meter-hertz relationship                       299 792 458           (exact)               Hz
+inverse meter-joule relationship                       1.986 445 501 e-25    0.000 000 099 e-25    J
+inverse meter-kelvin relationship                      1.438 7752 e-2        0.000 0025 e-2        K
+inverse meter-kilogram relationship                    2.210 218 70 e-42     0.000 000 11 e-42     kg
+inverse of conductance quantum                         12 906.403 7787       0.000 0088            ohm
+Josephson constant                                     483 597.891 e9        0.012 e9              Hz V^-1
+joule-atomic mass unit relationship                    6.700 536 41 e9       0.000 000 33 e9       u
+joule-electron volt relationship                       6.241 509 65 e18      0.000 000 16 e18      eV
+joule-hartree relationship                             2.293 712 69 e17      0.000 000 11 e17      E_h
+joule-hertz relationship                               1.509 190 450 e33     0.000 000 075 e33     Hz
+joule-inverse meter relationship                       5.034 117 47 e24      0.000 000 25 e24      m^-1
+joule-kelvin relationship                              7.242 963 e22         0.000 013 e22         K
+joule-kilogram relationship                            1.112 650 056... e-17 (exact)               kg
+kelvin-atomic mass unit relationship                   9.251 098 e-14        0.000 016 e-14        u
+kelvin-electron volt relationship                      8.617 343 e-5         0.000 015 e-5         eV
+kelvin-hartree relationship                            3.166 8153 e-6        0.000 0055 e-6        E_h
+kelvin-hertz relationship                              2.083 6644 e10        0.000 0036 e10        Hz
+kelvin-inverse meter relationship                      69.503 56             0.000 12              m^-1
+kelvin-joule relationship                              1.380 6504 e-23       0.000 0024 e-23       J
+kelvin-kilogram relationship                           1.536 1807 e-40       0.000 0027 e-40       kg
+kilogram-atomic mass unit relationship                 6.022 141 79 e26      0.000 000 30 e26      u
+kilogram-electron volt relationship                    5.609 589 12 e35      0.000 000 14 e35      eV
+kilogram-hartree relationship                          2.061 486 16 e34      0.000 000 10 e34      E_h
+kilogram-hertz relationship                            1.356 392 733 e50     0.000 000 068 e50     Hz
+kilogram-inverse meter relationship                    4.524 439 15 e41      0.000 000 23 e41      m^-1
+kilogram-joule relationship                            8.987 551 787... e16  (exact)               J
+kilogram-kelvin relationship                           6.509 651 e39         0.000 011 e39         K
+lattice parameter of silicon                           543.102 064 e-12      0.000 014 e-12        m
+Loschmidt constant (273.15 K, 101.325 kPa)             2.686 7774 e25        0.000 0047 e25        m^-3
+mag. constant                                          12.566 370 614... e-7 (exact)               N A^-2
+mag. flux quantum                                      2.067 833 667 e-15    0.000 000 052 e-15    Wb
+molar gas constant                                     8.314 472             0.000 015             J mol^-1 K^-1
+molar mass constant                                    1 e-3                 (exact)               kg mol^-1
+molar mass of carbon-12                                12 e-3                (exact)               kg mol^-1
+molar Planck constant                                  3.990 312 6821 e-10   0.000 000 0057 e-10   J s mol^-1
+molar Planck constant times c                          0.119 626 564 72      0.000 000 000 17      J m mol^-1
+molar volume of ideal gas (273.15 K, 100 kPa)          22.710 981 e-3        0.000 040 e-3         m^3 mol^-1
+molar volume of ideal gas (273.15 K, 101.325 kPa)      22.413 996 e-3        0.000 039 e-3         m^3 mol^-1
+molar volume of silicon                                12.058 8349 e-6       0.000 0011 e-6        m^3 mol^-1
+Mo x unit                                              1.002 099 55 e-13     0.000 000 53 e-13     m
+muon Compton wavelength                                11.734 441 04 e-15    0.000 000 30 e-15     m
+muon Compton wavelength over 2 pi                      1.867 594 295 e-15    0.000 000 047 e-15    m
+muon-electron mass ratio                               206.768 2823          0.000 0052
+muon g factor                                          -2.002 331 8414       0.000 000 0012
+muon mag. mom.                                         -4.490 447 86 e-26    0.000 000 16 e-26     J T^-1
+muon mag. mom. anomaly                                 1.165 920 69 e-3      0.000 000 60 e-3
+muon mag. mom. to Bohr magneton ratio                  -4.841 970 49 e-3     0.000 000 12 e-3
+muon mag. mom. to nuclear magneton ratio               -8.890 597 05         0.000 000 23
+muon mass                                              1.883 531 30 e-28     0.000 000 11 e-28     kg
+muon mass energy equivalent                            1.692 833 510 e-11    0.000 000 095 e-11    J
+muon mass energy equivalent in MeV                     105.658 3668          0.000 0038            MeV
+muon mass in u                                         0.113 428 9256        0.000 000 0029        u
+muon molar mass                                        0.113 428 9256 e-3    0.000 000 0029 e-3    kg mol^-1
+muon-neutron mass ratio                                0.112 454 5167        0.000 000 0029
+muon-proton mag. mom. ratio                            -3.183 345 137        0.000 000 085
+muon-proton mass ratio                                 0.112 609 5261        0.000 000 0029
+muon-tau mass ratio                                    5.945 92 e-2          0.000 97 e-2
+natural unit of action                                 1.054 571 628 e-34    0.000 000 053 e-34    J s
+natural unit of action in eV s                         6.582 118 99 e-16     0.000 000 16 e-16     eV s
+natural unit of energy                                 8.187 104 38 e-14     0.000 000 41 e-14     J
+natural unit of energy in MeV                          0.510 998 910         0.000 000 013         MeV
+natural unit of length                                 386.159 264 59 e-15   0.000 000 53 e-15     m
+natural unit of mass                                   9.109 382 15 e-31     0.000 000 45 e-31     kg
+natural unit of momentum                               2.730 924 06 e-22     0.000 000 14 e-22     kg m s^-1
+natural unit of momentum in MeV/c                      0.510 998 910         0.000 000 013         MeV/c
+natural unit of time                                   1.288 088 6570 e-21   0.000 000 0018 e-21   s
+natural unit of velocity                               299 792 458           (exact)               m s^-1
+neutron Compton wavelength                             1.319 590 8951 e-15   0.000 000 0020 e-15   m
+neutron Compton wavelength over 2 pi                   0.210 019 413 82 e-15 0.000 000 000 31 e-15 m
+neutron-electron mag. mom. ratio                       1.040 668 82 e-3      0.000 000 25 e-3
+neutron-electron mass ratio                            1838.683 6605         0.000 0011
+neutron g factor                                       -3.826 085 45         0.000 000 90
+neutron gyromag. ratio                                 1.832 471 85 e8       0.000 000 43 e8       s^-1 T^-1
+neutron gyromag. ratio over 2 pi                       29.164 6954           0.000 0069            MHz T^-1
+neutron mag. mom.                                      -0.966 236 41 e-26    0.000 000 23 e-26     J T^-1
+neutron mag. mom. to Bohr magneton ratio               -1.041 875 63 e-3     0.000 000 25 e-3
+neutron mag. mom. to nuclear magneton ratio            -1.913 042 73         0.000 000 45
+neutron mass                                           1.674 927 211 e-27    0.000 000 084 e-27    kg
+neutron mass energy equivalent                         1.505 349 505 e-10    0.000 000 075 e-10    J
+neutron mass energy equivalent in MeV                  939.565 346           0.000 023             MeV
+neutron mass in u                                      1.008 664 915 97      0.000 000 000 43      u
+neutron molar mass                                     1.008 664 915 97 e-3  0.000 000 000 43 e-3  kg mol^-1
+neutron-muon mass ratio                                8.892 484 09          0.000 000 23
+neutron-proton mag. mom. ratio                         -0.684 979 34         0.000 000 16
+neutron-proton mass ratio                              1.001 378 419 18      0.000 000 000 46
+neutron-tau mass ratio                                 0.528 740             0.000 086
+neutron to shielded proton mag. mom. ratio             -0.684 996 94         0.000 000 16
+Newtonian constant of gravitation                      6.674 28 e-11         0.000 67 e-11         m^3 kg^-1 s^-2
+Newtonian constant of gravitation over h-bar c         6.708 81 e-39         0.000 67 e-39         (GeV/c^2)^-2
+nuclear magneton                                       5.050 783 24 e-27     0.000 000 13 e-27     J T^-1
+nuclear magneton in eV/T                               3.152 451 2326 e-8    0.000 000 0045 e-8    eV T^-1
+nuclear magneton in inverse meters per tesla           2.542 623 616 e-2     0.000 000 064 e-2     m^-1 T^-1
+nuclear magneton in K/T                                3.658 2637 e-4        0.000 0064 e-4        K T^-1
+nuclear magneton in MHz/T                              7.622 593 84          0.000 000 19          MHz T^-1
+Planck constant                                        6.626 068 96 e-34     0.000 000 33 e-34     J s
+Planck constant in eV s                                4.135 667 33 e-15     0.000 000 10 e-15     eV s
+Planck constant over 2 pi                              1.054 571 628 e-34    0.000 000 053 e-34    J s
+Planck constant over 2 pi in eV s                      6.582 118 99 e-16     0.000 000 16 e-16     eV s
+Planck constant over 2 pi times c in MeV fm            197.326 9631          0.000 0049            MeV fm
+Planck length                                          1.616 252 e-35        0.000 081 e-35        m
+Planck mass                                            2.176 44 e-8          0.000 11 e-8          kg
+Planck mass energy equivalent in GeV                   1.220 892 e19         0.000 061 e19         GeV
+Planck temperature                                     1.416 785 e32         0.000 071 e32         K
+Planck time                                            5.391 24 e-44         0.000 27 e-44         s
+proton charge to mass quotient                         9.578 833 92 e7       0.000 000 24 e7       C kg^-1
+proton Compton wavelength                              1.321 409 8446 e-15   0.000 000 0019 e-15   m
+proton Compton wavelength over 2 pi                    0.210 308 908 61 e-15 0.000 000 000 30 e-15 m
+proton-electron mass ratio                             1836.152 672 47       0.000 000 80
+proton g factor                                        5.585 694 713         0.000 000 046
+proton gyromag. ratio                                  2.675 222 099 e8      0.000 000 070 e8      s^-1 T^-1
+proton gyromag. ratio over 2 pi                        42.577 4821           0.000 0011            MHz T^-1
+proton mag. mom.                                       1.410 606 662 e-26    0.000 000 037 e-26    J T^-1
+proton mag. mom. to Bohr magneton ratio                1.521 032 209 e-3     0.000 000 012 e-3
+proton mag. mom. to nuclear magneton ratio             2.792 847 356         0.000 000 023
+proton mag. shielding correction                       25.694 e-6            0.014 e-6
+proton mass                                            1.672 621 637 e-27    0.000 000 083 e-27    kg
+proton mass energy equivalent                          1.503 277 359 e-10    0.000 000 075 e-10    J
+proton mass energy equivalent in MeV                   938.272 013           0.000 023             MeV
+proton mass in u                                       1.007 276 466 77      0.000 000 000 10      u
+proton molar mass                                      1.007 276 466 77 e-3  0.000 000 000 10 e-3  kg mol^-1
+proton-muon mass ratio                                 8.880 243 39          0.000 000 23
+proton-neutron mag. mom. ratio                         -1.459 898 06         0.000 000 34
+proton-neutron mass ratio                              0.998 623 478 24      0.000 000 000 46
+proton rms charge radius                               0.8768 e-15           0.0069 e-15           m
+proton-tau mass ratio                                  0.528 012             0.000 086
+quantum of circulation                                 3.636 947 5199 e-4    0.000 000 0050 e-4    m^2 s^-1
+quantum of circulation times 2                         7.273 895 040 e-4     0.000 000 010 e-4     m^2 s^-1
+Rydberg constant                                       10 973 731.568 527    0.000 073             m^-1
+Rydberg constant times c in Hz                         3.289 841 960 361 e15 0.000 000 000 022 e15 Hz
+Rydberg constant times hc in eV                        13.605 691 93         0.000 000 34          eV
+Rydberg constant times hc in J                         2.179 871 97 e-18     0.000 000 11 e-18     J
+Sackur-Tetrode constant (1 K, 100 kPa)                 -1.151 7047           0.000 0044
+Sackur-Tetrode constant (1 K, 101.325 kPa)             -1.164 8677           0.000 0044
+second radiation constant                              1.438 7752 e-2        0.000 0025 e-2        m K
+shielded helion gyromag. ratio                         2.037 894 730 e8      0.000 000 056 e8      s^-1 T^-1
+shielded helion gyromag. ratio over 2 pi               32.434 101 98         0.000 000 90          MHz T^-1
+shielded helion mag. mom.                              -1.074 552 982 e-26   0.000 000 030 e-26    J T^-1
+shielded helion mag. mom. to Bohr magneton ratio       -1.158 671 471 e-3    0.000 000 014 e-3
+shielded helion mag. mom. to nuclear magneton ratio    -2.127 497 718        0.000 000 025
+shielded helion to proton mag. mom. ratio              -0.761 766 558        0.000 000 011
+shielded helion to shielded proton mag. mom. ratio     -0.761 786 1313       0.000 000 0033
+shielded proton gyromag. ratio                         2.675 153 362 e8      0.000 000 073 e8      s^-1 T^-1
+shielded proton gyromag. ratio over 2 pi               42.576 3881           0.000 0012            MHz T^-1
+shielded proton mag. mom.                              1.410 570 419 e-26    0.000 000 038 e-26    J T^-1
+shielded proton mag. mom. to Bohr magneton ratio       1.520 993 128 e-3     0.000 000 017 e-3
+shielded proton mag. mom. to nuclear magneton ratio    2.792 775 598         0.000 000 030
+speed of light in vacuum                               299 792 458           (exact)               m s^-1
+standard acceleration of gravity                       9.806 65              (exact)               m s^-2
+standard atmosphere                                    101 325               (exact)               Pa
+Stefan-Boltzmann constant                              5.670 400 e-8         0.000 040 e-8         W m^-2 K^-4
+tau Compton wavelength                                 0.697 72 e-15         0.000 11 e-15         m
+tau Compton wavelength over 2 pi                       0.111 046 e-15        0.000 018 e-15        m
+tau-electron mass ratio                                3477.48               0.57
+tau mass                                               3.167 77 e-27         0.000 52 e-27         kg
+tau mass energy equivalent                             2.847 05 e-10         0.000 46 e-10         J
+tau mass energy equivalent in MeV                      1776.99               0.29                  MeV
+tau mass in u                                          1.907 68              0.000 31              u
+tau molar mass                                         1.907 68 e-3          0.000 31 e-3          kg mol^-1
+tau-muon mass ratio                                    16.8183               0.0027
+tau-neutron mass ratio                                 1.891 29              0.000 31
+tau-proton mass ratio                                  1.893 90              0.000 31
+Thomson cross section                                  0.665 245 8558 e-28   0.000 000 0027 e-28   m^2
+triton-electron mag. mom. ratio                        -1.620 514 423 e-3    0.000 000 021 e-3
+triton-electron mass ratio                             5496.921 5269         0.000 0051
+triton g factor                                        5.957 924 896         0.000 000 076
+triton mag. mom.                                       1.504 609 361 e-26    0.000 000 042 e-26    J T^-1
+triton mag. mom. to Bohr magneton ratio                1.622 393 657 e-3     0.000 000 021 e-3
+triton mag. mom. to nuclear magneton ratio             2.978 962 448         0.000 000 038
+triton mass                                            5.007 355 88 e-27     0.000 000 25 e-27     kg
+triton mass energy equivalent                          4.500 387 03 e-10     0.000 000 22 e-10     J
+triton mass energy equivalent in MeV                   2808.920 906          0.000 070             MeV
+triton mass in u                                       3.015 500 7134        0.000 000 0025        u
+triton molar mass                                      3.015 500 7134 e-3    0.000 000 0025 e-3    kg mol^-1
+triton-neutron mag. mom. ratio                         -1.557 185 53         0.000 000 37
+triton-proton mag. mom. ratio                          1.066 639 908         0.000 000 010
+triton-proton mass ratio                               2.993 717 0309        0.000 000 0025
+unified atomic mass unit                               1.660 538 782 e-27    0.000 000 083 e-27    kg
+von Klitzing constant                                  25 812.807 557        0.000 018             ohm
+weak mixing angle                                      0.222 55              0.000 56
+Wien frequency displacement law constant               5.878 933 e10         0.000 010 e10         Hz K^-1
+Wien wavelength displacement law constant              2.897 7685 e-3        0.000 0051 e-3        m K"""
+
+
+def exact2006(exact):
+    mu0 = 4e-7 * math.pi
+    c = exact['speed of light in vacuum']
+    epsilon0 = 1 / (mu0 * c**2)
+    replace = {
+        'mag. constant': mu0,
+        'electric constant': epsilon0,
+        'atomic unit of permittivity': 4*math.pi*epsilon0,
+        'characteristic impedance of vacuum': math.sqrt(mu0 / epsilon0),
+        'hertz-inverse meter relationship': 1/c,
+        'joule-kilogram relationship': 1/c**2,
+        'kilogram-joule relationship': c**2,
+    }
+    return replace
+
+
+txt2010 = """\
+{220} lattice spacing of silicon                       192.015 5714 e-12     0.000 0032 e-12       m
+alpha particle-electron mass ratio                     7294.299 5361         0.000 0029
+alpha particle mass                                    6.644 656 75 e-27     0.000 000 29 e-27     kg
+alpha particle mass energy equivalent                  5.971 919 67 e-10     0.000 000 26 e-10     J
+alpha particle mass energy equivalent in MeV           3727.379 240          0.000 082             MeV
+alpha particle mass in u                               4.001 506 179 125     0.000 000 000 062     u
+alpha particle molar mass                              4.001 506 179 125 e-3 0.000 000 000 062 e-3 kg mol^-1
+alpha particle-proton mass ratio                       3.972 599 689 33      0.000 000 000 36
+Angstrom star                                          1.000 014 95 e-10     0.000 000 90 e-10     m
+atomic mass constant                                   1.660 538 921 e-27    0.000 000 073 e-27    kg
+atomic mass constant energy equivalent                 1.492 417 954 e-10    0.000 000 066 e-10    J
+atomic mass constant energy equivalent in MeV          931.494 061           0.000 021             MeV
+atomic mass unit-electron volt relationship            931.494 061 e6        0.000 021 e6          eV
+atomic mass unit-hartree relationship                  3.423 177 6845 e7     0.000 000 0024 e7     E_h
+atomic mass unit-hertz relationship                    2.252 342 7168 e23    0.000 000 0016 e23    Hz
+atomic mass unit-inverse meter relationship            7.513 006 6042 e14    0.000 000 0053 e14    m^-1
+atomic mass unit-joule relationship                    1.492 417 954 e-10    0.000 000 066 e-10    J
+atomic mass unit-kelvin relationship                   1.080 954 08 e13      0.000 000 98 e13      K
+atomic mass unit-kilogram relationship                 1.660 538 921 e-27    0.000 000 073 e-27    kg
+atomic unit of 1st hyperpolarizability                 3.206 361 449 e-53    0.000 000 071 e-53    C^3 m^3 J^-2
+atomic unit of 2nd hyperpolarizability                 6.235 380 54 e-65     0.000 000 28 e-65     C^4 m^4 J^-3
+atomic unit of action                                  1.054 571 726 e-34    0.000 000 047 e-34    J s
+atomic unit of charge                                  1.602 176 565 e-19    0.000 000 035 e-19    C
+atomic unit of charge density                          1.081 202 338 e12     0.000 000 024 e12     C m^-3
+atomic unit of current                                 6.623 617 95 e-3      0.000 000 15 e-3      A
+atomic unit of electric dipole mom.                    8.478 353 26 e-30     0.000 000 19 e-30     C m
+atomic unit of electric field                          5.142 206 52 e11      0.000 000 11 e11      V m^-1
+atomic unit of electric field gradient                 9.717 362 00 e21      0.000 000 21 e21      V m^-2
+atomic unit of electric polarizability                 1.648 777 2754 e-41   0.000 000 0016 e-41   C^2 m^2 J^-1
+atomic unit of electric potential                      27.211 385 05         0.000 000 60          V
+atomic unit of electric quadrupole mom.                4.486 551 331 e-40    0.000 000 099 e-40    C m^2
+atomic unit of energy                                  4.359 744 34 e-18     0.000 000 19 e-18     J
+atomic unit of force                                   8.238 722 78 e-8      0.000 000 36 e-8      N
+atomic unit of length                                  0.529 177 210 92 e-10 0.000 000 000 17 e-10 m
+atomic unit of mag. dipole mom.                        1.854 801 936 e-23    0.000 000 041 e-23    J T^-1
+atomic unit of mag. flux density                       2.350 517 464 e5      0.000 000 052 e5      T
+atomic unit of magnetizability                         7.891 036 607 e-29    0.000 000 013 e-29    J T^-2
+atomic unit of mass                                    9.109 382 91 e-31     0.000 000 40 e-31     kg
+atomic unit of mom.um                                  1.992 851 740 e-24    0.000 000 088 e-24    kg m s^-1
+atomic unit of permittivity                            1.112 650 056... e-10 (exact)               F m^-1
+atomic unit of time                                    2.418 884 326 502e-17 0.000 000 000 012e-17 s
+atomic unit of velocity                                2.187 691 263 79 e6   0.000 000 000 71 e6   m s^-1
+Avogadro constant                                      6.022 141 29 e23      0.000 000 27 e23      mol^-1
+Bohr magneton                                          927.400 968 e-26      0.000 020 e-26        J T^-1
+Bohr magneton in eV/T                                  5.788 381 8066 e-5    0.000 000 0038 e-5    eV T^-1
+Bohr magneton in Hz/T                                  13.996 245 55 e9      0.000 000 31 e9       Hz T^-1
+Bohr magneton in inverse meters per tesla              46.686 4498           0.000 0010            m^-1 T^-1
+Bohr magneton in K/T                                   0.671 713 88          0.000 000 61          K T^-1
+Bohr radius                                            0.529 177 210 92 e-10 0.000 000 000 17 e-10 m
+Boltzmann constant                                     1.380 6488 e-23       0.000 0013 e-23       J K^-1
+Boltzmann constant in eV/K                             8.617 3324 e-5        0.000 0078 e-5        eV K^-1
+Boltzmann constant in Hz/K                             2.083 6618 e10        0.000 0019 e10        Hz K^-1
+Boltzmann constant in inverse meters per kelvin        69.503 476            0.000 063             m^-1 K^-1
+characteristic impedance of vacuum                     376.730 313 461...    (exact)               ohm
+classical electron radius                              2.817 940 3267 e-15   0.000 000 0027 e-15   m
+Compton wavelength                                     2.426 310 2389 e-12   0.000 000 0016 e-12   m
+Compton wavelength over 2 pi                           386.159 268 00 e-15   0.000 000 25 e-15     m
+conductance quantum                                    7.748 091 7346 e-5    0.000 000 0025 e-5    S
+conventional value of Josephson constant               483 597.9 e9          (exact)               Hz V^-1
+conventional value of von Klitzing constant            25 812.807            (exact)               ohm
+Cu x unit                                              1.002 076 97 e-13     0.000 000 28 e-13     m
+deuteron-electron mag. mom. ratio                      -4.664 345 537 e-4    0.000 000 039 e-4
+deuteron-electron mass ratio                           3670.482 9652         0.000 0015
+deuteron g factor                                      0.857 438 2308        0.000 000 0072
+deuteron mag. mom.                                     0.433 073 489 e-26    0.000 000 010 e-26    J T^-1
+deuteron mag. mom. to Bohr magneton ratio              0.466 975 4556 e-3    0.000 000 0039 e-3
+deuteron mag. mom. to nuclear magneton ratio           0.857 438 2308        0.000 000 0072
+deuteron mass                                          3.343 583 48 e-27     0.000 000 15 e-27     kg
+deuteron mass energy equivalent                        3.005 062 97 e-10     0.000 000 13 e-10     J
+deuteron mass energy equivalent in MeV                 1875.612 859          0.000 041             MeV
+deuteron mass in u                                     2.013 553 212 712     0.000 000 000 077     u
+deuteron molar mass                                    2.013 553 212 712 e-3 0.000 000 000 077 e-3 kg mol^-1
+deuteron-neutron mag. mom. ratio                       -0.448 206 52         0.000 000 11
+deuteron-proton mag. mom. ratio                        0.307 012 2070        0.000 000 0024
+deuteron-proton mass ratio                             1.999 007 500 97      0.000 000 000 18
+deuteron rms charge radius                             2.1424 e-15           0.0021 e-15           m
+electric constant                                      8.854 187 817... e-12 (exact)               F m^-1
+electron charge to mass quotient                       -1.758 820 088 e11    0.000 000 039 e11     C kg^-1
+electron-deuteron mag. mom. ratio                      -2143.923 498         0.000 018
+electron-deuteron mass ratio                           2.724 437 1095 e-4    0.000 000 0011 e-4
+electron g factor                                      -2.002 319 304 361 53 0.000 000 000 000 53
+electron gyromag. ratio                                1.760 859 708 e11     0.000 000 039 e11     s^-1 T^-1
+electron gyromag. ratio over 2 pi                      28 024.952 66         0.000 62              MHz T^-1
+electron-helion mass ratio                             1.819 543 0761 e-4    0.000 000 0017 e-4
+electron mag. mom.                                     -928.476 430 e-26     0.000 021 e-26        J T^-1
+electron mag. mom. anomaly                             1.159 652 180 76 e-3  0.000 000 000 27 e-3
+electron mag. mom. to Bohr magneton ratio              -1.001 159 652 180 76 0.000 000 000 000 27
+electron mag. mom. to nuclear magneton ratio           -1838.281 970 90      0.000 000 75
+electron mass                                          9.109 382 91 e-31     0.000 000 40 e-31     kg
+electron mass energy equivalent                        8.187 105 06 e-14     0.000 000 36 e-14     J
+electron mass energy equivalent in MeV                 0.510 998 928         0.000 000 011         MeV
+electron mass in u                                     5.485 799 0946 e-4    0.000 000 0022 e-4    u
+electron molar mass                                    5.485 799 0946 e-7    0.000 000 0022 e-7    kg mol^-1
+electron-muon mag. mom. ratio                          206.766 9896          0.000 0052
+electron-muon mass ratio                               4.836 331 66 e-3      0.000 000 12 e-3
+electron-neutron mag. mom. ratio                       960.920 50            0.000 23
+electron-neutron mass ratio                            5.438 673 4461 e-4    0.000 000 0032 e-4
+electron-proton mag. mom. ratio                        -658.210 6848         0.000 0054
+electron-proton mass ratio                             5.446 170 2178 e-4    0.000 000 0022 e-4
+electron-tau mass ratio                                2.875 92 e-4          0.000 26 e-4
+electron to alpha particle mass ratio                  1.370 933 555 78 e-4  0.000 000 000 55 e-4
+electron to shielded helion mag. mom. ratio            864.058 257           0.000 010
+electron to shielded proton mag. mom. ratio            -658.227 5971         0.000 0072
+electron-triton mass ratio                             1.819 200 0653 e-4    0.000 000 0017 e-4
+electron volt                                          1.602 176 565 e-19    0.000 000 035 e-19    J
+electron volt-atomic mass unit relationship            1.073 544 150 e-9     0.000 000 024 e-9     u
+electron volt-hartree relationship                     3.674 932 379 e-2     0.000 000 081 e-2     E_h
+electron volt-hertz relationship                       2.417 989 348 e14     0.000 000 053 e14     Hz
+electron volt-inverse meter relationship               8.065 544 29 e5       0.000 000 18 e5       m^-1
+electron volt-joule relationship                       1.602 176 565 e-19    0.000 000 035 e-19    J
+electron volt-kelvin relationship                      1.160 4519 e4         0.000 0011 e4         K
+electron volt-kilogram relationship                    1.782 661 845 e-36    0.000 000 039 e-36    kg
+elementary charge                                      1.602 176 565 e-19    0.000 000 035 e-19    C
+elementary charge over h                               2.417 989 348 e14     0.000 000 053 e14     A J^-1
+Faraday constant                                       96 485.3365           0.0021                C mol^-1
+Faraday constant for conventional electric current     96 485.3321           0.0043                C_90 mol^-1
+Fermi coupling constant                                1.166 364 e-5         0.000 005 e-5         GeV^-2
+fine-structure constant                                7.297 352 5698 e-3    0.000 000 0024 e-3
+first radiation constant                               3.741 771 53 e-16     0.000 000 17 e-16     W m^2
+first radiation constant for spectral radiance         1.191 042 869 e-16    0.000 000 053 e-16    W m^2 sr^-1
+hartree-atomic mass unit relationship                  2.921 262 3246 e-8    0.000 000 0021 e-8    u
+hartree-electron volt relationship                     27.211 385 05         0.000 000 60          eV
+Hartree energy                                         4.359 744 34 e-18     0.000 000 19 e-18     J
+Hartree energy in eV                                   27.211 385 05         0.000 000 60          eV
+hartree-hertz relationship                             6.579 683 920 729 e15 0.000 000 000 033 e15 Hz
+hartree-inverse meter relationship                     2.194 746 313 708 e7  0.000 000 000 011 e7  m^-1
+hartree-joule relationship                             4.359 744 34 e-18     0.000 000 19 e-18     J
+hartree-kelvin relationship                            3.157 7504 e5         0.000 0029 e5         K
+hartree-kilogram relationship                          4.850 869 79 e-35     0.000 000 21 e-35     kg
+helion-electron mass ratio                             5495.885 2754         0.000 0050
+helion g factor                                        -4.255 250 613        0.000 000 050
+helion mag. mom.                                       -1.074 617 486 e-26   0.000 000 027 e-26    J T^-1
+helion mag. mom. to Bohr magneton ratio                -1.158 740 958 e-3    0.000 000 014 e-3
+helion mag. mom. to nuclear magneton ratio             -2.127 625 306        0.000 000 025
+helion mass                                            5.006 412 34 e-27     0.000 000 22 e-27     kg
+helion mass energy equivalent                          4.499 539 02 e-10     0.000 000 20 e-10     J
+helion mass energy equivalent in MeV                   2808.391 482          0.000 062             MeV
+helion mass in u                                       3.014 932 2468        0.000 000 0025        u
+helion molar mass                                      3.014 932 2468 e-3    0.000 000 0025 e-3    kg mol^-1
+helion-proton mass ratio                               2.993 152 6707        0.000 000 0025
+hertz-atomic mass unit relationship                    4.439 821 6689 e-24   0.000 000 0031 e-24   u
+hertz-electron volt relationship                       4.135 667 516 e-15    0.000 000 091 e-15    eV
+hertz-hartree relationship                             1.519 829 8460045e-16 0.000 000 0000076e-16 E_h
+hertz-inverse meter relationship                       3.335 640 951... e-9  (exact)               m^-1
+hertz-joule relationship                               6.626 069 57 e-34     0.000 000 29 e-34     J
+hertz-kelvin relationship                              4.799 2434 e-11       0.000 0044 e-11       K
+hertz-kilogram relationship                            7.372 496 68 e-51     0.000 000 33 e-51     kg
+inverse fine-structure constant                        137.035 999 074       0.000 000 044
+inverse meter-atomic mass unit relationship            1.331 025 051 20 e-15 0.000 000 000 94 e-15 u
+inverse meter-electron volt relationship               1.239 841 930 e-6     0.000 000 027 e-6     eV
+inverse meter-hartree relationship                     4.556 335 252 755 e-8 0.000 000 000 023 e-8 E_h
+inverse meter-hertz relationship                       299 792 458           (exact)               Hz
+inverse meter-joule relationship                       1.986 445 684 e-25    0.000 000 088 e-25    J
+inverse meter-kelvin relationship                      1.438 7770 e-2        0.000 0013 e-2        K
+inverse meter-kilogram relationship                    2.210 218 902 e-42    0.000 000 098 e-42    kg
+inverse of conductance quantum                         12 906.403 7217       0.000 0042            ohm
+Josephson constant                                     483 597.870 e9        0.011 e9              Hz V^-1
+joule-atomic mass unit relationship                    6.700 535 85 e9       0.000 000 30 e9       u
+joule-electron volt relationship                       6.241 509 34 e18      0.000 000 14 e18      eV
+joule-hartree relationship                             2.293 712 48 e17      0.000 000 10 e17      E_h
+joule-hertz relationship                               1.509 190 311 e33     0.000 000 067 e33     Hz
+joule-inverse meter relationship                       5.034 117 01 e24      0.000 000 22 e24      m^-1
+joule-kelvin relationship                              7.242 9716 e22        0.000 0066 e22        K
+joule-kilogram relationship                            1.112 650 056... e-17 (exact)               kg
+kelvin-atomic mass unit relationship                   9.251 0868 e-14       0.000 0084 e-14       u
+kelvin-electron volt relationship                      8.617 3324 e-5        0.000 0078 e-5        eV
+kelvin-hartree relationship                            3.166 8114 e-6        0.000 0029 e-6        E_h
+kelvin-hertz relationship                              2.083 6618 e10        0.000 0019 e10        Hz
+kelvin-inverse meter relationship                      69.503 476            0.000 063             m^-1
+kelvin-joule relationship                              1.380 6488 e-23       0.000 0013 e-23       J
+kelvin-kilogram relationship                           1.536 1790 e-40       0.000 0014 e-40       kg
+kilogram-atomic mass unit relationship                 6.022 141 29 e26      0.000 000 27 e26      u
+kilogram-electron volt relationship                    5.609 588 85 e35      0.000 000 12 e35      eV
+kilogram-hartree relationship                          2.061 485 968 e34     0.000 000 091 e34     E_h
+kilogram-hertz relationship                            1.356 392 608 e50     0.000 000 060 e50     Hz
+kilogram-inverse meter relationship                    4.524 438 73 e41      0.000 000 20 e41      m^-1
+kilogram-joule relationship                            8.987 551 787... e16  (exact)               J
+kilogram-kelvin relationship                           6.509 6582 e39        0.000 0059 e39        K
+lattice parameter of silicon                           543.102 0504 e-12     0.000 0089 e-12       m
+Loschmidt constant (273.15 K, 100 kPa)                 2.651 6462 e25        0.000 0024 e25        m^-3
+Loschmidt constant (273.15 K, 101.325 kPa)             2.686 7805 e25        0.000 0024 e25        m^-3
+mag. constant                                          12.566 370 614... e-7 (exact)               N A^-2
+mag. flux quantum                                      2.067 833 758 e-15    0.000 000 046 e-15    Wb
+molar gas constant                                     8.314 4621            0.000 0075            J mol^-1 K^-1
+molar mass constant                                    1 e-3                 (exact)               kg mol^-1
+molar mass of carbon-12                                12 e-3                (exact)               kg mol^-1
+molar Planck constant                                  3.990 312 7176 e-10   0.000 000 0028 e-10   J s mol^-1
+molar Planck constant times c                          0.119 626 565 779     0.000 000 000 084     J m mol^-1
+molar volume of ideal gas (273.15 K, 100 kPa)          22.710 953 e-3        0.000 021 e-3         m^3 mol^-1
+molar volume of ideal gas (273.15 K, 101.325 kPa)      22.413 968 e-3        0.000 020 e-3         m^3 mol^-1
+molar volume of silicon                                12.058 833 01 e-6     0.000 000 80 e-6      m^3 mol^-1
+Mo x unit                                              1.002 099 52 e-13     0.000 000 53 e-13     m
+muon Compton wavelength                                11.734 441 03 e-15    0.000 000 30 e-15     m
+muon Compton wavelength over 2 pi                      1.867 594 294 e-15    0.000 000 047 e-15    m
+muon-electron mass ratio                               206.768 2843          0.000 0052
+muon g factor                                          -2.002 331 8418       0.000 000 0013
+muon mag. mom.                                         -4.490 448 07 e-26    0.000 000 15 e-26     J T^-1
+muon mag. mom. anomaly                                 1.165 920 91 e-3      0.000 000 63 e-3
+muon mag. mom. to Bohr magneton ratio                  -4.841 970 44 e-3     0.000 000 12 e-3
+muon mag. mom. to nuclear magneton ratio               -8.890 596 97         0.000 000 22
+muon mass                                              1.883 531 475 e-28    0.000 000 096 e-28    kg
+muon mass energy equivalent                            1.692 833 667 e-11    0.000 000 086 e-11    J
+muon mass energy equivalent in MeV                     105.658 3715          0.000 0035            MeV
+muon mass in u                                         0.113 428 9267        0.000 000 0029        u
+muon molar mass                                        0.113 428 9267 e-3    0.000 000 0029 e-3    kg mol^-1
+muon-neutron mass ratio                                0.112 454 5177        0.000 000 0028
+muon-proton mag. mom. ratio                            -3.183 345 107        0.000 000 084
+muon-proton mass ratio                                 0.112 609 5272        0.000 000 0028
+muon-tau mass ratio                                    5.946 49 e-2          0.000 54 e-2
+natural unit of action                                 1.054 571 726 e-34    0.000 000 047 e-34    J s
+natural unit of action in eV s                         6.582 119 28 e-16     0.000 000 15 e-16     eV s
+natural unit of energy                                 8.187 105 06 e-14     0.000 000 36 e-14     J
+natural unit of energy in MeV                          0.510 998 928         0.000 000 011         MeV
+natural unit of length                                 386.159 268 00 e-15   0.000 000 25 e-15     m
+natural unit of mass                                   9.109 382 91 e-31     0.000 000 40 e-31     kg
+natural unit of mom.um                                 2.730 924 29 e-22     0.000 000 12 e-22     kg m s^-1
+natural unit of mom.um in MeV/c                        0.510 998 928         0.000 000 011         MeV/c
+natural unit of time                                   1.288 088 668 33 e-21 0.000 000 000 83 e-21 s
+natural unit of velocity                               299 792 458           (exact)               m s^-1
+neutron Compton wavelength                             1.319 590 9068 e-15   0.000 000 0011 e-15   m
+neutron Compton wavelength over 2 pi                   0.210 019 415 68 e-15 0.000 000 000 17 e-15 m
+neutron-electron mag. mom. ratio                       1.040 668 82 e-3      0.000 000 25 e-3
+neutron-electron mass ratio                            1838.683 6605         0.000 0011
+neutron g factor                                       -3.826 085 45         0.000 000 90
+neutron gyromag. ratio                                 1.832 471 79 e8       0.000 000 43 e8       s^-1 T^-1
+neutron gyromag. ratio over 2 pi                       29.164 6943           0.000 0069            MHz T^-1
+neutron mag. mom.                                      -0.966 236 47 e-26    0.000 000 23 e-26     J T^-1
+neutron mag. mom. to Bohr magneton ratio               -1.041 875 63 e-3     0.000 000 25 e-3
+neutron mag. mom. to nuclear magneton ratio            -1.913 042 72         0.000 000 45
+neutron mass                                           1.674 927 351 e-27    0.000 000 074 e-27    kg
+neutron mass energy equivalent                         1.505 349 631 e-10    0.000 000 066 e-10    J
+neutron mass energy equivalent in MeV                  939.565 379           0.000 021             MeV
+neutron mass in u                                      1.008 664 916 00      0.000 000 000 43      u
+neutron molar mass                                     1.008 664 916 00 e-3  0.000 000 000 43 e-3  kg mol^-1
+neutron-muon mass ratio                                8.892 484 00          0.000 000 22
+neutron-proton mag. mom. ratio                         -0.684 979 34         0.000 000 16
+neutron-proton mass difference                         2.305 573 92 e-30     0.000 000 76 e-30
+neutron-proton mass difference energy equivalent       2.072 146 50 e-13     0.000 000 68 e-13
+neutron-proton mass difference energy equivalent in MeV 1.293 332 17          0.000 000 42
+neutron-proton mass difference in u                    0.001 388 449 19      0.000 000 000 45
+neutron-proton mass ratio                              1.001 378 419 17      0.000 000 000 45
+neutron-tau mass ratio                                 0.528 790             0.000 048
+neutron to shielded proton mag. mom. ratio             -0.684 996 94         0.000 000 16
+Newtonian constant of gravitation                      6.673 84 e-11         0.000 80 e-11         m^3 kg^-1 s^-2
+Newtonian constant of gravitation over h-bar c         6.708 37 e-39         0.000 80 e-39         (GeV/c^2)^-2
+nuclear magneton                                       5.050 783 53 e-27     0.000 000 11 e-27     J T^-1
+nuclear magneton in eV/T                               3.152 451 2605 e-8    0.000 000 0022 e-8    eV T^-1
+nuclear magneton in inverse meters per tesla           2.542 623 527 e-2     0.000 000 056 e-2     m^-1 T^-1
+nuclear magneton in K/T                                3.658 2682 e-4        0.000 0033 e-4        K T^-1
+nuclear magneton in MHz/T                              7.622 593 57          0.000 000 17          MHz T^-1
+Planck constant                                        6.626 069 57 e-34     0.000 000 29 e-34     J s
+Planck constant in eV s                                4.135 667 516 e-15    0.000 000 091 e-15    eV s
+Planck constant over 2 pi                              1.054 571 726 e-34    0.000 000 047 e-34    J s
+Planck constant over 2 pi in eV s                      6.582 119 28 e-16     0.000 000 15 e-16     eV s
+Planck constant over 2 pi times c in MeV fm            197.326 9718          0.000 0044            MeV fm
+Planck length                                          1.616 199 e-35        0.000 097 e-35        m
+Planck mass                                            2.176 51 e-8          0.000 13 e-8          kg
+Planck mass energy equivalent in GeV                   1.220 932 e19         0.000 073 e19         GeV
+Planck temperature                                     1.416 833 e32         0.000 085 e32         K
+Planck time                                            5.391 06 e-44         0.000 32 e-44         s
+proton charge to mass quotient                         9.578 833 58 e7       0.000 000 21 e7       C kg^-1
+proton Compton wavelength                              1.321 409 856 23 e-15 0.000 000 000 94 e-15 m
+proton Compton wavelength over 2 pi                    0.210 308 910 47 e-15 0.000 000 000 15 e-15 m
+proton-electron mass ratio                             1836.152 672 45       0.000 000 75
+proton g factor                                        5.585 694 713         0.000 000 046
+proton gyromag. ratio                                  2.675 222 005 e8      0.000 000 063 e8      s^-1 T^-1
+proton gyromag. ratio over 2 pi                        42.577 4806           0.000 0010            MHz T^-1
+proton mag. mom.                                       1.410 606 743 e-26    0.000 000 033 e-26    J T^-1
+proton mag. mom. to Bohr magneton ratio                1.521 032 210 e-3     0.000 000 012 e-3
+proton mag. mom. to nuclear magneton ratio             2.792 847 356         0.000 000 023
+proton mag. shielding correction                       25.694 e-6            0.014 e-6
+proton mass                                            1.672 621 777 e-27    0.000 000 074 e-27    kg
+proton mass energy equivalent                          1.503 277 484 e-10    0.000 000 066 e-10    J
+proton mass energy equivalent in MeV                   938.272 046           0.000 021             MeV
+proton mass in u                                       1.007 276 466 812     0.000 000 000 090     u
+proton molar mass                                      1.007 276 466 812 e-3 0.000 000 000 090 e-3 kg mol^-1
+proton-muon mass ratio                                 8.880 243 31          0.000 000 22
+proton-neutron mag. mom. ratio                         -1.459 898 06         0.000 000 34
+proton-neutron mass ratio                              0.998 623 478 26      0.000 000 000 45
+proton rms charge radius                               0.8775 e-15           0.0051 e-15           m
+proton-tau mass ratio                                  0.528 063             0.000 048
+quantum of circulation                                 3.636 947 5520 e-4    0.000 000 0024 e-4    m^2 s^-1
+quantum of circulation times 2                         7.273 895 1040 e-4    0.000 000 0047 e-4    m^2 s^-1
+Rydberg constant                                       10 973 731.568 539    0.000 055             m^-1
+Rydberg constant times c in Hz                         3.289 841 960 364 e15 0.000 000 000 017 e15 Hz
+Rydberg constant times hc in eV                        13.605 692 53         0.000 000 30          eV
+Rydberg constant times hc in J                         2.179 872 171 e-18    0.000 000 096 e-18    J
+Sackur-Tetrode constant (1 K, 100 kPa)                 -1.151 7078           0.000 0023
+Sackur-Tetrode constant (1 K, 101.325 kPa)             -1.164 8708           0.000 0023
+second radiation constant                              1.438 7770 e-2        0.000 0013 e-2        m K
+shielded helion gyromag. ratio                         2.037 894 659 e8      0.000 000 051 e8      s^-1 T^-1
+shielded helion gyromag. ratio over 2 pi               32.434 100 84         0.000 000 81          MHz T^-1
+shielded helion mag. mom.                              -1.074 553 044 e-26   0.000 000 027 e-26    J T^-1
+shielded helion mag. mom. to Bohr magneton ratio       -1.158 671 471 e-3    0.000 000 014 e-3
+shielded helion mag. mom. to nuclear magneton ratio    -2.127 497 718        0.000 000 025
+shielded helion to proton mag. mom. ratio              -0.761 766 558        0.000 000 011
+shielded helion to shielded proton mag. mom. ratio     -0.761 786 1313       0.000 000 0033
+shielded proton gyromag. ratio                         2.675 153 268 e8      0.000 000 066 e8      s^-1 T^-1
+shielded proton gyromag. ratio over 2 pi               42.576 3866           0.000 0010            MHz T^-1
+shielded proton mag. mom.                              1.410 570 499 e-26    0.000 000 035 e-26    J T^-1
+shielded proton mag. mom. to Bohr magneton ratio       1.520 993 128 e-3     0.000 000 017 e-3
+shielded proton mag. mom. to nuclear magneton ratio    2.792 775 598         0.000 000 030
+speed of light in vacuum                               299 792 458           (exact)               m s^-1
+standard acceleration of gravity                       9.806 65              (exact)               m s^-2
+standard atmosphere                                    101 325               (exact)               Pa
+standard-state pressure                                100 000               (exact)               Pa
+Stefan-Boltzmann constant                              5.670 373 e-8         0.000 021 e-8         W m^-2 K^-4
+tau Compton wavelength                                 0.697 787 e-15        0.000 063 e-15        m
+tau Compton wavelength over 2 pi                       0.111 056 e-15        0.000 010 e-15        m
+tau-electron mass ratio                                3477.15               0.31
+tau mass                                               3.167 47 e-27         0.000 29 e-27         kg
+tau mass energy equivalent                             2.846 78 e-10         0.000 26 e-10         J
+tau mass energy equivalent in MeV                      1776.82               0.16                  MeV
+tau mass in u                                          1.907 49              0.000 17              u
+tau molar mass                                         1.907 49 e-3          0.000 17 e-3          kg mol^-1
+tau-muon mass ratio                                    16.8167               0.0015
+tau-neutron mass ratio                                 1.891 11              0.000 17
+tau-proton mass ratio                                  1.893 72              0.000 17
+Thomson cross section                                  0.665 245 8734 e-28   0.000 000 0013 e-28   m^2
+triton-electron mass ratio                             5496.921 5267         0.000 0050
+triton g factor                                        5.957 924 896         0.000 000 076
+triton mag. mom.                                       1.504 609 447 e-26    0.000 000 038 e-26    J T^-1
+triton mag. mom. to Bohr magneton ratio                1.622 393 657 e-3     0.000 000 021 e-3
+triton mag. mom. to nuclear magneton ratio             2.978 962 448         0.000 000 038
+triton mass                                            5.007 356 30 e-27     0.000 000 22 e-27     kg
+triton mass energy equivalent                          4.500 387 41 e-10     0.000 000 20 e-10     J
+triton mass energy equivalent in MeV                   2808.921 005          0.000 062             MeV
+triton mass in u                                       3.015 500 7134        0.000 000 0025        u
+triton molar mass                                      3.015 500 7134 e-3    0.000 000 0025 e-3    kg mol^-1
+triton-proton mass ratio                               2.993 717 0308        0.000 000 0025
+unified atomic mass unit                               1.660 538 921 e-27    0.000 000 073 e-27    kg
+von Klitzing constant                                  25 812.807 4434       0.000 0084            ohm
+weak mixing angle                                      0.2223                0.0021
+Wien frequency displacement law constant               5.878 9254 e10        0.000 0053 e10        Hz K^-1
+Wien wavelength displacement law constant              2.897 7721 e-3        0.000 0026 e-3        m K"""
+
+
+exact2010 = exact2006
+
+
+txt2014 = """\
+{220} lattice spacing of silicon                       192.015 5714 e-12     0.000 0032 e-12       m
+alpha particle-electron mass ratio                     7294.299 541 36       0.000 000 24
+alpha particle mass                                    6.644 657 230 e-27    0.000 000 082 e-27    kg
+alpha particle mass energy equivalent                  5.971 920 097 e-10    0.000 000 073 e-10    J
+alpha particle mass energy equivalent in MeV           3727.379 378          0.000 023             MeV
+alpha particle mass in u                               4.001 506 179 127     0.000 000 000 063     u
+alpha particle molar mass                              4.001 506 179 127 e-3 0.000 000 000 063 e-3 kg mol^-1
+alpha particle-proton mass ratio                       3.972 599 689 07      0.000 000 000 36
+Angstrom star                                          1.000 014 95 e-10     0.000 000 90 e-10     m
+atomic mass constant                                   1.660 539 040 e-27    0.000 000 020 e-27    kg
+atomic mass constant energy equivalent                 1.492 418 062 e-10    0.000 000 018 e-10    J
+atomic mass constant energy equivalent in MeV          931.494 0954          0.000 0057            MeV
+atomic mass unit-electron volt relationship            931.494 0954 e6       0.000 0057 e6         eV
+atomic mass unit-hartree relationship                  3.423 177 6902 e7     0.000 000 0016 e7     E_h
+atomic mass unit-hertz relationship                    2.252 342 7206 e23    0.000 000 0010 e23    Hz
+atomic mass unit-inverse meter relationship            7.513 006 6166 e14    0.000 000 0034 e14    m^-1
+atomic mass unit-joule relationship                    1.492 418 062 e-10    0.000 000 018 e-10    J
+atomic mass unit-kelvin relationship                   1.080 954 38 e13      0.000 000 62 e13      K
+atomic mass unit-kilogram relationship                 1.660 539 040 e-27    0.000 000 020 e-27    kg
+atomic unit of 1st hyperpolarizability                 3.206 361 329 e-53    0.000 000 020 e-53    C^3 m^3 J^-2
+atomic unit of 2nd hyperpolarizability                 6.235 380 085 e-65    0.000 000 077 e-65    C^4 m^4 J^-3
+atomic unit of action                                  1.054 571 800 e-34    0.000 000 013 e-34    J s
+atomic unit of charge                                  1.602 176 6208 e-19   0.000 000 0098 e-19   C
+atomic unit of charge density                          1.081 202 3770 e12    0.000 000 0067 e12    C m^-3
+atomic unit of current                                 6.623 618 183 e-3     0.000 000 041 e-3     A
+atomic unit of electric dipole mom.                    8.478 353 552 e-30    0.000 000 052 e-30    C m
+atomic unit of electric field                          5.142 206 707 e11     0.000 000 032 e11     V m^-1
+atomic unit of electric field gradient                 9.717 362 356 e21     0.000 000 060 e21     V m^-2
+atomic unit of electric polarizability                 1.648 777 2731 e-41   0.000 000 0011 e-41   C^2 m^2 J^-1
+atomic unit of electric potential                      27.211 386 02         0.000 000 17          V
+atomic unit of electric quadrupole mom.                4.486 551 484 e-40    0.000 000 028 e-40    C m^2
+atomic unit of energy                                  4.359 744 650 e-18    0.000 000 054 e-18    J
+atomic unit of force                                   8.238 723 36 e-8      0.000 000 10 e-8      N
+atomic unit of length                                  0.529 177 210 67 e-10 0.000 000 000 12 e-10 m
+atomic unit of mag. dipole mom.                        1.854 801 999 e-23    0.000 000 011 e-23    J T^-1
+atomic unit of mag. flux density                       2.350 517 550 e5      0.000 000 014 e5      T
+atomic unit of magnetizability                         7.891 036 5886 e-29   0.000 000 0090 e-29   J T^-2
+atomic unit of mass                                    9.109 383 56 e-31     0.000 000 11 e-31     kg
+atomic unit of mom.um                                  1.992 851 882 e-24    0.000 000 024 e-24    kg m s^-1
+atomic unit of permittivity                            1.112 650 056... e-10 (exact)               F m^-1
+atomic unit of time                                    2.418 884 326509e-17  0.000 000 000014e-17  s
+atomic unit of velocity                                2.187 691 262 77 e6   0.000 000 000 50 e6   m s^-1
+Avogadro constant                                      6.022 140 857 e23     0.000 000 074 e23     mol^-1
+Bohr magneton                                          927.400 9994 e-26     0.000 0057 e-26       J T^-1
+Bohr magneton in eV/T                                  5.788 381 8012 e-5    0.000 000 0026 e-5    eV T^-1
+Bohr magneton in Hz/T                                  13.996 245 042 e9     0.000 000 086 e9      Hz T^-1
+Bohr magneton in inverse meters per tesla              46.686 448 14         0.000 000 29          m^-1 T^-1
+Bohr magneton in K/T                                   0.671 714 05          0.000 000 39          K T^-1
+Bohr radius                                            0.529 177 210 67 e-10 0.000 000 000 12 e-10 m
+Boltzmann constant                                     1.380 648 52 e-23     0.000 000 79 e-23     J K^-1
+Boltzmann constant in eV/K                             8.617 3303 e-5        0.000 0050 e-5        eV K^-1
+Boltzmann constant in Hz/K                             2.083 6612 e10        0.000 0012 e10        Hz K^-1
+Boltzmann constant in inverse meters per kelvin        69.503 457            0.000 040             m^-1 K^-1
+characteristic impedance of vacuum                     376.730 313 461...    (exact)               ohm
+classical electron radius                              2.817 940 3227 e-15   0.000 000 0019 e-15   m
+Compton wavelength                                     2.426 310 2367 e-12   0.000 000 0011 e-12   m
+Compton wavelength over 2 pi                           386.159 267 64 e-15   0.000 000 18 e-15     m
+conductance quantum                                    7.748 091 7310 e-5    0.000 000 0018 e-5    S
+conventional value of Josephson constant               483 597.9 e9          (exact)               Hz V^-1
+conventional value of von Klitzing constant            25 812.807            (exact)               ohm
+Cu x unit                                              1.002 076 97 e-13     0.000 000 28 e-13     m
+deuteron-electron mag. mom. ratio                      -4.664 345 535 e-4    0.000 000 026 e-4
+deuteron-electron mass ratio                           3670.482 967 85       0.000 000 13
+deuteron g factor                                      0.857 438 2311        0.000 000 0048
+deuteron mag. mom.                                     0.433 073 5040 e-26   0.000 000 0036 e-26   J T^-1
+deuteron mag. mom. to Bohr magneton ratio              0.466 975 4554 e-3    0.000 000 0026 e-3
+deuteron mag. mom. to nuclear magneton ratio           0.857 438 2311        0.000 000 0048
+deuteron mass                                          3.343 583 719 e-27    0.000 000 041 e-27    kg
+deuteron mass energy equivalent                        3.005 063 183 e-10    0.000 000 037 e-10    J
+deuteron mass energy equivalent in MeV                 1875.612 928          0.000 012             MeV
+deuteron mass in u                                     2.013 553 212 745     0.000 000 000 040     u
+deuteron molar mass                                    2.013 553 212 745 e-3 0.000 000 000 040 e-3 kg mol^-1
+deuteron-neutron mag. mom. ratio                       -0.448 206 52         0.000 000 11
+deuteron-proton mag. mom. ratio                        0.307 012 2077        0.000 000 0015
+deuteron-proton mass ratio                             1.999 007 500 87      0.000 000 000 19
+deuteron rms charge radius                             2.1413 e-15           0.0025 e-15           m
+electric constant                                      8.854 187 817... e-12 (exact)               F m^-1
+electron charge to mass quotient                       -1.758 820 024 e11    0.000 000 011 e11     C kg^-1
+electron-deuteron mag. mom. ratio                      -2143.923 499         0.000 012
+electron-deuteron mass ratio                           2.724 437 107 484 e-4 0.000 000 000 096 e-4
+electron g factor                                      -2.002 319 304 361 82 0.000 000 000 000 52
+electron gyromag. ratio                                1.760 859 644 e11     0.000 000 011 e11     s^-1 T^-1
+electron gyromag. ratio over 2 pi                      28 024.951 64         0.000 17              MHz T^-1
+electron-helion mass ratio                             1.819 543 074 854 e-4 0.000 000 000 088 e-4
+electron mag. mom.                                     -928.476 4620 e-26    0.000 0057 e-26       J T^-1
+electron mag. mom. anomaly                             1.159 652 180 91 e-3  0.000 000 000 26 e-3
+electron mag. mom. to Bohr magneton ratio              -1.001 159 652 180 91 0.000 000 000 000 26
+electron mag. mom. to nuclear magneton ratio           -1838.281 972 34      0.000 000 17
+electron mass                                          9.109 383 56 e-31     0.000 000 11 e-31     kg
+electron mass energy equivalent                        8.187 105 65 e-14     0.000 000 10 e-14     J
+electron mass energy equivalent in MeV                 0.510 998 9461        0.000 000 0031        MeV
+electron mass in u                                     5.485 799 090 70 e-4  0.000 000 000 16 e-4  u
+electron molar mass                                    5.485 799 090 70 e-7  0.000 000 000 16 e-7  kg mol^-1
+electron-muon mag. mom. ratio                          206.766 9880          0.000 0046
+electron-muon mass ratio                               4.836 331 70 e-3      0.000 000 11 e-3
+electron-neutron mag. mom. ratio                       960.920 50            0.000 23
+electron-neutron mass ratio                            5.438 673 4428 e-4    0.000 000 0027 e-4
+electron-proton mag. mom. ratio                        -658.210 6866         0.000 0020
+electron-proton mass ratio                             5.446 170 213 52 e-4  0.000 000 000 52 e-4
+electron-tau mass ratio                                2.875 92 e-4          0.000 26 e-4
+electron to alpha particle mass ratio                  1.370 933 554 798 e-4 0.000 000 000 045 e-4
+electron to shielded helion mag. mom. ratio            864.058 257           0.000 010
+electron to shielded proton mag. mom. ratio            -658.227 5971         0.000 0072
+electron-triton mass ratio                             1.819 200 062 203 e-4 0.000 000 000 084 e-4
+electron volt                                          1.602 176 6208 e-19   0.000 000 0098 e-19   J
+electron volt-atomic mass unit relationship            1.073 544 1105 e-9    0.000 000 0066 e-9    u
+electron volt-hartree relationship                     3.674 932 248 e-2     0.000 000 023 e-2     E_h
+electron volt-hertz relationship                       2.417 989 262 e14     0.000 000 015 e14     Hz
+electron volt-inverse meter relationship               8.065 544 005 e5      0.000 000 050 e5      m^-1
+electron volt-joule relationship                       1.602 176 6208 e-19   0.000 000 0098 e-19   J
+electron volt-kelvin relationship                      1.160 452 21 e4       0.000 000 67 e4       K
+electron volt-kilogram relationship                    1.782 661 907 e-36    0.000 000 011 e-36    kg
+elementary charge                                      1.602 176 6208 e-19   0.000 000 0098 e-19   C
+elementary charge over h                               2.417 989 262 e14     0.000 000 015 e14     A J^-1
+Faraday constant                                       96 485.332 89         0.000 59              C mol^-1
+Faraday constant for conventional electric current     96 485.3251           0.0012                C_90 mol^-1
+Fermi coupling constant                                1.166 3787 e-5        0.000 0006 e-5        GeV^-2
+fine-structure constant                                7.297 352 5664 e-3    0.000 000 0017 e-3
+first radiation constant                               3.741 771 790 e-16    0.000 000 046 e-16    W m^2
+first radiation constant for spectral radiance         1.191 042 953 e-16    0.000 000 015 e-16    W m^2 sr^-1
+hartree-atomic mass unit relationship                  2.921 262 3197 e-8    0.000 000 0013 e-8    u
+hartree-electron volt relationship                     27.211 386 02         0.000 000 17          eV
+Hartree energy                                         4.359 744 650 e-18    0.000 000 054 e-18    J
+Hartree energy in eV                                   27.211 386 02         0.000 000 17          eV
+hartree-hertz relationship                             6.579 683 920 711 e15 0.000 000 000 039 e15 Hz
+hartree-inverse meter relationship                     2.194 746 313 702 e7  0.000 000 000 013 e7  m^-1
+hartree-joule relationship                             4.359 744 650 e-18    0.000 000 054 e-18    J
+hartree-kelvin relationship                            3.157 7513 e5         0.000 0018 e5         K
+hartree-kilogram relationship                          4.850 870 129 e-35    0.000 000 060 e-35    kg
+helion-electron mass ratio                             5495.885 279 22       0.000 000 27
+helion g factor                                        -4.255 250 616        0.000 000 050
+helion mag. mom.                                       -1.074 617 522 e-26   0.000 000 014 e-26    J T^-1
+helion mag. mom. to Bohr magneton ratio                -1.158 740 958 e-3    0.000 000 014 e-3
+helion mag. mom. to nuclear magneton ratio             -2.127 625 308        0.000 000 025
+helion mass                                            5.006 412 700 e-27    0.000 000 062 e-27    kg
+helion mass energy equivalent                          4.499 539 341 e-10    0.000 000 055 e-10    J
+helion mass energy equivalent in MeV                   2808.391 586          0.000 017             MeV
+helion mass in u                                       3.014 932 246 73      0.000 000 000 12      u
+helion molar mass                                      3.014 932 246 73 e-3  0.000 000 000 12 e-3  kg mol^-1
+helion-proton mass ratio                               2.993 152 670 46      0.000 000 000 29
+hertz-atomic mass unit relationship                    4.439 821 6616 e-24   0.000 000 0020 e-24   u
+hertz-electron volt relationship                       4.135 667 662 e-15    0.000 000 025 e-15    eV
+hertz-hartree relationship                             1.5198298460088 e-16  0.0000000000090e-16   E_h
+hertz-inverse meter relationship                       3.335 640 951... e-9  (exact)               m^-1
+hertz-joule relationship                               6.626 070 040 e-34    0.000 000 081 e-34    J
+hertz-kelvin relationship                              4.799 2447 e-11       0.000 0028 e-11       K
+hertz-kilogram relationship                            7.372 497 201 e-51    0.000 000 091 e-51    kg
+inverse fine-structure constant                        137.035 999 139       0.000 000 031
+inverse meter-atomic mass unit relationship            1.331 025 049 00 e-15 0.000 000 000 61 e-15 u
+inverse meter-electron volt relationship               1.239 841 9739 e-6    0.000 000 0076 e-6    eV
+inverse meter-hartree relationship                     4.556 335 252 767 e-8 0.000 000 000 027 e-8 E_h
+inverse meter-hertz relationship                       299 792 458           (exact)               Hz
+inverse meter-joule relationship                       1.986 445 824 e-25    0.000 000 024 e-25    J
+inverse meter-kelvin relationship                      1.438 777 36 e-2      0.000 000 83 e-2      K
+inverse meter-kilogram relationship                    2.210 219 057 e-42    0.000 000 027 e-42    kg
+inverse of conductance quantum                         12 906.403 7278       0.000 0029            ohm
+Josephson constant                                     483 597.8525 e9       0.0030 e9             Hz V^-1
+joule-atomic mass unit relationship                    6.700 535 363 e9      0.000 000 082 e9      u
+joule-electron volt relationship                       6.241 509 126 e18     0.000 000 038 e18     eV
+joule-hartree relationship                             2.293 712 317 e17     0.000 000 028 e17     E_h
+joule-hertz relationship                               1.509 190 205 e33     0.000 000 019 e33     Hz
+joule-inverse meter relationship                       5.034 116 651 e24     0.000 000 062 e24     m^-1
+joule-kelvin relationship                              7.242 9731 e22        0.000 0042 e22        K
+joule-kilogram relationship                            1.112 650 056... e-17 (exact)               kg
+kelvin-atomic mass unit relationship                   9.251 0842 e-14       0.000 0053 e-14       u
+kelvin-electron volt relationship                      8.617 3303 e-5        0.000 0050 e-5        eV
+kelvin-hartree relationship                            3.166 8105 e-6        0.000 0018 e-6        E_h
+kelvin-hertz relationship                              2.083 6612 e10        0.000 0012 e10        Hz
+kelvin-inverse meter relationship                      69.503 457            0.000 040             m^-1
+kelvin-joule relationship                              1.380 648 52 e-23     0.000 000 79 e-23     J
+kelvin-kilogram relationship                           1.536 178 65 e-40     0.000 000 88 e-40     kg
+kilogram-atomic mass unit relationship                 6.022 140 857 e26     0.000 000 074 e26     u
+kilogram-electron volt relationship                    5.609 588 650 e35     0.000 000 034 e35     eV
+kilogram-hartree relationship                          2.061 485 823 e34     0.000 000 025 e34     E_h
+kilogram-hertz relationship                            1.356 392 512 e50     0.000 000 017 e50     Hz
+kilogram-inverse meter relationship                    4.524 438 411 e41     0.000 000 056 e41     m^-1
+kilogram-joule relationship                            8.987 551 787... e16  (exact)               J
+kilogram-kelvin relationship                           6.509 6595 e39        0.000 0037 e39        K
+lattice parameter of silicon                           543.102 0504 e-12     0.000 0089 e-12       m
+Loschmidt constant (273.15 K, 100 kPa)                 2.651 6467 e25        0.000 0015 e25        m^-3
+Loschmidt constant (273.15 K, 101.325 kPa)             2.686 7811 e25        0.000 0015 e25        m^-3
+mag. constant                                          12.566 370 614... e-7 (exact)               N A^-2
+mag. flux quantum                                      2.067 833 831 e-15    0.000 000 013 e-15    Wb
+molar gas constant                                     8.314 4598            0.000 0048            J mol^-1 K^-1
+molar mass constant                                    1 e-3                 (exact)               kg mol^-1
+molar mass of carbon-12                                12 e-3                (exact)               kg mol^-1
+molar Planck constant                                  3.990 312 7110 e-10   0.000 000 0018 e-10   J s mol^-1
+molar Planck constant times c                          0.119 626 565 582     0.000 000 000 054     J m mol^-1
+molar volume of ideal gas (273.15 K, 100 kPa)          22.710 947 e-3        0.000 013 e-3         m^3 mol^-1
+molar volume of ideal gas (273.15 K, 101.325 kPa)      22.413 962 e-3        0.000 013 e-3         m^3 mol^-1
+molar volume of silicon                                12.058 832 14 e-6     0.000 000 61 e-6      m^3 mol^-1
+Mo x unit                                              1.002 099 52 e-13     0.000 000 53 e-13     m
+muon Compton wavelength                                11.734 441 11 e-15    0.000 000 26 e-15     m
+muon Compton wavelength over 2 pi                      1.867 594 308 e-15    0.000 000 042 e-15    m
+muon-electron mass ratio                               206.768 2826          0.000 0046
+muon g factor                                          -2.002 331 8418       0.000 000 0013
+muon mag. mom.                                         -4.490 448 26 e-26    0.000 000 10 e-26     J T^-1
+muon mag. mom. anomaly                                 1.165 920 89 e-3      0.000 000 63 e-3
+muon mag. mom. to Bohr magneton ratio                  -4.841 970 48 e-3     0.000 000 11 e-3
+muon mag. mom. to nuclear magneton ratio               -8.890 597 05         0.000 000 20
+muon mass                                              1.883 531 594 e-28    0.000 000 048 e-28    kg
+muon mass energy equivalent                            1.692 833 774 e-11    0.000 000 043 e-11    J
+muon mass energy equivalent in MeV                     105.658 3745          0.000 0024            MeV
+muon mass in u                                         0.113 428 9257        0.000 000 0025        u
+muon molar mass                                        0.113 428 9257 e-3    0.000 000 0025 e-3    kg mol^-1
+muon-neutron mass ratio                                0.112 454 5167        0.000 000 0025
+muon-proton mag. mom. ratio                            -3.183 345 142        0.000 000 071
+muon-proton mass ratio                                 0.112 609 5262        0.000 000 0025
+muon-tau mass ratio                                    5.946 49 e-2          0.000 54 e-2
+natural unit of action                                 1.054 571 800 e-34    0.000 000 013 e-34    J s
+natural unit of action in eV s                         6.582 119 514 e-16    0.000 000 040 e-16    eV s
+natural unit of energy                                 8.187 105 65 e-14     0.000 000 10 e-14     J
+natural unit of energy in MeV                          0.510 998 9461        0.000 000 0031        MeV
+natural unit of length                                 386.159 267 64 e-15   0.000 000 18 e-15     m
+natural unit of mass                                   9.109 383 56 e-31     0.000 000 11 e-31     kg
+natural unit of mom.um                                 2.730 924 488 e-22    0.000 000 034 e-22    kg m s^-1
+natural unit of mom.um in MeV/c                        0.510 998 9461        0.000 000 0031        MeV/c
+natural unit of time                                   1.288 088 667 12 e-21 0.000 000 000 58 e-21 s
+natural unit of velocity                               299 792 458           (exact)               m s^-1
+neutron Compton wavelength                             1.319 590 904 81 e-15 0.000 000 000 88 e-15 m
+neutron Compton wavelength over 2 pi                   0.210 019 415 36 e-15 0.000 000 000 14 e-15 m
+neutron-electron mag. mom. ratio                       1.040 668 82 e-3      0.000 000 25 e-3
+neutron-electron mass ratio                            1838.683 661 58       0.000 000 90
+neutron g factor                                       -3.826 085 45         0.000 000 90
+neutron gyromag. ratio                                 1.832 471 72 e8       0.000 000 43 e8       s^-1 T^-1
+neutron gyromag. ratio over 2 pi                       29.164 6933           0.000 0069            MHz T^-1
+neutron mag. mom.                                      -0.966 236 50 e-26    0.000 000 23 e-26     J T^-1
+neutron mag. mom. to Bohr magneton ratio               -1.041 875 63 e-3     0.000 000 25 e-3
+neutron mag. mom. to nuclear magneton ratio            -1.913 042 73         0.000 000 45
+neutron mass                                           1.674 927 471 e-27    0.000 000 021 e-27    kg
+neutron mass energy equivalent                         1.505 349 739 e-10    0.000 000 019 e-10    J
+neutron mass energy equivalent in MeV                  939.565 4133          0.000 0058            MeV
+neutron mass in u                                      1.008 664 915 88      0.000 000 000 49      u
+neutron molar mass                                     1.008 664 915 88 e-3  0.000 000 000 49 e-3  kg mol^-1
+neutron-muon mass ratio                                8.892 484 08          0.000 000 20
+neutron-proton mag. mom. ratio                         -0.684 979 34         0.000 000 16
+neutron-proton mass difference                         2.305 573 77 e-30     0.000 000 85 e-30
+neutron-proton mass difference energy equivalent       2.072 146 37 e-13     0.000 000 76 e-13
+neutron-proton mass difference energy equivalent in MeV 1.293 332 05         0.000 000 48
+neutron-proton mass difference in u                    0.001 388 449 00      0.000 000 000 51
+neutron-proton mass ratio                              1.001 378 418 98      0.000 000 000 51
+neutron-tau mass ratio                                 0.528 790             0.000 048
+neutron to shielded proton mag. mom. ratio             -0.684 996 94         0.000 000 16
+Newtonian constant of gravitation                      6.674 08 e-11         0.000 31 e-11         m^3 kg^-1 s^-2
+Newtonian constant of gravitation over h-bar c         6.708 61 e-39         0.000 31 e-39         (GeV/c^2)^-2
+nuclear magneton                                       5.050 783 699 e-27    0.000 000 031 e-27    J T^-1
+nuclear magneton in eV/T                               3.152 451 2550 e-8    0.000 000 0015 e-8    eV T^-1
+nuclear magneton in inverse meters per tesla           2.542 623 432 e-2     0.000 000 016 e-2     m^-1 T^-1
+nuclear magneton in K/T                                3.658 2690 e-4        0.000 0021 e-4        K T^-1
+nuclear magneton in MHz/T                              7.622 593 285         0.000 000 047         MHz T^-1
+Planck constant                                        6.626 070 040 e-34    0.000 000 081 e-34    J s
+Planck constant in eV s                                4.135 667 662 e-15    0.000 000 025 e-15    eV s
+Planck constant over 2 pi                              1.054 571 800 e-34    0.000 000 013 e-34    J s
+Planck constant over 2 pi in eV s                      6.582 119 514 e-16    0.000 000 040 e-16    eV s
+Planck constant over 2 pi times c in MeV fm            197.326 9788          0.000 0012            MeV fm
+Planck length                                          1.616 229 e-35        0.000 038 e-35        m
+Planck mass                                            2.176 470 e-8         0.000 051 e-8         kg
+Planck mass energy equivalent in GeV                   1.220 910 e19         0.000 029 e19         GeV
+Planck temperature                                     1.416 808 e32         0.000 033 e32         K
+Planck time                                            5.391 16 e-44         0.000 13 e-44         s
+proton charge to mass quotient                         9.578 833 226 e7      0.000 000 059 e7      C kg^-1
+proton Compton wavelength                              1.321 409 853 96 e-15 0.000 000 000 61 e-15 m
+proton Compton wavelength over 2 pi                    0.210 308910109e-15   0.000 000 000097e-15  m
+proton-electron mass ratio                             1836.152 673 89       0.000 000 17
+proton g factor                                        5.585 694 702         0.000 000 017
+proton gyromag. ratio                                  2.675 221 900 e8      0.000 000 018 e8      s^-1 T^-1
+proton gyromag. ratio over 2 pi                        42.577 478 92         0.000 000 29          MHz T^-1
+proton mag. mom.                                       1.410 606 7873 e-26   0.000 000 0097 e-26   J T^-1
+proton mag. mom. to Bohr magneton ratio                1.521 032 2053 e-3    0.000 000 0046 e-3
+proton mag. mom. to nuclear magneton ratio             2.792 847 3508        0.000 000 0085
+proton mag. shielding correction                       25.691 e-6            0.011 e-6
+proton mass                                            1.672 621 898 e-27    0.000 000 021 e-27    kg
+proton mass energy equivalent                          1.503 277 593 e-10    0.000 000 018 e-10    J
+proton mass energy equivalent in MeV                   938.272 0813          0.000 0058            MeV
+proton mass in u                                       1.007 276 466 879     0.000 000 000 091     u
+proton molar mass                                      1.007 276 466 879 e-3 0.000 000 000 091 e-3 kg mol^-1
+proton-muon mass ratio                                 8.880 243 38          0.000 000 20
+proton-neutron mag. mom. ratio                         -1.459 898 05         0.000 000 34
+proton-neutron mass ratio                              0.998 623 478 44      0.000 000 000 51
+proton rms charge radius                               0.8751 e-15           0.0061 e-15           m
+proton-tau mass ratio                                  0.528 063             0.000 048
+quantum of circulation                                 3.636 947 5486 e-4    0.000 000 0017 e-4    m^2 s^-1
+quantum of circulation times 2                         7.273 895 0972 e-4    0.000 000 0033 e-4    m^2 s^-1
+Rydberg constant                                       10 973 731.568 508    0.000 065             m^-1
+Rydberg constant times c in Hz                         3.289 841 960 355 e15 0.000 000 000 019 e15 Hz
+Rydberg constant times hc in eV                        13.605 693 009        0.000 000 084         eV
+Rydberg constant times hc in J                         2.179 872 325 e-18    0.000 000 027 e-18    J
+Sackur-Tetrode constant (1 K, 100 kPa)                 -1.151 7084           0.000 0014
+Sackur-Tetrode constant (1 K, 101.325 kPa)             -1.164 8714           0.000 0014
+second radiation constant                              1.438 777 36 e-2      0.000 000 83 e-2      m K
+shielded helion gyromag. ratio                         2.037 894 585 e8      0.000 000 027 e8      s^-1 T^-1
+shielded helion gyromag. ratio over 2 pi               32.434 099 66         0.000 000 43          MHz T^-1
+shielded helion mag. mom.                              -1.074 553 080 e-26   0.000 000 014 e-26    J T^-1
+shielded helion mag. mom. to Bohr magneton ratio       -1.158 671 471 e-3    0.000 000 014 e-3
+shielded helion mag. mom. to nuclear magneton ratio    -2.127 497 720        0.000 000 025
+shielded helion to proton mag. mom. ratio              -0.761 766 5603       0.000 000 0092
+shielded helion to shielded proton mag. mom. ratio     -0.761 786 1313       0.000 000 0033
+shielded proton gyromag. ratio                         2.675 153 171 e8      0.000 000 033 e8      s^-1 T^-1
+shielded proton gyromag. ratio over 2 pi               42.576 385 07         0.000 000 53          MHz T^-1
+shielded proton mag. mom.                              1.410 570 547 e-26    0.000 000 018 e-26    J T^-1
+shielded proton mag. mom. to Bohr magneton ratio       1.520 993 128 e-3     0.000 000 017 e-3
+shielded proton mag. mom. to nuclear magneton ratio    2.792 775 600         0.000 000 030
+speed of light in vacuum                               299 792 458           (exact)               m s^-1
+standard acceleration of gravity                       9.806 65              (exact)               m s^-2
+standard atmosphere                                    101 325               (exact)               Pa
+standard-state pressure                                100 000               (exact)               Pa
+Stefan-Boltzmann constant                              5.670 367 e-8         0.000 013 e-8         W m^-2 K^-4
+tau Compton wavelength                                 0.697 787 e-15        0.000 063 e-15        m
+tau Compton wavelength over 2 pi                       0.111 056 e-15        0.000 010 e-15        m
+tau-electron mass ratio                                3477.15               0.31
+tau mass                                               3.167 47 e-27         0.000 29 e-27         kg
+tau mass energy equivalent                             2.846 78 e-10         0.000 26 e-10         J
+tau mass energy equivalent in MeV                      1776.82               0.16                  MeV
+tau mass in u                                          1.907 49              0.000 17              u
+tau molar mass                                         1.907 49 e-3          0.000 17 e-3          kg mol^-1
+tau-muon mass ratio                                    16.8167               0.0015
+tau-neutron mass ratio                                 1.891 11              0.000 17
+tau-proton mass ratio                                  1.893 72              0.000 17
+Thomson cross section                                  0.665 245 871 58 e-28 0.000 000 000 91 e-28 m^2
+triton-electron mass ratio                             5496.921 535 88       0.000 000 26
+triton g factor                                        5.957 924 920         0.000 000 028
+triton mag. mom.                                       1.504 609 503 e-26    0.000 000 012 e-26    J T^-1
+triton mag. mom. to Bohr magneton ratio                1.622 393 6616 e-3    0.000 000 0076 e-3
+triton mag. mom. to nuclear magneton ratio             2.978 962 460         0.000 000 014
+triton mass                                            5.007 356 665 e-27    0.000 000 062 e-27    kg
+triton mass energy equivalent                          4.500 387 735 e-10    0.000 000 055 e-10    J
+triton mass energy equivalent in MeV                   2808.921 112          0.000 017             MeV
+triton mass in u                                       3.015 500 716 32      0.000 000 000 11      u
+triton molar mass                                      3.015 500 716 32 e-3  0.000 000 000 11 e-3  kg mol^-1
+triton-proton mass ratio                               2.993 717 033 48      0.000 000 000 22
+unified atomic mass unit                               1.660 539 040 e-27    0.000 000 020 e-27    kg
+von Klitzing constant                                  25 812.807 4555       0.000 0059            ohm
+weak mixing angle                                      0.2223                0.0021
+Wien frequency displacement law constant               5.878 9238 e10        0.000 0034 e10        Hz K^-1
+Wien wavelength displacement law constant              2.897 7729 e-3        0.000 0017 e-3        m K"""
+
+
+exact2014 = exact2010
+
+
+txt2018 = """\
+alpha particle-electron mass ratio                          7294.299 541 42          0.000 000 24
+alpha particle mass                                         6.644 657 3357 e-27      0.000 000 0020 e-27      kg
+alpha particle mass energy equivalent                       5.971 920 1914 e-10      0.000 000 0018 e-10      J
+alpha particle mass energy equivalent in MeV                3727.379 4066            0.000 0011               MeV
+alpha particle mass in u                                    4.001 506 179 127        0.000 000 000 063        u
+alpha particle molar mass                                   4.001 506 1777 e-3       0.000 000 0012 e-3       kg mol^-1
+alpha particle-proton mass ratio                            3.972 599 690 09         0.000 000 000 22
+alpha particle relative atomic mass                         4.001 506 179 127        0.000 000 000 063
+Angstrom star                                               1.000 014 95 e-10        0.000 000 90 e-10        m
+atomic mass constant                                        1.660 539 066 60 e-27    0.000 000 000 50 e-27    kg
+atomic mass constant energy equivalent                      1.492 418 085 60 e-10    0.000 000 000 45 e-10    J
+atomic mass constant energy equivalent in MeV               931.494 102 42           0.000 000 28             MeV
+atomic mass unit-electron volt relationship                 9.314 941 0242 e8        0.000 000 0028 e8        eV
+atomic mass unit-hartree relationship                       3.423 177 6874 e7        0.000 000 0010 e7        E_h
+atomic mass unit-hertz relationship                         2.252 342 718 71 e23     0.000 000 000 68 e23     Hz
+atomic mass unit-inverse meter relationship                 7.513 006 6104 e14       0.000 000 0023 e14       m^-1
+atomic mass unit-joule relationship                         1.492 418 085 60 e-10    0.000 000 000 45 e-10    J
+atomic mass unit-kelvin relationship                        1.080 954 019 16 e13     0.000 000 000 33 e13     K
+atomic mass unit-kilogram relationship                      1.660 539 066 60 e-27    0.000 000 000 50 e-27    kg
+atomic unit of 1st hyperpolarizability                      3.206 361 3061 e-53      0.000 000 0015 e-53      C^3 m^3 J^-2
+atomic unit of 2nd hyperpolarizability                      6.235 379 9905 e-65      0.000 000 0038 e-65      C^4 m^4 J^-3
+atomic unit of action                                       1.054 571 817... e-34    (exact)                  J s
+atomic unit of charge                                       1.602 176 634 e-19       (exact)                  C
+atomic unit of charge density                               1.081 202 384 57 e12     0.000 000 000 49 e12     C m^-3
+atomic unit of current                                      6.623 618 237 510 e-3    0.000 000 000 013 e-3    A
+atomic unit of electric dipole mom.                         8.478 353 6255 e-30      0.000 000 0013 e-30      C m
+atomic unit of electric field                               5.142 206 747 63 e11     0.000 000 000 78 e11     V m^-1
+atomic unit of electric field gradient                      9.717 362 4292 e21       0.000 000 0029 e21       V m^-2
+atomic unit of electric polarizability                      1.648 777 274 36 e-41    0.000 000 000 50 e-41    C^2 m^2 J^-1
+atomic unit of electric potential                           27.211 386 245 988       0.000 000 000 053        V
+atomic unit of electric quadrupole mom.                     4.486 551 5246 e-40      0.000 000 0014 e-40      C m^2
+atomic unit of energy                                       4.359 744 722 2071 e-18  0.000 000 000 0085 e-18  J
+atomic unit of force                                        8.238 723 4983 e-8       0.000 000 0012 e-8       N
+atomic unit of length                                       5.291 772 109 03 e-11    0.000 000 000 80 e-11    m
+atomic unit of mag. dipole mom.                             1.854 802 015 66 e-23    0.000 000 000 56 e-23    J T^-1
+atomic unit of mag. flux density                            2.350 517 567 58 e5      0.000 000 000 71 e5      T
+atomic unit of magnetizability                              7.891 036 6008 e-29      0.000 000 0048 e-29      J T^-2
+atomic unit of mass                                         9.109 383 7015 e-31      0.000 000 0028 e-31      kg
+atomic unit of momentum                                     1.992 851 914 10 e-24    0.000 000 000 30 e-24    kg m s^-1
+atomic unit of permittivity                                 1.112 650 055 45 e-10    0.000 000 000 17 e-10    F m^-1
+atomic unit of time                                         2.418 884 326 5857 e-17  0.000 000 000 0047 e-17  s
+atomic unit of velocity                                     2.187 691 263 64 e6      0.000 000 000 33 e6      m s^-1
+Avogadro constant                                           6.022 140 76 e23         (exact)                  mol^-1
+Bohr magneton                                               9.274 010 0783 e-24      0.000 000 0028 e-24      J T^-1
+Bohr magneton in eV/T                                       5.788 381 8060 e-5       0.000 000 0017 e-5       eV T^-1
+Bohr magneton in Hz/T                                       1.399 624 493 61 e10     0.000 000 000 42 e10     Hz T^-1
+Bohr magneton in inverse meter per tesla                    46.686 447 783           0.000 000 014            m^-1 T^-1
+Bohr magneton in K/T                                        0.671 713 815 63         0.000 000 000 20         K T^-1
+Bohr radius                                                 5.291 772 109 03 e-11    0.000 000 000 80 e-11    m
+Boltzmann constant                                          1.380 649 e-23           (exact)                  J K^-1
+Boltzmann constant in eV/K                                  8.617 333 262... e-5     (exact)                  eV K^-1
+Boltzmann constant in Hz/K                                  2.083 661 912... e10     (exact)                  Hz K^-1
+Boltzmann constant in inverse meter per kelvin              69.503 480 04...         (exact)                  m^-1 K^-1
+characteristic impedance of vacuum                          376.730 313 668          0.000 000 057            ohm
+classical electron radius                                   2.817 940 3262 e-15      0.000 000 0013 e-15      m
+Compton wavelength                                          2.426 310 238 67 e-12    0.000 000 000 73 e-12    m
+conductance quantum                                         7.748 091 729... e-5     (exact)                  S
+conventional value of ampere-90                             1.000 000 088 87...      (exact)                  A
+conventional value of coulomb-90                            1.000 000 088 87...      (exact)                  C
+conventional value of farad-90                              0.999 999 982 20...      (exact)                  F
+conventional value of henry-90                              1.000 000 017 79...      (exact)                  H
+conventional value of Josephson constant                    483 597.9 e9             (exact)                  Hz V^-1
+conventional value of ohm-90                                1.000 000 017 79...      (exact)                  ohm
+conventional value of volt-90                               1.000 000 106 66...      (exact)                  V
+conventional value of von Klitzing constant                 25 812.807               (exact)                  ohm
+conventional value of watt-90                               1.000 000 195 53...      (exact)                  W
+Cu x unit                                                   1.002 076 97 e-13        0.000 000 28 e-13        m
+deuteron-electron mag. mom. ratio                           -4.664 345 551 e-4       0.000 000 012 e-4
+deuteron-electron mass ratio                                3670.482 967 88          0.000 000 13
+deuteron g factor                                           0.857 438 2338           0.000 000 0022
+deuteron mag. mom.                                          4.330 735 094 e-27       0.000 000 011 e-27       J T^-1
+deuteron mag. mom. to Bohr magneton ratio                   4.669 754 570 e-4        0.000 000 012 e-4
+deuteron mag. mom. to nuclear magneton ratio                0.857 438 2338           0.000 000 0022
+deuteron mass                                               3.343 583 7724 e-27      0.000 000 0010 e-27      kg
+deuteron mass energy equivalent                             3.005 063 231 02 e-10    0.000 000 000 91 e-10    J
+deuteron mass energy equivalent in MeV                      1875.612 942 57          0.000 000 57             MeV
+deuteron mass in u                                          2.013 553 212 745        0.000 000 000 040        u
+deuteron molar mass                                         2.013 553 212 05 e-3     0.000 000 000 61 e-3     kg mol^-1
+deuteron-neutron mag. mom. ratio                            -0.448 206 53            0.000 000 11
+deuteron-proton mag. mom. ratio                             0.307 012 209 39         0.000 000 000 79
+deuteron-proton mass ratio                                  1.999 007 501 39         0.000 000 000 11
+deuteron relative atomic mass                               2.013 553 212 745        0.000 000 000 040
+deuteron rms charge radius                                  2.127 99 e-15            0.000 74 e-15            m
+electron charge to mass quotient                            -1.758 820 010 76 e11    0.000 000 000 53 e11     C kg^-1
+electron-deuteron mag. mom. ratio                           -2143.923 4915           0.000 0056
+electron-deuteron mass ratio                                2.724 437 107 462 e-4    0.000 000 000 096 e-4
+electron g factor                                           -2.002 319 304 362 56    0.000 000 000 000 35
+electron gyromag. ratio                                     1.760 859 630 23 e11     0.000 000 000 53 e11     s^-1 T^-1
+electron gyromag. ratio in MHz/T                            28 024.951 4242          0.000 0085               MHz T^-1
+electron-helion mass ratio                                  1.819 543 074 573 e-4    0.000 000 000 079 e-4
+electron mag. mom.                                          -9.284 764 7043 e-24     0.000 000 0028 e-24      J T^-1
+electron mag. mom. anomaly                                  1.159 652 181 28 e-3     0.000 000 000 18 e-3
+electron mag. mom. to Bohr magneton ratio                   -1.001 159 652 181 28    0.000 000 000 000 18
+electron mag. mom. to nuclear magneton ratio                -1838.281 971 88         0.000 000 11
+electron mass                                               9.109 383 7015 e-31      0.000 000 0028 e-31      kg
+electron mass energy equivalent                             8.187 105 7769 e-14      0.000 000 0025 e-14      J
+electron mass energy equivalent in MeV                      0.510 998 950 00         0.000 000 000 15         MeV
+electron mass in u                                          5.485 799 090 65 e-4     0.000 000 000 16 e-4     u
+electron molar mass                                         5.485 799 0888 e-7       0.000 000 0017 e-7       kg mol^-1
+electron-muon mag. mom. ratio                               206.766 9883             0.000 0046
+electron-muon mass ratio                                    4.836 331 69 e-3         0.000 000 11 e-3
+electron-neutron mag. mom. ratio                            960.920 50               0.000 23
+electron-neutron mass ratio                                 5.438 673 4424 e-4       0.000 000 0026 e-4
+electron-proton mag. mom. ratio                             -658.210 687 89          0.000 000 20
+electron-proton mass ratio                                  5.446 170 214 87 e-4     0.000 000 000 33 e-4
+electron relative atomic mass                               5.485 799 090 65 e-4     0.000 000 000 16 e-4
+electron-tau mass ratio                                     2.875 85 e-4             0.000 19 e-4
+electron to alpha particle mass ratio                       1.370 933 554 787 e-4    0.000 000 000 045 e-4
+electron to shielded helion mag. mom. ratio                 864.058 257              0.000 010
+electron to shielded proton mag. mom. ratio                 -658.227 5971            0.000 0072
+electron-triton mass ratio                                  1.819 200 062 251 e-4    0.000 000 000 090 e-4
+electron volt                                               1.602 176 634 e-19       (exact)                  J
+electron volt-atomic mass unit relationship                 1.073 544 102 33 e-9     0.000 000 000 32 e-9     u
+electron volt-hartree relationship                          3.674 932 217 5655 e-2   0.000 000 000 0071 e-2   E_h
+electron volt-hertz relationship                            2.417 989 242... e14     (exact)                  Hz
+electron volt-inverse meter relationship                    8.065 543 937... e5      (exact)                  m^-1
+electron volt-joule relationship                            1.602 176 634 e-19       (exact)                  J
+electron volt-kelvin relationship                           1.160 451 812... e4      (exact)                  K
+electron volt-kilogram relationship                         1.782 661 921... e-36    (exact)                  kg
+elementary charge                                           1.602 176 634 e-19       (exact)                  C
+elementary charge over h-bar                                1.519 267 447... e15     (exact)                  A J^-1
+Faraday constant                                            96 485.332 12...         (exact)                  C mol^-1
+Fermi coupling constant                                     1.166 3787 e-5           0.000 0006 e-5           GeV^-2
+fine-structure constant                                     7.297 352 5693 e-3       0.000 000 0011 e-3
+first radiation constant                                    3.741 771 852... e-16    (exact)                  W m^2
+first radiation constant for spectral radiance              1.191 042 972... e-16    (exact)                  W m^2 sr^-1
+hartree-atomic mass unit relationship                       2.921 262 322 05 e-8     0.000 000 000 88 e-8     u
+hartree-electron volt relationship                          27.211 386 245 988       0.000 000 000 053        eV
+Hartree energy                                              4.359 744 722 2071 e-18  0.000 000 000 0085 e-18  J
+Hartree energy in eV                                        27.211 386 245 988       0.000 000 000 053        eV
+hartree-hertz relationship                                  6.579 683 920 502 e15    0.000 000 000 013 e15    Hz
+hartree-inverse meter relationship                          2.194 746 313 6320 e7    0.000 000 000 0043 e7    m^-1
+hartree-joule relationship                                  4.359 744 722 2071 e-18  0.000 000 000 0085 e-18  J
+hartree-kelvin relationship                                 3.157 750 248 0407 e5    0.000 000 000 0061 e5    K
+hartree-kilogram relationship                               4.850 870 209 5432 e-35  0.000 000 000 0094 e-35  kg
+helion-electron mass ratio                                  5495.885 280 07          0.000 000 24
+helion g factor                                             -4.255 250 615           0.000 000 050
+helion mag. mom.                                            -1.074 617 532 e-26      0.000 000 013 e-26       J T^-1
+helion mag. mom. to Bohr magneton ratio                     -1.158 740 958 e-3       0.000 000 014 e-3
+helion mag. mom. to nuclear magneton ratio                  -2.127 625 307           0.000 000 025
+helion mass                                                 5.006 412 7796 e-27      0.000 000 0015 e-27      kg
+helion mass energy equivalent                               4.499 539 4125 e-10      0.000 000 0014 e-10      J
+helion mass energy equivalent in MeV                        2808.391 607 43          0.000 000 85             MeV
+helion mass in u                                            3.014 932 247 175        0.000 000 000 097        u
+helion molar mass                                           3.014 932 246 13 e-3     0.000 000 000 91 e-3     kg mol^-1
+helion-proton mass ratio                                    2.993 152 671 67         0.000 000 000 13
+helion relative atomic mass                                 3.014 932 247 175        0.000 000 000 097
+helion shielding shift                                      5.996 743 e-5            0.000 010 e-5
+hertz-atomic mass unit relationship                         4.439 821 6652 e-24      0.000 000 0013 e-24      u
+hertz-electron volt relationship                            4.135 667 696... e-15    (exact)                  eV
+hertz-hartree relationship                                  1.519 829 846 0570 e-16  0.000 000 000 0029 e-16  E_h
+hertz-inverse meter relationship                            3.335 640 951... e-9     (exact)                  m^-1
+hertz-joule relationship                                    6.626 070 15 e-34        (exact)                  J
+hertz-kelvin relationship                                   4.799 243 073... e-11    (exact)                  K
+hertz-kilogram relationship                                 7.372 497 323... e-51    (exact)                  kg
+hyperfine transition frequency of Cs-133                    9 192 631 770            (exact)                  Hz
+inverse fine-structure constant                             137.035 999 084          0.000 000 021
+inverse meter-atomic mass unit relationship                 1.331 025 050 10 e-15    0.000 000 000 40 e-15    u
+inverse meter-electron volt relationship                    1.239 841 984... e-6     (exact)                  eV
+inverse meter-hartree relationship                          4.556 335 252 9120 e-8   0.000 000 000 0088 e-8   E_h
+inverse meter-hertz relationship                            299 792 458              (exact)                  Hz
+inverse meter-joule relationship                            1.986 445 857... e-25    (exact)                  J
+inverse meter-kelvin relationship                           1.438 776 877... e-2     (exact)                  K
+inverse meter-kilogram relationship                         2.210 219 094... e-42    (exact)                  kg
+inverse of conductance quantum                              12 906.403 72...         (exact)                  ohm
+Josephson constant                                          483 597.848 4... e9      (exact)                  Hz V^-1
+joule-atomic mass unit relationship                         6.700 535 2565 e9        0.000 000 0020 e9        u
+joule-electron volt relationship                            6.241 509 074... e18     (exact)                  eV
+joule-hartree relationship                                  2.293 712 278 3963 e17   0.000 000 000 0045 e17   E_h
+joule-hertz relationship                                    1.509 190 179... e33     (exact)                  Hz
+joule-inverse meter relationship                            5.034 116 567... e24     (exact)                  m^-1
+joule-kelvin relationship                                   7.242 970 516... e22     (exact)                  K
+joule-kilogram relationship                                 1.112 650 056... e-17    (exact)                  kg
+kelvin-atomic mass unit relationship                        9.251 087 3014 e-14      0.000 000 0028 e-14      u
+kelvin-electron volt relationship                           8.617 333 262... e-5     (exact)                  eV
+kelvin-hartree relationship                                 3.166 811 563 4556 e-6   0.000 000 000 0061 e-6   E_h
+kelvin-hertz relationship                                   2.083 661 912... e10     (exact)                  Hz
+kelvin-inverse meter relationship                           69.503 480 04...         (exact)                  m^-1
+kelvin-joule relationship                                   1.380 649 e-23           (exact)                  J
+kelvin-kilogram relationship                                1.536 179 187... e-40    (exact)                  kg
+kilogram-atomic mass unit relationship                      6.022 140 7621 e26       0.000 000 0018 e26       u
+kilogram-electron volt relationship                         5.609 588 603... e35     (exact)                  eV
+kilogram-hartree relationship                               2.061 485 788 7409 e34   0.000 000 000 0040 e34   E_h
+kilogram-hertz relationship                                 1.356 392 489... e50     (exact)                  Hz
+kilogram-inverse meter relationship                         4.524 438 335... e41     (exact)                  m^-1
+kilogram-joule relationship                                 8.987 551 787... e16     (exact)                  J
+kilogram-kelvin relationship                                6.509 657 260... e39     (exact)                  K
+lattice parameter of silicon                                5.431 020 511 e-10       0.000 000 089 e-10       m
+lattice spacing of ideal Si (220)                           1.920 155 716 e-10       0.000 000 032 e-10       m
+Loschmidt constant (273.15 K, 100 kPa)                      2.651 645 804... e25     (exact)                  m^-3
+Loschmidt constant (273.15 K, 101.325 kPa)                  2.686 780 111... e25     (exact)                  m^-3
+luminous efficacy                                           683                      (exact)                  lm W^-1
+mag. flux quantum                                           2.067 833 848... e-15    (exact)                  Wb
+molar gas constant                                          8.314 462 618...         (exact)                  J mol^-1 K^-1
+molar mass constant                                         0.999 999 999 65 e-3     0.000 000 000 30 e-3     kg mol^-1
+molar mass of carbon-12                                     11.999 999 9958 e-3      0.000 000 0036 e-3       kg mol^-1
+molar Planck constant                                       3.990 312 712... e-10    (exact)                  J Hz^-1 mol^-1
+molar volume of ideal gas (273.15 K, 100 kPa)               22.710 954 64... e-3     (exact)                  m^3 mol^-1
+molar volume of ideal gas (273.15 K, 101.325 kPa)           22.413 969 54... e-3     (exact)                  m^3 mol^-1
+molar volume of silicon                                     1.205 883 199 e-5        0.000 000 060 e-5        m^3 mol^-1
+Mo x unit                                                   1.002 099 52 e-13        0.000 000 53 e-13        m
+muon Compton wavelength                                     1.173 444 110 e-14       0.000 000 026 e-14       m
+muon-electron mass ratio                                    206.768 2830             0.000 0046
+muon g factor                                               -2.002 331 8418          0.000 000 0013
+muon mag. mom.                                              -4.490 448 30 e-26       0.000 000 10 e-26        J T^-1
+muon mag. mom. anomaly                                      1.165 920 89 e-3         0.000 000 63 e-3
+muon mag. mom. to Bohr magneton ratio                       -4.841 970 47 e-3        0.000 000 11 e-3
+muon mag. mom. to nuclear magneton ratio                    -8.890 597 03            0.000 000 20
+muon mass                                                   1.883 531 627 e-28       0.000 000 042 e-28       kg
+muon mass energy equivalent                                 1.692 833 804 e-11       0.000 000 038 e-11       J
+muon mass energy equivalent in MeV                          105.658 3755             0.000 0023               MeV
+muon mass in u                                              0.113 428 9259           0.000 000 0025           u
+muon molar mass                                             1.134 289 259 e-4        0.000 000 025 e-4        kg mol^-1
+muon-neutron mass ratio                                     0.112 454 5170           0.000 000 0025
+muon-proton mag. mom. ratio                                 -3.183 345 142           0.000 000 071
+muon-proton mass ratio                                      0.112 609 5264           0.000 000 0025
+muon-tau mass ratio                                         5.946 35 e-2             0.000 40 e-2
+natural unit of action                                      1.054 571 817... e-34    (exact)                  J s
+natural unit of action in eV s                              6.582 119 569... e-16    (exact)                  eV s
+natural unit of energy                                      8.187 105 7769 e-14      0.000 000 0025 e-14      J
+natural unit of energy in MeV                               0.510 998 950 00         0.000 000 000 15         MeV
+natural unit of length                                      3.861 592 6796 e-13      0.000 000 0012 e-13      m
+natural unit of mass                                        9.109 383 7015 e-31      0.000 000 0028 e-31      kg
+natural unit of momentum                                    2.730 924 530 75 e-22    0.000 000 000 82 e-22    kg m s^-1
+natural unit of momentum in MeV/c                           0.510 998 950 00         0.000 000 000 15         MeV/c
+natural unit of time                                        1.288 088 668 19 e-21    0.000 000 000 39 e-21    s
+natural unit of velocity                                    299 792 458              (exact)                  m s^-1
+neutron Compton wavelength                                  1.319 590 905 81 e-15    0.000 000 000 75 e-15    m
+neutron-electron mag. mom. ratio                            1.040 668 82 e-3         0.000 000 25 e-3
+neutron-electron mass ratio                                 1838.683 661 73          0.000 000 89
+neutron g factor                                            -3.826 085 45            0.000 000 90
+neutron gyromag. ratio                                      1.832 471 71 e8          0.000 000 43 e8          s^-1 T^-1
+neutron gyromag. ratio in MHz/T                             29.164 6931              0.000 0069               MHz T^-1
+neutron mag. mom.                                           -9.662 3651 e-27         0.000 0023 e-27          J T^-1
+neutron mag. mom. to Bohr magneton ratio                    -1.041 875 63 e-3        0.000 000 25 e-3
+neutron mag. mom. to nuclear magneton ratio                 -1.913 042 73            0.000 000 45
+neutron mass                                                1.674 927 498 04 e-27    0.000 000 000 95 e-27    kg
+neutron mass energy equivalent                              1.505 349 762 87 e-10    0.000 000 000 86 e-10    J
+neutron mass energy equivalent in MeV                       939.565 420 52           0.000 000 54             MeV
+neutron mass in u                                           1.008 664 915 95         0.000 000 000 49         u
+neutron molar mass                                          1.008 664 915 60 e-3     0.000 000 000 57 e-3     kg mol^-1
+neutron-muon mass ratio                                     8.892 484 06             0.000 000 20
+neutron-proton mag. mom. ratio                              -0.684 979 34            0.000 000 16
+neutron-proton mass difference                              2.305 574 35 e-30        0.000 000 82 e-30        kg
+neutron-proton mass difference energy equivalent            2.072 146 89 e-13        0.000 000 74 e-13        J
+neutron-proton mass difference energy equivalent in MeV     1.293 332 36             0.000 000 46             MeV
+neutron-proton mass difference in u                         1.388 449 33 e-3         0.000 000 49 e-3         u
+neutron-proton mass ratio                                   1.001 378 419 31         0.000 000 000 49
+neutron relative atomic mass                                1.008 664 915 95         0.000 000 000 49
+neutron-tau mass ratio                                      0.528 779                0.000 036
+neutron to shielded proton mag. mom. ratio                  -0.684 996 94            0.000 000 16
+Newtonian constant of gravitation                           6.674 30 e-11            0.000 15 e-11            m^3 kg^-1 s^-2
+Newtonian constant of gravitation over h-bar c              6.708 83 e-39            0.000 15 e-39            (GeV/c^2)^-2
+nuclear magneton                                            5.050 783 7461 e-27      0.000 000 0015 e-27      J T^-1
+nuclear magneton in eV/T                                    3.152 451 258 44 e-8     0.000 000 000 96 e-8     eV T^-1
+nuclear magneton in inverse meter per tesla                 2.542 623 413 53 e-2     0.000 000 000 78 e-2     m^-1 T^-1
+nuclear magneton in K/T                                     3.658 267 7756 e-4       0.000 000 0011 e-4       K T^-1
+nuclear magneton in MHz/T                                   7.622 593 2291           0.000 000 0023           MHz T^-1
+Planck constant                                             6.626 070 15 e-34        (exact)                  J Hz^-1
+Planck constant in eV/Hz                                    4.135 667 696... e-15    (exact)                  eV Hz^-1
+Planck length                                               1.616 255 e-35           0.000 018 e-35           m
+Planck mass                                                 2.176 434 e-8            0.000 024 e-8            kg
+Planck mass energy equivalent in GeV                        1.220 890 e19            0.000 014 e19            GeV
+Planck temperature                                          1.416 784 e32            0.000 016 e32            K
+Planck time                                                 5.391 247 e-44           0.000 060 e-44           s
+proton charge to mass quotient                              9.578 833 1560 e7        0.000 000 0029 e7        C kg^-1
+proton Compton wavelength                                   1.321 409 855 39 e-15    0.000 000 000 40 e-15    m
+proton-electron mass ratio                                  1836.152 673 43          0.000 000 11
+proton g factor                                             5.585 694 6893           0.000 000 0016
+proton gyromag. ratio                                       2.675 221 8744 e8        0.000 000 0011 e8        s^-1 T^-1
+proton gyromag. ratio in MHz/T                              42.577 478 518           0.000 000 018            MHz T^-1
+proton mag. mom.                                            1.410 606 797 36 e-26    0.000 000 000 60 e-26    J T^-1
+proton mag. mom. to Bohr magneton ratio                     1.521 032 202 30 e-3     0.000 000 000 46 e-3
+proton mag. mom. to nuclear magneton ratio                  2.792 847 344 63         0.000 000 000 82
+proton mag. shielding correction                            2.5689 e-5               0.0011 e-5
+proton mass                                                 1.672 621 923 69 e-27    0.000 000 000 51 e-27    kg
+proton mass energy equivalent                               1.503 277 615 98 e-10    0.000 000 000 46 e-10    J
+proton mass energy equivalent in MeV                        938.272 088 16           0.000 000 29             MeV
+proton mass in u                                            1.007 276 466 621        0.000 000 000 053        u
+proton molar mass                                           1.007 276 466 27 e-3     0.000 000 000 31 e-3     kg mol^-1
+proton-muon mass ratio                                      8.880 243 37             0.000 000 20
+proton-neutron mag. mom. ratio                              -1.459 898 05            0.000 000 34
+proton-neutron mass ratio                                   0.998 623 478 12         0.000 000 000 49
+proton relative atomic mass                                 1.007 276 466 621        0.000 000 000 053
+proton rms charge radius                                    8.414 e-16               0.019 e-16               m
+proton-tau mass ratio                                       0.528 051                0.000 036
+quantum of circulation                                      3.636 947 5516 e-4       0.000 000 0011 e-4       m^2 s^-1
+quantum of circulation times 2                              7.273 895 1032 e-4       0.000 000 0022 e-4       m^2 s^-1
+reduced Compton wavelength                                  3.861 592 6796 e-13      0.000 000 0012 e-13      m
+reduced muon Compton wavelength                             1.867 594 306 e-15       0.000 000 042 e-15       m
+reduced neutron Compton wavelength                          2.100 194 1552 e-16      0.000 000 0012 e-16      m
+reduced Planck constant                                     1.054 571 817... e-34    (exact)                  J s
+reduced Planck constant in eV s                             6.582 119 569... e-16    (exact)                  eV s
+reduced Planck constant times c in MeV fm                   197.326 980 4...         (exact)                  MeV fm
+reduced proton Compton wavelength                           2.103 089 103 36 e-16    0.000 000 000 64 e-16    m
+reduced tau Compton wavelength                              1.110 538 e-16           0.000 075 e-16           m
+Rydberg constant                                            10 973 731.568 160       0.000 021                m^-1
+Rydberg constant times c in Hz                              3.289 841 960 2508 e15   0.000 000 000 0064 e15   Hz
+Rydberg constant times hc in eV                             13.605 693 122 994       0.000 000 000 026        eV
+Rydberg constant times hc in J                              2.179 872 361 1035 e-18  0.000 000 000 0042 e-18  J
+Sackur-Tetrode constant (1 K, 100 kPa)                      -1.151 707 537 06        0.000 000 000 45
+Sackur-Tetrode constant (1 K, 101.325 kPa)                  -1.164 870 523 58        0.000 000 000 45
+second radiation constant                                   1.438 776 877... e-2     (exact)                  m K
+shielded helion gyromag. ratio                              2.037 894 569 e8         0.000 000 024 e8         s^-1 T^-1
+shielded helion gyromag. ratio in MHz/T                     32.434 099 42            0.000 000 38             MHz T^-1
+shielded helion mag. mom.                                   -1.074 553 090 e-26      0.000 000 013 e-26       J T^-1
+shielded helion mag. mom. to Bohr magneton ratio            -1.158 671 471 e-3       0.000 000 014 e-3
+shielded helion mag. mom. to nuclear magneton ratio         -2.127 497 719           0.000 000 025
+shielded helion to proton mag. mom. ratio                   -0.761 766 5618          0.000 000 0089
+shielded helion to shielded proton mag. mom. ratio          -0.761 786 1313          0.000 000 0033
+shielded proton gyromag. ratio                              2.675 153 151 e8         0.000 000 029 e8         s^-1 T^-1
+shielded proton gyromag. ratio in MHz/T                     42.576 384 74            0.000 000 46             MHz T^-1
+shielded proton mag. mom.                                   1.410 570 560 e-26       0.000 000 015 e-26       J T^-1
+shielded proton mag. mom. to Bohr magneton ratio            1.520 993 128 e-3        0.000 000 017 e-3
+shielded proton mag. mom. to nuclear magneton ratio         2.792 775 599            0.000 000 030
+shielding difference of d and p in HD                       2.0200 e-8               0.0020 e-8
+shielding difference of t and p in HT                       2.4140 e-8               0.0020 e-8
+speed of light in vacuum                                    299 792 458              (exact)                  m s^-1
+standard acceleration of gravity                            9.806 65                 (exact)                  m s^-2
+standard atmosphere                                         101 325                  (exact)                  Pa
+standard-state pressure                                     100 000                  (exact)                  Pa
+Stefan-Boltzmann constant                                   5.670 374 419... e-8     (exact)                  W m^-2 K^-4
+tau Compton wavelength                                      6.977 71 e-16            0.000 47 e-16            m
+tau-electron mass ratio                                     3477.23                  0.23
+tau energy equivalent                                       1776.86                  0.12                     MeV
+tau mass                                                    3.167 54 e-27            0.000 21 e-27            kg
+tau mass energy equivalent                                  2.846 84 e-10            0.000 19 e-10            J
+tau mass in u                                               1.907 54                 0.000 13                 u
+tau molar mass                                              1.907 54 e-3             0.000 13 e-3             kg mol^-1
+tau-muon mass ratio                                         16.8170                  0.0011
+tau-neutron mass ratio                                      1.891 15                 0.000 13
+tau-proton mass ratio                                       1.893 76                 0.000 13
+Thomson cross section                                       6.652 458 7321 e-29      0.000 000 0060 e-29      m^2
+triton-electron mass ratio                                  5496.921 535 73          0.000 000 27
+triton g factor                                             5.957 924 931            0.000 000 012
+triton mag. mom.                                            1.504 609 5202 e-26      0.000 000 0030 e-26      J T^-1
+triton mag. mom. to Bohr magneton ratio                     1.622 393 6651 e-3       0.000 000 0032 e-3
+triton mag. mom. to nuclear magneton ratio                  2.978 962 4656           0.000 000 0059
+triton mass                                                 5.007 356 7446 e-27      0.000 000 0015 e-27      kg
+triton mass energy equivalent                               4.500 387 8060 e-10      0.000 000 0014 e-10      J
+triton mass energy equivalent in MeV                        2808.921 132 98          0.000 000 85             MeV
+triton mass in u                                            3.015 500 716 21         0.000 000 000 12         u
+triton molar mass                                           3.015 500 715 17 e-3     0.000 000 000 92 e-3     kg mol^-1
+triton-proton mass ratio                                    2.993 717 034 14         0.000 000 000 15
+triton relative atomic mass                                 3.015 500 716 21         0.000 000 000 12
+triton to proton mag. mom. ratio                            1.066 639 9191           0.000 000 0021
+unified atomic mass unit                                    1.660 539 066 60 e-27    0.000 000 000 50 e-27    kg
+vacuum electric permittivity                                8.854 187 8128 e-12      0.000 000 0013 e-12      F m^-1
+vacuum mag. permeability                                    1.256 637 062 12 e-6     0.000 000 000 19 e-6     N A^-2
+von Klitzing constant                                       25 812.807 45...         (exact)                  ohm
+weak mixing angle                                           0.222 90                 0.000 30
+Wien frequency displacement law constant                    5.878 925 757... e10     (exact)                  Hz K^-1
+Wien wavelength displacement law constant                   2.897 771 955... e-3     (exact)                  m K
+W to Z mass ratio                                           0.881 53                 0.000 17                   """
+
+
+def exact2018(exact):
+    # SI base constants
+    c = exact['speed of light in vacuum']
+    h = exact['Planck constant']
+    e = exact['elementary charge']
+    k = exact['Boltzmann constant']
+    N_A = exact['Avogadro constant']
+
+    # Other useful constants
+    R = N_A * k
+    hbar = h / (2*math.pi)
+    G_0 = 2 * e**2 / h
+
+    # Wien law numerical constants: https://en.wikipedia.org/wiki/Wien%27s_displacement_law
+    # (alpha - 3)*exp(alpha) + 3 = 0
+    # (x - 5)*exp(x) + 5 = 0
+    alpha_W = 2.821439372122078893403  # 3 + lambertw(-3 * exp(-3))
+    x_W = 4.965114231744276303699  # 5 + lambertw(-5 * exp(-5))
+
+    # Conventional electrical unit
+    # See https://en.wikipedia.org/wiki/Conventional_electrical_unit
+    K_J90 = exact['conventional value of Josephson constant']
+    K_J = 2 * e / h
+    R_K90 = exact['conventional value of von Klitzing constant']
+    R_K = h / e**2
+    V_90 = K_J90 / K_J
+    ohm_90 = R_K / R_K90
+    A_90 = V_90 / ohm_90
+
+    replace = {
+        'atomic unit of action': hbar,
+        'Boltzmann constant in eV/K': k / e,
+        'Boltzmann constant in Hz/K': k / h,
+        'Boltzmann constant in inverse meter per kelvin': k / (h * c),
+        'conductance quantum': G_0,
+        'conventional value of ampere-90': A_90,
+        'conventional value of coulomb-90': A_90,
+        'conventional value of farad-90': 1 / ohm_90,
+        'conventional value of henry-90': ohm_90,
+        'conventional value of ohm-90': ohm_90,
+        'conventional value of volt-90': V_90,
+        'conventional value of watt-90': V_90**2 / ohm_90,
+        'electron volt-hertz relationship': e / h,
+        'electron volt-inverse meter relationship': e / (h * c),
+        'electron volt-kelvin relationship': e / k,
+        'electron volt-kilogram relationship': e / c**2,
+        'elementary charge over h-bar': e / hbar,
+        'Faraday constant': e * N_A,
+        'first radiation constant': 2 * math.pi * h * c**2,
+        'first radiation constant for spectral radiance': 2 * h * c**2,
+        'hertz-electron volt relationship': h / e,
+        'hertz-inverse meter relationship': 1 / c,
+        'hertz-kelvin relationship': h / k,
+        'hertz-kilogram relationship': h / c**2,
+        'inverse meter-electron volt relationship': (h * c) / e,
+        'inverse meter-joule relationship': h * c,
+        'inverse meter-kelvin relationship': h * c / k,
+        'inverse meter-kilogram relationship': h / c,
+        'inverse of conductance quantum': 1 / G_0,
+        'Josephson constant': K_J,
+        'joule-electron volt relationship': 1 / e,
+        'joule-hertz relationship': 1 / h,
+        'joule-inverse meter relationship': 1 / (h * c),
+        'joule-kelvin relationship': 1 / k,
+        'joule-kilogram relationship': 1 / c**2,
+        'kelvin-electron volt relationship': k / e,
+        'kelvin-hertz relationship': k / h,
+        'kelvin-inverse meter relationship': k / (h * c),
+        'kelvin-kilogram relationship': k / c**2,
+        'kilogram-electron volt relationship': c**2 / e,
+        'kilogram-hertz relationship': c**2 / h,
+        'kilogram-inverse meter relationship': c / h,
+        'kilogram-joule relationship': c**2,
+        'kilogram-kelvin relationship': c**2 / k,
+        'Loschmidt constant (273.15 K, 100 kPa)': 100e3 / 273.15 / k,
+        'Loschmidt constant (273.15 K, 101.325 kPa)': 101.325e3 / 273.15 / k,
+        'mag. flux quantum': h / (2 * e),
+        'molar gas constant': R,
+        'molar Planck constant': h * N_A,
+        'molar volume of ideal gas (273.15 K, 100 kPa)': R * 273.15 / 100e3,
+        'molar volume of ideal gas (273.15 K, 101.325 kPa)': R * 273.15 / 101.325e3,
+        'natural unit of action': hbar,
+        'natural unit of action in eV s': hbar / e,
+        'Planck constant in eV/Hz': h / e,
+        'reduced Planck constant': hbar,
+        'reduced Planck constant in eV s': hbar / e,
+        'reduced Planck constant times c in MeV fm': hbar * c / (e * 1e6 * 1e-15),
+        'second radiation constant': h * c / k,
+        'Stefan-Boltzmann constant': 2 * math.pi**5 * k**4 / (15 * h**3 * c**2),
+        'von Klitzing constant': R_K,
+        'Wien frequency displacement law constant': alpha_W * k / h,
+        'Wien wavelength displacement law constant': h * c / (x_W * k),
+    }
+    return replace
+
+
+txt2022 = """\
+alpha particle-electron mass ratio                          7294.299 541 71          0.000 000 17             
+alpha particle mass                                         6.644 657 3450 e-27      0.000 000 0021 e-27      kg
+alpha particle mass energy equivalent                       5.971 920 1997 e-10      0.000 000 0019 e-10      J
+alpha particle mass energy equivalent in MeV                3727.379 4118            0.000 0012               MeV
+alpha particle mass in u                                    4.001 506 179 129        0.000 000 000 062        u
+alpha particle molar mass                                   4.001 506 1833 e-3       0.000 000 0012 e-3       kg mol^-1
+alpha particle-proton mass ratio                            3.972 599 690 252        0.000 000 000 070        
+alpha particle relative atomic mass                         4.001 506 179 129        0.000 000 000 062        
+alpha particle rms charge radius                            1.6785 e-15              0.0021 e-15              m
+Angstrom star                                               1.000 014 95 e-10        0.000 000 90 e-10        m
+atomic mass constant                                        1.660 539 068 92 e-27    0.000 000 000 52 e-27    kg
+atomic mass constant energy equivalent                      1.492 418 087 68 e-10    0.000 000 000 46 e-10    J
+atomic mass constant energy equivalent in MeV               931.494 103 72           0.000 000 29             MeV
+atomic mass unit-electron volt relationship                 9.314 941 0372 e8        0.000 000 0029 e8        eV
+atomic mass unit-hartree relationship                       3.423 177 6922 e7        0.000 000 0011 e7        E_h
+atomic mass unit-hertz relationship                         2.252 342 721 85 e23     0.000 000 000 70 e23     Hz
+atomic mass unit-inverse meter relationship                 7.513 006 6209 e14       0.000 000 0023 e14       m^-1
+atomic mass unit-joule relationship                         1.492 418 087 68 e-10    0.000 000 000 46 e-10    J
+atomic mass unit-kelvin relationship                        1.080 954 020 67 e13     0.000 000 000 34 e13     K
+atomic mass unit-kilogram relationship                      1.660 539 068 92 e-27    0.000 000 000 52 e-27    kg
+atomic unit of 1st hyperpolarizability                      3.206 361 2996 e-53      0.000 000 0015 e-53      C^3 m^3 J^-2
+atomic unit of 2nd hyperpolarizability                      6.235 379 9735 e-65      0.000 000 0039 e-65      C^4 m^4 J^-3
+atomic unit of action                                       1.054 571 817... e-34    (exact)                  J s
+atomic unit of charge                                       1.602 176 634 e-19       (exact)                  C
+atomic unit of charge density                               1.081 202 386 77 e12     0.000 000 000 51 e12     C m^-3
+atomic unit of current                                      6.623 618 237 5082 e-3   0.000 000 000 0072 e-3   A
+atomic unit of electric dipole mom.                         8.478 353 6198 e-30      0.000 000 0013 e-30      C m
+atomic unit of electric field                               5.142 206 751 12 e11     0.000 000 000 80 e11     V m^-1
+atomic unit of electric field gradient                      9.717 362 4424 e21       0.000 000 0030 e21       V m^-2
+atomic unit of electric polarizability                      1.648 777 272 12 e-41    0.000 000 000 51 e-41    C^2 m^2 J^-1
+atomic unit of electric potential                           27.211 386 245 981       0.000 000 000 030        V
+atomic unit of electric quadrupole mom.                     4.486 551 5185 e-40      0.000 000 0014 e-40      C m^2
+atomic unit of energy                                       4.359 744 722 2060 e-18  0.000 000 000 0048 e-18  J
+atomic unit of force                                        8.238 723 5038 e-8       0.000 000 0013 e-8       N
+atomic unit of length                                       5.291 772 105 44 e-11    0.000 000 000 82 e-11    m
+atomic unit of mag. dipole mom.                             1.854 802 013 15 e-23    0.000 000 000 58 e-23    J T^-1
+atomic unit of mag. flux density                            2.350 517 570 77 e5      0.000 000 000 73 e5      T
+atomic unit of magnetizability                              7.891 036 5794 e-29      0.000 000 0049 e-29      J T^-2
+atomic unit of mass                                         9.109 383 7139 e-31      0.000 000 0028 e-31      kg
+atomic unit of momentum                                     1.992 851 915 45 e-24    0.000 000 000 31 e-24    kg m s^-1
+atomic unit of permittivity                                 1.112 650 056 20 e-10    0.000 000 000 17 e-10    F m^-1
+atomic unit of time                                         2.418 884 326 5864 e-17  0.000 000 000 0026 e-17  s
+atomic unit of velocity                                     2.187 691 262 16 e6      0.000 000 000 34 e6      m s^-1
+Avogadro constant                                           6.022 140 76 e23         (exact)                  mol^-1
+Bohr magneton                                               9.274 010 0657 e-24      0.000 000 0029 e-24      J T^-1
+Bohr magneton in eV/T                                       5.788 381 7982 e-5       0.000 000 0018 e-5       eV T^-1
+Bohr magneton in Hz/T                                       1.399 624 491 71 e10     0.000 000 000 44 e10     Hz T^-1
+Bohr magneton in inverse meter per tesla                    46.686 447 719           0.000 000 015            m^-1 T^-1
+Bohr magneton in K/T                                        0.671 713 814 72         0.000 000 000 21         K T^-1
+Bohr radius                                                 5.291 772 105 44 e-11    0.000 000 000 82 e-11    m
+Boltzmann constant                                          1.380 649 e-23           (exact)                  J K^-1
+Boltzmann constant in eV/K                                  8.617 333 262... e-5     (exact)                  eV K^-1
+Boltzmann constant in Hz/K                                  2.083 661 912... e10     (exact)                  Hz K^-1
+Boltzmann constant in inverse meter per kelvin              69.503 480 04...         (exact)                  m^-1 K^-1
+characteristic impedance of vacuum                          376.730 313 412          0.000 000 059            ohm
+classical electron radius                                   2.817 940 3205 e-15      0.000 000 0013 e-15      m
+Compton wavelength                                          2.426 310 235 38 e-12    0.000 000 000 76 e-12    m
+conductance quantum                                         7.748 091 729... e-5     (exact)                  S
+conventional value of ampere-90                             1.000 000 088 87...      (exact)                  A
+conventional value of coulomb-90                            1.000 000 088 87...      (exact)                  C
+conventional value of farad-90                              0.999 999 982 20...      (exact)                  F
+conventional value of henry-90                              1.000 000 017 79...      (exact)                  H
+conventional value of Josephson constant                    483 597.9 e9             (exact)                  Hz V^-1
+conventional value of ohm-90                                1.000 000 017 79...      (exact)                  ohm
+conventional value of volt-90                               1.000 000 106 66...      (exact)                  V
+conventional value of von Klitzing constant                 25 812.807               (exact)                  ohm
+conventional value of watt-90                               1.000 000 195 53...      (exact)                  W
+Copper x unit                                               1.002 076 97 e-13        0.000 000 28 e-13        m
+deuteron-electron mag. mom. ratio                           -4.664 345 550 e-4       0.000 000 012 e-4        
+deuteron-electron mass ratio                                3670.482 967 655         0.000 000 063            
+deuteron g factor                                           0.857 438 2335           0.000 000 0022           
+deuteron mag. mom.                                          4.330 735 087 e-27       0.000 000 011 e-27       J T^-1
+deuteron mag. mom. to Bohr magneton ratio                   4.669 754 568 e-4        0.000 000 012 e-4        
+deuteron mag. mom. to nuclear magneton ratio                0.857 438 2335           0.000 000 0022           
+deuteron mass                                               3.343 583 7768 e-27      0.000 000 0010 e-27      kg
+deuteron mass energy equivalent                             3.005 063 234 91 e-10    0.000 000 000 94 e-10    J
+deuteron mass energy equivalent in MeV                      1875.612 945 00          0.000 000 58             MeV
+deuteron mass in u                                          2.013 553 212 544        0.000 000 000 015        u
+deuteron molar mass                                         2.013 553 214 66 e-3     0.000 000 000 63 e-3     kg mol^-1
+deuteron-neutron mag. mom. ratio                            -0.448 206 52            0.000 000 11             
+deuteron-proton mag. mom. ratio                             0.307 012 209 30         0.000 000 000 79         
+deuteron-proton mass ratio                                  1.999 007 501 2699       0.000 000 000 0084       
+deuteron relative atomic mass                               2.013 553 212 544        0.000 000 000 015        
+deuteron rms charge radius                                  2.127 78 e-15            0.000 27 e-15            m
+electron charge to mass quotient                            -1.758 820 008 38 e11    0.000 000 000 55 e11     C kg^-1
+electron-deuteron mag. mom. ratio                           -2143.923 4921           0.000 0056               
+electron-deuteron mass ratio                                2.724 437 107 629 e-4    0.000 000 000 047 e-4    
+electron g factor                                           -2.002 319 304 360 92    0.000 000 000 000 36     
+electron gyromag. ratio                                     1.760 859 627 84 e11     0.000 000 000 55 e11     s^-1 T^-1
+electron gyromag. ratio in MHz/T                            28 024.951 3861          0.000 0087               MHz T^-1
+electron-helion mass ratio                                  1.819 543 074 649 e-4    0.000 000 000 053 e-4    
+electron mag. mom.                                          -9.284 764 6917 e-24     0.000 000 0029 e-24      J T^-1
+electron mag. mom. anomaly                                  1.159 652 180 46 e-3     0.000 000 000 18 e-3     
+electron mag. mom. to Bohr magneton ratio                   -1.001 159 652 180 46    0.000 000 000 000 18     
+electron mag. mom. to nuclear magneton ratio                -1838.281 971 877        0.000 000 032            
+electron mass                                               9.109 383 7139 e-31      0.000 000 0028 e-31      kg
+electron mass energy equivalent                             8.187 105 7880 e-14      0.000 000 0026 e-14      J
+electron mass energy equivalent in MeV                      0.510 998 950 69         0.000 000 000 16         MeV
+electron mass in u                                          5.485 799 090 441 e-4    0.000 000 000 097 e-4    u
+electron molar mass                                         5.485 799 0962 e-7       0.000 000 0017 e-7       kg mol^-1
+electron-muon mag. mom. ratio                               206.766 9881             0.000 0046               
+electron-muon mass ratio                                    4.836 331 70 e-3         0.000 000 11 e-3         
+electron-neutron mag. mom. ratio                            960.920 48               0.000 23                 
+electron-neutron mass ratio                                 5.438 673 4416 e-4       0.000 000 0022 e-4       
+electron-proton mag. mom. ratio                             -658.210 687 89          0.000 000 19             
+electron-proton mass ratio                                  5.446 170 214 889 e-4    0.000 000 000 094 e-4    
+electron relative atomic mass                               5.485 799 090 441 e-4    0.000 000 000 097 e-4    
+electron-tau mass ratio                                     2.875 85 e-4             0.000 19 e-4             
+electron to alpha particle mass ratio                       1.370 933 554 733 e-4    0.000 000 000 032 e-4    
+electron to shielded helion mag. mom. ratio                 864.058 239 86           0.000 000 70             
+electron to shielded proton mag. mom. ratio                 -658.227 5856            0.000 0027               
+electron-triton mass ratio                                  1.819 200 062 327 e-4    0.000 000 000 068 e-4    
+electron volt                                               1.602 176 634 e-19       (exact)                  J
+electron volt-atomic mass unit relationship                 1.073 544 100 83 e-9     0.000 000 000 33 e-9     u
+electron volt-hartree relationship                          3.674 932 217 5665 e-2   0.000 000 000 0040 e-2   E_h
+electron volt-hertz relationship                            2.417 989 242... e14     (exact)                  Hz
+electron volt-inverse meter relationship                    8.065 543 937... e5      (exact)                  m^-1
+electron volt-joule relationship                            1.602 176 634 e-19       (exact)                  J
+electron volt-kelvin relationship                           1.160 451 812... e4      (exact)                  K
+electron volt-kilogram relationship                         1.782 661 921... e-36    (exact)                  kg
+elementary charge                                           1.602 176 634 e-19       (exact)                  C
+elementary charge over h-bar                                1.519 267 447... e15     (exact)                  A J^-1
+Faraday constant                                            96 485.332 12...         (exact)                  C mol^-1
+Fermi coupling constant                                     1.166 3787 e-5           0.000 0006 e-5           GeV^-2
+fine-structure constant                                     7.297 352 5643 e-3       0.000 000 0011 e-3       
+first radiation constant                                    3.741 771 852... e-16    (exact)                  W m^2
+first radiation constant for spectral radiance              1.191 042 972... e-16    (exact)                  W m^2 sr^-1
+hartree-atomic mass unit relationship                       2.921 262 317 97 e-8     0.000 000 000 91 e-8     u
+hartree-electron volt relationship                          27.211 386 245 981       0.000 000 000 030        eV
+Hartree energy                                              4.359 744 722 2060 e-18  0.000 000 000 0048 e-18  J
+Hartree energy in eV                                        27.211 386 245 981       0.000 000 000 030        eV
+hartree-hertz relationship                                  6.579 683 920 4999 e15   0.000 000 000 0072 e15   Hz
+hartree-inverse meter relationship                          2.194 746 313 6314 e7    0.000 000 000 0024 e7    m^-1
+hartree-joule relationship                                  4.359 744 722 2060 e-18  0.000 000 000 0048 e-18  J
+hartree-kelvin relationship                                 3.157 750 248 0398 e5    0.000 000 000 0034 e5    K
+hartree-kilogram relationship                               4.850 870 209 5419 e-35  0.000 000 000 0053 e-35  kg
+helion-electron mass ratio                                  5495.885 279 84          0.000 000 16             
+helion g factor                                             -4.255 250 6995          0.000 000 0034           
+helion mag. mom.                                            -1.074 617 551 98 e-26   0.000 000 000 93 e-26    J T^-1
+helion mag. mom. to Bohr magneton ratio                     -1.158 740 980 83 e-3    0.000 000 000 94 e-3     
+helion mag. mom. to nuclear magneton ratio                  -2.127 625 3498          0.000 000 0017           
+helion mass                                                 5.006 412 7862 e-27      0.000 000 0016 e-27      kg
+helion mass energy equivalent                               4.499 539 4185 e-10      0.000 000 0014 e-10      J
+helion mass energy equivalent in MeV                        2808.391 611 12          0.000 000 88             MeV
+helion mass in u                                            3.014 932 246 932        0.000 000 000 074        u
+helion molar mass                                           3.014 932 250 10 e-3     0.000 000 000 94 e-3     kg mol^-1
+helion-proton mass ratio                                    2.993 152 671 552        0.000 000 000 070        
+helion relative atomic mass                                 3.014 932 246 932        0.000 000 000 074        
+helion shielding shift                                      5.996 7029 e-5           0.000 0023 e-5           
+hertz-atomic mass unit relationship                         4.439 821 6590 e-24      0.000 000 0014 e-24      u
+hertz-electron volt relationship                            4.135 667 696... e-15    (exact)                  eV
+hertz-hartree relationship                                  1.519 829 846 0574 e-16  0.000 000 000 0017 e-16  E_h
+hertz-inverse meter relationship                            3.335 640 951... e-9     (exact)                  m^-1
+hertz-joule relationship                                    6.626 070 15 e-34        (exact)                  J
+hertz-kelvin relationship                                   4.799 243 073... e-11    (exact)                  K
+hertz-kilogram relationship                                 7.372 497 323... e-51    (exact)                  kg
+hyperfine transition frequency of Cs-133                    9 192 631 770            (exact)                  Hz
+inverse fine-structure constant                             137.035 999 177          0.000 000 021            
+inverse meter-atomic mass unit relationship                 1.331 025 048 24 e-15    0.000 000 000 41 e-15    u
+inverse meter-electron volt relationship                    1.239 841 984... e-6     (exact)                  eV
+inverse meter-hartree relationship                          4.556 335 252 9132 e-8   0.000 000 000 0050 e-8   E_h
+inverse meter-hertz relationship                            299 792 458              (exact)                  Hz
+inverse meter-joule relationship                            1.986 445 857... e-25    (exact)                  J
+inverse meter-kelvin relationship                           1.438 776 877... e-2     (exact)                  K
+inverse meter-kilogram relationship                         2.210 219 094... e-42    (exact)                  kg
+inverse of conductance quantum                              12 906.403 72...         (exact)                  ohm
+Josephson constant                                          483 597.848 4... e9      (exact)                  Hz V^-1
+joule-atomic mass unit relationship                         6.700 535 2471 e9        0.000 000 0021 e9        u
+joule-electron volt relationship                            6.241 509 074... e18     (exact)                  eV
+joule-hartree relationship                                  2.293 712 278 3969 e17   0.000 000 000 0025 e17   E_h
+joule-hertz relationship                                    1.509 190 179... e33     (exact)                  Hz
+joule-inverse meter relationship                            5.034 116 567... e24     (exact)                  m^-1
+joule-kelvin relationship                                   7.242 970 516... e22     (exact)                  K
+joule-kilogram relationship                                 1.112 650 056... e-17    (exact)                  kg
+kelvin-atomic mass unit relationship                        9.251 087 2884 e-14      0.000 000 0029 e-14      u
+kelvin-electron volt relationship                           8.617 333 262... e-5     (exact)                  eV
+kelvin-hartree relationship                                 3.166 811 563 4564 e-6   0.000 000 000 0035 e-6   E_h
+kelvin-hertz relationship                                   2.083 661 912... e10     (exact)                  Hz
+kelvin-inverse meter relationship                           69.503 480 04...         (exact)                  m^-1
+kelvin-joule relationship                                   1.380 649 e-23           (exact)                  J
+kelvin-kilogram relationship                                1.536 179 187... e-40    (exact)                  kg
+kilogram-atomic mass unit relationship                      6.022 140 7537 e26       0.000 000 0019 e26       u
+kilogram-electron volt relationship                         5.609 588 603... e35     (exact)                  eV
+kilogram-hartree relationship                               2.061 485 788 7415 e34   0.000 000 000 0022 e34   E_h
+kilogram-hertz relationship                                 1.356 392 489... e50     (exact)                  Hz
+kilogram-inverse meter relationship                         4.524 438 335... e41     (exact)                  m^-1
+kilogram-joule relationship                                 8.987 551 787... e16     (exact)                  J
+kilogram-kelvin relationship                                6.509 657 260... e39     (exact)                  K
+lattice parameter of silicon                                5.431 020 511 e-10       0.000 000 089 e-10       m
+lattice spacing of ideal Si (220)                           1.920 155 716 e-10       0.000 000 032 e-10       m
+Loschmidt constant (273.15 K, 100 kPa)                      2.651 645 804... e25     (exact)                  m^-3
+Loschmidt constant (273.15 K, 101.325 kPa)                  2.686 780 111... e25     (exact)                  m^-3
+luminous efficacy                                           683                      (exact)                  lm W^-1
+mag. flux quantum                                           2.067 833 848... e-15    (exact)                  Wb
+molar gas constant                                          8.314 462 618...         (exact)                  J mol^-1 K^-1
+molar mass constant                                         1.000 000 001 05 e-3     0.000 000 000 31 e-3     kg mol^-1
+molar mass of carbon-12                                     12.000 000 0126 e-3      0.000 000 0037 e-3       kg mol^-1
+molar Planck constant                                       3.990 312 712... e-10    (exact)                  J Hz^-1 mol^-1
+molar volume of ideal gas (273.15 K, 100 kPa)               22.710 954 64... e-3     (exact)                  m^3 mol^-1
+molar volume of ideal gas (273.15 K, 101.325 kPa)           22.413 969 54... e-3     (exact)                  m^3 mol^-1
+molar volume of silicon                                     1.205 883 199 e-5        0.000 000 060 e-5        m^3 mol^-1
+Molybdenum x unit                                           1.002 099 52 e-13        0.000 000 53 e-13        m
+muon Compton wavelength                                     1.173 444 110 e-14       0.000 000 026 e-14       m
+muon-electron mass ratio                                    206.768 2827             0.000 0046               
+muon g factor                                               -2.002 331 841 23        0.000 000 000 82         
+muon mag. mom.                                              -4.490 448 30 e-26       0.000 000 10 e-26        J T^-1
+muon mag. mom. anomaly                                      1.165 920 62 e-3         0.000 000 41 e-3         
+muon mag. mom. to Bohr magneton ratio                       -4.841 970 48 e-3        0.000 000 11 e-3         
+muon mag. mom. to nuclear magneton ratio                    -8.890 597 04            0.000 000 20             
+muon mass                                                   1.883 531 627 e-28       0.000 000 042 e-28       kg
+muon mass energy equivalent                                 1.692 833 804 e-11       0.000 000 038 e-11       J
+muon mass energy equivalent in MeV                          105.658 3755             0.000 0023               MeV
+muon mass in u                                              0.113 428 9257           0.000 000 0025           u
+muon molar mass                                             1.134 289 258 e-4        0.000 000 025 e-4        kg mol^-1
+muon-neutron mass ratio                                     0.112 454 5168           0.000 000 0025           
+muon-proton mag. mom. ratio                                 -3.183 345 146           0.000 000 071            
+muon-proton mass ratio                                      0.112 609 5262           0.000 000 0025           
+muon-tau mass ratio                                         5.946 35 e-2             0.000 40 e-2             
+natural unit of action                                      1.054 571 817... e-34    (exact)                  J s
+natural unit of action in eV s                              6.582 119 569... e-16    (exact)                  eV s
+natural unit of energy                                      8.187 105 7880 e-14      0.000 000 0026 e-14      J
+natural unit of energy in MeV                               0.510 998 950 69         0.000 000 000 16         MeV
+natural unit of length                                      3.861 592 6744 e-13      0.000 000 0012 e-13      m
+natural unit of mass                                        9.109 383 7139 e-31      0.000 000 0028 e-31      kg
+natural unit of momentum                                    2.730 924 534 46 e-22    0.000 000 000 85 e-22    kg m s^-1
+natural unit of momentum in MeV/c                           0.510 998 950 69         0.000 000 000 16         MeV/c
+natural unit of time                                        1.288 088 666 44 e-21    0.000 000 000 40 e-21    s
+natural unit of velocity                                    299 792 458              (exact)                  m s^-1
+neutron Compton wavelength                                  1.319 590 903 82 e-15    0.000 000 000 67 e-15    m
+neutron-electron mag. mom. ratio                            1.040 668 84 e-3         0.000 000 24 e-3         
+neutron-electron mass ratio                                 1838.683 662 00          0.000 000 74             
+neutron g factor                                            -3.826 085 52            0.000 000 90             
+neutron gyromag. ratio                                      1.832 471 74 e8          0.000 000 43 e8          s^-1 T^-1
+neutron gyromag. ratio in MHz/T                             29.164 6935              0.000 0069               MHz T^-1
+neutron mag. mom.                                           -9.662 3653 e-27         0.000 0023 e-27          J T^-1
+neutron mag. mom. to Bohr magneton ratio                    -1.041 875 65 e-3        0.000 000 25 e-3         
+neutron mag. mom. to nuclear magneton ratio                 -1.913 042 76            0.000 000 45             
+neutron mass                                                1.674 927 500 56 e-27    0.000 000 000 85 e-27    kg
+neutron mass energy equivalent                              1.505 349 765 14 e-10    0.000 000 000 76 e-10    J
+neutron mass energy equivalent in MeV                       939.565 421 94           0.000 000 48             MeV
+neutron mass in u                                           1.008 664 916 06         0.000 000 000 40         u
+neutron molar mass                                          1.008 664 917 12 e-3     0.000 000 000 51 e-3     kg mol^-1
+neutron-muon mass ratio                                     8.892 484 08             0.000 000 20             
+neutron-proton mag. mom. ratio                              -0.684 979 35            0.000 000 16             
+neutron-proton mass difference                              2.305 574 61 e-30        0.000 000 67 e-30        kg
+neutron-proton mass difference energy equivalent            2.072 147 12 e-13        0.000 000 60 e-13        J
+neutron-proton mass difference energy equivalent in MeV     1.293 332 51             0.000 000 38             MeV
+neutron-proton mass difference in u                         1.388 449 48 e-3         0.000 000 40 e-3         u
+neutron-proton mass ratio                                   1.001 378 419 46         0.000 000 000 40         
+neutron relative atomic mass                                1.008 664 916 06         0.000 000 000 40         
+neutron-tau mass ratio                                      0.528 779                0.000 036                
+neutron to shielded proton mag. mom. ratio                  -0.684 996 94            0.000 000 16             
+Newtonian constant of gravitation                           6.674 30 e-11            0.000 15 e-11            m^3 kg^-1 s^-2
+Newtonian constant of gravitation over h-bar c              6.708 83 e-39            0.000 15 e-39            (GeV/c^2)^-2
+nuclear magneton                                            5.050 783 7393 e-27      0.000 000 0016 e-27      J T^-1
+nuclear magneton in eV/T                                    3.152 451 254 17 e-8     0.000 000 000 98 e-8     eV T^-1
+nuclear magneton in inverse meter per tesla                 2.542 623 410 09 e-2     0.000 000 000 79 e-2     m^-1 T^-1
+nuclear magneton in K/T                                     3.658 267 7706 e-4       0.000 000 0011 e-4       K T^-1
+nuclear magneton in MHz/T                                   7.622 593 2188           0.000 000 0024           MHz T^-1
+Planck constant                                             6.626 070 15 e-34        (exact)                  J Hz^-1
+Planck constant in eV/Hz                                    4.135 667 696... e-15    (exact)                  eV Hz^-1
+Planck length                                               1.616 255 e-35           0.000 018 e-35           m
+Planck mass                                                 2.176 434 e-8            0.000 024 e-8            kg
+Planck mass energy equivalent in GeV                        1.220 890 e19            0.000 014 e19            GeV
+Planck temperature                                          1.416 784 e32            0.000 016 e32            K
+Planck time                                                 5.391 247 e-44           0.000 060 e-44           s
+proton charge to mass quotient                              9.578 833 1430 e7        0.000 000 0030 e7        C kg^-1
+proton Compton wavelength                                   1.321 409 853 60 e-15    0.000 000 000 41 e-15    m
+proton-electron mass ratio                                  1836.152 673 426         0.000 000 032            
+proton g factor                                             5.585 694 6893           0.000 000 0016           
+proton gyromag. ratio                                       2.675 221 8708 e8        0.000 000 0011 e8        s^-1 T^-1
+proton gyromag. ratio in MHz/T                              42.577 478 461           0.000 000 018            MHz T^-1
+proton mag. mom.                                            1.410 606 795 45 e-26    0.000 000 000 60 e-26    J T^-1
+proton mag. mom. to Bohr magneton ratio                     1.521 032 202 30 e-3     0.000 000 000 45 e-3     
+proton mag. mom. to nuclear magneton ratio                  2.792 847 344 63         0.000 000 000 82         
+proton mag. shielding correction                            2.567 15 e-5             0.000 41 e-5             
+proton mass                                                 1.672 621 925 95 e-27    0.000 000 000 52 e-27    kg
+proton mass energy equivalent                               1.503 277 618 02 e-10    0.000 000 000 47 e-10    J
+proton mass energy equivalent in MeV                        938.272 089 43           0.000 000 29             MeV
+proton mass in u                                            1.007 276 466 5789       0.000 000 000 0083       u
+proton molar mass                                           1.007 276 467 64 e-3     0.000 000 000 31 e-3     kg mol^-1
+proton-muon mass ratio                                      8.880 243 38             0.000 000 20             
+proton-neutron mag. mom. ratio                              -1.459 898 02            0.000 000 34             
+proton-neutron mass ratio                                   0.998 623 477 97         0.000 000 000 40         
+proton relative atomic mass                                 1.007 276 466 5789       0.000 000 000 0083       
+proton rms charge radius                                    8.4075 e-16              0.0064 e-16              m
+proton-tau mass ratio                                       0.528 051                0.000 036                
+quantum of circulation                                      3.636 947 5467 e-4       0.000 000 0011 e-4       m^2 s^-1
+quantum of circulation times 2                              7.273 895 0934 e-4       0.000 000 0023 e-4       m^2 s^-1
+reduced Compton wavelength                                  3.861 592 6744 e-13      0.000 000 0012 e-13      m
+reduced muon Compton wavelength                             1.867 594 306 e-15       0.000 000 042 e-15       m
+reduced neutron Compton wavelength                          2.100 194 1520 e-16      0.000 000 0011 e-16      m
+reduced Planck constant                                     1.054 571 817... e-34    (exact)                  J s
+reduced Planck constant in eV s                             6.582 119 569... e-16    (exact)                  eV s
+reduced Planck constant times c in MeV fm                   197.326 980 4...         (exact)                  MeV fm
+reduced proton Compton wavelength                           2.103 089 100 51 e-16    0.000 000 000 66 e-16    m
+reduced tau Compton wavelength                              1.110 538 e-16           0.000 075 e-16           m
+Rydberg constant                                            10 973 731.568 157       0.000 012                m^-1
+Rydberg constant times c in Hz                              3.289 841 960 2500 e15   0.000 000 000 0036 e15   Hz
+Rydberg constant times hc in eV                             13.605 693 122 990       0.000 000 000 015        eV
+Rydberg constant times hc in J                              2.179 872 361 1030 e-18  0.000 000 000 0024 e-18  J
+Sackur-Tetrode constant (1 K, 100 kPa)                      -1.151 707 534 96        0.000 000 000 47         
+Sackur-Tetrode constant (1 K, 101.325 kPa)                  -1.164 870 521 49        0.000 000 000 47         
+second radiation constant                                   1.438 776 877... e-2     (exact)                  m K
+shielded helion gyromag. ratio                              2.037 894 6078 e8        0.000 000 0018 e8        s^-1 T^-1
+shielded helion gyromag. ratio in MHz/T                     32.434 100 033           0.000 000 028            MHz T^-1
+shielded helion mag. mom.                                   -1.074 553 110 35 e-26   0.000 000 000 93 e-26    J T^-1
+shielded helion mag. mom. to Bohr magneton ratio            -1.158 671 494 57 e-3    0.000 000 000 94 e-3     
+shielded helion mag. mom. to nuclear magneton ratio         -2.127 497 7624          0.000 000 0017           
+shielded helion to proton mag. mom. ratio                   -0.761 766 577 21        0.000 000 000 66         
+shielded helion to shielded proton mag. mom. ratio          -0.761 786 1334          0.000 000 0031           
+shielded proton gyromag. ratio                              2.675 153 194 e8         0.000 000 011 e8         s^-1 T^-1
+shielded proton gyromag. ratio in MHz/T                     42.576 385 43            0.000 000 17             MHz T^-1
+shielded proton mag. mom.                                   1.410 570 5830 e-26      0.000 000 0058 e-26      J T^-1
+shielded proton mag. mom. to Bohr magneton ratio            1.520 993 1551 e-3       0.000 000 0062 e-3       
+shielded proton mag. mom. to nuclear magneton ratio         2.792 775 648            0.000 000 011            
+shielding difference of d and p in HD                       1.987 70 e-8             0.000 10 e-8             
+shielding difference of t and p in HT                       2.394 50 e-8             0.000 20 e-8             
+speed of light in vacuum                                    299 792 458              (exact)                  m s^-1
+standard acceleration of gravity                            9.806 65                 (exact)                  m s^-2
+standard atmosphere                                         101 325                  (exact)                  Pa
+standard-state pressure                                     100 000                  (exact)                  Pa
+Stefan-Boltzmann constant                                   5.670 374 419... e-8     (exact)                  W m^-2 K^-4
+tau Compton wavelength                                      6.977 71 e-16            0.000 47 e-16            m
+tau-electron mass ratio                                     3477.23                  0.23                     
+tau energy equivalent                                       1776.86                  0.12                     MeV
+tau mass                                                    3.167 54 e-27            0.000 21 e-27            kg
+tau mass energy equivalent                                  2.846 84 e-10            0.000 19 e-10            J
+tau mass in u                                               1.907 54                 0.000 13                 u
+tau molar mass                                              1.907 54 e-3             0.000 13 e-3             kg mol^-1
+tau-muon mass ratio                                         16.8170                  0.0011                   
+tau-neutron mass ratio                                      1.891 15                 0.000 13                 
+tau-proton mass ratio                                       1.893 76                 0.000 13                 
+Thomson cross section                                       6.652 458 7051 e-29      0.000 000 0062 e-29      m^2
+triton-electron mass ratio                                  5496.921 535 51          0.000 000 21             
+triton g factor                                             5.957 924 930            0.000 000 012            
+triton mag. mom.                                            1.504 609 5178 e-26      0.000 000 0030 e-26      J T^-1
+triton mag. mom. to Bohr magneton ratio                     1.622 393 6648 e-3       0.000 000 0032 e-3       
+triton mag. mom. to nuclear magneton ratio                  2.978 962 4650           0.000 000 0059           
+triton mass                                                 5.007 356 7512 e-27      0.000 000 0016 e-27      kg
+triton mass energy equivalent                               4.500 387 8119 e-10      0.000 000 0014 e-10      J
+triton mass energy equivalent in MeV                        2808.921 136 68          0.000 000 88             MeV
+triton mass in u                                            3.015 500 715 97         0.000 000 000 10         u
+triton molar mass                                           3.015 500 719 13 e-3     0.000 000 000 94 e-3     kg mol^-1
+triton-proton mass ratio                                    2.993 717 034 03         0.000 000 000 10         
+triton relative atomic mass                                 3.015 500 715 97         0.000 000 000 10         
+triton to proton mag. mom. ratio                            1.066 639 9189           0.000 000 0021           
+unified atomic mass unit                                    1.660 539 068 92 e-27    0.000 000 000 52 e-27    kg
+vacuum electric permittivity                                8.854 187 8188 e-12      0.000 000 0014 e-12      F m^-1
+vacuum mag. permeability                                    1.256 637 061 27 e-6     0.000 000 000 20 e-6     N A^-2
+von Klitzing constant                                       25 812.807 45...         (exact)                  ohm
+weak mixing angle                                           0.223 05                 0.000 23                 
+Wien frequency displacement law constant                    5.878 925 757... e10     (exact)                  Hz K^-1
+Wien wavelength displacement law constant                   2.897 771 955... e-3     (exact)                  m K
+W to Z mass ratio                                           0.881 45                 0.000 13                    """
+
+
+exact2022 = exact2018
+
+
+# -----------------------------------------------------------------------------
+
+
+def parse_constants_2002to2014(
+    d: str, exact_func: Callable[[Any], Any]
+) -> dict[str, tuple[float, str, float]]:
+    constants: dict[str, tuple[float, str, float]] = {}
+    exact: dict[str, float] = {}
+    need_replace = set()
+    for line in d.split('\n'):
+        name = line[:55].rstrip()
+        val = float(line[55:77].replace(' ', '').replace('...', ''))
+        is_truncated = '...' in line[55:77]
+        is_exact = '(exact)' in line[77:99]
+        if is_truncated and is_exact:
+            # missing decimals, use computed exact value
+            need_replace.add(name)
+        elif is_exact:
+            exact[name] = val
+        else:
+            assert not is_truncated
+        uncert = float(line[77:99].replace(' ', '').replace('(exact)', '0'))
+        units = line[99:].rstrip()
+        constants[name] = (val, units, uncert)
+    replace = exact_func(exact)
+    replace_exact(constants, need_replace, replace)
+    return constants
+
+
+def parse_constants_2018toXXXX(
+    d: str, exact_func: Callable[[Any], Any]
+) -> dict[str, tuple[float, str, float]]:
+    constants: dict[str, tuple[float, str, float]] = {}
+    exact: dict[str, float] = {}
+    need_replace = set()
+    for line in d.split('\n'):
+        name = line[:60].rstrip()
+        val = float(line[60:85].replace(' ', '').replace('...', ''))
+        is_truncated = '...' in line[60:85]
+        is_exact = '(exact)' in line[85:110]
+        if is_truncated and is_exact:
+            # missing decimals, use computed exact value
+            need_replace.add(name)
+        elif is_exact:
+            exact[name] = val
+        else:
+            assert not is_truncated
+        uncert = float(line[85:110].replace(' ', '').replace('(exact)', '0'))
+        units = line[110:].rstrip()
+        constants[name] = (val, units, uncert)
+    replace = exact_func(exact)
+    replace_exact(constants, need_replace, replace)
+    return constants
+
+
+def replace_exact(d, to_replace, exact):
+    for name in to_replace:
+        assert name in exact, f'Missing exact value: {name}'
+        assert abs(exact[name]/d[name][0] - 1) <= 1e-9, \
+            f'Bad exact value: {name}: { exact[name]}, {d[name][0]}'
+        d[name] = (exact[name],) + d[name][1:]
+    assert set(exact.keys()) == set(to_replace)
+
+
+_physical_constants_2002 = parse_constants_2002to2014(txt2002, exact2002)
+_physical_constants_2006 = parse_constants_2002to2014(txt2006, exact2006)
+_physical_constants_2010 = parse_constants_2002to2014(txt2010, exact2010)
+_physical_constants_2014 = parse_constants_2002to2014(txt2014, exact2014)
+_physical_constants_2018 = parse_constants_2018toXXXX(txt2018, exact2018)
+_physical_constants_2022 = parse_constants_2018toXXXX(txt2022, exact2022)
+
+physical_constants: dict[str, tuple[float, str, float]] = {}
+physical_constants.update(_physical_constants_2002)
+physical_constants.update(_physical_constants_2006)
+physical_constants.update(_physical_constants_2010)
+physical_constants.update(_physical_constants_2014)
+physical_constants.update(_physical_constants_2018)
+physical_constants.update(_physical_constants_2022)
+_current_constants = _physical_constants_2022
+_current_codata = "CODATA 2022"
+
+# check obsolete values
+_obsolete_constants = {}
+for k in physical_constants:
+    if k not in _current_constants:
+        _obsolete_constants[k] = True
+
+# generate some additional aliases
+_aliases = {}
+for k in _physical_constants_2002:
+    if 'magn.' in k:
+        _aliases[k] = k.replace('magn.', 'mag.')
+for k in _physical_constants_2006:
+    if 'momentum' in k:
+        _aliases[k] = k.replace('momentum', 'mom.um')
+for k in _physical_constants_2018:
+    if 'momentum' in k:
+        _aliases[k] = k.replace('momentum', 'mom.um')
+for k in _physical_constants_2022:
+    if 'momentum' in k:
+        _aliases[k] = k.replace('momentum', 'mom.um')        
+
+# CODATA 2018 and 2022: renamed and no longer exact; use as aliases
+_aliases['mag. constant'] = 'vacuum mag. permeability'
+_aliases['electric constant'] = 'vacuum electric permittivity'
+
+
+_extra_alias_keys = ['natural unit of velocity',
+                     'natural unit of action',
+                     'natural unit of action in eV s',
+                     'natural unit of mass',
+                     'natural unit of energy',
+                     'natural unit of energy in MeV',
+                     'natural unit of mom.um',
+                     'natural unit of mom.um in MeV/c',
+                     'natural unit of length',
+                     'natural unit of time']
+
+# finally, insert aliases for values
+for k, v in list(_aliases.items()):
+    if v in _current_constants or v in _extra_alias_keys:
+        physical_constants[k] = physical_constants[v]
+    else:
+        del _aliases[k]
+
+
+class ConstantWarning(DeprecationWarning):
+    """Accessing a constant no longer in current CODATA data set"""
+    pass
+
+
+def _check_obsolete(key: str) -> None:
+    if key in _obsolete_constants and key not in _aliases:
+        warnings.warn(f"Constant '{key}' is not in current {_current_codata} data set",
+                      ConstantWarning, stacklevel=3)
+
+
+@xp_capabilities(out_of_scope=True)
+def value(key: str) -> float:
+    """
+    Value in physical_constants indexed by key
+
+    Parameters
+    ----------
+    key : Python string
+        Key in dictionary `physical_constants`
+
+    Returns
+    -------
+    value : float
+        Value in `physical_constants` corresponding to `key`
+
+    Examples
+    --------
+    >>> from scipy import constants
+    >>> constants.value('elementary charge')
+    1.602176634e-19
+
+    """
+    _check_obsolete(key)
+    return physical_constants[key][0]
+
+
+@xp_capabilities(out_of_scope=True)
+def unit(key: str) -> str:
+    """
+    Unit in physical_constants indexed by key
+
+    Parameters
+    ----------
+    key : Python string
+        Key in dictionary `physical_constants`
+
+    Returns
+    -------
+    unit : Python string
+        Unit in `physical_constants` corresponding to `key`
+
+    Examples
+    --------
+    >>> from scipy import constants
+    >>> constants.unit('proton mass')
+    'kg'
+
+    """
+    _check_obsolete(key)
+    return physical_constants[key][1]
+
+
+@xp_capabilities(out_of_scope=True)
+def precision(key: str) -> float:
+    """
+    Relative precision in physical_constants indexed by key
+
+    Parameters
+    ----------
+    key : Python string
+        Key in dictionary `physical_constants`
+
+    Returns
+    -------
+    prec : float
+        Relative precision in `physical_constants` corresponding to `key`
+
+    Examples
+    --------
+    >>> from scipy import constants
+    >>> constants.precision('proton mass')
+    5.1e-37
+
+    """
+    _check_obsolete(key)
+    return physical_constants[key][2] / physical_constants[key][0]
+
+
+@xp_capabilities(out_of_scope=True)
+def find(sub: str | None = None, disp: bool = False) -> Any:
+    """
+    Return list of physical_constant keys containing a given string.
+
+    Parameters
+    ----------
+    sub : str
+        Sub-string to search keys for. By default, return all keys.
+    disp : bool
+        If True, print the keys that are found and return None.
+        Otherwise, return the list of keys without printing anything.
+
+    Returns
+    -------
+    keys : list or None
+        If `disp` is False, the list of keys is returned.
+        Otherwise, None is returned.
+
+    Examples
+    --------
+    >>> from scipy.constants import find, physical_constants
+
+    Which keys in the ``physical_constants`` dictionary contain 'boltzmann'?
+
+    >>> find('boltzmann')
+    ['Boltzmann constant',
+     'Boltzmann constant in Hz/K',
+     'Boltzmann constant in eV/K',
+     'Boltzmann constant in inverse meter per kelvin',
+     'Stefan-Boltzmann constant']
+
+    Get the constant called 'Boltzmann constant in Hz/K':
+
+    >>> physical_constants['Boltzmann constant in Hz/K']
+    (20836619120.0, 'Hz K^-1', 0.0)
+
+    Find constants with 'radius' in the key:
+
+    >>> find('radius')
+    ['Bohr radius',
+     'alpha particle rms charge radius',
+     'classical electron radius',
+     'deuteron rms charge radius',
+     'proton rms charge radius']
+    >>> physical_constants['classical electron radius']
+    (2.8179403262e-15, 'm', 1.3e-24)
+
+    """
+    if sub is None:
+        result = list(_current_constants.keys())
+    else:
+        result = [key for key in _current_constants
+                  if sub.lower() in key.lower()]
+
+    result.sort()
+    if disp:
+        for key in result:
+            print(key)
+        return
+    else:
+        return result
+
+# This is not used here, but it must be defined to pass
+# scipy/_lib/tests/test_public_api.py::test_private_but_present_deprecation
+c = value('speed of light in vacuum')
diff --git a/.venv/lib/python3.11/site-packages/scipy/constants/_constants.py b/.venv/lib/python3.11/site-packages/scipy/constants/_constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..fc4bdf83d60e37e039b9b830f74f7024c1759d32
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/constants/_constants.py
@@ -0,0 +1,369 @@
+"""
+Collection of physical constants and conversion factors.
+
+Most constants are in SI units, so you can do
+print '10 mile per minute is', 10*mile/minute, 'm/s or', 10*mile/(minute*knot), 'knots'
+
+The list is not meant to be comprehensive, but just convenient for everyday use.
+"""
+
+import math as _math
+from typing import TYPE_CHECKING, Any
+
+from ._codata import value as _cd
+
+if TYPE_CHECKING:
+    import numpy.typing as npt
+
+from scipy._lib._array_api import array_namespace, _asarray, xp_capabilities
+
+
+"""
+BasSw 2006
+physical constants: imported from CODATA
+unit conversion: see e.g., NIST special publication 811
+Use at own risk: double-check values before calculating your Mars orbit-insertion burn.
+Some constants exist in a few variants, which are marked with suffixes.
+The ones without any suffix should be the most common ones.
+"""
+
+__all__ = [
+    'Avogadro', 'Boltzmann', 'Btu', 'Btu_IT', 'Btu_th', 'G',
+    'Julian_year', 'N_A', 'Planck', 'R', 'Rydberg',
+    'Stefan_Boltzmann', 'Wien', 'acre', 'alpha',
+    'angstrom', 'arcmin', 'arcminute', 'arcsec',
+    'arcsecond', 'astronomical_unit', 'atm',
+    'atmosphere', 'atomic_mass', 'atto', 'au', 'bar',
+    'barrel', 'bbl', 'blob', 'c', 'calorie',
+    'calorie_IT', 'calorie_th', 'carat', 'centi',
+    'convert_temperature', 'day', 'deci', 'degree',
+    'degree_Fahrenheit', 'deka', 'dyn', 'dyne', 'e',
+    'eV', 'electron_mass', 'electron_volt',
+    'elementary_charge', 'epsilon_0', 'erg',
+    'exa', 'exbi', 'femto', 'fermi', 'fine_structure',
+    'fluid_ounce', 'fluid_ounce_US', 'fluid_ounce_imp',
+    'foot', 'g', 'gallon', 'gallon_US', 'gallon_imp',
+    'gas_constant', 'gibi', 'giga', 'golden', 'golden_ratio',
+    'grain', 'gram', 'gravitational_constant', 'h', 'hbar',
+    'hectare', 'hecto', 'horsepower', 'hour', 'hp',
+    'inch', 'k', 'kgf', 'kibi', 'kilo', 'kilogram_force',
+    'kmh', 'knot', 'lambda2nu', 'lb', 'lbf',
+    'light_year', 'liter', 'litre', 'long_ton', 'm_e',
+    'm_n', 'm_p', 'm_u', 'mach', 'mebi', 'mega',
+    'metric_ton', 'micro', 'micron', 'mil', 'mile',
+    'milli', 'minute', 'mmHg', 'mph', 'mu_0', 'nano',
+    'nautical_mile', 'neutron_mass', 'nu2lambda',
+    'ounce', 'oz', 'parsec', 'pebi', 'peta',
+    'pi', 'pico', 'point', 'pound', 'pound_force',
+    'proton_mass', 'psi', 'pt', 'quecto', 'quetta', 'ronna', 'ronto',
+    'short_ton', 'sigma', 'slinch', 'slug', 'speed_of_light',
+    'speed_of_sound', 'stone', 'survey_foot',
+    'survey_mile', 'tebi', 'tera', 'ton_TNT',
+    'torr', 'troy_ounce', 'troy_pound', 'u',
+    'week', 'yard', 'year', 'yobi', 'yocto',
+    'yotta', 'zebi', 'zepto', 'zero_Celsius', 'zetta'
+]
+
+
+# mathematical constants
+pi = _math.pi
+golden = golden_ratio = (1 + _math.sqrt(5)) / 2
+
+# SI prefixes
+quetta = 1e30
+ronna = 1e27
+yotta = 1e24
+zetta = 1e21
+exa = 1e18
+peta = 1e15
+tera = 1e12
+giga = 1e9
+mega = 1e6
+kilo = 1e3
+hecto = 1e2
+deka = 1e1
+deci = 1e-1
+centi = 1e-2
+milli = 1e-3
+micro = 1e-6
+nano = 1e-9
+pico = 1e-12
+femto = 1e-15
+atto = 1e-18
+zepto = 1e-21
+yocto = 1e-24
+ronto = 1e-27
+quecto = 1e-30
+
+# binary prefixes
+kibi = 2**10
+mebi = 2**20
+gibi = 2**30
+tebi = 2**40
+pebi = 2**50
+exbi = 2**60
+zebi = 2**70
+yobi = 2**80
+
+# physical constants
+c = speed_of_light = _cd('speed of light in vacuum')
+mu_0 = _cd('vacuum mag. permeability')
+epsilon_0 = _cd('vacuum electric permittivity')
+h = Planck = _cd('Planck constant')
+hbar = _cd('reduced Planck constant')
+G = gravitational_constant = _cd('Newtonian constant of gravitation')
+g = _cd('standard acceleration of gravity')
+e = elementary_charge = _cd('elementary charge')
+R = gas_constant = _cd('molar gas constant')
+alpha = fine_structure = _cd('fine-structure constant')
+N_A = Avogadro = _cd('Avogadro constant')
+k = Boltzmann = _cd('Boltzmann constant')
+sigma = Stefan_Boltzmann = _cd('Stefan-Boltzmann constant')
+Wien = _cd('Wien wavelength displacement law constant')
+Rydberg = _cd('Rydberg constant')
+
+# mass in kg
+gram = 1e-3
+metric_ton = 1e3
+grain = 64.79891e-6
+lb = pound = 7000 * grain  # avoirdupois
+blob = slinch = pound * g / 0.0254  # lbf*s**2/in (added in 1.0.0)
+slug = blob / 12  # lbf*s**2/foot (added in 1.0.0)
+oz = ounce = pound / 16
+stone = 14 * pound
+long_ton = 2240 * pound
+short_ton = 2000 * pound
+
+troy_ounce = 480 * grain  # only for metals / gems
+troy_pound = 12 * troy_ounce
+carat = 200e-6
+
+m_e = electron_mass = _cd('electron mass')
+m_p = proton_mass = _cd('proton mass')
+m_n = neutron_mass = _cd('neutron mass')
+m_u = u = atomic_mass = _cd('atomic mass constant')
+
+# angle in rad
+degree = pi / 180
+arcmin = arcminute = degree / 60
+arcsec = arcsecond = arcmin / 60
+
+# time in second
+minute = 60.0
+hour = 60 * minute
+day = 24 * hour
+week = 7 * day
+year = 365 * day
+Julian_year = 365.25 * day
+
+# length in meter
+inch = 0.0254
+foot = 12 * inch
+yard = 3 * foot
+mile = 1760 * yard
+mil = inch / 1000
+pt = point = inch / 72  # typography
+survey_foot = 1200.0 / 3937
+survey_mile = 5280 * survey_foot
+nautical_mile = 1852.0
+fermi = 1e-15
+angstrom = 1e-10
+micron = 1e-6
+au = astronomical_unit = 149597870700.0
+light_year = Julian_year * c
+parsec = au / arcsec
+
+# pressure in pascal
+atm = atmosphere = _cd('standard atmosphere')
+bar = 1e5
+torr = mmHg = atm / 760
+psi = pound * g / (inch * inch)
+
+# area in meter**2
+hectare = 1e4
+acre = 43560 * foot**2
+
+# volume in meter**3
+litre = liter = 1e-3
+gallon = gallon_US = 231 * inch**3  # US
+# pint = gallon_US / 8
+fluid_ounce = fluid_ounce_US = gallon_US / 128
+bbl = barrel = 42 * gallon_US  # for oil
+
+gallon_imp = 4.54609e-3  # UK
+fluid_ounce_imp = gallon_imp / 160
+
+# speed in meter per second
+kmh = 1e3 / hour
+mph = mile / hour
+# approx value of mach at 15 degrees in 1 atm. Is this a common value?
+mach = speed_of_sound = 340.5
+knot = nautical_mile / hour
+
+# temperature in kelvin
+zero_Celsius = 273.15
+degree_Fahrenheit = 1/1.8  # only for differences
+
+# energy in joule
+eV = electron_volt = elementary_charge  # * 1 Volt
+calorie = calorie_th = 4.184
+calorie_IT = 4.1868
+erg = 1e-7
+Btu_th = pound * degree_Fahrenheit * calorie_th / gram
+Btu = Btu_IT = pound * degree_Fahrenheit * calorie_IT / gram
+ton_TNT = 1e9 * calorie_th
+# Wh = watt_hour
+
+# power in watt
+hp = horsepower = 550 * foot * pound * g
+
+# force in newton
+dyn = dyne = 1e-5
+lbf = pound_force = pound * g
+kgf = kilogram_force = g  # * 1 kg
+
+# functions for conversions that are not linear
+
+
+@xp_capabilities()
+def convert_temperature(
+    val: "npt.ArrayLike",
+    old_scale: str,
+    new_scale: str,
+) -> Any:
+    """
+    Convert from a temperature scale to another one among Celsius, Kelvin,
+    Fahrenheit, and Rankine scales.
+
+    Parameters
+    ----------
+    val : array_like
+        Value(s) of the temperature(s) to be converted expressed in the
+        original scale.
+    old_scale : str
+        Specifies as a string the original scale from which the temperature
+        value(s) will be converted. Supported scales are Celsius ('Celsius',
+        'celsius', 'C' or 'c'), Kelvin ('Kelvin', 'kelvin', 'K', 'k'),
+        Fahrenheit ('Fahrenheit', 'fahrenheit', 'F' or 'f'), and Rankine
+        ('Rankine', 'rankine', 'R', 'r').
+    new_scale : str
+        Specifies as a string the new scale to which the temperature
+        value(s) will be converted. Supported scales are Celsius ('Celsius',
+        'celsius', 'C' or 'c'), Kelvin ('Kelvin', 'kelvin', 'K', 'k'),
+        Fahrenheit ('Fahrenheit', 'fahrenheit', 'F' or 'f'), and Rankine
+        ('Rankine', 'rankine', 'R', 'r').
+
+    Returns
+    -------
+    res : float or array of floats
+        Value(s) of the converted temperature(s) expressed in the new scale.
+
+    Notes
+    -----
+    .. versionadded:: 0.18.0
+
+    Examples
+    --------
+    >>> from scipy.constants import convert_temperature
+    >>> import numpy as np
+    >>> convert_temperature(np.array([-40, 40]), 'Celsius', 'Kelvin')
+    array([ 233.15,  313.15])
+
+    """
+    xp = array_namespace(val)
+    _val = _asarray(val, xp=xp, subok=True)
+    # Convert from `old_scale` to Kelvin
+    if old_scale.lower() in ['celsius', 'c']:
+        tempo = _val + zero_Celsius
+    elif old_scale.lower() in ['kelvin', 'k']:
+        tempo = _val
+    elif old_scale.lower() in ['fahrenheit', 'f']:
+        tempo = (_val - 32) * 5 / 9 + zero_Celsius
+    elif old_scale.lower() in ['rankine', 'r']:
+        tempo = _val * 5 / 9
+    else:
+        raise NotImplementedError(f"{old_scale=} is unsupported: supported scales "
+                                   "are Celsius, Kelvin, Fahrenheit, and "
+                                   "Rankine")
+    # and from Kelvin to `new_scale`.
+    if new_scale.lower() in ['celsius', 'c']:
+        res = tempo - zero_Celsius
+    elif new_scale.lower() in ['kelvin', 'k']:
+        res = tempo
+    elif new_scale.lower() in ['fahrenheit', 'f']:
+        res = (tempo - zero_Celsius) * 9 / 5 + 32
+    elif new_scale.lower() in ['rankine', 'r']:
+        res = tempo * 9 / 5
+    else:
+        raise NotImplementedError(f"{new_scale=} is unsupported: supported "
+                                   "scales are 'Celsius', 'Kelvin', "
+                                   "'Fahrenheit', and 'Rankine'")
+
+    return res
+
+
+# optics
+
+
+@xp_capabilities()
+def lambda2nu(lambda_: "npt.ArrayLike") -> Any:
+    """
+    Convert wavelength to optical frequency
+
+    Parameters
+    ----------
+    lambda_ : array_like
+        Wavelength(s) to be converted.
+
+    Returns
+    -------
+    nu : float or array of floats
+        Equivalent optical frequency.
+
+    Notes
+    -----
+    Computes ``nu = c / lambda`` where c = 299792458.0, i.e., the
+    (vacuum) speed of light in meters/second.
+
+    Examples
+    --------
+    >>> from scipy.constants import lambda2nu, speed_of_light
+    >>> import numpy as np
+    >>> lambda2nu(np.array((1, speed_of_light)))
+    array([  2.99792458e+08,   1.00000000e+00])
+
+    """
+    xp = array_namespace(lambda_)
+    return c / _asarray(lambda_, xp=xp, subok=True)
+
+
+@xp_capabilities()
+def nu2lambda(nu: "npt.ArrayLike") -> Any:
+    """
+    Convert optical frequency to wavelength.
+
+    Parameters
+    ----------
+    nu : array_like
+        Optical frequency to be converted.
+
+    Returns
+    -------
+    lambda : float or array of floats
+        Equivalent wavelength(s).
+
+    Notes
+    -----
+    Computes ``lambda = c / nu`` where c = 299792458.0, i.e., the
+    (vacuum) speed of light in meters/second.
+
+    Examples
+    --------
+    >>> from scipy.constants import nu2lambda, speed_of_light
+    >>> import numpy as np
+    >>> nu2lambda(np.array((1, speed_of_light)))
+    array([  2.99792458e+08,   1.00000000e+00])
+
+    """
+    xp = array_namespace(nu)
+    return c / _asarray(nu, xp=xp, subok=True)
diff --git a/.venv/lib/python3.11/site-packages/scipy/constants/codata.py b/.venv/lib/python3.11/site-packages/scipy/constants/codata.py
new file mode 100644
index 0000000000000000000000000000000000000000..912e0bbf7c4f14d23ced4546b6704f7789996d97
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/constants/codata.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.constants` namespace for importing the functions
+# included below.
+
+from scipy._lib.deprecation import _sub_module_deprecation
+
+__all__ = [  # noqa: F822
+    'physical_constants', 'value', 'unit', 'precision', 'find',
+    'ConstantWarning', 'k', 'c',
+
+]
+
+
+def __dir__():
+    return __all__
+
+
+def __getattr__(name):
+    return _sub_module_deprecation(sub_package="constants", module="codata",
+                                   private_modules=["_codata"], all=__all__,
+                                   attribute=name)
diff --git a/.venv/lib/python3.11/site-packages/scipy/constants/constants.py b/.venv/lib/python3.11/site-packages/scipy/constants/constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..855901ba802881090b99b7e8972de741331c7ab9
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/constants/constants.py
@@ -0,0 +1,53 @@
+# This file is not meant for public use and will be removed in SciPy v2.0.0.
+# Use the `scipy.constants` namespace for importing the functions
+# included below.
+
+from scipy._lib.deprecation import _sub_module_deprecation
+
+
+__all__ = [  # noqa: F822
+    'Avogadro', 'Boltzmann', 'Btu', 'Btu_IT', 'Btu_th', 'G',
+    'Julian_year', 'N_A', 'Planck', 'R', 'Rydberg',
+    'Stefan_Boltzmann', 'Wien', 'acre', 'alpha',
+    'angstrom', 'arcmin', 'arcminute', 'arcsec',
+    'arcsecond', 'astronomical_unit', 'atm',
+    'atmosphere', 'atomic_mass', 'atto', 'au', 'bar',
+    'barrel', 'bbl', 'blob', 'c', 'calorie',
+    'calorie_IT', 'calorie_th', 'carat', 'centi',
+    'convert_temperature', 'day', 'deci', 'degree',
+    'degree_Fahrenheit', 'deka', 'dyn', 'dyne', 'e',
+    'eV', 'electron_mass', 'electron_volt',
+    'elementary_charge', 'epsilon_0', 'erg',
+    'exa', 'exbi', 'femto', 'fermi', 'fine_structure',
+    'fluid_ounce', 'fluid_ounce_US', 'fluid_ounce_imp',
+    'foot', 'g', 'gallon', 'gallon_US', 'gallon_imp',
+    'gas_constant', 'gibi', 'giga', 'golden', 'golden_ratio',
+    'grain', 'gram', 'gravitational_constant', 'h', 'hbar',
+    'hectare', 'hecto', 'horsepower', 'hour', 'hp',
+    'inch', 'k', 'kgf', 'kibi', 'kilo', 'kilogram_force',
+    'kmh', 'knot', 'lambda2nu', 'lb', 'lbf',
+    'light_year', 'liter', 'litre', 'long_ton', 'm_e',
+    'm_n', 'm_p', 'm_u', 'mach', 'mebi', 'mega',
+    'metric_ton', 'micro', 'micron', 'mil', 'mile',
+    'milli', 'minute', 'mmHg', 'mph', 'mu_0', 'nano',
+    'nautical_mile', 'neutron_mass', 'nu2lambda',
+    'ounce', 'oz', 'parsec', 'pebi', 'peta',
+    'pi', 'pico', 'point', 'pound', 'pound_force',
+    'proton_mass', 'psi', 'pt', 'short_ton',
+    'sigma', 'slinch', 'slug', 'speed_of_light',
+    'speed_of_sound', 'stone', 'survey_foot',
+    'survey_mile', 'tebi', 'tera', 'ton_TNT',
+    'torr', 'troy_ounce', 'troy_pound', 'u',
+    'week', 'yard', 'year', 'yobi', 'yocto',
+    'yotta', 'zebi', 'zepto', 'zero_Celsius', 'zetta'
+]
+
+
+def __dir__():
+    return __all__
+
+
+def __getattr__(name):
+    return _sub_module_deprecation(sub_package="constants", module="constants",
+                                   private_modules=["_constants"], all=__all__,
+                                   attribute=name)
diff --git a/.venv/lib/python3.11/site-packages/scipy/constants/tests/__init__.py b/.venv/lib/python3.11/site-packages/scipy/constants/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/.venv/lib/python3.11/site-packages/scipy/constants/tests/test_codata.py b/.venv/lib/python3.11/site-packages/scipy/constants/tests/test_codata.py
new file mode 100644
index 0000000000000000000000000000000000000000..ece796136a43ef6f3078272e4d0223263c9e31e8
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/constants/tests/test_codata.py
@@ -0,0 +1,78 @@
+from scipy.constants import find, value, c, speed_of_light, precision
+from numpy.testing import assert_equal, assert_, assert_almost_equal
+import scipy.constants._codata as _cd
+from scipy import constants
+
+
+def test_find():
+    keys = find('weak mixing', disp=False)
+    assert_equal(keys, ['weak mixing angle'])
+
+    keys = find('qwertyuiop', disp=False)
+    assert_equal(keys, [])
+
+    keys = find('natural unit', disp=False)
+    assert_equal(keys, sorted(['natural unit of velocity',
+                                'natural unit of action',
+                                'natural unit of action in eV s',
+                                'natural unit of mass',
+                                'natural unit of energy',
+                                'natural unit of energy in MeV',
+                                'natural unit of momentum',
+                                'natural unit of momentum in MeV/c',
+                                'natural unit of length',
+                                'natural unit of time']))
+
+
+def test_basic_table_parse():
+    c_s = 'speed of light in vacuum'
+    assert_equal(value(c_s), c)
+    assert_equal(value(c_s), speed_of_light)
+
+
+def test_basic_lookup():
+    assert_equal('{} {}'.format(int(_cd.value('speed of light in vacuum')),
+                                _cd.unit('speed of light in vacuum')),
+                 '299792458 m s^-1')
+
+
+def test_find_all():
+    assert_(len(find(disp=False)) > 300)
+
+
+def test_find_single():
+    assert_equal(find('Wien freq', disp=False)[0],
+                 'Wien frequency displacement law constant')
+
+
+def test_2002_vs_2006():
+    assert_almost_equal(value('magn. flux quantum'),
+                        value('mag. flux quantum'))
+
+
+def test_exact_values():
+    # Check that updating stored values with exact ones worked.
+    exact = dict((k, v[0]) for k, v in _cd._physical_constants_2018.items())
+    replace = _cd.exact2018(exact)
+    for key, val in replace.items():
+        assert_equal(val, value(key))
+        assert precision(key) == 0
+
+
+def test_gh11341():
+    # gh-11341 noted that these three constants should exist (for backward
+    # compatibility) and should always have the same value:
+    a = constants.epsilon_0
+    b = constants.physical_constants['electric constant'][0]
+    c = constants.physical_constants['vacuum electric permittivity'][0]
+    assert a == b == c
+
+
+def test_gh14467():
+    # gh-14467 noted that some physical constants in CODATA are rounded
+    # to only ten significant figures even though they are supposed to be
+    # exact. Check that (at least) the case mentioned in the issue is resolved.
+    res = constants.physical_constants['Boltzmann constant in eV/K'][0]
+    ref = (constants.physical_constants['Boltzmann constant'][0]
+           / constants.physical_constants['elementary charge'][0])
+    assert res == ref
diff --git a/.venv/lib/python3.11/site-packages/scipy/constants/tests/test_constants.py b/.venv/lib/python3.11/site-packages/scipy/constants/tests/test_constants.py
new file mode 100644
index 0000000000000000000000000000000000000000..672fac18884ddb012232d6e1a09a6d1ec8e277ef
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/constants/tests/test_constants.py
@@ -0,0 +1,83 @@
+import pytest
+
+import scipy.constants as sc
+from scipy._lib._array_api_no_0d import xp_assert_equal, xp_assert_close
+from scipy._lib._array_api import make_xp_test_case
+
+lazy_xp_modules = [sc]
+
+
+@make_xp_test_case(sc.convert_temperature)
+class TestConvertTemperature:
+    def test_convert_temperature(self, xp):
+        xp_assert_equal(sc.convert_temperature(xp.asarray(32.), 'f', 'Celsius'),
+                        xp.asarray(0.0))
+        xp_assert_equal(sc.convert_temperature(xp.asarray([0., 0.]),
+                                               'celsius', 'Kelvin'),
+                        xp.asarray([273.15, 273.15]))
+        xp_assert_equal(sc.convert_temperature(xp.asarray([0., 0.]), 'kelvin', 'c'),
+                        xp.asarray([-273.15, -273.15]))
+        xp_assert_equal(sc.convert_temperature(xp.asarray([32., 32.]), 'f', 'k'),
+                        xp.asarray([273.15, 273.15]))
+        xp_assert_equal(sc.convert_temperature(xp.asarray([273.15, 273.15]),
+                                               'kelvin', 'F'),
+                        xp.asarray([32., 32.]))
+        xp_assert_equal(sc.convert_temperature(xp.asarray([0., 0.]), 'C', 'fahrenheit'),
+                        xp.asarray([32., 32.]))
+        xp_assert_close(sc.convert_temperature(xp.asarray([0., 0.], dtype=xp.float64),
+                                               'c', 'r'),
+                        xp.asarray([491.67, 491.67], dtype=xp.float64),
+                        rtol=0., atol=1e-13)
+        xp_assert_close(sc.convert_temperature(xp.asarray([491.67, 491.67],
+                                                        dtype=xp.float64),
+                                               'Rankine', 'C'),
+                        xp.asarray([0., 0.], dtype=xp.float64), rtol=0., atol=1e-13)
+        xp_assert_close(sc.convert_temperature(xp.asarray([491.67, 491.67],
+                                                        dtype=xp.float64),
+                                               'r', 'F'),
+                        xp.asarray([32., 32.], dtype=xp.float64), rtol=0., atol=1e-13)
+        xp_assert_close(sc.convert_temperature(xp.asarray([32., 32.], dtype=xp.float64),
+                                               'fahrenheit', 'R'),
+                        xp.asarray([491.67, 491.67], dtype=xp.float64),
+                        rtol=0., atol=1e-13)
+        xp_assert_close(sc.convert_temperature(xp.asarray([273.15, 273.15],
+                                                        dtype=xp.float64),
+                                               'K', 'R'),
+                        xp.asarray([491.67, 491.67], dtype=xp.float64),
+                        rtol=0., atol=1e-13)
+        xp_assert_close(sc.convert_temperature(xp.asarray([491.67, 0.],
+                                                          dtype=xp.float64),
+                                               'rankine', 'kelvin'),
+                        xp.asarray([273.15, 0.], dtype=xp.float64), rtol=0., atol=1e-13)
+
+    def test_convert_temperature_array_like(self):
+        xp_assert_close(sc.convert_temperature([491.67, 0.], 'rankine', 'kelvin'),
+                        [273.15, 0.], rtol=0., atol=1e-13)
+
+
+    def test_convert_temperature_errors(self):
+        with pytest.raises(NotImplementedError, match="old_scale="):
+            sc.convert_temperature(1, old_scale="cheddar", new_scale="kelvin")
+        with pytest.raises(NotImplementedError, match="new_scale="):
+            sc.convert_temperature(1, old_scale="kelvin", new_scale="brie")
+
+
+@make_xp_test_case(sc.lambda2nu)
+class TestLambdaToNu:
+    def test_lambda_to_nu(self, xp):
+        xp_assert_equal(sc.lambda2nu(xp.asarray([sc.speed_of_light, 1])),
+                        xp.asarray([1, sc.speed_of_light]))
+
+
+    def test_lambda_to_nu_array_like(self):
+        xp_assert_close(sc.lambda2nu([sc.speed_of_light, 1]), [1, sc.speed_of_light])
+
+
+@make_xp_test_case(sc.nu2lambda)
+class TestNuToLambda:
+    def test_nu_to_lambda(self, xp):
+        xp_assert_equal(sc.nu2lambda(xp.asarray([sc.speed_of_light, 1])),
+                        xp.asarray([1, sc.speed_of_light]))
+
+    def test_nu_to_lambda_array_like(self):
+        xp_assert_close(sc.nu2lambda([sc.speed_of_light, 1]), [1, sc.speed_of_light])
diff --git a/.venv/lib/python3.11/site-packages/scipy/datasets/__init__.py b/.venv/lib/python3.11/site-packages/scipy/datasets/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..fdd4ffebec4c57f6d399a0f76df2b66056f0b225
--- /dev/null
+++ b/.venv/lib/python3.11/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/.venv/lib/python3.11/site-packages/scipy/datasets/_download_all.py b/.venv/lib/python3.11/site-packages/scipy/datasets/_download_all.py
new file mode 100644
index 0000000000000000000000000000000000000000..d410e968f4d4e25d788b93913c8901d1cd74b521
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/datasets/_download_all.py
@@ -0,0 +1,74 @@
+"""
+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 
+"""
+
+from scipy._lib._array_api import xp_capabilities
+
+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
+
+
+@xp_capabilities(out_of_scope=True)
+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.
+
+    Examples
+    --------
+    Download the datasets to the default cache location:
+
+    >>> from scipy import datasets
+    >>> datasets.download_all()
+
+    Download the datasets to the current directory:
+
+    >>> datasets.download_all(".")
+
+    """
+    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')
+    # https://github.com/scipy/scipy/issues/21879
+    downloader = pooch.HTTPDownloader(headers={"User-Agent": "SciPy"})
+    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, downloader=downloader)
+
+
+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/.venv/lib/python3.11/site-packages/scipy/datasets/_fetchers.py b/.venv/lib/python3.11/site-packages/scipy/datasets/_fetchers.py
new file mode 100644
index 0000000000000000000000000000000000000000..f6389a0ca5da32ce86b018ac7f92b1e49300645b
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/datasets/_fetchers.py
@@ -0,0 +1,229 @@
+import sys
+
+from numpy import array, frombuffer, load
+from ._registry import registry, registry_urls
+
+from scipy._lib._array_api import xp_capabilities
+
+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'.")
+    # https://github.com/scipy/scipy/issues/21879
+    downloader = pooch.HTTPDownloader(
+        headers={"User-Agent": f"SciPy {sys.modules['scipy'].__version__}"}
+    )
+    # The "fetch" method returns the full path to the downloaded data file.
+    return data_fetcher.fetch(dataset_name, downloader=downloader)
+
+
+@xp_capabilities(out_of_scope=True)
+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()
+    np.uint8(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
+
+
+@xp_capabilities(out_of_scope=True)
+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], shape=(108000,))
+    >>> 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
+
+
+@xp_capabilities(out_of_scope=True)
+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()
+    np.uint8(255)
+
+    >>> 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').reshape((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/.venv/lib/python3.11/site-packages/scipy/datasets/_registry.py b/.venv/lib/python3.11/site-packages/scipy/datasets/_registry.py
new file mode 100644
index 0000000000000000000000000000000000000000..969384ad9843159e766100bfa9755aed8102dd09
--- /dev/null
+++ b/.venv/lib/python3.11/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/.venv/lib/python3.11/site-packages/scipy/datasets/_utils.py b/.venv/lib/python3.11/site-packages/scipy/datasets/_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..a78206da9fae576c30cf456f6348708e7e869b1c
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/datasets/_utils.py
@@ -0,0 +1,84 @@
+import os
+import shutil
+from ._registry import method_files_map
+
+from scipy._lib._array_api import xp_capabilities
+
+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.")
+
+
+@xp_capabilities(out_of_scope=True)
+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/.venv/lib/python3.11/site-packages/scipy/datasets/tests/__init__.py b/.venv/lib/python3.11/site-packages/scipy/datasets/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/.venv/lib/python3.11/site-packages/scipy/datasets/tests/test_data.py b/.venv/lib/python3.11/site-packages/scipy/datasets/tests/test_data.py
new file mode 100644
index 0000000000000000000000000000000000000000..243176bd89b7b6f16406d66293d1872ac2712252
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/datasets/tests/test_data.py
@@ -0,0 +1,128 @@
+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
+from threading import get_ident
+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(10)
+    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
+    thread_basepath = tmp_path / str(get_ident())
+    thread_basepath.mkdir()
+
+    dummy_basepath = thread_basepath / "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/.venv/lib/python3.11/site-packages/scipy/differentiate/__init__.py b/.venv/lib/python3.11/site-packages/scipy/differentiate/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..c3a7ccc4b33f27dbae7958641a89106cf9580326
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/differentiate/__init__.py
@@ -0,0 +1,27 @@
+"""
+==============================================================
+Finite Difference Differentiation (:mod:`scipy.differentiate`)
+==============================================================
+
+.. currentmodule:: scipy.differentiate
+
+SciPy ``differentiate`` provides functions for performing finite difference
+numerical differentiation of black-box functions.
+
+.. autosummary::
+   :toctree: generated/
+
+   derivative
+   jacobian
+   hessian
+
+"""
+
+
+from ._differentiate import *
+
+__all__ = ['derivative', 'jacobian', 'hessian']
+
+from scipy._lib._testutils import PytestTester
+test = PytestTester(__name__)
+del PytestTester
diff --git a/.venv/lib/python3.11/site-packages/scipy/differentiate/_differentiate.py b/.venv/lib/python3.11/site-packages/scipy/differentiate/_differentiate.py
new file mode 100644
index 0000000000000000000000000000000000000000..70ea2b1696d73cfd9b8b5dd0a2c5a5cb9aba5f5e
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/differentiate/_differentiate.py
@@ -0,0 +1,1140 @@
+# mypy: disable-error-code="attr-defined"
+import warnings
+import numpy as np
+import scipy._lib._elementwise_iterative_method as eim
+from scipy._lib._util import _RichResult
+from scipy._lib._array_api import array_namespace, xp_copy, xp_promote, xp_capabilities
+import scipy._lib.array_api_extra as xpx
+
+_EERRORINCREASE = -1  # used in derivative
+
+def _derivative_iv(f, x, args, tolerances, maxiter, order, initial_step,
+                   step_factor, step_direction, preserve_shape, callback):
+    # Input validation for `derivative`
+    xp = array_namespace(x)
+
+    if not callable(f):
+        raise ValueError('`f` must be callable.')
+
+    if not np.iterable(args):
+        args = (args,)
+
+    tolerances = {} if tolerances is None else tolerances
+    atol = tolerances.get('atol', None)
+    rtol = tolerances.get('rtol', None)
+
+    # tolerances are floats, not arrays; OK to use NumPy
+    message = 'Tolerances and step parameters must be non-negative scalars.'
+    tols = np.asarray([atol if atol is not None else 1,
+                       rtol if rtol is not None else 1,
+                       step_factor])
+    if (not np.issubdtype(tols.dtype, np.number) or np.any(tols < 0)
+            or np.any(np.isnan(tols)) or tols.shape != (3,)):
+        raise ValueError(message)
+    step_factor = float(tols[2])
+
+    maxiter_int = int(maxiter)
+    if maxiter != maxiter_int or maxiter <= 0:
+        raise ValueError('`maxiter` must be a positive integer.')
+
+    order_int = int(order)
+    if order_int != order or order <= 0:
+        raise ValueError('`order` must be a positive integer.')
+
+    step_direction = xp.asarray(step_direction)
+    initial_step = xp.asarray(initial_step)
+    temp = xp.broadcast_arrays(x, step_direction, initial_step)
+    x, step_direction, initial_step = temp
+
+    message = '`preserve_shape` must be True or False.'
+    if preserve_shape not in {True, False}:
+        raise ValueError(message)
+
+    if callback is not None and not callable(callback):
+        raise ValueError('`callback` must be callable.')
+
+    return (f, x, args, atol, rtol, maxiter_int, order_int, initial_step,
+            step_factor, step_direction, preserve_shape, callback)
+
+
+
+_array_api_strict_skip_reason = 'Array API does not support fancy indexing assignment.'
+_dask_reason = 'boolean indexing assignment'
+
+
+@xp_capabilities(skip_backends=[('array_api_strict', _array_api_strict_skip_reason),
+                                ('dask.array', _dask_reason)], jax_jit=False)
+def derivative(f, x, *, args=(), tolerances=None, maxiter=10,
+               order=8, initial_step=0.5, step_factor=2.0,
+               step_direction=0, preserve_shape=False, callback=None):
+    """Evaluate the derivative of an elementwise, real scalar function numerically.
+
+    For each element of the output of `f`, `derivative` approximates the first
+    derivative of `f` at the corresponding element of `x` using finite difference
+    differentiation.
+
+    This function works elementwise when `x`, `step_direction`, and `args` contain
+    (broadcastable) arrays.
+
+    Parameters
+    ----------
+    f : callable
+        The function whose derivative is desired. The signature must be::
+
+            f(xi: ndarray, *argsi) -> ndarray
+
+        where each element of ``xi`` is a finite real number and ``argsi`` is a tuple,
+        which may contain an arbitrary number of arrays that are broadcastable with
+        ``xi``. `f` must be an elementwise function: each scalar element ``f(xi)[j]``
+        must equal ``f(xi[j])`` for valid indices ``j``. It must not mutate the array
+        ``xi`` or the arrays in ``argsi``.
+    x : float array_like
+        Abscissae at which to evaluate the derivative. Must be broadcastable with
+        `args` and `step_direction`.
+    args : tuple of array_like, optional
+        Additional positional array arguments to be passed to `f`. Arrays
+        must be broadcastable with one another and the arrays of `init`.
+        If the callable for which the root is desired requires arguments that are
+        not broadcastable with `x`, wrap that callable with `f` such that `f`
+        accepts only `x` and broadcastable ``*args``.
+    tolerances : dictionary of floats, optional
+        Absolute and relative tolerances. Valid keys of the dictionary are:
+
+        - ``atol`` - absolute tolerance on the derivative
+        - ``rtol`` - relative tolerance on the derivative
+
+        Iteration will stop when ``res.error < atol + rtol * abs(res.df)``. The default
+        `atol` is the smallest normal number of the appropriate dtype, and
+        the default `rtol` is the square root of the precision of the
+        appropriate dtype.
+    order : int, default: 8
+        The (positive integer) order of the finite difference formula to be
+        used. Odd integers will be rounded up to the next even integer.
+    initial_step : float array_like, default: 0.5
+        The (absolute) initial step size for the finite difference derivative
+        approximation.
+    step_factor : float, default: 2.0
+        The factor by which the step size is *reduced* in each iteration; i.e.
+        the step size in iteration 1 is ``initial_step/step_factor``. If
+        ``step_factor < 1``, subsequent steps will be greater than the initial
+        step; this may be useful if steps smaller than some threshold are
+        undesirable (e.g. due to subtractive cancellation error).
+    maxiter : int, default: 10
+        The maximum number of iterations of the algorithm to perform. See
+        Notes.
+    step_direction : integer array_like
+        An array representing the direction of the finite difference steps (for
+        use when `x` lies near to the boundary of the domain of the function.)
+        Must be broadcastable with `x` and all `args`.
+        Where 0 (default), central differences are used; where negative (e.g.
+        -1), steps are non-positive; and where positive (e.g. 1), all steps are
+        non-negative.
+    preserve_shape : bool, default: False
+        In the following, "arguments of `f`" refers to the array ``xi`` and
+        any arrays within ``argsi``. Let ``shape`` be the broadcasted shape
+        of `x` and all elements of `args` (which is conceptually
+        distinct from ``xi` and ``argsi`` passed into `f`).
+
+        - When ``preserve_shape=False`` (default), `f` must accept arguments
+          of *any* broadcastable shapes.
+
+        - When ``preserve_shape=True``, `f` must accept arguments of shape
+          ``shape`` *or* ``shape + (n,)``, where ``(n,)`` is the number of
+          abscissae at which the function is being evaluated.
+
+        In either case, for each scalar element ``xi[j]`` within ``xi``, the array
+        returned by `f` must include the scalar ``f(xi[j])`` at the same index.
+        Consequently, the shape of the output is always the shape of the input
+        ``xi``.
+
+        See Examples.
+    callback : callable, optional
+        An optional user-supplied function to be called before the first
+        iteration and after each iteration.
+        Called as ``callback(res)``, where ``res`` is a ``_RichResult``
+        similar to that returned by `derivative` (but containing the current
+        iterate's values of all variables). If `callback` raises a
+        ``StopIteration``, the algorithm will terminate immediately and
+        `derivative` will return a result. `callback` must not mutate
+        `res` or its attributes.
+
+    Returns
+    -------
+    res : _RichResult
+        An object similar to an instance of `scipy.optimize.OptimizeResult` with the
+        following attributes. The descriptions are written as though the values will
+        be scalars; however, if `f` returns an array, the outputs will be
+        arrays of the same shape.
+
+        success : bool array
+            ``True`` where the algorithm terminated successfully (status ``0``);
+            ``False`` otherwise.
+        status : int array
+            An integer representing the exit status of the algorithm.
+
+            - ``0`` : The algorithm converged to the specified tolerances.
+            - ``-1`` : The error estimate increased, so iteration was terminated.
+            - ``-2`` : The maximum number of iterations was reached.
+            - ``-3`` : A non-finite value was encountered.
+            - ``-4`` : Iteration was terminated by `callback`.
+            - ``1`` : The algorithm is proceeding normally (in `callback` only).
+
+        df : float array
+            The derivative of `f` at `x`, if the algorithm terminated
+            successfully.
+        error : float array
+            An estimate of the error: the magnitude of the difference between
+            the current estimate of the derivative and the estimate in the
+            previous iteration.
+        nit : int array
+            The number of iterations of the algorithm that were performed.
+        nfev : int array
+            The number of points at which `f` was evaluated.
+        x : float array
+            The value at which the derivative of `f` was evaluated
+            (after broadcasting with `args` and `step_direction`).
+
+    See Also
+    --------
+    jacobian, hessian
+
+    Notes
+    -----
+    The implementation was inspired by jacobi [1]_, numdifftools [2]_, and
+    DERIVEST [3]_, but the implementation follows the theory of Taylor series
+    more straightforwardly (and arguably naively so).
+    In the first iteration, the derivative is estimated using a finite
+    difference formula of order `order` with maximum step size `initial_step`.
+    Each subsequent iteration, the maximum step size is reduced by
+    `step_factor`, and the derivative is estimated again until a termination
+    condition is reached. The error estimate is the magnitude of the difference
+    between the current derivative approximation and that of the previous
+    iteration.
+
+    The stencils of the finite difference formulae are designed such that
+    abscissae are "nested": after `f` is evaluated at ``order + 1``
+    points in the first iteration, `f` is evaluated at only two new points
+    in each subsequent iteration; ``order - 1`` previously evaluated function
+    values required by the finite difference formula are reused, and two
+    function values (evaluations at the points furthest from `x`) are unused.
+
+    Step sizes are absolute. When the step size is small relative to the
+    magnitude of `x`, precision is lost; for example, if `x` is ``1e20``, the
+    default initial step size of ``0.5`` cannot be resolved. Accordingly,
+    consider using larger initial step sizes for large magnitudes of `x`.
+
+    The default tolerances are challenging to satisfy at points where the
+    true derivative is exactly zero. If the derivative may be exactly zero,
+    consider specifying an absolute tolerance (e.g. ``atol=1e-12``) to
+    improve convergence.
+
+    References
+    ----------
+    .. [1] Hans Dembinski (@HDembinski). jacobi.
+           https://github.com/HDembinski/jacobi
+    .. [2] Per A. Brodtkorb and John D'Errico. numdifftools.
+           https://numdifftools.readthedocs.io/en/latest/
+    .. [3] John D'Errico. DERIVEST: Adaptive Robust Numerical Differentiation.
+           https://www.mathworks.com/matlabcentral/fileexchange/13490-adaptive-robust-numerical-differentiation
+    .. [4] Numerical Differentition. Wikipedia.
+           https://en.wikipedia.org/wiki/Numerical_differentiation
+
+    Examples
+    --------
+    Evaluate the derivative of ``np.exp`` at several points ``x``.
+
+    >>> import numpy as np
+    >>> from scipy.differentiate import derivative
+    >>> f = np.exp
+    >>> df = np.exp  # true derivative
+    >>> x = np.linspace(1, 2, 5)
+    >>> res = derivative(f, x)
+    >>> res.df  # approximation of the derivative
+    array([2.71828183, 3.49034296, 4.48168907, 5.75460268, 7.3890561 ])
+    >>> res.error  # estimate of the error
+    array([7.13740178e-12, 9.16600129e-12, 1.17594823e-11, 1.51061386e-11,
+           1.94262384e-11])
+    >>> abs(res.df - df(x))  # true error
+    array([2.53130850e-14, 3.55271368e-14, 5.77315973e-14, 5.59552404e-14,
+           6.92779167e-14])
+
+    Show the convergence of the approximation as the step size is reduced.
+    Each iteration, the step size is reduced by `step_factor`, so for
+    sufficiently small initial step, each iteration reduces the error by a
+    factor of ``1/step_factor**order`` until finite precision arithmetic
+    inhibits further improvement.
+
+    >>> import matplotlib.pyplot as plt
+    >>> iter = list(range(1, 12))  # maximum iterations
+    >>> hfac = 2  # step size reduction per iteration
+    >>> hdir = [-1, 0, 1]  # compare left-, central-, and right- steps
+    >>> order = 4  # order of differentiation formula
+    >>> x = 1
+    >>> ref = df(x)
+    >>> errors = []  # true error
+    >>> for i in iter:
+    ...     res = derivative(f, x, maxiter=i, step_factor=hfac,
+    ...                      step_direction=hdir, order=order,
+    ...                      # prevent early termination
+    ...                      tolerances=dict(atol=0, rtol=0))
+    ...     errors.append(abs(res.df - ref))
+    >>> errors = np.array(errors)
+    >>> plt.semilogy(iter, errors[:, 0], label='left differences')
+    >>> plt.semilogy(iter, errors[:, 1], label='central differences')
+    >>> plt.semilogy(iter, errors[:, 2], label='right differences')
+    >>> plt.xlabel('iteration')
+    >>> plt.ylabel('error')
+    >>> plt.legend()
+    >>> plt.show()
+    >>> (errors[1, 1] / errors[0, 1], 1 / hfac**order)
+    (0.06215223140159822, 0.0625)
+
+    The implementation is vectorized over `x`, `step_direction`, and `args`.
+    The function is evaluated once before the first iteration to perform input
+    validation and standardization, and once per iteration thereafter.
+
+    >>> def f(x, p):
+    ...     f.nit += 1
+    ...     return x**p
+    >>> f.nit = 0
+    >>> def df(x, p):
+    ...     return p*x**(p-1)
+    >>> x = np.arange(1, 5)
+    >>> p = np.arange(1, 6).reshape((-1, 1))
+    >>> hdir = np.arange(-1, 2).reshape((-1, 1, 1))
+    >>> res = derivative(f, x, args=(p,), step_direction=hdir, maxiter=1)
+    >>> np.allclose(res.df, df(x, p))
+    True
+    >>> res.df.shape
+    (3, 5, 4)
+    >>> f.nit
+    2
+
+    By default, `preserve_shape` is False, and therefore the callable
+    `f` may be called with arrays of any broadcastable shapes.
+    For example:
+
+    >>> shapes = []
+    >>> def f(x, c):
+    ...    shape = np.broadcast_shapes(x.shape, c.shape)
+    ...    shapes.append(shape)
+    ...    return np.sin(c*x)
+    >>>
+    >>> c = [1, 5, 10, 20]
+    >>> res = derivative(f, 0, args=(c,))
+    >>> shapes
+    [(4,), (4, 8), (4, 2), (3, 2), (2, 2), (1, 2)]
+
+    To understand where these shapes are coming from - and to better
+    understand how `derivative` computes accurate results - note that
+    higher values of ``c`` correspond with higher frequency sinusoids.
+    The higher frequency sinusoids make the function's derivative change
+    faster, so more function evaluations are required to achieve the target
+    accuracy:
+
+    >>> res.nfev
+    array([11, 13, 15, 17], dtype=int32)
+
+    The initial ``shape``, ``(4,)``, corresponds with evaluating the
+    function at a single abscissa and all four frequencies; this is used
+    for input validation and to determine the size and dtype of the arrays
+    that store results. The next shape corresponds with evaluating the
+    function at an initial grid of abscissae and all four frequencies.
+    Successive calls to the function evaluate the function at two more
+    abscissae, increasing the effective order of the approximation by two.
+    However, in later function evaluations, the function is evaluated at
+    fewer frequencies because the corresponding derivative has already
+    converged to the required tolerance. This saves function evaluations to
+    improve performance, but it requires the function to accept arguments of
+    any shape.
+
+    "Vector-valued" functions are unlikely to satisfy this requirement.
+    For example, consider
+
+    >>> def f(x):
+    ...    return [x, np.sin(3*x), x+np.sin(10*x), np.sin(20*x)*(x-1)**2]
+
+    This integrand is not compatible with `derivative` as written; for instance,
+    the shape of the output will not be the same as the shape of ``x``. Such a
+    function *could* be converted to a compatible form with the introduction of
+    additional parameters, but this would be inconvenient. In such cases,
+    a simpler solution would be to use `preserve_shape`.
+
+    >>> shapes = []
+    >>> def f(x):
+    ...     shapes.append(x.shape)
+    ...     x0, x1, x2, x3 = x
+    ...     return [x0, np.sin(3*x1), x2+np.sin(10*x2), np.sin(20*x3)*(x3-1)**2]
+    >>>
+    >>> x = np.zeros(4)
+    >>> res = derivative(f, x, preserve_shape=True)
+    >>> shapes
+    [(4,), (4, 8), (4, 2), (4, 2), (4, 2), (4, 2)]
+
+    Here, the shape of ``x`` is ``(4,)``. With ``preserve_shape=True``, the
+    function may be called with argument ``x`` of shape ``(4,)`` or ``(4, n)``,
+    and this is what we observe.
+
+    """
+    # TODO (followup):
+    #  - investigate behavior at saddle points
+    #  - multivariate functions?
+    #  - relative steps?
+    #  - show example of `np.vectorize`
+
+    res = _derivative_iv(f, x, args, tolerances, maxiter, order, initial_step,
+                            step_factor, step_direction, preserve_shape, callback)
+    (func, x, args, atol, rtol, maxiter, order,
+     h0, fac, hdir, preserve_shape, callback) = res
+
+    # Initialization
+    # Since f(x) (no step) is not needed for central differences, it may be
+    # possible to eliminate this function evaluation. However, it's useful for
+    # input validation and standardization, and everything else is designed to
+    # reduce function calls, so let's keep it simple.
+    temp = eim._initialize(func, (x,), args, preserve_shape=preserve_shape)
+    func, xs, fs, args, shape, dtype, xp = temp
+
+    finfo = xp.finfo(dtype)
+    atol = finfo.smallest_normal if atol is None else atol
+    rtol = finfo.eps**0.5 if rtol is None else rtol  # keep same as `hessian`
+
+    x, f = xs[0], fs[0]
+    df = xp.full_like(f, xp.nan)
+
+    # Ideally we'd broadcast the shape of `hdir` in `_elementwise_algo_init`, but
+    # it's simpler to do it here than to generalize `_elementwise_algo_init` further.
+    # `hdir` and `x` are already broadcasted in `_derivative_iv`, so we know
+    # that `hdir` can be broadcasted to the final shape. Same with `h0`.
+    hdir = xp.broadcast_to(hdir, shape)
+    hdir = xp.reshape(hdir, (-1,))
+    hdir = xp.astype(xp.sign(hdir), dtype)
+    h0 = xp.broadcast_to(h0, shape)
+    h0 = xp.reshape(h0, (-1,))
+    h0 = xp.astype(h0, dtype)
+    h0 = xpx.at(h0)[h0 <= 0].set(xp.nan)
+
+    status = xp.full_like(x, eim._EINPROGRESS, dtype=xp.int32)  # in progress
+    nit, nfev = 0, 1  # one function evaluations performed above
+    # Boolean indices of left, central, right, and (all) one-sided steps
+    il = hdir < 0
+    ic = hdir == 0
+    ir = hdir > 0
+    io = il | ir
+
+    # Most of these attributes are reasonably obvious, but:
+    # - `fs` holds all the function values of all active `x`. The zeroth
+    #   axis corresponds with active points `x`, the first axis corresponds
+    #   with the different steps (in the order described in
+    #   `_derivative_weights`).
+    # - `terms` (which could probably use a better name) is half the `order`,
+    #   which is always even.
+    work = _RichResult(x=x, df=df, fs=f[:, xp.newaxis], error=xp.nan, h=h0,
+                       df_last=xp.nan, error_last=xp.nan, fac=fac,
+                       atol=atol, rtol=rtol, nit=nit, nfev=nfev,
+                       status=status, dtype=dtype, terms=(order+1)//2,
+                       hdir=hdir, il=il, ic=ic, ir=ir, io=io,
+                       # Store the weights in an object so they can't get compressed
+                       # Using RichResult to allow dot notation, but a dict would work
+                       diff_state=_RichResult(central=[], right=[], fac=None))
+
+    # This is the correspondence between terms in the `work` object and the
+    # final result. In this case, the mapping is trivial. Note that `success`
+    # is prepended automatically.
+    res_work_pairs = [('status', 'status'), ('df', 'df'), ('error', 'error'),
+                      ('nit', 'nit'), ('nfev', 'nfev'), ('x', 'x')]
+
+    def pre_func_eval(work):
+        """Determine the abscissae at which the function needs to be evaluated.
+
+        See `_derivative_weights` for a description of the stencil (pattern
+        of the abscissae).
+
+        In the first iteration, there is only one stored function value in
+        `work.fs`, `f(x)`, so we need to evaluate at `order` new points. In
+        subsequent iterations, we evaluate at two new points. Note that
+        `work.x` is always flattened into a 1D array after broadcasting with
+        all `args`, so we add a new axis at the end and evaluate all point
+        in one call to the function.
+
+        For improvement:
+        - Consider measuring the step size actually taken, since ``(x + h) - x``
+          is not identically equal to `h` with floating point arithmetic.
+        - Adjust the step size automatically if `x` is too big to resolve the
+          step.
+        - We could probably save some work if there are no central difference
+          steps or no one-sided steps.
+        """
+        n = work.terms  # half the order
+        h = work.h[:, xp.newaxis]  # step size
+        c = work.fac  # step reduction factor
+        d = c**0.5  # square root of step reduction factor (one-sided stencil)
+        # Note - no need to be careful about dtypes until we allocate `x_eval`
+
+        if work.nit == 0:
+            hc = h / c**xp.arange(n, dtype=work.dtype)
+            hc = xp.concat((-xp.flip(hc, axis=-1), hc), axis=-1)
+        else:
+            hc = xp.concat((-h, h), axis=-1) / c**(n-1)
+
+        if work.nit == 0:
+            hr = h / d**xp.arange(2*n, dtype=work.dtype)
+        else:
+            hr = xp.concat((h, h/d), axis=-1) / c**(n-1)
+
+        n_new = 2*n if work.nit == 0 else 2  # number of new abscissae
+        x_eval = xp.zeros((work.hdir.shape[0], n_new), dtype=work.dtype)
+        il, ic, ir = work.il, work.ic, work.ir
+        x_eval = xpx.at(x_eval)[ir].set(work.x[ir][:, xp.newaxis] + hr[ir])
+        x_eval = xpx.at(x_eval)[ic].set(work.x[ic][:, xp.newaxis] + hc[ic])
+        x_eval = xpx.at(x_eval)[il].set(work.x[il][:, xp.newaxis] - hr[il])
+        return x_eval
+
+    def post_func_eval(x, f, work):
+        """ Estimate the derivative and error from the function evaluations
+
+        As in `pre_func_eval`: in the first iteration, there is only one stored
+        function value in `work.fs`, `f(x)`, so we need to add the `order` new
+        points. In subsequent iterations, we add two new points. The tricky
+        part is getting the order to match that of the weights, which is
+        described in `_derivative_weights`.
+
+        For improvement:
+        - Change the order of the weights (and steps in `pre_func_eval`) to
+          simplify `work_fc` concatenation and eliminate `fc` concatenation.
+        - It would be simple to do one-step Richardson extrapolation with `df`
+          and `df_last` to increase the order of the estimate and/or improve
+          the error estimate.
+        - Process the function evaluations in a more numerically favorable
+          way. For instance, combining the pairs of central difference evals
+          into a second-order approximation and using Richardson extrapolation
+          to produce a higher order approximation seemed to retain accuracy up
+          to very high order.
+        - Alternatively, we could use `polyfit` like Jacobi. An advantage of
+          fitting polynomial to more points than necessary is improved noise
+          tolerance.
+        """
+        n = work.terms
+        n_new = n if work.nit == 0 else 1
+        il, ic, io = work.il, work.ic, work.io
+
+        # Central difference
+        # `work_fc` is *all* the points at which the function has been evaluated
+        # `fc` is the points we're using *this iteration* to produce the estimate
+        work_fc = (f[ic][:, :n_new], work.fs[ic], f[ic][:, -n_new:])
+        work_fc = xp.concat(work_fc, axis=-1)
+        if work.nit == 0:
+            fc = work_fc
+        else:
+            fc = (work_fc[:, :n], work_fc[:, n:n+1], work_fc[:, -n:])
+            fc = xp.concat(fc, axis=-1)
+
+        # One-sided difference
+        work_fo = xp.concat((work.fs[io], f[io]), axis=-1)
+        if work.nit == 0:
+            fo = work_fo
+        else:
+            fo = xp.concat((work_fo[:, 0:1], work_fo[:, -2*n:]), axis=-1)
+
+        work.fs = xp.zeros((ic.shape[0], work.fs.shape[-1] + 2*n_new), dtype=work.dtype)
+        work.fs = xpx.at(work.fs)[ic].set(work_fc)
+        work.fs = xpx.at(work.fs)[io].set(work_fo)
+
+        wc, wo = _derivative_weights(work, n, xp)
+        work.df_last = xp.asarray(work.df, copy=True)
+        work.df = xpx.at(work.df)[ic].set(fc @ wc / work.h[ic])
+        work.df = xpx.at(work.df)[io].set(fo @ wo / work.h[io])
+        work.df = xpx.at(work.df)[il].multiply(-1)
+
+        work.h /= work.fac
+        work.error_last = work.error
+        # Simple error estimate - the difference in derivative estimates between
+        # this iteration and the last. This is typically conservative because if
+        # convergence has begin, the true error is much closer to the difference
+        # between the current estimate and the *next* error estimate. However,
+        # we could use Richarson extrapolation to produce an error estimate that
+        # is one order higher, and take the difference between that and
+        # `work.df` (which would just be constant factor that depends on `fac`.)
+        work.error = xp.abs(work.df - work.df_last)
+
+    def check_termination(work):
+        """Terminate due to convergence, non-finite values, or error increase"""
+        stop = xp.astype(xp.zeros_like(work.df), xp.bool)
+
+        i = work.error < work.atol + work.rtol*abs(work.df)
+        work.status = xpx.at(work.status)[i].set(eim._ECONVERGED)
+        stop = xpx.at(stop)[i].set(True)
+
+        if work.nit > 0:
+            i = ~((xp.isfinite(work.x) & xp.isfinite(work.df)) | stop)
+            work.df = xpx.at(work.df)[i].set(xp.nan)
+            work.status = xpx.at(work.status)[i].set(eim._EVALUEERR)
+            stop = xpx.at(stop)[i].set(True)
+
+        # With infinite precision, there is a step size below which
+        # all smaller step sizes will reduce the error. But in floating point
+        # arithmetic, catastrophic cancellation will begin to cause the error
+        # to increase again. This heuristic tries to avoid step sizes that are
+        # too small. There may be more theoretically sound approaches for
+        # detecting a step size that minimizes the total error, but this
+        # heuristic seems simple and effective.
+        i = (work.error > work.error_last*10) & ~stop
+        work.status = xpx.at(work.status)[i].set(_EERRORINCREASE)
+        stop = xpx.at(stop)[i].set(True)
+
+        return stop
+
+    def post_termination_check(work):
+        return
+
+    def customize_result(res, shape):
+        return shape
+
+    return eim._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)
+
+
+def _derivative_weights(work, n, xp):
+    # This produces the weights of the finite difference formula for a given
+    # stencil. In experiments, use of a second-order central difference formula
+    # with Richardson extrapolation was more accurate numerically, but it was
+    # more complicated, and it would have become even more complicated when
+    # adding support for one-sided differences. However, now that all the
+    # function evaluation values are stored, they can be processed in whatever
+    # way is desired to produce the derivative estimate. We leave alternative
+    # approaches to future work. To be more self-contained, here is the theory
+    # for deriving the weights below.
+    #
+    # Recall that the Taylor expansion of a univariate, scalar-values function
+    # about a point `x` may be expressed as:
+    #      f(x + h)  =     f(x) + f'(x)*h + f''(x)/2!*h**2  + O(h**3)
+    # Suppose we evaluate f(x), f(x+h), and f(x-h).  We have:
+    #      f(x)      =     f(x)
+    #      f(x + h)  =     f(x) + f'(x)*h + f''(x)/2!*h**2  + O(h**3)
+    #      f(x - h)  =     f(x) - f'(x)*h + f''(x)/2!*h**2  + O(h**3)
+    # We can solve for weights `wi` such that:
+    #   w1*f(x)      = w1*(f(x))
+    # + w2*f(x + h)  = w2*(f(x) + f'(x)*h + f''(x)/2!*h**2) + O(h**3)
+    # + w3*f(x - h)  = w3*(f(x) - f'(x)*h + f''(x)/2!*h**2) + O(h**3)
+    #                =     0    + f'(x)*h + 0               + O(h**3)
+    # Then
+    #     f'(x) ~ (w1*f(x) + w2*f(x+h) + w3*f(x-h))/h
+    # is a finite difference derivative approximation with error O(h**2),
+    # and so it is said to be a "second-order" approximation. Under certain
+    # conditions (e.g. well-behaved function, `h` sufficiently small), the
+    # error in the approximation will decrease with h**2; that is, if `h` is
+    # reduced by a factor of 2, the error is reduced by a factor of 4.
+    #
+    # By default, we use eighth-order formulae. Our central-difference formula
+    # uses abscissae:
+    #   x-h/c**3, x-h/c**2, x-h/c, x-h, x, x+h, x+h/c, x+h/c**2, x+h/c**3
+    # where `c` is the step factor. (Typically, the step factor is greater than
+    # one, so the outermost points - as written above - are actually closest to
+    # `x`.) This "stencil" is chosen so that each iteration, the step can be
+    # reduced by the factor `c`, and most of the function evaluations can be
+    # reused with the new step size. For example, in the next iteration, we
+    # will have:
+    #   x-h/c**4, x-h/c**3, x-h/c**2, x-h/c, x, x+h/c, x+h/c**2, x+h/c**3, x+h/c**4
+    # We do not reuse `x-h` and `x+h` for the new derivative estimate.
+    # While this would increase the order of the formula and thus the
+    # theoretical convergence rate, it is also less stable numerically.
+    # (As noted above, there are other ways of processing the values that are
+    # more stable. Thus, even now we store `f(x-h)` and `f(x+h)` in `work.fs`
+    # to simplify future development of this sort of improvement.)
+    #
+    # The (right) one-sided formula is produced similarly using abscissae
+    #   x, x+h, x+h/d, x+h/d**2, ..., x+h/d**6, x+h/d**7, x+h/d**7
+    # where `d` is the square root of `c`. (The left one-sided formula simply
+    # uses -h.) When the step size is reduced by factor `c = d**2`, we have
+    # abscissae:
+    #   x, x+h/d**2, x+h/d**3..., x+h/d**8, x+h/d**9, x+h/d**9
+    # `d` is chosen as the square root of `c` so that the rate of the step-size
+    # reduction is the same per iteration as in the central difference case.
+    # Note that because the central difference formulas are inherently of even
+    # order, for simplicity, we use only even-order formulas for one-sided
+    # differences, too.
+
+    # It's possible for the user to specify `fac` in, say, double precision but
+    # `x` and `args` in single precision. `fac` gets converted to single
+    # precision, but we should always use double precision for the intermediate
+    # calculations here to avoid additional error in the weights.
+    fac = float(work.fac)
+
+    # Note that if the user switches back to floating point precision with
+    # `x` and `args`, then `fac` will not necessarily equal the (lower
+    # precision) cached `_derivative_weights.fac`, and the weights will
+    # need to be recalculated. This could be fixed, but it's late, and of
+    # low consequence.
+
+    diff_state = work.diff_state
+    if fac != diff_state.fac:
+        diff_state.central = []
+        diff_state.right = []
+        diff_state.fac = fac
+
+    if len(diff_state.central) != 2*n + 1:
+        # Central difference weights. Consider refactoring this; it could
+        # probably be more compact.
+        # Note: Using NumPy here is OK; we convert to xp-type at the end
+        i = np.arange(-n, n + 1)
+        p = np.abs(i) - 1.  # center point has power `p` -1, but sign `s` is 0
+        s = np.sign(i)
+
+        h = s / fac ** p
+        A = np.vander(h, increasing=True).T
+        b = np.zeros(2*n + 1)
+        b[1] = 1
+        weights = np.linalg.solve(A, b)
+
+        # Enforce identities to improve accuracy
+        weights[n] = 0
+        for i in range(n):
+            weights[-i-1] = -weights[i]
+
+        # Cache the weights. We only need to calculate them once unless
+        # the step factor changes.
+        diff_state.central = weights
+
+        # One-sided difference weights. The left one-sided weights (with
+        # negative steps) are simply the negative of the right one-sided
+        # weights, so no need to compute them separately.
+        i = np.arange(2*n + 1)
+        p = i - 1.
+        s = np.sign(i)
+
+        h = s / np.sqrt(fac) ** p
+        A = np.vander(h, increasing=True).T
+        b = np.zeros(2 * n + 1)
+        b[1] = 1
+        weights = np.linalg.solve(A, b)
+
+        diff_state.right = weights
+
+    return (xp.asarray(diff_state.central, dtype=work.dtype),
+            xp.asarray(diff_state.right, dtype=work.dtype))
+
+
+@xp_capabilities(skip_backends=[('array_api_strict', _array_api_strict_skip_reason),
+                                ('dask.array', _dask_reason)], jax_jit=False)
+def jacobian(f, x, *, tolerances=None, maxiter=10, order=8, initial_step=0.5,
+             step_factor=2.0, step_direction=0):
+    r"""Evaluate the Jacobian of a function numerically.
+
+    Parameters
+    ----------
+    f : callable
+        The function whose Jacobian is desired. The signature must be::
+
+            f(xi: ndarray) -> ndarray
+
+        where each element of ``xi`` is a finite real. If the function to be
+        differentiated accepts additional arguments, wrap it (e.g. using
+        `functools.partial` or ``lambda``) and pass the wrapped callable
+        into `jacobian`. `f` must not mutate the array ``xi``. See Notes
+        regarding vectorization and the dimensionality of the input and output.
+    x : float array_like
+        Points at which to evaluate the Jacobian. Must have at least one dimension.
+        See Notes regarding the dimensionality and vectorization.
+    tolerances : dictionary of floats, optional
+        Absolute and relative tolerances. Valid keys of the dictionary are:
+
+        - ``atol`` - absolute tolerance on the derivative
+        - ``rtol`` - relative tolerance on the derivative
+
+        Iteration will stop when ``res.error < atol + rtol * abs(res.df)``. The default
+        `atol` is the smallest normal number of the appropriate dtype, and
+        the default `rtol` is the square root of the precision of the
+        appropriate dtype.
+    maxiter : int, default: 10
+        The maximum number of iterations of the algorithm to perform. See
+        Notes.
+    order : int, default: 8
+        The (positive integer) order of the finite difference formula to be
+        used. Odd integers will be rounded up to the next even integer.
+    initial_step : float array_like, default: 0.5
+        The (absolute) initial step size for the finite difference derivative
+        approximation. Must be broadcastable with `x` and `step_direction`.
+    step_factor : float, default: 2.0
+        The factor by which the step size is *reduced* in each iteration; i.e.
+        the step size in iteration 1 is ``initial_step/step_factor``. If
+        ``step_factor < 1``, subsequent steps will be greater than the initial
+        step; this may be useful if steps smaller than some threshold are
+        undesirable (e.g. due to subtractive cancellation error).
+    step_direction : integer array_like
+        An array representing the direction of the finite difference steps (e.g.
+        for use when `x` lies near to the boundary of the domain of the function.)
+        Must be broadcastable with `x` and `initial_step`.
+        Where 0 (default), central differences are used; where negative (e.g.
+        -1), steps are non-positive; and where positive (e.g. 1), all steps are
+        non-negative.
+
+    Returns
+    -------
+    res : _RichResult
+        An object similar to an instance of `scipy.optimize.OptimizeResult` with the
+        following attributes. The descriptions are written as though the values will
+        be scalars; however, if `f` returns an array, the outputs will be
+        arrays of the same shape.
+
+        success : bool array
+            ``True`` where the algorithm terminated successfully (status ``0``);
+            ``False`` otherwise.
+        status : int array
+            An integer representing the exit status of the algorithm.
+
+            - ``0`` : The algorithm converged to the specified tolerances.
+            - ``-1`` : The error estimate increased, so iteration was terminated.
+            - ``-2`` : The maximum number of iterations was reached.
+            - ``-3`` : A non-finite value was encountered.
+
+        df : float array
+            The Jacobian of `f` at `x`, if the algorithm terminated
+            successfully.
+        error : float array
+            An estimate of the error: the magnitude of the difference between
+            the current estimate of the Jacobian and the estimate in the
+            previous iteration.
+        nit : int array
+            The number of iterations of the algorithm that were performed.
+        nfev : int array
+            The number of points at which `f` was evaluated.
+
+        Each element of an attribute is associated with the corresponding
+        element of `df`. For instance, element ``i`` of `nfev` is the
+        number of points at which `f` was evaluated for the sake of
+        computing element ``i`` of `df`.
+
+    See Also
+    --------
+    derivative, hessian
+
+    Notes
+    -----
+    Suppose we wish to evaluate the Jacobian of a function
+    :math:`f: \mathbf{R}^m \rightarrow \mathbf{R}^n`. Assign to variables
+    ``m`` and ``n`` the positive integer values of :math:`m` and :math:`n`,
+    respectively, and let ``...`` represent an arbitrary tuple of integers.
+    If we wish to evaluate the Jacobian at a single point, then:
+
+    - argument `x` must be an array of shape ``(m,)``
+    - argument `f` must be vectorized to accept an array of shape ``(m, ...)``.
+      The first axis represents the :math:`m` inputs of :math:`f`; the remainder
+      are for evaluating the function at multiple points in a single call.
+    - argument `f` must return an array of shape ``(n, ...)``. The first
+      axis represents the :math:`n` outputs of :math:`f`; the remainder
+      are for the result of evaluating the function at multiple points.
+    - attribute ``df`` of the result object will be an array of shape ``(n, m)``,
+      the Jacobian.
+
+    This function is also vectorized in the sense that the Jacobian can be
+    evaluated at ``k`` points in a single call. In this case, `x` would be an
+    array of shape ``(m, k)``, `f` would accept an array of shape
+    ``(m, k, ...)`` and return an array of shape ``(n, k, ...)``, and the ``df``
+    attribute of the result would have shape ``(n, m, k)``.
+
+    Suppose the desired callable ``f_not_vectorized`` is not vectorized; it can
+    only accept an array of shape ``(m,)``. A simple solution to satisfy the required
+    interface is to wrap ``f_not_vectorized`` as follows::
+
+        def f(x):
+            return np.apply_along_axis(f_not_vectorized, axis=0, arr=x)
+
+    Alternatively, suppose the desired callable ``f_vec_q`` is vectorized, but
+    only for 2-D arrays of shape ``(m, q)``. To satisfy the required interface,
+    consider::
+
+        def f(x):
+            m, batch = x.shape[0], x.shape[1:]  # x.shape is (m, ...)
+            x = np.reshape(x, (m, -1))  # `-1` is short for q = prod(batch)
+            res = f_vec_q(x)  # pass shape (m, q) to function
+            n = res.shape[0]
+            return np.reshape(res, (n,) + batch)  # return shape (n, ...)
+
+    Then pass the wrapped callable ``f`` as the first argument of `jacobian`.
+
+    References
+    ----------
+    .. [1] Jacobian matrix and determinant, *Wikipedia*,
+           https://en.wikipedia.org/wiki/Jacobian_matrix_and_determinant
+
+    Examples
+    --------
+    The Rosenbrock function maps from :math:`\mathbf{R}^m \rightarrow \mathbf{R}`;
+    the SciPy implementation `scipy.optimize.rosen` is vectorized to accept an
+    array of shape ``(m, p)`` and return an array of shape ``p``. Suppose we wish
+    to evaluate the Jacobian (AKA the gradient because the function returns a scalar)
+    at ``[0.5, 0.5, 0.5]``.
+
+    >>> import numpy as np
+    >>> from scipy.differentiate import jacobian
+    >>> from scipy.optimize import rosen, rosen_der
+    >>> m = 3
+    >>> x = np.full(m, 0.5)
+    >>> res = jacobian(rosen, x)
+    >>> ref = rosen_der(x)  # reference value of the gradient
+    >>> res.df, ref
+    (array([-51.,  -1.,  50.]), array([-51.,  -1.,  50.]))
+
+    As an example of a function with multiple outputs, consider Example 4
+    from [1]_.
+
+    >>> def f(x):
+    ...     x1, x2, x3 = x
+    ...     return [x1, 5*x3, 4*x2**2 - 2*x3, x3*np.sin(x1)]
+
+    The true Jacobian is given by:
+
+    >>> def df(x):
+    ...         x1, x2, x3 = x
+    ...         one = np.ones_like(x1)
+    ...         return [[one, 0*one, 0*one],
+    ...                 [0*one, 0*one, 5*one],
+    ...                 [0*one, 8*x2, -2*one],
+    ...                 [x3*np.cos(x1), 0*one, np.sin(x1)]]
+
+    Evaluate the Jacobian at an arbitrary point.
+
+    >>> rng = np.random.default_rng(389252938452)
+    >>> x = rng.random(size=3)
+    >>> res = jacobian(f, x)
+    >>> ref = df(x)
+    >>> res.df.shape == (4, 3)
+    True
+    >>> np.allclose(res.df, ref)
+    True
+
+    Evaluate the Jacobian at 10 arbitrary points in a single call.
+
+    >>> x = rng.random(size=(3, 10))
+    >>> res = jacobian(f, x)
+    >>> ref = df(x)
+    >>> res.df.shape == (4, 3, 10)
+    True
+    >>> np.allclose(res.df, ref)
+    True
+
+    """
+    xp = array_namespace(x)
+    x0 = xp_promote(x, force_floating=True, xp=xp)
+
+    if x0.ndim < 1:
+        message = "Argument `x` must be at least 1-D."
+        raise ValueError(message)
+
+    m = x0.shape[0]
+    i = xp.arange(m)
+
+    def wrapped(x):
+        p = () if x.ndim == x0.ndim else (x.shape[-1],)  # number of abscissae
+
+        new_shape = (m, m) + x0.shape[1:] + p
+        xph = xp.expand_dims(x0, axis=1)
+        if x.ndim != x0.ndim:
+            xph = xp.expand_dims(xph, axis=-1)
+        xph = xp_copy(xp.broadcast_to(xph, new_shape), xp=xp)
+        xph = xpx.at(xph)[i, i].set(x)
+        return f(xph)
+
+    res = derivative(wrapped, x, tolerances=tolerances,
+                     maxiter=maxiter, order=order, initial_step=initial_step,
+                     step_factor=step_factor, preserve_shape=True,
+                     step_direction=step_direction)
+
+    del res.x  # the user knows `x`, and the way it gets broadcasted is meaningless here
+    return res
+
+
+@xp_capabilities(skip_backends=[('array_api_strict', _array_api_strict_skip_reason),
+                                ('dask.array', _dask_reason)], jax_jit=False)
+def hessian(f, x, *, tolerances=None, maxiter=10,
+            order=8, initial_step=0.5, step_factor=2.0):
+    r"""Evaluate the Hessian of a function numerically.
+
+    Parameters
+    ----------
+    f : callable
+        The function whose Hessian is desired. The signature must be::
+
+            f(xi: ndarray) -> ndarray
+
+        where each element of ``xi`` is a finite real. If the function to be
+        differentiated accepts additional arguments, wrap it (e.g. using
+        `functools.partial` or ``lambda``) and pass the wrapped callable
+        into `hessian`. `f` must not mutate the array ``xi``. See Notes
+        regarding vectorization and the dimensionality of the input and output.
+    x : float array_like
+        Points at which to evaluate the Hessian. Must have at least one dimension.
+        See Notes regarding the dimensionality and vectorization.
+    tolerances : dictionary of floats, optional
+        Absolute and relative tolerances. Valid keys of the dictionary are:
+
+        - ``atol`` - absolute tolerance on the derivative
+        - ``rtol`` - relative tolerance on the derivative
+
+        Iteration will stop when ``res.error < atol + rtol * abs(res.df)``. The default
+        `atol` is the smallest normal number of the appropriate dtype, and
+        the default `rtol` is the square root of the precision of the
+        appropriate dtype.
+    order : int, default: 8
+        The (positive integer) order of the finite difference formula to be
+        used. Odd integers will be rounded up to the next even integer.
+    initial_step : float, default: 0.5
+        The (absolute) initial step size for the finite difference derivative
+        approximation.
+    step_factor : float, default: 2.0
+        The factor by which the step size is *reduced* in each iteration; i.e.
+        the step size in iteration 1 is ``initial_step/step_factor``. If
+        ``step_factor < 1``, subsequent steps will be greater than the initial
+        step; this may be useful if steps smaller than some threshold are
+        undesirable (e.g. due to subtractive cancellation error).
+    maxiter : int, default: 10
+        The maximum number of iterations of the algorithm to perform. See
+        Notes.
+
+    Returns
+    -------
+    res : _RichResult
+        An object similar to an instance of `scipy.optimize.OptimizeResult` with the
+        following attributes. The descriptions are written as though the values will
+        be scalars; however, if `f` returns an array, the outputs will be
+        arrays of the same shape.
+
+        success : bool array
+            ``True`` where the algorithm terminated successfully (status ``0``);
+            ``False`` otherwise.
+        status : int array
+            An integer representing the exit status of the algorithm.
+
+            - ``0`` : The algorithm converged to the specified tolerances.
+            - ``-1`` : The error estimate increased, so iteration was terminated.
+            - ``-2`` : The maximum number of iterations was reached.
+            - ``-3`` : A non-finite value was encountered.
+
+        ddf : float array
+            The Hessian of `f` at `x`, if the algorithm terminated
+            successfully.
+        error : float array
+            An estimate of the error: the magnitude of the difference between
+            the current estimate of the Hessian and the estimate in the
+            previous iteration.
+        nfev : int array
+            The number of points at which `f` was evaluated.
+
+        Each element of an attribute is associated with the corresponding
+        element of `ddf`. For instance, element ``[i, j]`` of `nfev` is the
+        number of points at which `f` was evaluated for the sake of
+        computing element ``[i, j]`` of `ddf`.
+
+    See Also
+    --------
+    derivative, jacobian
+
+    Notes
+    -----
+    Suppose we wish to evaluate the Hessian of a function
+    :math:`f: \mathbf{R}^m \rightarrow \mathbf{R}`, and we assign to variable
+    ``m`` the positive integer value of :math:`m`. If we wish to evaluate
+    the Hessian at a single point, then:
+
+    - argument `x` must be an array of shape ``(m,)``
+    - argument `f` must be vectorized to accept an array of shape
+      ``(m, ...)``. The first axis represents the :math:`m` inputs of
+      :math:`f`; the remaining axes indicated by ellipses are for evaluating
+      the function at several abscissae in a single call.
+    - argument `f` must return an array of shape ``(...)``.
+    - attribute ``dff`` of the result object will be an array of shape ``(m, m)``,
+      the Hessian.
+
+    This function is also vectorized in the sense that the Hessian can be
+    evaluated at ``k`` points in a single call. In this case, `x` would be an
+    array of shape ``(m, k)``, `f` would accept an array of shape
+    ``(m, ...)`` and return an array of shape ``(...)``, and the ``ddf``
+    attribute of the result would have shape ``(m, m, k)``. Note that the
+    axis associated with the ``k`` points is included within the axes
+    denoted by ``(...)``.
+
+    Currently, `hessian` is implemented by nesting calls to `jacobian`.
+    All options passed to `hessian` are used for both the inner and outer
+    calls with one exception: the `rtol` used in the inner `jacobian` call
+    is tightened by a factor of 100 with the expectation that the inner
+    error can be ignored. A consequence is that `rtol` should not be set
+    less than 100 times the precision of the dtype of `x`; a warning is
+    emitted otherwise.
+
+    References
+    ----------
+    .. [1] Hessian matrix, *Wikipedia*,
+           https://en.wikipedia.org/wiki/Hessian_matrix
+
+    Examples
+    --------
+    The Rosenbrock function maps from :math:`\mathbf{R}^m \rightarrow \mathbf{R}`;
+    the SciPy implementation `scipy.optimize.rosen` is vectorized to accept an
+    array of shape ``(m, ...)`` and return an array of shape ``...``. Suppose we
+    wish to evaluate the Hessian at ``[0.5, 0.5, 0.5]``.
+
+    >>> import numpy as np
+    >>> from scipy.differentiate import hessian
+    >>> from scipy.optimize import rosen, rosen_hess
+    >>> m = 3
+    >>> x = np.full(m, 0.5)
+    >>> res = hessian(rosen, x)
+    >>> ref = rosen_hess(x)  # reference value of the Hessian
+    >>> np.allclose(res.ddf, ref)
+    True
+
+    `hessian` is vectorized to evaluate the Hessian at multiple points
+    in a single call.
+
+    >>> rng = np.random.default_rng(4589245925010)
+    >>> x = rng.random((m, 10))
+    >>> res = hessian(rosen, x)
+    >>> ref = [rosen_hess(xi) for xi in x.T]
+    >>> ref = np.moveaxis(ref, 0, -1)
+    >>> np.allclose(res.ddf, ref)
+    True
+
+    """
+    # todo:
+    # - add ability to vectorize over additional parameters (*args?)
+    # - error estimate stack with inner jacobian (or use legit 2D stencil)
+
+    kwargs = dict(maxiter=maxiter, order=order, initial_step=initial_step,
+                  step_factor=step_factor)
+    tolerances = {} if tolerances is None else tolerances
+    atol = tolerances.get('atol', None)
+    rtol = tolerances.get('rtol', None)
+
+    xp = array_namespace(x)
+    x0 = xp_promote(x, force_floating=True, xp=xp)
+
+    finfo = xp.finfo(x0.dtype)
+    rtol = finfo.eps**0.5 if rtol is None else rtol  # keep same as `derivative`
+
+    # tighten the inner tolerance to make the inner error negligible
+    rtol_min = finfo.eps * 100
+    message = (f"The specified `{rtol=}`, but error estimates are likely to be "
+               f"unreliable when `rtol < {rtol_min}`.")
+    if 0 < rtol < rtol_min:  # rtol <= 0 is an error
+        warnings.warn(message, RuntimeWarning, stacklevel=2)
+        rtol = rtol_min
+
+    def df(x):
+        tolerances = dict(rtol=rtol/100, atol=atol)
+        temp = jacobian(f, x, tolerances=tolerances, **kwargs)
+        nfev.append(temp.nfev if len(nfev) == 0 else temp.nfev.sum(axis=-1))
+        return temp.df
+
+    nfev = []  # track inner function evaluations
+    res = jacobian(df, x, tolerances=tolerances, **kwargs)  # jacobian of jacobian
+
+    nfev = xp.cumulative_sum(xp.stack(nfev), axis=0)
+    res_nit = xp.astype(res.nit[xp.newaxis, ...], xp.int64)  # appease torch
+    res.nfev = xp.take_along_axis(nfev, res_nit, axis=0)[0]
+    res.ddf = res.df
+    del res.df  # this is renamed to ddf
+    del res.nit  # this is only the outer-jacobian nit
+
+    return res
diff --git a/.venv/lib/python3.11/site-packages/scipy/differentiate/tests/__init__.py b/.venv/lib/python3.11/site-packages/scipy/differentiate/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/.venv/lib/python3.11/site-packages/scipy/differentiate/tests/test_differentiate.py b/.venv/lib/python3.11/site-packages/scipy/differentiate/tests/test_differentiate.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a53aabe5f7a44cc1fd6e2be11d0462c2124adf7
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/differentiate/tests/test_differentiate.py
@@ -0,0 +1,690 @@
+import math
+import pytest
+
+import numpy as np
+
+import scipy._lib._elementwise_iterative_method as eim
+import scipy._lib.array_api_extra as xpx
+from scipy._lib._array_api_no_0d import xp_assert_close, xp_assert_equal, xp_assert_less
+from scipy._lib._array_api import is_numpy, is_torch, make_xp_test_case
+
+from scipy import stats, optimize, special
+from scipy.differentiate import derivative, jacobian, hessian
+from scipy.differentiate._differentiate import _EERRORINCREASE
+
+
+
+@make_xp_test_case(derivative)
+class TestDerivative:
+
+    def f(self, x):
+        return special.ndtr(x)
+
+    @pytest.mark.parametrize('x', [0.6, np.linspace(-0.05, 1.05, 10)])
+    def test_basic(self, x, xp):
+        # Invert distribution CDF and compare against distribution `ppf`
+        default_dtype = xp.asarray(1.).dtype
+        res = derivative(self.f, xp.asarray(x, dtype=default_dtype))
+        ref = xp.asarray(stats.norm().pdf(x), dtype=default_dtype)
+        xp_assert_close(res.df, ref)
+        # This would be nice, but doesn't always work out. `error` is an
+        # estimate, not a bound.
+        if not is_torch(xp):
+            xp_assert_less(xp.abs(res.df - ref), res.error)
+
+    @pytest.mark.parametrize('case', stats._distr_params.distcont)
+    def test_accuracy(self, case):
+        distname, params = case
+        dist = getattr(stats, distname)(*params)
+        x = dist.median() + 0.1
+        res = derivative(dist.cdf, x)
+        ref = dist.pdf(x)
+        xp_assert_close(res.df, ref, atol=1e-10)
+
+    @pytest.mark.parametrize('order', [1, 6])
+    @pytest.mark.parametrize('shape', [tuple(), (12,), (3, 4), (3, 2, 2)])
+    def test_vectorization(self, order, shape, xp):
+        # Test for correct functionality, output shapes, and dtypes for various
+        # input shapes.
+        x = np.linspace(-0.05, 1.05, 12).reshape(shape) if shape else 0.6
+        n = np.size(x)
+        state = {}
+
+        @np.vectorize
+        def _derivative_single(x):
+            return derivative(self.f, x, order=order)
+
+        def f(x, *args, **kwargs):
+            state['nit'] += 1
+            state['feval'] += 1 if (x.size == n or x.ndim <=1) else x.shape[-1]
+            return self.f(x, *args, **kwargs)
+
+        state['nit'] = -1
+        state['feval'] = 0
+
+        res = derivative(f, xp.asarray(x, dtype=xp.float64), order=order)
+        refs = _derivative_single(x).ravel()
+
+        ref_x = [ref.x for ref in refs]
+        xp_assert_close(xp.reshape(res.x, (-1,)), xp.asarray(ref_x))
+
+        ref_df = [ref.df for ref in refs]
+        xp_assert_close(xp.reshape(res.df, (-1,)), xp.asarray(ref_df))
+
+        ref_error = [ref.error for ref in refs]
+        xp_assert_close(xp.reshape(res.error, (-1,)), xp.asarray(ref_error),
+                        atol=1e-12)
+
+        ref_success = [bool(ref.success) for ref in refs]
+        xp_assert_equal(xp.reshape(res.success, (-1,)), xp.asarray(ref_success))
+
+        ref_flag = [np.int32(ref.status) for ref in refs]
+        xp_assert_equal(xp.reshape(res.status, (-1,)), xp.asarray(ref_flag))
+
+        ref_nfev = [np.int32(ref.nfev) for ref in refs]
+        xp_assert_equal(xp.reshape(res.nfev, (-1,)), xp.asarray(ref_nfev))
+        if is_numpy(xp):  # can't expect other backends to be exactly the same
+            assert xp.max(res.nfev) == state['feval']
+
+        ref_nit = [np.int32(ref.nit) for ref in refs]
+        xp_assert_equal(xp.reshape(res.nit, (-1,)), xp.asarray(ref_nit))
+        if is_numpy(xp):  # can't expect other backends to be exactly the same
+            assert xp.max(res.nit) == state['nit']
+
+    def test_flags(self, xp):
+        # Test cases that should produce different status flags; show that all
+        # can be produced simultaneously.
+        rng = np.random.default_rng(5651219684984213)
+        def f(xs, js):
+            f.nit += 1
+            funcs = [lambda x: x - 2.5,  # converges
+                     lambda x: xp.exp(x)*rng.random(),  # error increases
+                     lambda x: xp.exp(x),  # reaches maxiter due to order=2
+                     lambda x: xp.full_like(x, xp.nan)]  # stops due to NaN
+            res = [funcs[int(j)](x) for x, j in zip(xs, xp.reshape(js, (-1,)))]
+            return xp.stack(res)
+        f.nit = 0
+
+        args = (xp.arange(4, dtype=xp.int64),)
+        res = derivative(f, xp.ones(4, dtype=xp.float64),
+                         tolerances=dict(rtol=1e-14),
+                         order=2, args=args)
+
+        ref_flags = xp.asarray([eim._ECONVERGED,
+                                _EERRORINCREASE,
+                                eim._ECONVERR,
+                                eim._EVALUEERR], dtype=xp.int32)
+        xp_assert_equal(res.status, ref_flags)
+
+    def test_flags_preserve_shape(self, xp):
+        # Same test as above but using `preserve_shape` option to simplify.
+        rng = np.random.default_rng(5651219684984213)
+        def f(x):
+            out = [x - 2.5,  # converges
+                   xp.exp(x)*rng.random(),  # error increases
+                   xp.exp(x),  # reaches maxiter due to order=2
+                   xp.full_like(x, xp.nan)]  # stops due to NaN
+            return xp.stack(out)
+
+        res = derivative(f, xp.asarray(1, dtype=xp.float64),
+                         tolerances=dict(rtol=1e-14),
+                         order=2, preserve_shape=True)
+
+        ref_flags = xp.asarray([eim._ECONVERGED,
+                                _EERRORINCREASE,
+                                eim._ECONVERR,
+                                eim._EVALUEERR], dtype=xp.int32)
+        xp_assert_equal(res.status, ref_flags)
+
+    def test_preserve_shape(self, xp):
+        # Test `preserve_shape` option
+        def f(x):
+            out = [x, xp.sin(3*x), x+xp.sin(10*x), xp.sin(20*x)*(x-1)**2]
+            return xp.stack(out)
+
+        x = xp.asarray(0.)
+        ref = xp.asarray([xp.asarray(1), 3*xp.cos(3*x), 1+10*xp.cos(10*x),
+                          20*xp.cos(20*x)*(x-1)**2 + 2*xp.sin(20*x)*(x-1)])
+        res = derivative(f, x, preserve_shape=True)
+        xp_assert_close(res.df, ref)
+
+    def test_convergence(self, xp):
+        # Test that the convergence tolerances behave as expected
+        x = xp.asarray(1., dtype=xp.float64)
+        f = special.ndtr
+        ref = float(stats.norm.pdf(1.))
+        tolerances0 = dict(atol=0, rtol=0)
+
+        tolerances = tolerances0.copy()
+        tolerances['atol'] = 1e-3
+        res1 = derivative(f, x, tolerances=tolerances, order=4)
+        assert abs(res1.df - ref) < 1e-3
+        tolerances['atol'] = 1e-6
+        res2 = derivative(f, x, tolerances=tolerances, order=4)
+        assert abs(res2.df - ref) < 1e-6
+        assert abs(res2.df - ref) < abs(res1.df - ref)
+
+        tolerances = tolerances0.copy()
+        tolerances['rtol'] = 1e-3
+        res1 = derivative(f, x, tolerances=tolerances, order=4)
+        assert abs(res1.df - ref) < 1e-3 * ref
+        tolerances['rtol'] = 1e-6
+        res2 = derivative(f, x, tolerances=tolerances, order=4)
+        assert abs(res2.df - ref) < 1e-6 * ref
+        assert abs(res2.df - ref) < abs(res1.df - ref)
+
+    def test_step_parameters(self, xp):
+        # Test that step factors have the expected effect on accuracy
+        x = xp.asarray(1., dtype=xp.float64)
+        f = special.ndtr
+        ref = float(stats.norm.pdf(1.))
+
+        res1 = derivative(f, x, initial_step=0.5, maxiter=1)
+        res2 = derivative(f, x, initial_step=0.05, maxiter=1)
+        assert abs(res2.df - ref) < abs(res1.df - ref)
+
+        res1 = derivative(f, x, step_factor=2, maxiter=1)
+        res2 = derivative(f, x, step_factor=20, maxiter=1)
+        assert abs(res2.df - ref) < abs(res1.df - ref)
+
+        # `step_factor` can be less than 1: `initial_step` is the minimum step
+        kwargs = dict(order=4, maxiter=1, step_direction=0)
+        res = derivative(f, x, initial_step=0.5, step_factor=0.5, **kwargs)
+        ref = derivative(f, x, initial_step=1, step_factor=2, **kwargs)
+        xp_assert_close(res.df, ref.df, rtol=5e-15)
+
+        # This is a similar test for one-sided difference
+        kwargs = dict(order=2, maxiter=1, step_direction=1)
+        res = derivative(f, x, initial_step=1, step_factor=2, **kwargs)
+        ref = derivative(f, x, initial_step=1/np.sqrt(2), step_factor=0.5, **kwargs)
+        xp_assert_close(res.df, ref.df, rtol=5e-15)
+
+        kwargs['step_direction'] = -1
+        res = derivative(f, x, initial_step=1, step_factor=2, **kwargs)
+        ref = derivative(f, x, initial_step=1/np.sqrt(2), step_factor=0.5, **kwargs)
+        xp_assert_close(res.df, ref.df, rtol=5e-15)
+
+    def test_step_direction(self, xp):
+        # test that `step_direction` works as expected
+        def f(x):
+            y = xp.exp(x)
+            y = xpx.at(y)[(x < 0) + (x > 2)].set(xp.nan)
+            return y
+
+        x = xp.linspace(0, 2, 10)
+        step_direction = xp.zeros_like(x)
+        step_direction = xpx.at(step_direction)[x < 0.6].set(1)
+        step_direction = xpx.at(step_direction)[x > 1.4].set(-1)
+        res = derivative(f, x, step_direction=step_direction)
+        xp_assert_close(res.df, xp.exp(x))
+        assert xp.all(res.success)
+
+    def test_vectorized_step_direction_args(self, xp):
+        # test that `step_direction` and `args` are vectorized properly
+        def f(x, p):
+            return x ** p
+
+        def df(x, p):
+            return p * x ** (p - 1)
+
+        x = xp.reshape(xp.asarray([1, 2, 3, 4]), (-1, 1, 1))
+        hdir = xp.reshape(xp.asarray([-1, 0, 1]), (1, -1, 1))
+        p = xp.reshape(xp.asarray([2, 3]), (1, 1, -1))
+        res = derivative(f, x, step_direction=hdir, args=(p,))
+        ref = xp.broadcast_to(df(x, p), res.df.shape)
+        ref = xp.asarray(ref, dtype=xp.asarray(1.).dtype)
+        xp_assert_close(res.df, ref)
+
+    def test_initial_step(self, xp):
+        # Test that `initial_step` works as expected and is vectorized
+        def f(x):
+            return xp.exp(x)
+
+        x = xp.asarray(0., dtype=xp.float64)
+        step_direction = xp.asarray([-1, 0, 1])
+        h0 = xp.reshape(xp.logspace(-3, 0, 10), (-1, 1))
+        res = derivative(f, x, initial_step=h0, order=2, maxiter=1,
+                         step_direction=step_direction)
+        err = xp.abs(res.df - f(x))
+
+        # error should be smaller for smaller step sizes
+        assert xp.all(err[:-1, ...] < err[1:, ...])
+
+        # results of vectorized call should match results with
+        # initial_step taken one at a time
+        for i in range(h0.shape[0]):
+            ref = derivative(f, x, initial_step=h0[i, 0], order=2, maxiter=1,
+                             step_direction=step_direction)
+            xp_assert_close(res.df[i, :], ref.df, rtol=1e-14)
+
+    def test_maxiter_callback(self, xp):
+        # Test behavior of `maxiter` parameter and `callback` interface
+        x = xp.asarray(0.612814, dtype=xp.float64)
+        maxiter = 3
+
+        def f(x):
+            res = special.ndtr(x)
+            return res
+
+        default_order = 8
+        res = derivative(f, x, maxiter=maxiter, tolerances=dict(rtol=1e-15))
+        assert not xp.any(res.success)
+        assert xp.all(res.nfev == default_order + 1 + (maxiter - 1)*2)
+        assert xp.all(res.nit == maxiter)
+
+        def callback(res):
+            callback.iter += 1
+            callback.res = res
+            assert hasattr(res, 'x')
+            assert float(res.df) not in callback.dfs
+            callback.dfs.add(float(res.df))
+            assert res.status == eim._EINPROGRESS
+            if callback.iter == maxiter:
+                raise StopIteration
+        callback.iter = -1  # callback called once before first iteration
+        callback.res = None
+        callback.dfs = set()
+
+        res2 = derivative(f, x, callback=callback, tolerances=dict(rtol=1e-15))
+        # terminating with callback is identical to terminating due to maxiter
+        # (except for `status`)
+        for key in res.keys():
+            if key == 'status':
+                assert res[key] == eim._ECONVERR
+                assert res2[key] == eim._ECALLBACK
+            elif key == 'error':
+                # switched from equality check to accommodate
+                # macosx-x86_64/Accelerate
+                xp_assert_close(res2[key], res[key], atol=1e-14)
+                xp_assert_close(callback.res[key], res[key], atol=1e-14)
+            else:
+                assert res2[key] == callback.res[key] == res[key]
+
+    @pytest.mark.parametrize("hdir", (-1, 0, 1))
+    @pytest.mark.parametrize("x", (0.65, [0.65, 0.7]))
+    @pytest.mark.parametrize("dtype", ('float32', 'float64'))
+    def test_dtype(self, hdir, x, dtype, xp):
+        # Test that dtypes are preserved
+        dtype = getattr(xp, dtype)
+        x = xp.asarray(x, dtype=dtype)
+
+        def f(x):
+            assert x.dtype == dtype
+            return xp.exp(x)
+
+        def callback(res):
+            assert res.x.dtype == dtype
+            assert res.df.dtype == dtype
+            assert res.error.dtype == dtype
+
+        res = derivative(f, x, order=4, step_direction=hdir, callback=callback)
+        assert res.x.dtype == dtype
+        assert res.df.dtype == dtype
+        assert res.error.dtype == dtype
+        eps = xp.finfo(dtype).eps
+        # not sure why torch is less accurate here; might be worth investigating
+        rtol = eps**0.5 * 50 if is_torch(xp) else eps**0.5
+        xp_assert_close(res.df, xp.exp(res.x), rtol=rtol)
+
+    def test_input_validation(self, xp):
+        # Test input validation for appropriate error messages
+        one = xp.asarray(1)
+
+        message = '`f` must be callable.'
+        with pytest.raises(ValueError, match=message):
+            derivative(None, one)
+
+        message = 'Abscissae and function output must be real numbers.'
+        with pytest.raises(ValueError, match=message):
+            derivative(lambda x: x, xp.asarray(-4+1j))
+
+        message = "When `preserve_shape=False`, the shape of the array..."
+        with pytest.raises(ValueError, match=message):
+            derivative(lambda x: [1, 2, 3], xp.asarray([-2, -3]))
+
+        message = 'Tolerances and step parameters must be non-negative...'
+        with pytest.raises(ValueError, match=message):
+            derivative(lambda x: x, one, tolerances=dict(atol=-1))
+        with pytest.raises(ValueError, match=message):
+            derivative(lambda x: x, one, tolerances=dict(rtol='ekki'))
+        with pytest.raises(ValueError, match=message):
+            derivative(lambda x: x, one, step_factor=object())
+
+        message = '`maxiter` must be a positive integer.'
+        with pytest.raises(ValueError, match=message):
+            derivative(lambda x: x, one, maxiter=1.5)
+        with pytest.raises(ValueError, match=message):
+            derivative(lambda x: x, one, maxiter=0)
+
+        message = '`order` must be a positive integer'
+        with pytest.raises(ValueError, match=message):
+            derivative(lambda x: x, one, order=1.5)
+        with pytest.raises(ValueError, match=message):
+            derivative(lambda x: x, one, order=0)
+
+        message = '`preserve_shape` must be True or False.'
+        with pytest.raises(ValueError, match=message):
+            derivative(lambda x: x, one, preserve_shape='herring')
+
+        message = '`callback` must be callable.'
+        with pytest.raises(ValueError, match=message):
+            derivative(lambda x: x, one, callback='shrubbery')
+
+    def test_special_cases(self, xp):
+        # Test edge cases and other special cases
+
+        # Test that integers are not passed to `f`
+        # (otherwise this would overflow)
+        def f(x):
+            assert xp.isdtype(x.dtype, 'real floating')
+            return x ** 99 - 1
+
+        if not is_torch(xp):  # torch defaults to float32
+            res = derivative(f, xp.asarray(7), tolerances=dict(rtol=1e-10))
+            assert res.success
+            xp_assert_close(res.df, xp.asarray(99*7.**98))
+
+        # Test invalid step size and direction
+        res = derivative(xp.exp, xp.asarray(1), step_direction=xp.nan)
+        xp_assert_equal(res.df, xp.asarray(xp.nan))
+        xp_assert_equal(res.status, xp.asarray(-3, dtype=xp.int32))
+
+        res = derivative(xp.exp, xp.asarray(1), initial_step=0)
+        xp_assert_equal(res.df, xp.asarray(xp.nan))
+        xp_assert_equal(res.status, xp.asarray(-3, dtype=xp.int32))
+
+        # Test that if success is achieved in the correct number
+        # of iterations if function is a polynomial. Ideally, all polynomials
+        # of order 0-2 would get exact result with 0 refinement iterations,
+        # all polynomials of order 3-4 would be differentiated exactly after
+        # 1 iteration, etc. However, it seems that `derivative` needs an
+        # extra iteration to detect convergence based on the error estimate.
+
+        for n in range(6):
+            x = xp.asarray(1.5, dtype=xp.float64)
+            def f(x):
+                return 2*x**n
+
+            ref = 2*n*x**(n-1)
+
+            res = derivative(f, x, maxiter=1, order=max(1, n))
+            xp_assert_close(res.df, ref, rtol=1e-15)
+            xp_assert_equal(res.error, xp.asarray(xp.nan, dtype=xp.float64))
+
+            res = derivative(f, x, order=max(1, n))
+            assert res.success
+            assert res.nit == 2
+            xp_assert_close(res.df, ref, rtol=1e-15)
+
+        # Test scalar `args` (not in tuple)
+        def f(x, c):
+            return c*x - 1
+
+        res = derivative(f, xp.asarray(2), args=xp.asarray(3))
+        xp_assert_close(res.df, xp.asarray(3.))
+
+    # no need to run a test on multiple backends if it's xfailed
+    @pytest.mark.skip_xp_backends(np_only=True)
+    @pytest.mark.xfail
+    @pytest.mark.parametrize("case", (  # function, evaluation point
+        (lambda x: (x - 1) ** 3, 1),
+        (lambda x: np.where(x > 1, (x - 1) ** 5, (x - 1) ** 3), 1)
+    ))
+    def test_saddle_gh18811(self, case, xp):
+        # With default settings, `derivative` will not always converge when
+        # the true derivative is exactly zero. This tests that specifying a
+        # (tight) `atol` alleviates the problem. See discussion in gh-18811.
+        atol = 1e-16
+        res = derivative(*case, step_direction=[-1, 0, 1], atol=atol)
+        assert np.all(res.success)
+        xp_assert_close(res.df, 0, atol=atol)
+
+
+class JacobianHessianTest:
+    def test_iv(self, xp):
+        jh_func = self.jh_func.__func__
+
+        # Test input validation
+        message = "Argument `x` must be at least 1-D."
+        with pytest.raises(ValueError, match=message):
+            jh_func(xp.sin, 1, tolerances=dict(atol=-1))
+
+        # Confirm that other parameters are being passed to `derivative`,
+        # which raises an appropriate error message.
+        x = xp.ones(3)
+        func = optimize.rosen
+        message = 'Tolerances and step parameters must be non-negative scalars.'
+        with pytest.raises(ValueError, match=message):
+            jh_func(func, x, tolerances=dict(atol=-1))
+        with pytest.raises(ValueError, match=message):
+            jh_func(func, x, tolerances=dict(rtol=-1))
+        with pytest.raises(ValueError, match=message):
+            jh_func(func, x, step_factor=-1)
+
+        message = '`order` must be a positive integer.'
+        with pytest.raises(ValueError, match=message):
+            jh_func(func, x, order=-1)
+
+        message = '`maxiter` must be a positive integer.'
+        with pytest.raises(ValueError, match=message):
+            jh_func(func, x, maxiter=-1)
+
+
+@make_xp_test_case(jacobian)
+class TestJacobian(JacobianHessianTest):
+    jh_func = jacobian
+
+    # Example functions and Jacobians from Wikipedia:
+    # https://en.wikipedia.org/wiki/Jacobian_matrix_and_determinant#Examples
+
+    def f1(z, xp):
+        x, y = z
+        return xp.stack([x ** 2 * y, 5 * x + xp.sin(y)])
+
+    def df1(z):
+        x, y = z
+        return [[2 * x * y, x ** 2], [np.full_like(x, 5), np.cos(y)]]
+
+    f1.mn = 2, 2  # type: ignore[attr-defined]
+    f1.ref = df1  # type: ignore[attr-defined]
+
+    def f2(z, xp):
+        r, phi = z
+        return xp.stack([r * xp.cos(phi), r * xp.sin(phi)])
+
+    def df2(z):
+        r, phi = z
+        return [[np.cos(phi), -r * np.sin(phi)],
+                [np.sin(phi), r * np.cos(phi)]]
+
+    f2.mn = 2, 2  # type: ignore[attr-defined]
+    f2.ref = df2  # type: ignore[attr-defined]
+
+    def f3(z, xp):
+        r, phi, th = z
+        return xp.stack([r * xp.sin(phi) * xp.cos(th), r * xp.sin(phi) * xp.sin(th),
+                         r * xp.cos(phi)])
+
+    def df3(z):
+        r, phi, th = z
+        return [[np.sin(phi) * np.cos(th), r * np.cos(phi) * np.cos(th),
+                 -r * np.sin(phi) * np.sin(th)],
+                [np.sin(phi) * np.sin(th), r * np.cos(phi) * np.sin(th),
+                 r * np.sin(phi) * np.cos(th)],
+                [np.cos(phi), -r * np.sin(phi), np.zeros_like(r)]]
+
+    f3.mn = 3, 3  # type: ignore[attr-defined]
+    f3.ref = df3  # type: ignore[attr-defined]
+
+    def f4(x, xp):
+        x1, x2, x3 = x
+        return xp.stack([x1, 5 * x3, 4 * x2 ** 2 - 2 * x3, x3 * xp.sin(x1)])
+
+    def df4(x):
+        x1, x2, x3 = x
+        one = np.ones_like(x1)
+        return [[one, 0 * one, 0 * one],
+                [0 * one, 0 * one, 5 * one],
+                [0 * one, 8 * x2, -2 * one],
+                [x3 * np.cos(x1), 0 * one, np.sin(x1)]]
+
+    f4.mn = 3, 4  # type: ignore[attr-defined]
+    f4.ref = df4  # type: ignore[attr-defined]
+
+    def f5(x, xp):
+        x1, x2, x3 = x
+        return xp.stack([5 * x2, 4 * x1 ** 2 - 2 * xp.sin(x2 * x3), x2 * x3])
+
+    def df5(x):
+        x1, x2, x3 = x
+        one = np.ones_like(x1)
+        return [[0 * one, 5 * one, 0 * one],
+                [8 * x1, -2 * x3 * np.cos(x2 * x3), -2 * x2 * np.cos(x2 * x3)],
+                [0 * one, x3, x2]]
+
+    f5.mn = 3, 3  # type: ignore[attr-defined]
+    f5.ref = df5  # type: ignore[attr-defined]
+
+    def rosen(x, _): return optimize.rosen(x)
+    rosen.mn = 5, 1  # type: ignore[attr-defined]
+    rosen.ref = optimize.rosen_der  # type: ignore[attr-defined]
+
+    @pytest.mark.parametrize('dtype', ('float32', 'float64'))
+    @pytest.mark.parametrize('size', [(), (6,), (2, 3)])
+    @pytest.mark.parametrize('func', [f1, f2, f3, f4, f5, rosen])
+    def test_examples(self, dtype, size, func, xp):
+        atol = 1e-10 if dtype == 'float64' else 1.99e-3
+        dtype = getattr(xp, dtype)
+        rng = np.random.default_rng(458912319542)
+        m, n = func.mn
+        x = rng.random(size=(m,) + size)
+        res = jacobian(lambda x: func(x , xp), xp.asarray(x, dtype=dtype))
+        # convert list of arrays to single array before converting to xp array
+        ref = xp.asarray(np.asarray(func.ref(x)), dtype=dtype)
+        xp_assert_close(res.df, ref, atol=atol)
+
+    def test_attrs(self, xp):
+        # Test attributes of result object
+        z = xp.asarray([0.5, 0.25])
+
+        # case in which some elements of the Jacobian are harder
+        # to calculate than others
+        def df1(z):
+            x, y = z
+            return xp.stack([xp.cos(0.5*x) * xp.cos(y), xp.sin(2*x) * y**2])
+
+        def df1_0xy(x, y):
+            return xp.cos(0.5*x) * xp.cos(y)
+
+        def df1_1xy(x, y):
+            return xp.sin(2*x) * y**2
+
+        res = jacobian(df1, z, initial_step=10)
+        # FIXME https://github.com/scipy/scipy/pull/22320#discussion_r1914898175
+        if not is_torch(xp):
+            assert xpx.nunique(res.nit) == 4
+            assert xpx.nunique(res.nfev) == 4
+
+        res00 = jacobian(lambda x: df1_0xy(x, z[1]), z[0:1], initial_step=10)
+        res01 = jacobian(lambda y: df1_0xy(z[0], y), z[1:2], initial_step=10)
+        res10 = jacobian(lambda x: df1_1xy(x, z[1]), z[0:1], initial_step=10)
+        res11 = jacobian(lambda y: df1_1xy(z[0], y), z[1:2], initial_step=10)
+        ref = optimize.OptimizeResult()
+        for attr in ['success', 'status', 'df', 'nit', 'nfev']:
+            ref_attr = xp.asarray([[getattr(res00, attr), getattr(res01, attr)],
+                                   [getattr(res10, attr), getattr(res11, attr)]])
+            ref[attr] = xp.squeeze(
+                ref_attr,
+                axis=tuple(ax for ax, size in enumerate(ref_attr.shape) if size == 1)
+            )
+            rtol = 1.5e-5 if res[attr].dtype == xp.float32 else 1.5e-14
+            xp_assert_close(res[attr], ref[attr], rtol=rtol)
+
+    def test_step_direction_size(self, xp):
+        # Check that `step_direction` and `initial_step` can be used to ensure that
+        # the usable domain of a function is respected.
+        rng = np.random.default_rng(23892589425245)
+        b = rng.random(3)
+        eps = 1e-7  # torch needs wiggle room?
+
+        def f(x):
+            x = xpx.at(x)[0, x[0] < b[0]].set(xp.nan)
+            x = xpx.at(x)[0, x[0] > b[0] + 0.25].set(xp.nan)
+            x = xpx.at(x)[1, x[1] > b[1]].set(xp.nan)
+            x = xpx.at(x)[1, x[1] < b[1] - 0.1-eps].set(xp.nan)
+            return TestJacobian.f5(x, xp)
+
+        dir = [1, -1, 0]
+        h0 = [0.25, 0.1, 0.5]
+        atol = {'atol': 1e-8}
+        res = jacobian(f, xp.asarray(b, dtype=xp.float64), initial_step=h0,
+                       step_direction=dir, tolerances=atol)
+        ref = xp.asarray(TestJacobian.df5(b), dtype=xp.float64)
+        xp_assert_close(res.df, ref, atol=1e-8)
+        assert xp.all(xp.isfinite(ref))
+
+
+@make_xp_test_case(hessian)
+class TestHessian(JacobianHessianTest):
+    jh_func = hessian
+
+    @pytest.mark.parametrize('shape', [(), (4,), (2, 4)])
+    def test_example(self, shape, xp):
+        rng = np.random.default_rng(458912319542)
+        m = 3
+        x = xp.asarray(rng.random((m,) + shape), dtype=xp.float64)
+        res = hessian(optimize.rosen, x)
+        if shape:
+            x = xp.reshape(x, (m, -1))
+            ref = xp.stack([optimize.rosen_hess(xi) for xi in x.T])
+            ref = xp.moveaxis(ref, 0, -1)
+            ref = xp.reshape(ref, (m, m,) + shape)
+        else:
+            ref = optimize.rosen_hess(x)
+        xp_assert_close(res.ddf, ref, atol=1e-8)
+
+        # # Removed symmetry enforcement; consider adding back in as a feature
+        # # check symmetry
+        # for key in ['ddf', 'error', 'nfev', 'success', 'status']:
+        #     assert_equal(res[key], np.swapaxes(res[key], 0, 1))
+
+    def test_float32(self, xp):
+        rng = np.random.default_rng(458912319542)
+        x = xp.asarray(rng.random(3), dtype=xp.float32)
+        res = hessian(optimize.rosen, x)
+        ref = optimize.rosen_hess(x)
+        mask = (ref != 0)
+        xp_assert_close(res.ddf[mask], ref[mask])
+        atol = 1e-2 * xp.abs(xp.min(ref[mask]))
+        xp_assert_close(res.ddf[~mask], ref[~mask], atol=atol)
+
+    def test_nfev(self, xp):
+        z = xp.asarray([0.5, 0.25])
+
+        def f1(z):
+            x, y = xp.broadcast_arrays(*z)
+            f1.nfev = f1.nfev + (math.prod(x.shape[2:]) if x.ndim > 2 else 1)
+            return xp.sin(x) * y ** 3
+        f1.nfev = 0
+
+
+        res = hessian(f1, z, initial_step=10)
+        f1.nfev = 0
+        res00 = hessian(lambda x: f1([x[0], z[1]]), z[0:1], initial_step=10)
+        assert res.nfev[0, 0] == f1.nfev == res00.nfev[0, 0]
+
+        f1.nfev = 0
+        res11 = hessian(lambda y: f1([z[0], y[0]]), z[1:2], initial_step=10)
+        assert res.nfev[1, 1] == f1.nfev == res11.nfev[0, 0]
+
+        # Removed symmetry enforcement; consider adding back in as a feature
+        # assert_equal(res.nfev, res.nfev.T)  # check symmetry
+        # assert np.unique(res.nfev).size == 3
+
+
+    @pytest.mark.skip_xp_backends(np_only=True,
+                                  reason='Python list input uses NumPy backend')
+    def test_small_rtol_warning(self, xp):
+        message = 'The specified `rtol=1e-15`, but...'
+        with pytest.warns(RuntimeWarning, match=message):
+            hessian(xp.sin, [1.], tolerances=dict(rtol=1e-15))
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/__init__.py b/.venv/lib/python3.11/site-packages/scipy/fft/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..c545a00b9fd63427088ac873fa3fa65678b77f71
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/__init__.py
@@ -0,0 +1,114 @@
+"""
+==============================================
+Discrete Fourier transforms (:mod:`scipy.fft`)
+==============================================
+
+.. currentmodule:: scipy.fft
+
+Fast Fourier Transforms (FFTs)
+==============================
+
+.. autosummary::
+   :toctree: generated/
+
+   fft - Fast (discrete) Fourier Transform (FFT)
+   ifft - Inverse FFT
+   fft2 - 2-D FFT
+   ifft2 - 2-D inverse FFT
+   fftn - N-D FFT
+   ifftn - N-D inverse FFT
+   rfft - FFT of strictly real-valued sequence
+   irfft - Inverse of rfft
+   rfft2 - 2-D FFT of real sequence
+   irfft2 - Inverse of rfft2
+   rfftn - N-D FFT of real sequence
+   irfftn - Inverse of rfftn
+   hfft - FFT of a Hermitian sequence (real spectrum)
+   ihfft - Inverse of hfft
+   hfft2 - 2-D FFT of a Hermitian sequence
+   ihfft2 - Inverse of hfft2
+   hfftn - N-D FFT of a Hermitian sequence
+   ihfftn - Inverse of hfftn
+
+Discrete Sin and Cosine Transforms (DST and DCT)
+================================================
+
+.. autosummary::
+   :toctree: generated/
+
+   dct - Discrete cosine transform
+   idct - Inverse discrete cosine transform
+   dctn - N-D Discrete cosine transform
+   idctn - N-D Inverse discrete cosine transform
+   dst - Discrete sine transform
+   idst - Inverse discrete sine transform
+   dstn - N-D Discrete sine transform
+   idstn - N-D Inverse discrete sine transform
+
+Fast Hankel Transforms
+======================
+
+.. autosummary::
+   :toctree: generated/
+
+   fht - Fast Hankel transform
+   ifht - Inverse of fht
+
+Helper functions
+================
+
+.. autosummary::
+   :toctree: generated/
+
+   fftshift - Shift the zero-frequency component to the center of the spectrum
+   ifftshift - The inverse of `fftshift`
+   fftfreq - Return the Discrete Fourier Transform sample frequencies
+   rfftfreq - DFT sample frequencies (for usage with rfft, irfft)
+   fhtoffset - Compute an optimal offset for the Fast Hankel Transform
+   next_fast_len - Find the optimal length to zero-pad an FFT for speed
+   prev_fast_len - Find the maximum slice length that results in a fast FFT
+   set_workers - Context manager to set default number of workers
+   get_workers - Get the current default number of workers
+
+Backend control
+===============
+
+.. autosummary::
+   :toctree: generated/
+
+   set_backend - Context manager to set the backend within a fixed scope
+   skip_backend - Context manager to skip a backend within a fixed scope
+   set_global_backend - Sets the global fft backend
+   register_backend - Register a backend for permanent use
+
+"""
+
+from ._basic import (
+    fft, ifft, fft2, ifft2, fftn, ifftn,
+    rfft, irfft, rfft2, irfft2, rfftn, irfftn,
+    hfft, ihfft, hfft2, ihfft2, hfftn, ihfftn)
+from ._realtransforms import dct, idct, dst, idst, dctn, idctn, dstn, idstn
+from ._fftlog import fht, ifht, fhtoffset
+from ._helper import (
+    next_fast_len, prev_fast_len, fftfreq,
+    rfftfreq, fftshift, ifftshift)
+from ._backend import (set_backend, skip_backend, set_global_backend,
+                       register_backend)
+from ._pocketfft.helper import set_workers, get_workers
+
+__all__ = [
+    'fft', 'ifft', 'fft2', 'ifft2', 'fftn', 'ifftn',
+    'rfft', 'irfft', 'rfft2', 'irfft2', 'rfftn', 'irfftn',
+    'hfft', 'ihfft', 'hfft2', 'ihfft2', 'hfftn', 'ihfftn',
+    'fftfreq', 'rfftfreq', 'fftshift', 'ifftshift',
+    'next_fast_len', 'prev_fast_len',
+    'dct', 'idct', 'dst', 'idst', 'dctn', 'idctn', 'dstn', 'idstn',
+    'fht', 'ifht',
+    'fhtoffset',
+    'set_backend', 'skip_backend', 'set_global_backend', 'register_backend',
+    'get_workers', 'set_workers']
+
+
+from scipy._lib._testutils import PytestTester
+test = PytestTester(__name__)
+del PytestTester
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/_backend.py b/.venv/lib/python3.11/site-packages/scipy/fft/_backend.py
new file mode 100644
index 0000000000000000000000000000000000000000..db044fef38d696bb63f3800be8cbfc69ace1303b
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/_backend.py
@@ -0,0 +1,201 @@
+import scipy._lib.uarray as ua
+from scipy._lib._array_api import xp_capabilities
+from . import _basic_backend
+from . import _realtransforms_backend
+from . import _fftlog_backend
+
+
+class _ScipyBackend:
+    """The default backend for fft calculations
+
+    Notes
+    -----
+    We use the domain ``numpy.scipy`` rather than ``scipy`` because ``uarray``
+    treats the domain as a hierarchy. This means the user can install a single
+    backend for ``numpy`` and have it implement ``numpy.scipy.fft`` as well.
+    """
+    __ua_domain__ = "numpy.scipy.fft"
+
+    @staticmethod
+    def __ua_function__(method, args, kwargs):
+
+        fn = getattr(_basic_backend, method.__name__, None)
+        if fn is None:
+            fn = getattr(_realtransforms_backend, method.__name__, None)
+        if fn is None:
+            fn = getattr(_fftlog_backend, method.__name__, None)
+        if fn is None:
+            return NotImplemented
+        return fn(*args, **kwargs)
+
+
+_named_backends = {
+    'scipy': _ScipyBackend,
+}
+
+
+def _backend_from_arg(backend):
+    """Maps strings to known backends and validates the backend"""
+
+    if isinstance(backend, str):
+        try:
+            backend = _named_backends[backend]
+        except KeyError as e:
+            raise ValueError(f'Unknown backend {backend}') from e
+
+    if backend.__ua_domain__ != 'numpy.scipy.fft':
+        raise ValueError('Backend does not implement "numpy.scipy.fft"')
+
+    return backend
+
+
+@xp_capabilities(out_of_scope=True)
+def set_global_backend(backend, coerce=False, only=False, try_last=False):
+    """Sets the global fft backend
+
+    This utility method replaces the default backend for permanent use. It
+    will be tried in the list of backends automatically, unless the
+    ``only`` flag is set on a backend. This will be the first tried
+    backend outside the :obj:`set_backend` context manager.
+
+    Parameters
+    ----------
+    backend : {object, 'scipy'}
+        The backend to use.
+        Can either be a ``str`` containing the name of a known backend
+        {'scipy'} or an object that implements the uarray protocol.
+    coerce : bool
+        Whether to coerce input types when trying this backend.
+    only : bool
+        If ``True``, no more backends will be tried if this fails.
+        Implied by ``coerce=True``.
+    try_last : bool
+        If ``True``, the global backend is tried after registered backends.
+
+    Raises
+    ------
+    ValueError: If the backend does not implement ``numpy.scipy.fft``.
+
+    Notes
+    -----
+    This will overwrite the previously set global backend, which, by default, is
+    the SciPy implementation.
+
+    Examples
+    --------
+    We can set the global fft backend:
+
+    >>> from scipy.fft import fft, set_global_backend
+    >>> set_global_backend("scipy")  # Sets global backend (default is "scipy").
+    >>> fft([1])  # Calls the global backend
+    array([1.+0.j])
+    """
+    backend = _backend_from_arg(backend)
+    ua.set_global_backend(backend, coerce=coerce, only=only, try_last=try_last)
+
+
+@xp_capabilities(out_of_scope=True)
+def register_backend(backend):
+    """
+    Register a backend for permanent use.
+
+    Registered backends have the lowest priority and will be tried after the
+    global backend.
+
+    Parameters
+    ----------
+    backend : {object, 'scipy'}
+        The backend to use.
+        Can either be a ``str`` containing the name of a known backend
+        {'scipy'} or an object that implements the uarray protocol.
+
+    Raises
+    ------
+    ValueError: If the backend does not implement ``numpy.scipy.fft``.
+
+    Examples
+    --------
+    We can register a new fft backend:
+
+    >>> from scipy.fft import fft, register_backend, set_global_backend
+    >>> class NoopBackend:  # Define an invalid Backend
+    ...     __ua_domain__ = "numpy.scipy.fft"
+    ...     def __ua_function__(self, func, args, kwargs):
+    ...          return NotImplemented
+    >>> set_global_backend(NoopBackend())  # Set the invalid backend as global
+    >>> register_backend("scipy")  # Register a new backend
+    # The registered backend is called because
+    # the global backend returns `NotImplemented`
+    >>> fft([1])
+    array([1.+0.j])
+    >>> set_global_backend("scipy")  # Restore global backend to default
+
+    """
+    backend = _backend_from_arg(backend)
+    ua.register_backend(backend)
+
+
+@xp_capabilities(out_of_scope=True)
+def set_backend(backend, coerce=False, only=False):
+    """Context manager to set the backend within a fixed scope.
+
+    Upon entering the ``with`` statement, the given backend will be added to
+    the list of available backends with the highest priority. Upon exit, the
+    backend is reset to the state before entering the scope.
+
+    Parameters
+    ----------
+    backend : {object, 'scipy'}
+        The backend to use.
+        Can either be a ``str`` containing the name of a known backend
+        {'scipy'} or an object that implements the uarray protocol.
+    coerce : bool, optional
+        Whether to allow expensive conversions for the ``x`` parameter. e.g.,
+        copying a NumPy array to the GPU for a CuPy backend. Implies ``only``.
+    only : bool, optional
+        If only is ``True`` and this backend returns ``NotImplemented``, then a
+        BackendNotImplemented error will be raised immediately. Ignoring any
+        lower priority backends.
+
+    Examples
+    --------
+    >>> import scipy.fft as fft
+    >>> with fft.set_backend('scipy', only=True):
+    ...     fft.fft([1])  # Always calls the scipy implementation
+    array([1.+0.j])
+    """
+    backend = _backend_from_arg(backend)
+    return ua.set_backend(backend, coerce=coerce, only=only)
+
+
+@xp_capabilities(out_of_scope=True)
+def skip_backend(backend):
+    """Context manager to skip a backend within a fixed scope.
+
+    Within the context of a ``with`` statement, the given backend will not be
+    called. This covers backends registered both locally and globally. Upon
+    exit, the backend will again be considered.
+
+    Parameters
+    ----------
+    backend : {object, 'scipy'}
+        The backend to skip.
+        Can either be a ``str`` containing the name of a known backend
+        {'scipy'} or an object that implements the uarray protocol.
+
+    Examples
+    --------
+    >>> import scipy.fft as fft
+    >>> fft.fft([1])  # Calls default SciPy backend
+    array([1.+0.j])
+    >>> with fft.skip_backend('scipy'):  # We explicitly skip the SciPy backend
+    ...     fft.fft([1])                 # leaving no implementation available
+    Traceback (most recent call last):
+        ...
+    BackendNotImplementedError: No selected backends had an implementation ...
+    """
+    backend = _backend_from_arg(backend)
+    return ua.skip_backend(backend)
+
+
+set_global_backend('scipy', try_last=True)
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/_basic.py b/.venv/lib/python3.11/site-packages/scipy/fft/_basic.py
new file mode 100644
index 0000000000000000000000000000000000000000..eb15e40e6562090f5e0e02a13982bc7247ed7380
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/_basic.py
@@ -0,0 +1,1670 @@
+from scipy._lib.uarray import generate_multimethod, Dispatchable
+from scipy._lib._array_api import xp_capabilities
+
+import numpy as np
+
+
+def _x_replacer(args, kwargs, dispatchables):
+    """
+    uarray argument replacer to replace the transform input array (``x``)
+    """
+    if len(args) > 0:
+        return (dispatchables[0],) + args[1:], kwargs
+    kw = kwargs.copy()
+    kw['x'] = dispatchables[0]
+    return args, kw
+
+
+def _dispatch(func):
+    """
+    Function annotation that creates a uarray multimethod from the function
+    """
+    return generate_multimethod(func, _x_replacer, domain="numpy.scipy.fft")
+
+
+@xp_capabilities(allow_dask_compute=True)
+@_dispatch
+def fft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None, *,
+        plan=None):
+    """
+    Compute the 1-D discrete Fourier Transform.
+
+    This function computes the 1-D *n*-point discrete Fourier
+    Transform (DFT) with the efficient Fast Fourier Transform (FFT)
+    algorithm [1]_.
+
+    Parameters
+    ----------
+    x : array_like
+        Input array, can be complex.
+    n : int, optional
+        Length of the transformed axis of the output.
+        If `n` is smaller than the length of the input, the input is cropped.
+        If it is larger, the input is padded with zeros. If `n` is not given,
+        the length of the input along the axis specified by `axis` is used.
+    axis : int, optional
+        Axis over which to compute the FFT. If not given, the last axis is
+        used.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode. Default is "backward", meaning no normalization on
+        the forward transforms and scaling by ``1/n`` on the `ifft`.
+        "forward" instead applies the ``1/n`` factor on the forward transform.
+        For ``norm="ortho"``, both directions are scaled by ``1/sqrt(n)``.
+
+        .. versionadded:: 1.6.0
+           ``norm={"forward", "backward"}`` options were added
+
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+        See the notes below for more details.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``. See below for more
+        details.
+    plan : object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    out : complex ndarray
+        The truncated or zero-padded input, transformed along the axis
+        indicated by `axis`, or the last one if `axis` is not specified.
+
+    Raises
+    ------
+    IndexError
+        if `axes` is larger than the last axis of `x`.
+
+    See Also
+    --------
+    ifft : The inverse of `fft`.
+    fft2 : The 2-D FFT.
+    fftn : The N-D FFT.
+    rfftn : The N-D FFT of real input.
+    fftfreq : Frequency bins for given FFT parameters.
+    next_fast_len : Size to pad input to for most efficient transforms
+
+    Notes
+    -----
+    FFT (Fast Fourier Transform) refers to a way the discrete Fourier Transform
+    (DFT) can be calculated efficiently, by using symmetries in the calculated
+    terms. The symmetry is highest when `n` is a power of 2, and the transform
+    is therefore most efficient for these sizes. For poorly factorizable sizes,
+    `scipy.fft` uses Bluestein's algorithm [2]_ and so is never worse than
+    O(`n` log `n`). Further performance improvements may be seen by zero-padding
+    the input using `next_fast_len`.
+
+    If ``x`` is a 1d array, then the `fft` is equivalent to ::
+
+        y[k] = np.sum(x * np.exp(-2j * np.pi * k * np.arange(n)/n))
+
+    The frequency term ``f=k/n`` is found at ``y[k]``. At ``y[n/2]`` we reach
+    the Nyquist frequency and wrap around to the negative-frequency terms. So,
+    for an 8-point transform, the frequencies of the result are
+    [0, 1, 2, 3, -4, -3, -2, -1]. To rearrange the fft output so that the
+    zero-frequency component is centered, like [-4, -3, -2, -1, 0, 1, 2, 3],
+    use `fftshift`.
+
+    Transforms can be done in single, double, or extended precision (long
+    double) floating point. Half precision inputs will be converted to single
+    precision and non-floating-point inputs will be converted to double
+    precision.
+
+    If the data type of ``x`` is real, a "real FFT" algorithm is automatically
+    used, which roughly halves the computation time. To increase efficiency
+    a little further, use `rfft`, which does the same calculation, but only
+    outputs half of the symmetrical spectrum. If the data are both real and
+    symmetrical, the `dct` can again double the efficiency, by generating
+    half of the spectrum from half of the signal.
+
+    When ``overwrite_x=True`` is specified, the memory referenced by ``x`` may
+    be used by the implementation in any way. This may include reusing the
+    memory for the result, but this is in no way guaranteed. You should not
+    rely on the contents of ``x`` after the transform as this may change in
+    future without warning.
+
+    The ``workers`` argument specifies the maximum number of parallel jobs to
+    split the FFT computation into. This will execute independent 1-D
+    FFTs within ``x``. So, ``x`` must be at least 2-D and the
+    non-transformed axes must be large enough to split into chunks. If ``x`` is
+    too small, fewer jobs may be used than requested.
+
+    References
+    ----------
+    .. [1] Cooley, James W., and John W. Tukey, 1965, "An algorithm for the
+           machine calculation of complex Fourier series," *Math. Comput.*
+           19: 297-301.
+    .. [2] Bluestein, L., 1970, "A linear filtering approach to the
+           computation of discrete Fourier transform". *IEEE Transactions on
+           Audio and Electroacoustics.* 18 (4): 451-455.
+
+    Examples
+    --------
+    >>> import scipy.fft
+    >>> import numpy as np
+    >>> scipy.fft.fft(np.exp(2j * np.pi * np.arange(8) / 8))
+    array([-2.33486982e-16+1.14423775e-17j,  8.00000000e+00-1.25557246e-15j,
+            2.33486982e-16+2.33486982e-16j,  0.00000000e+00+1.22464680e-16j,
+           -1.14423775e-17+2.33486982e-16j,  0.00000000e+00+5.20784380e-16j,
+            1.14423775e-17+1.14423775e-17j,  0.00000000e+00+1.22464680e-16j])
+
+    In this example, real input has an FFT which is Hermitian, i.e., symmetric
+    in the real part and anti-symmetric in the imaginary part:
+
+    >>> from scipy.fft import fft, fftfreq, fftshift
+    >>> import matplotlib.pyplot as plt
+    >>> t = np.arange(256)
+    >>> sp = fftshift(fft(np.sin(t)))
+    >>> freq = fftshift(fftfreq(t.shape[-1]))
+    >>> plt.plot(freq, sp.real, freq, sp.imag)
+    [,
+     ]
+    >>> plt.show()
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(allow_dask_compute=True)
+@_dispatch
+def ifft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None, *,
+         plan=None):
+    """
+    Compute the 1-D inverse discrete Fourier Transform.
+
+    This function computes the inverse of the 1-D *n*-point
+    discrete Fourier transform computed by `fft`.  In other words,
+    ``ifft(fft(x)) == x`` to within numerical accuracy.
+
+    The input should be ordered in the same way as is returned by `fft`,
+    i.e.,
+
+    * ``x[0]`` should contain the zero frequency term,
+    * ``x[1:n//2]`` should contain the positive-frequency terms,
+    * ``x[n//2 + 1:]`` should contain the negative-frequency terms, in
+      increasing order starting from the most negative frequency.
+
+    For an even number of input points, ``x[n//2]`` represents the sum of
+    the values at the positive and negative Nyquist frequencies, as the two
+    are aliased together. See `fft` for details.
+
+    Parameters
+    ----------
+    x : array_like
+        Input array, can be complex.
+    n : int, optional
+        Length of the transformed axis of the output.
+        If `n` is smaller than the length of the input, the input is cropped.
+        If it is larger, the input is padded with zeros. If `n` is not given,
+        the length of the input along the axis specified by `axis` is used.
+        See notes about padding issues.
+    axis : int, optional
+        Axis over which to compute the inverse DFT. If not given, the last
+        axis is used.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see `fft`). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+        See :func:`fft` for more details.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    plan : object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    out : complex ndarray
+        The truncated or zero-padded input, transformed along the axis
+        indicated by `axis`, or the last one if `axis` is not specified.
+
+    Raises
+    ------
+    IndexError
+        If `axes` is larger than the last axis of `x`.
+
+    See Also
+    --------
+    fft : The 1-D (forward) FFT, of which `ifft` is the inverse.
+    ifft2 : The 2-D inverse FFT.
+    ifftn : The N-D inverse FFT.
+
+    Notes
+    -----
+    If the input parameter `n` is larger than the size of the input, the input
+    is padded by appending zeros at the end. Even though this is the common
+    approach, it might lead to surprising results. If a different padding is
+    desired, it must be performed before calling `ifft`.
+
+    If ``x`` is a 1-D array, then the `ifft` is equivalent to ::
+
+        y[k] = np.sum(x * np.exp(2j * np.pi * k * np.arange(n)/n)) / len(x)
+
+    As with `fft`, `ifft` has support for all floating point types and is
+    optimized for real input.
+
+    Examples
+    --------
+    >>> import scipy.fft
+    >>> import numpy as np
+    >>> scipy.fft.ifft([0, 4, 0, 0])
+    array([ 1.+0.j,  0.+1.j, -1.+0.j,  0.-1.j]) # may vary
+
+    Create and plot a band-limited signal with random phases:
+
+    >>> import matplotlib.pyplot as plt
+    >>> rng = np.random.default_rng()
+    >>> t = np.arange(400)
+    >>> n = np.zeros((400,), dtype=complex)
+    >>> n[40:60] = np.exp(1j*rng.uniform(0, 2*np.pi, (20,)))
+    >>> s = scipy.fft.ifft(n)
+    >>> plt.plot(t, s.real, 'b-', t, s.imag, 'r--')
+    [, ]
+    >>> plt.legend(('real', 'imaginary'))
+    
+    >>> plt.show()
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(allow_dask_compute=True)
+@_dispatch
+def rfft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None, *,
+         plan=None):
+    """
+    Compute the 1-D discrete Fourier Transform for real input.
+
+    This function computes the 1-D *n*-point discrete Fourier
+    Transform (DFT) of a real-valued array by means of an efficient algorithm
+    called the Fast Fourier Transform (FFT).
+
+    Parameters
+    ----------
+    x : array_like
+        Input array
+    n : int, optional
+        Number of points along transformation axis in the input to use.
+        If `n` is smaller than the length of the input, the input is cropped.
+        If it is larger, the input is padded with zeros. If `n` is not given,
+        the length of the input along the axis specified by `axis` is used.
+    axis : int, optional
+        Axis over which to compute the FFT. If not given, the last axis is
+        used.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see `fft`). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+        See :func:`fft` for more details.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    plan : object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    out : complex ndarray
+        The truncated or zero-padded input, transformed along the axis
+        indicated by `axis`, or the last one if `axis` is not specified.
+        If `n` is even, the length of the transformed axis is ``(n/2)+1``.
+        If `n` is odd, the length is ``(n+1)/2``.
+
+    Raises
+    ------
+    IndexError
+        If `axis` is larger than the last axis of `a`.
+
+    See Also
+    --------
+    irfft : The inverse of `rfft`.
+    fft : The 1-D FFT of general (complex) input.
+    fftn : The N-D FFT.
+    rfft2 : The 2-D FFT of real input.
+    rfftn : The N-D FFT of real input.
+
+    Notes
+    -----
+    When the DFT is computed for purely real input, the output is
+    Hermitian-symmetric, i.e., the negative frequency terms are just the complex
+    conjugates of the corresponding positive-frequency terms, and the
+    negative-frequency terms are therefore redundant. This function does not
+    compute the negative frequency terms, and the length of the transformed
+    axis of the output is therefore ``n//2 + 1``.
+
+    When ``X = rfft(x)`` and fs is the sampling frequency, ``X[0]`` contains
+    the zero-frequency term 0*fs, which is real due to Hermitian symmetry.
+
+    If `n` is even, ``A[-1]`` contains the term representing both positive
+    and negative Nyquist frequency (+fs/2 and -fs/2), and must also be purely
+    real. If `n` is odd, there is no term at fs/2; ``A[-1]`` contains
+    the largest positive frequency (fs/2*(n-1)/n), and is complex in the
+    general case.
+
+    If the input `a` contains an imaginary part, it is silently discarded.
+
+    Examples
+    --------
+    >>> import scipy.fft
+    >>> scipy.fft.fft([0, 1, 0, 0])
+    array([ 1.+0.j,  0.-1.j, -1.+0.j,  0.+1.j]) # may vary
+    >>> scipy.fft.rfft([0, 1, 0, 0])
+    array([ 1.+0.j,  0.-1.j, -1.+0.j]) # may vary
+
+    Notice how the final element of the `fft` output is the complex conjugate
+    of the second element, for real input. For `rfft`, this symmetry is
+    exploited to compute only the non-negative frequency terms.
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(allow_dask_compute=True)
+@_dispatch
+def irfft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None, *,
+          plan=None):
+    """
+    Computes the inverse of `rfft`.
+
+    This function computes the inverse of the 1-D *n*-point
+    discrete Fourier Transform of real input computed by `rfft`.
+    In other words, ``irfft(rfft(x), len(x)) == x`` to within numerical
+    accuracy. (See Notes below for why ``len(a)`` is necessary here.)
+
+    The input is expected to be in the form returned by `rfft`, i.e., the
+    real zero-frequency term followed by the complex positive frequency terms
+    in order of increasing frequency. Since the discrete Fourier Transform of
+    real input is Hermitian-symmetric, the negative frequency terms are taken
+    to be the complex conjugates of the corresponding positive frequency terms.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    n : int, optional
+        Length of the transformed axis of the output.
+        For `n` output points, ``n//2+1`` input points are necessary. If the
+        input is longer than this, it is cropped. If it is shorter than this,
+        it is padded with zeros. If `n` is not given, it is taken to be
+        ``2*(m-1)``, where ``m`` is the length of the input along the axis
+        specified by `axis`.
+    axis : int, optional
+        Axis over which to compute the inverse FFT. If not given, the last
+        axis is used.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see `fft`). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+        See :func:`fft` for more details.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    plan : object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    out : ndarray
+        The truncated or zero-padded input, transformed along the axis
+        indicated by `axis`, or the last one if `axis` is not specified.
+        The length of the transformed axis is `n`, or, if `n` is not given,
+        ``2*(m-1)`` where ``m`` is the length of the transformed axis of the
+        input. To get an odd number of output points, `n` must be specified.
+
+    Raises
+    ------
+    IndexError
+        If `axis` is larger than the last axis of `x`.
+
+    See Also
+    --------
+    rfft : The 1-D FFT of real input, of which `irfft` is inverse.
+    fft : The 1-D FFT.
+    irfft2 : The inverse of the 2-D FFT of real input.
+    irfftn : The inverse of the N-D FFT of real input.
+
+    Notes
+    -----
+    Returns the real valued `n`-point inverse discrete Fourier transform
+    of `x`, where `x` contains the non-negative frequency terms of a
+    Hermitian-symmetric sequence. `n` is the length of the result, not the
+    input.
+
+    If you specify an `n` such that `a` must be zero-padded or truncated, the
+    extra/removed values will be added/removed at high frequencies. One can
+    thus resample a series to `m` points via Fourier interpolation by:
+    ``a_resamp = irfft(rfft(a), m)``.
+
+    The default value of `n` assumes an even output length. By the Hermitian
+    symmetry, the last imaginary component must be 0 and so is ignored. To
+    avoid losing information, the correct length of the real input *must* be
+    given.
+
+    Examples
+    --------
+    >>> import scipy.fft
+    >>> scipy.fft.ifft([1, -1j, -1, 1j])
+    array([0.+0.j,  1.+0.j,  0.+0.j,  0.+0.j]) # may vary
+    >>> scipy.fft.irfft([1, -1j, -1])
+    array([0.,  1.,  0.,  0.])
+
+    Notice how the last term in the input to the ordinary `ifft` is the
+    complex conjugate of the second term, and the output has zero imaginary
+    part everywhere. When calling `irfft`, the negative frequencies are not
+    specified, and the output array is purely real.
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(allow_dask_compute=True)
+@_dispatch
+def hfft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None, *,
+         plan=None):
+    """
+    Compute the FFT of a signal that has Hermitian symmetry, i.e., a real
+    spectrum.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    n : int, optional
+        Length of the transformed axis of the output. For `n` output
+        points, ``n//2 + 1`` input points are necessary. If the input is
+        longer than this, it is cropped. If it is shorter than this, it is
+        padded with zeros. If `n` is not given, it is taken to be ``2*(m-1)``,
+        where ``m`` is the length of the input along the axis specified by
+        `axis`.
+    axis : int, optional
+        Axis over which to compute the FFT. If not given, the last
+        axis is used.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see `fft`). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+        See `fft` for more details.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    plan : object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    out : ndarray
+        The truncated or zero-padded input, transformed along the axis
+        indicated by `axis`, or the last one if `axis` is not specified.
+        The length of the transformed axis is `n`, or, if `n` is not given,
+        ``2*m - 2``, where ``m`` is the length of the transformed axis of
+        the input. To get an odd number of output points, `n` must be
+        specified, for instance, as ``2*m - 1`` in the typical case,
+
+    Raises
+    ------
+    IndexError
+        If `axis` is larger than the last axis of `a`.
+
+    See Also
+    --------
+    rfft : Compute the 1-D FFT for real input.
+    ihfft : The inverse of `hfft`.
+    hfftn : Compute the N-D FFT of a Hermitian signal.
+
+    Notes
+    -----
+    `hfft`/`ihfft` are a pair analogous to `rfft`/`irfft`, but for the
+    opposite case: here the signal has Hermitian symmetry in the time
+    domain and is real in the frequency domain. So, here, it's `hfft`, for
+    which you must supply the length of the result if it is to be odd.
+    * even: ``ihfft(hfft(a, 2*len(a) - 2) == a``, within roundoff error,
+    * odd: ``ihfft(hfft(a, 2*len(a) - 1) == a``, within roundoff error.
+
+    Examples
+    --------
+    >>> from scipy.fft import fft, hfft
+    >>> import numpy as np
+    >>> a = 2 * np.pi * np.arange(10) / 10
+    >>> signal = np.cos(a) + 3j * np.sin(3 * a)
+    >>> fft(signal).round(10)
+    array([ -0.+0.j,   5.+0.j,  -0.+0.j,  15.-0.j,   0.+0.j,   0.+0.j,
+            -0.+0.j, -15.-0.j,   0.+0.j,   5.+0.j])
+    >>> hfft(signal[:6]).round(10) # Input first half of signal
+    array([  0.,   5.,   0.,  15.,  -0.,   0.,   0., -15.,  -0.,   5.])
+    >>> hfft(signal, 10)  # Input entire signal and truncate
+    array([  0.,   5.,   0.,  15.,  -0.,   0.,   0., -15.,  -0.,   5.])
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(allow_dask_compute=True)
+@_dispatch
+def ihfft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None, *,
+          plan=None):
+    """
+    Compute the inverse FFT of a signal that has Hermitian symmetry.
+
+    Parameters
+    ----------
+    x : array_like
+        Input array.
+    n : int, optional
+        Length of the inverse FFT, the number of points along
+        transformation axis in the input to use.  If `n` is smaller than
+        the length of the input, the input is cropped. If it is larger,
+        the input is padded with zeros. If `n` is not given, the length of
+        the input along the axis specified by `axis` is used.
+    axis : int, optional
+        Axis over which to compute the inverse FFT. If not given, the last
+        axis is used.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see `fft`). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+        See `fft` for more details.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    plan : object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    out : complex ndarray
+        The truncated or zero-padded input, transformed along the axis
+        indicated by `axis`, or the last one if `axis` is not specified.
+        The length of the transformed axis is ``n//2 + 1``.
+
+    See Also
+    --------
+    hfft, irfft
+
+    Notes
+    -----
+    `hfft`/`ihfft` are a pair analogous to `rfft`/`irfft`, but for the
+    opposite case: here, the signal has Hermitian symmetry in the time
+    domain and is real in the frequency domain. So, here, it's `hfft`, for
+    which you must supply the length of the result if it is to be odd:
+    * even: ``ihfft(hfft(a, 2*len(a) - 2) == a``, within roundoff error,
+    * odd: ``ihfft(hfft(a, 2*len(a) - 1) == a``, within roundoff error.
+
+    Examples
+    --------
+    >>> from scipy.fft import ifft, ihfft
+    >>> import numpy as np
+    >>> spectrum = np.array([ 15, -4, 0, -1, 0, -4])
+    >>> ifft(spectrum)
+    array([1.+0.j,  2.+0.j,  3.+0.j,  4.+0.j,  3.+0.j,  2.+0.j]) # may vary
+    >>> ihfft(spectrum)
+    array([ 1.-0.j,  2.-0.j,  3.-0.j,  4.-0.j]) # may vary
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(allow_dask_compute=True)
+@_dispatch
+def fftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None, *,
+         plan=None):
+    """
+    Compute the N-D discrete Fourier Transform.
+
+    This function computes the N-D discrete Fourier Transform over
+    any number of axes in an M-D array by means of the Fast Fourier
+    Transform (FFT).
+
+    Parameters
+    ----------
+    x : array_like
+        Input array, can be complex.
+    s : sequence of ints, optional
+        Shape (length of each transformed axis) of the output
+        (``s[0]`` refers to axis 0, ``s[1]`` to axis 1, etc.).
+        This corresponds to ``n`` for ``fft(x, n)``.
+        Along any axis, if the given shape is smaller than that of the input,
+        the input is cropped. If it is larger, the input is padded with zeros.
+        if `s` is not given, the shape of the input along the axes specified
+        by `axes` is used.
+    axes : sequence of ints, optional
+        Axes over which to compute the FFT. If not given, the last ``len(s)``
+        axes are used, or all axes if `s` is also not specified.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see `fft`). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+        See :func:`fft` for more details.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    plan : object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    out : complex ndarray
+        The truncated or zero-padded input, transformed along the axes
+        indicated by `axes`, or by a combination of `s` and `x`,
+        as explained in the parameters section above.
+
+    Raises
+    ------
+    ValueError
+        If `s` and `axes` have different length.
+    IndexError
+        If an element of `axes` is larger than the number of axes of `x`.
+
+    See Also
+    --------
+    ifftn : The inverse of `fftn`, the inverse N-D FFT.
+    fft : The 1-D FFT, with definitions and conventions used.
+    rfftn : The N-D FFT of real input.
+    fft2 : The 2-D FFT.
+    fftshift : Shifts zero-frequency terms to centre of array.
+
+    Notes
+    -----
+    The output, analogously to `fft`, contains the term for zero frequency in
+    the low-order corner of all axes, the positive frequency terms in the
+    first half of all axes, the term for the Nyquist frequency in the middle
+    of all axes and the negative frequency terms in the second half of all
+    axes, in order of decreasingly negative frequency.
+
+    Examples
+    --------
+    >>> import scipy.fft
+    >>> import numpy as np
+    >>> x = np.mgrid[:3, :3, :3][0]
+    >>> scipy.fft.fftn(x, axes=(1, 2))
+    array([[[ 0.+0.j,   0.+0.j,   0.+0.j], # may vary
+            [ 0.+0.j,   0.+0.j,   0.+0.j],
+            [ 0.+0.j,   0.+0.j,   0.+0.j]],
+           [[ 9.+0.j,   0.+0.j,   0.+0.j],
+            [ 0.+0.j,   0.+0.j,   0.+0.j],
+            [ 0.+0.j,   0.+0.j,   0.+0.j]],
+           [[18.+0.j,   0.+0.j,   0.+0.j],
+            [ 0.+0.j,   0.+0.j,   0.+0.j],
+            [ 0.+0.j,   0.+0.j,   0.+0.j]]])
+    >>> scipy.fft.fftn(x, (2, 2), axes=(0, 1))
+    array([[[ 2.+0.j,  2.+0.j,  2.+0.j], # may vary
+            [ 0.+0.j,  0.+0.j,  0.+0.j]],
+           [[-2.+0.j, -2.+0.j, -2.+0.j],
+            [ 0.+0.j,  0.+0.j,  0.+0.j]]])
+
+    >>> import matplotlib.pyplot as plt
+    >>> rng = np.random.default_rng()
+    >>> [X, Y] = np.meshgrid(2 * np.pi * np.arange(200) / 12,
+    ...                      2 * np.pi * np.arange(200) / 34)
+    >>> S = np.sin(X) + np.cos(Y) + rng.uniform(0, 1, X.shape)
+    >>> FS = scipy.fft.fftn(S)
+    >>> plt.imshow(np.log(np.abs(scipy.fft.fftshift(FS))**2))
+    
+    >>> plt.show()
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(allow_dask_compute=True)
+@_dispatch
+def ifftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None, *,
+          plan=None):
+    """
+    Compute the N-D inverse discrete Fourier Transform.
+
+    This function computes the inverse of the N-D discrete
+    Fourier Transform over any number of axes in an M-D array by
+    means of the Fast Fourier Transform (FFT).  In other words,
+    ``ifftn(fftn(x)) == x`` to within numerical accuracy.
+
+    The input, analogously to `ifft`, should be ordered in the same way as is
+    returned by `fftn`, i.e., it should have the term for zero frequency
+    in all axes in the low-order corner, the positive frequency terms in the
+    first half of all axes, the term for the Nyquist frequency in the middle
+    of all axes and the negative frequency terms in the second half of all
+    axes, in order of decreasingly negative frequency.
+
+    Parameters
+    ----------
+    x : array_like
+        Input array, can be complex.
+    s : sequence of ints, optional
+        Shape (length of each transformed axis) of the output
+        (``s[0]`` refers to axis 0, ``s[1]`` to axis 1, etc.).
+        This corresponds to ``n`` for ``ifft(x, n)``.
+        Along any axis, if the given shape is smaller than that of the input,
+        the input is cropped. If it is larger, the input is padded with zeros.
+        if `s` is not given, the shape of the input along the axes specified
+        by `axes` is used. See notes for issue on `ifft` zero padding.
+    axes : sequence of ints, optional
+        Axes over which to compute the IFFT.  If not given, the last ``len(s)``
+        axes are used, or all axes if `s` is also not specified.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see `fft`). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+        See :func:`fft` for more details.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    plan : object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    out : complex ndarray
+        The truncated or zero-padded input, transformed along the axes
+        indicated by `axes`, or by a combination of `s` or `x`,
+        as explained in the parameters section above.
+
+    Raises
+    ------
+    ValueError
+        If `s` and `axes` have different length.
+    IndexError
+        If an element of `axes` is larger than the number of axes of `x`.
+
+    See Also
+    --------
+    fftn : The forward N-D FFT, of which `ifftn` is the inverse.
+    ifft : The 1-D inverse FFT.
+    ifft2 : The 2-D inverse FFT.
+    ifftshift : Undoes `fftshift`, shifts zero-frequency terms to beginning
+        of array.
+
+    Notes
+    -----
+    Zero-padding, analogously with `ifft`, is performed by appending zeros to
+    the input along the specified dimension. Although this is the common
+    approach, it might lead to surprising results. If another form of zero
+    padding is desired, it must be performed before `ifftn` is called.
+
+    Examples
+    --------
+    >>> import scipy.fft
+    >>> import numpy as np
+    >>> x = np.eye(4)
+    >>> scipy.fft.ifftn(scipy.fft.fftn(x, axes=(0,)), axes=(1,))
+    array([[1.+0.j,  0.+0.j,  0.+0.j,  0.+0.j], # may vary
+           [0.+0.j,  1.+0.j,  0.+0.j,  0.+0.j],
+           [0.+0.j,  0.+0.j,  1.+0.j,  0.+0.j],
+           [0.+0.j,  0.+0.j,  0.+0.j,  1.+0.j]])
+
+
+    Create and plot an image with band-limited frequency content:
+
+    >>> import matplotlib.pyplot as plt
+    >>> rng = np.random.default_rng()
+    >>> n = np.zeros((200,200), dtype=complex)
+    >>> n[60:80, 20:40] = np.exp(1j*rng.uniform(0, 2*np.pi, (20, 20)))
+    >>> im = scipy.fft.ifftn(n).real
+    >>> plt.imshow(im)
+    
+    >>> plt.show()
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(allow_dask_compute=True)
+@_dispatch
+def fft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None, *,
+         plan=None):
+    """
+    Compute the 2-D discrete Fourier Transform
+
+    This function computes the N-D discrete Fourier Transform
+    over any axes in an M-D array by means of the
+    Fast Fourier Transform (FFT). By default, the transform is computed over
+    the last two axes of the input array, i.e., a 2-dimensional FFT.
+
+    Parameters
+    ----------
+    x : array_like
+        Input array, can be complex
+    s : sequence of ints, optional
+        Shape (length of each transformed axis) of the output
+        (``s[0]`` refers to axis 0, ``s[1]`` to axis 1, etc.).
+        This corresponds to ``n`` for ``fft(x, n)``.
+        Along each axis, if the given shape is smaller than that of the input,
+        the input is cropped. If it is larger, the input is padded with zeros.
+        if `s` is not given, the shape of the input along the axes specified
+        by `axes` is used.
+    axes : sequence of ints, optional
+        Axes over which to compute the FFT. If not given, the last two axes are
+        used.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see `fft`). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+        See :func:`fft` for more details.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    plan : object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    out : complex ndarray
+        The truncated or zero-padded input, transformed along the axes
+        indicated by `axes`, or the last two axes if `axes` is not given.
+
+    Raises
+    ------
+    ValueError
+        If `s` and `axes` have different length, or `axes` not given and
+        ``len(s) != 2``.
+    IndexError
+        If an element of `axes` is larger than the number of axes of `x`.
+
+    See Also
+    --------
+    ifft2 : The inverse 2-D FFT.
+    fft : The 1-D FFT.
+    fftn : The N-D FFT.
+    fftshift : Shifts zero-frequency terms to the center of the array.
+        For 2-D input, swaps first and third quadrants, and second
+        and fourth quadrants.
+
+    Notes
+    -----
+    `fft2` is just `fftn` with a different default for `axes`.
+
+    The output, analogously to `fft`, contains the term for zero frequency in
+    the low-order corner of the transformed axes, the positive frequency terms
+    in the first half of these axes, the term for the Nyquist frequency in the
+    middle of the axes and the negative frequency terms in the second half of
+    the axes, in order of decreasingly negative frequency.
+
+    See `fftn` for details and a plotting example, and `fft` for
+    definitions and conventions used.
+
+
+    Examples
+    --------
+    >>> import scipy.fft
+    >>> import numpy as np
+    >>> x = np.mgrid[:5, :5][0]
+    >>> scipy.fft.fft2(x)
+    array([[ 50.  +0.j        ,   0.  +0.j        ,   0.  +0.j        , # may vary
+              0.  +0.j        ,   0.  +0.j        ],
+           [-12.5+17.20477401j,   0.  +0.j        ,   0.  +0.j        ,
+              0.  +0.j        ,   0.  +0.j        ],
+           [-12.5 +4.0614962j ,   0.  +0.j        ,   0.  +0.j        ,
+              0.  +0.j        ,   0.  +0.j        ],
+           [-12.5 -4.0614962j ,   0.  +0.j        ,   0.  +0.j        ,
+              0.  +0.j        ,   0.  +0.j        ],
+           [-12.5-17.20477401j,   0.  +0.j        ,   0.  +0.j        ,
+              0.  +0.j        ,   0.  +0.j        ]])
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(allow_dask_compute=True)
+@_dispatch
+def ifft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None, *,
+          plan=None):
+    """
+    Compute the 2-D inverse discrete Fourier Transform.
+
+    This function computes the inverse of the 2-D discrete Fourier
+    Transform over any number of axes in an M-D array by means of
+    the Fast Fourier Transform (FFT). In other words, ``ifft2(fft2(x)) == x``
+    to within numerical accuracy. By default, the inverse transform is
+    computed over the last two axes of the input array.
+
+    The input, analogously to `ifft`, should be ordered in the same way as is
+    returned by `fft2`, i.e., it should have the term for zero frequency
+    in the low-order corner of the two axes, the positive frequency terms in
+    the first half of these axes, the term for the Nyquist frequency in the
+    middle of the axes and the negative frequency terms in the second half of
+    both axes, in order of decreasingly negative frequency.
+
+    Parameters
+    ----------
+    x : array_like
+        Input array, can be complex.
+    s : sequence of ints, optional
+        Shape (length of each axis) of the output (``s[0]`` refers to axis 0,
+        ``s[1]`` to axis 1, etc.). This corresponds to `n` for ``ifft(x, n)``.
+        Along each axis, if the given shape is smaller than that of the input,
+        the input is cropped. If it is larger, the input is padded with zeros.
+        if `s` is not given, the shape of the input along the axes specified
+        by `axes` is used.  See notes for issue on `ifft` zero padding.
+    axes : sequence of ints, optional
+        Axes over which to compute the FFT. If not given, the last two
+        axes are used.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see `fft`). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+        See :func:`fft` for more details.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    plan : object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    out : complex ndarray
+        The truncated or zero-padded input, transformed along the axes
+        indicated by `axes`, or the last two axes if `axes` is not given.
+
+    Raises
+    ------
+    ValueError
+        If `s` and `axes` have different length, or `axes` not given and
+        ``len(s) != 2``.
+    IndexError
+        If an element of `axes` is larger than the number of axes of `x`.
+
+    See Also
+    --------
+    fft2 : The forward 2-D FFT, of which `ifft2` is the inverse.
+    ifftn : The inverse of the N-D FFT.
+    fft : The 1-D FFT.
+    ifft : The 1-D inverse FFT.
+
+    Notes
+    -----
+    `ifft2` is just `ifftn` with a different default for `axes`.
+
+    See `ifftn` for details and a plotting example, and `fft` for
+    definition and conventions used.
+
+    Zero-padding, analogously with `ifft`, is performed by appending zeros to
+    the input along the specified dimension. Although this is the common
+    approach, it might lead to surprising results. If another form of zero
+    padding is desired, it must be performed before `ifft2` is called.
+
+    Examples
+    --------
+    >>> import scipy.fft
+    >>> import numpy as np
+    >>> x = 4 * np.eye(4)
+    >>> scipy.fft.ifft2(x)
+    array([[1.+0.j,  0.+0.j,  0.+0.j,  0.+0.j], # may vary
+           [0.+0.j,  0.+0.j,  0.+0.j,  1.+0.j],
+           [0.+0.j,  0.+0.j,  1.+0.j,  0.+0.j],
+           [0.+0.j,  1.+0.j,  0.+0.j,  0.+0.j]])
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(allow_dask_compute=True)
+@_dispatch
+def rfftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None, *,
+          plan=None):
+    """
+    Compute the N-D discrete Fourier Transform for real input.
+
+    This function computes the N-D discrete Fourier Transform over
+    any number of axes in an M-D real array by means of the Fast
+    Fourier Transform (FFT). By default, all axes are transformed, with the
+    real transform performed over the last axis, while the remaining
+    transforms are complex.
+
+    Parameters
+    ----------
+    x : array_like
+        Input array, taken to be real.
+    s : sequence of ints, optional
+        Shape (length along each transformed axis) to use from the input.
+        (``s[0]`` refers to axis 0, ``s[1]`` to axis 1, etc.).
+        The final element of `s` corresponds to `n` for ``rfft(x, n)``, while
+        for the remaining axes, it corresponds to `n` for ``fft(x, n)``.
+        Along any axis, if the given shape is smaller than that of the input,
+        the input is cropped. If it is larger, the input is padded with zeros.
+        if `s` is not given, the shape of the input along the axes specified
+        by `axes` is used.
+    axes : sequence of ints, optional
+        Axes over which to compute the FFT. If not given, the last ``len(s)``
+        axes are used, or all axes if `s` is also not specified.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see `fft`). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+        See :func:`fft` for more details.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    plan : object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    out : complex ndarray
+        The truncated or zero-padded input, transformed along the axes
+        indicated by `axes`, or by a combination of `s` and `x`,
+        as explained in the parameters section above.
+        The length of the last axis transformed will be ``s[-1]//2+1``,
+        while the remaining transformed axes will have lengths according to
+        `s`, or unchanged from the input.
+
+    Raises
+    ------
+    ValueError
+        If `s` and `axes` have different length.
+    IndexError
+        If an element of `axes` is larger than the number of axes of `x`.
+
+    See Also
+    --------
+    irfftn : The inverse of `rfftn`, i.e., the inverse of the N-D FFT
+         of real input.
+    fft : The 1-D FFT, with definitions and conventions used.
+    rfft : The 1-D FFT of real input.
+    fftn : The N-D FFT.
+    rfft2 : The 2-D FFT of real input.
+
+    Notes
+    -----
+    The transform for real input is performed over the last transformation
+    axis, as by `rfft`, then the transform over the remaining axes is
+    performed as by `fftn`. The order of the output is as for `rfft` for the
+    final transformation axis, and as for `fftn` for the remaining
+    transformation axes.
+
+    See `fft` for details, definitions and conventions used.
+
+    Examples
+    --------
+    >>> import scipy.fft
+    >>> import numpy as np
+    >>> x = np.ones((2, 2, 2))
+    >>> scipy.fft.rfftn(x)
+    array([[[8.+0.j,  0.+0.j], # may vary
+            [0.+0.j,  0.+0.j]],
+           [[0.+0.j,  0.+0.j],
+            [0.+0.j,  0.+0.j]]])
+
+    >>> scipy.fft.rfftn(x, axes=(2, 0))
+    array([[[4.+0.j,  0.+0.j], # may vary
+            [4.+0.j,  0.+0.j]],
+           [[0.+0.j,  0.+0.j],
+            [0.+0.j,  0.+0.j]]])
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(allow_dask_compute=True)
+@_dispatch
+def rfft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None, *,
+          plan=None):
+    """
+    Compute the 2-D FFT of a real array.
+
+    Parameters
+    ----------
+    x : array
+        Input array, taken to be real.
+    s : sequence of ints, optional
+        Shape of the FFT.
+    axes : sequence of ints, optional
+        Axes over which to compute the FFT.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see `fft`). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+        See :func:`fft` for more details.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    plan : object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    out : ndarray
+        The result of the real 2-D FFT.
+
+    See Also
+    --------
+    irfft2 : The inverse of the 2-D FFT of real input.
+    rfft : The 1-D FFT of real input.
+    rfftn : Compute the N-D discrete Fourier Transform for real
+            input.
+
+    Notes
+    -----
+    This is really just `rfftn` with different default behavior.
+    For more details see `rfftn`.
+
+    Examples
+    --------
+    >>> import scipy.fft
+    >>> import numpy as np
+    >>> x = np.broadcast_to([1, 0, -1, 0], (4, 4))
+    >>> scipy.fft.rfft2(x)
+    array([[0.+0.j, 8.+0.j, 0.+0.j],
+           [0.+0.j, 0.+0.j, 0.+0.j],
+           [0.+0.j, 0.+0.j, 0.+0.j],
+           [0.+0.j, 0.+0.j, 0.+0.j]])
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(allow_dask_compute=True)
+@_dispatch
+def irfftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None, *,
+           plan=None):
+    """
+    Computes the inverse of `rfftn`
+
+    This function computes the inverse of the N-D discrete
+    Fourier Transform for real input over any number of axes in an
+    M-D array by means of the Fast Fourier Transform (FFT). In
+    other words, ``irfftn(rfftn(x), x.shape) == x`` to within numerical
+    accuracy. (The ``a.shape`` is necessary like ``len(a)`` is for `irfft`,
+    and for the same reason.)
+
+    The input should be ordered in the same way as is returned by `rfftn`,
+    i.e., as for `irfft` for the final transformation axis, and as for `ifftn`
+    along all the other axes.
+
+    Parameters
+    ----------
+    x : array_like
+        Input array.
+    s : sequence of ints, optional
+        Shape (length of each transformed axis) of the output
+        (``s[0]`` refers to axis 0, ``s[1]`` to axis 1, etc.). `s` is also the
+        number of input points used along this axis, except for the last axis,
+        where ``s[-1]//2+1`` points of the input are used.
+        Along any axis, if the shape indicated by `s` is smaller than that of
+        the input, the input is cropped. If it is larger, the input is padded
+        with zeros. If `s` is not given, the shape of the input along the axes
+        specified by axes is used. Except for the last axis which is taken to be
+        ``2*(m-1)``, where ``m`` is the length of the input along that axis.
+    axes : sequence of ints, optional
+        Axes over which to compute the inverse FFT. If not given, the last
+        `len(s)` axes are used, or all axes if `s` is also not specified.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see `fft`). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+        See :func:`fft` for more details.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    plan : object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    out : ndarray
+        The truncated or zero-padded input, transformed along the axes
+        indicated by `axes`, or by a combination of `s` or `x`,
+        as explained in the parameters section above.
+        The length of each transformed axis is as given by the corresponding
+        element of `s`, or the length of the input in every axis except for the
+        last one if `s` is not given. In the final transformed axis the length
+        of the output when `s` is not given is ``2*(m-1)``, where ``m`` is the
+        length of the final transformed axis of the input. To get an odd
+        number of output points in the final axis, `s` must be specified.
+
+    Raises
+    ------
+    ValueError
+        If `s` and `axes` have different length.
+    IndexError
+        If an element of `axes` is larger than the number of axes of `x`.
+
+    See Also
+    --------
+    rfftn : The forward N-D FFT of real input,
+            of which `ifftn` is the inverse.
+    fft : The 1-D FFT, with definitions and conventions used.
+    irfft : The inverse of the 1-D FFT of real input.
+    irfft2 : The inverse of the 2-D FFT of real input.
+
+    Notes
+    -----
+    See `fft` for definitions and conventions used.
+
+    See `rfft` for definitions and conventions used for real input.
+
+    The default value of `s` assumes an even output length in the final
+    transformation axis. When performing the final complex to real
+    transformation, the Hermitian symmetry requires that the last imaginary
+    component along that axis must be 0 and so it is ignored. To avoid losing
+    information, the correct length of the real input *must* be given.
+
+    Examples
+    --------
+    >>> import scipy.fft
+    >>> import numpy as np
+    >>> x = np.zeros((3, 2, 2))
+    >>> x[0, 0, 0] = 3 * 2 * 2
+    >>> scipy.fft.irfftn(x)
+    array([[[1.,  1.],
+            [1.,  1.]],
+           [[1.,  1.],
+            [1.,  1.]],
+           [[1.,  1.],
+            [1.,  1.]]])
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(allow_dask_compute=True)
+@_dispatch
+def irfft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None, *,
+           plan=None):
+    """
+    Computes the inverse of `rfft2`
+
+    Parameters
+    ----------
+    x : array_like
+        The input array
+    s : sequence of ints, optional
+        Shape of the real output to the inverse FFT.
+    axes : sequence of ints, optional
+        The axes over which to compute the inverse fft.
+        Default is the last two axes.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see `fft`). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+        See :func:`fft` for more details.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    plan : object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    out : ndarray
+        The result of the inverse real 2-D FFT.
+
+    See Also
+    --------
+    rfft2 : The 2-D FFT of real input.
+    irfft : The inverse of the 1-D FFT of real input.
+    irfftn : The inverse of the N-D FFT of real input.
+
+    Notes
+    -----
+    This is really `irfftn` with different defaults.
+    For more details see `irfftn`.
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(allow_dask_compute=True)
+@_dispatch
+def hfftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None, *,
+          plan=None):
+    """
+    Compute the N-D FFT of Hermitian symmetric complex input, i.e., a
+    signal with a real spectrum.
+
+    This function computes the N-D discrete Fourier Transform for a
+    Hermitian symmetric complex input over any number of axes in an
+    M-D array by means of the Fast Fourier Transform (FFT). In other
+    words, ``ihfftn(hfftn(x, s)) == x`` to within numerical accuracy. (``s``
+    here is ``x.shape`` with ``s[-1] = x.shape[-1] * 2 - 1``, this is necessary
+    for the same reason ``x.shape`` would be necessary for `irfft`.)
+
+    Parameters
+    ----------
+    x : array_like
+        Input array.
+    s : sequence of ints, optional
+        Shape (length of each transformed axis) of the output
+        (``s[0]`` refers to axis 0, ``s[1]`` to axis 1, etc.). `s` is also the
+        number of input points used along this axis, except for the last axis,
+        where ``s[-1]//2+1`` points of the input are used.
+        Along any axis, if the shape indicated by `s` is smaller than that of
+        the input, the input is cropped. If it is larger, the input is padded
+        with zeros. If `s` is not given, the shape of the input along the axes
+        specified by axes is used. Except for the last axis which is taken to be
+        ``2*(m-1)`` where ``m`` is the length of the input along that axis.
+    axes : sequence of ints, optional
+        Axes over which to compute the inverse FFT. If not given, the last
+        `len(s)` axes are used, or all axes if `s` is also not specified.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see `fft`). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+        See :func:`fft` for more details.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    plan : object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    out : ndarray
+        The truncated or zero-padded input, transformed along the axes
+        indicated by `axes`, or by a combination of `s` or `x`,
+        as explained in the parameters section above.
+        The length of each transformed axis is as given by the corresponding
+        element of `s`, or the length of the input in every axis except for the
+        last one if `s` is not given.  In the final transformed axis the length
+        of the output when `s` is not given is ``2*(m-1)`` where ``m`` is the
+        length of the final transformed axis of the input.  To get an odd
+        number of output points in the final axis, `s` must be specified.
+
+    Raises
+    ------
+    ValueError
+        If `s` and `axes` have different length.
+    IndexError
+        If an element of `axes` is larger than the number of axes of `x`.
+
+    See Also
+    --------
+    ihfftn : The inverse N-D FFT with real spectrum. Inverse of `hfftn`.
+    fft : The 1-D FFT, with definitions and conventions used.
+    rfft : Forward FFT of real input.
+
+    Notes
+    -----
+    For a 1-D signal ``x`` to have a real spectrum, it must satisfy
+    the Hermitian property::
+
+        x[i] == np.conj(x[-i]) for all i
+
+    This generalizes into higher dimensions by reflecting over each axis in
+    turn::
+
+        x[i, j, k, ...] == np.conj(x[-i, -j, -k, ...]) for all i, j, k, ...
+
+    This should not be confused with a Hermitian matrix, for which the
+    transpose is its own conjugate::
+
+        x[i, j] == np.conj(x[j, i]) for all i, j
+
+
+    The default value of `s` assumes an even output length in the final
+    transformation axis. When performing the final complex to real
+    transformation, the Hermitian symmetry requires that the last imaginary
+    component along that axis must be 0 and so it is ignored. To avoid losing
+    information, the correct length of the real input *must* be given.
+
+    Examples
+    --------
+    >>> import scipy.fft
+    >>> import numpy as np
+    >>> x = np.ones((3, 2, 2))
+    >>> scipy.fft.hfftn(x)
+    array([[[12.,  0.],
+            [ 0.,  0.]],
+           [[ 0.,  0.],
+            [ 0.,  0.]],
+           [[ 0.,  0.],
+            [ 0.,  0.]]])
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(allow_dask_compute=True)
+@_dispatch
+def hfft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None, *,
+          plan=None):
+    """
+    Compute the 2-D FFT of a Hermitian complex array.
+
+    Parameters
+    ----------
+    x : array
+        Input array, taken to be Hermitian complex.
+    s : sequence of ints, optional
+        Shape of the real output.
+    axes : sequence of ints, optional
+        Axes over which to compute the FFT.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see `fft`). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+        See `fft` for more details.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    plan : object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    out : ndarray
+        The real result of the 2-D Hermitian complex real FFT.
+
+    See Also
+    --------
+    hfftn : Compute the N-D discrete Fourier Transform for Hermitian
+            complex input.
+
+    Notes
+    -----
+    This is really just `hfftn` with different default behavior.
+    For more details see `hfftn`.
+
+    Examples
+    --------
+    >>> import scipy.fft
+    >>> import numpy as np
+    >>> x = np.array([[1+0j, 2+0j], [2+0j, 1+0j]])  # Hermitian-symmetric input
+    >>> scipy.fft.hfft2(x, s=(2, 2))
+    array([[ 6.,  0.],
+           [ 0., -2.]])
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(allow_dask_compute=True)
+@_dispatch
+def ihfftn(x, s=None, axes=None, norm=None, overwrite_x=False, workers=None, *,
+           plan=None):
+    """
+    Compute the N-D inverse discrete Fourier Transform for a real
+    spectrum.
+
+    This function computes the N-D inverse discrete Fourier Transform
+    over any number of axes in an M-D real array by means of the Fast
+    Fourier Transform (FFT). By default, all axes are transformed, with the
+    real transform performed over the last axis, while the remaining transforms
+    are complex.
+
+    Parameters
+    ----------
+    x : array_like
+        Input array, taken to be real.
+    s : sequence of ints, optional
+        Shape (length along each transformed axis) to use from the input.
+        (``s[0]`` refers to axis 0, ``s[1]`` to axis 1, etc.).
+        Along any axis, if the given shape is smaller than that of the input,
+        the input is cropped. If it is larger, the input is padded with zeros.
+        if `s` is not given, the shape of the input along the axes specified
+        by `axes` is used.
+    axes : sequence of ints, optional
+        Axes over which to compute the FFT. If not given, the last ``len(s)``
+        axes are used, or all axes if `s` is also not specified.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see `fft`). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+        See :func:`fft` for more details.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    plan : object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    out : complex ndarray
+        The truncated or zero-padded input, transformed along the axes
+        indicated by `axes`, or by a combination of `s` and `x`,
+        as explained in the parameters section above.
+        The length of the last axis transformed will be ``s[-1]//2+1``,
+        while the remaining transformed axes will have lengths according to
+        `s`, or unchanged from the input.
+
+    Raises
+    ------
+    ValueError
+        If `s` and `axes` have different length.
+    IndexError
+        If an element of `axes` is larger than the number of axes of `x`.
+
+    See Also
+    --------
+    hfftn : The forward N-D FFT of Hermitian input.
+    hfft : The 1-D FFT of Hermitian input.
+    fft : The 1-D FFT, with definitions and conventions used.
+    fftn : The N-D FFT.
+    hfft2 : The 2-D FFT of Hermitian input.
+
+    Notes
+    -----
+    The transform for real input is performed over the last transformation
+    axis, as by `ihfft`, then the transform over the remaining axes is
+    performed as by `ifftn`. The order of the output is the positive part of
+    the Hermitian output signal, in the same format as `rfft`.
+
+    Examples
+    --------
+    >>> import scipy.fft
+    >>> import numpy as np
+    >>> x = np.ones((2, 2, 2))
+    >>> scipy.fft.ihfftn(x)
+    array([[[1.+0.j,  0.+0.j], # may vary
+            [0.+0.j,  0.+0.j]],
+           [[0.+0.j,  0.+0.j],
+            [0.+0.j,  0.+0.j]]])
+    >>> scipy.fft.ihfftn(x, axes=(2, 0))
+    array([[[1.+0.j,  0.+0.j], # may vary
+            [1.+0.j,  0.+0.j]],
+           [[0.+0.j,  0.+0.j],
+            [0.+0.j,  0.+0.j]]])
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(allow_dask_compute=True)
+@_dispatch
+def ihfft2(x, s=None, axes=(-2, -1), norm=None, overwrite_x=False, workers=None, *,
+           plan=None):
+    """
+    Compute the 2-D inverse FFT of a real spectrum.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array
+    s : sequence of ints, optional
+        Shape of the real input to the inverse FFT.
+    axes : sequence of ints, optional
+        The axes over which to compute the inverse fft.
+        Default is the last two axes.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see `fft`). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+        See :func:`fft` for more details.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    plan : object, optional
+        This argument is reserved for passing in a precomputed plan provided
+        by downstream FFT vendors. It is currently not used in SciPy.
+
+        .. versionadded:: 1.5.0
+
+    Returns
+    -------
+    out : ndarray
+        The result of the inverse real 2-D FFT.
+
+    See Also
+    --------
+    ihfftn : Compute the inverse of the N-D FFT of Hermitian input.
+
+    Notes
+    -----
+    This is really `ihfftn` with different defaults.
+    For more details see `ihfftn`.
+
+    """
+    return (Dispatchable(x, np.ndarray),)
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/_basic_backend.py b/.venv/lib/python3.11/site-packages/scipy/fft/_basic_backend.py
new file mode 100644
index 0000000000000000000000000000000000000000..9ca20dd6470934cdd064b4ee02c623f01cfac8be
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/_basic_backend.py
@@ -0,0 +1,197 @@
+from scipy._lib._array_api import (
+    array_namespace, is_numpy, xp_unsupported_param_msg, is_complex, xp_float_to_complex
+)
+from . import _pocketfft
+import numpy as np
+
+
+def _validate_fft_args(workers, plan, norm):
+    if workers is not None:
+        raise ValueError(xp_unsupported_param_msg("workers"))
+    if plan is not None:
+        raise ValueError(xp_unsupported_param_msg("plan"))
+    if norm is None:
+        norm = 'backward'
+    return norm
+
+
+# these functions expect complex input in the fft standard extension
+complex_funcs = {'fft', 'ifft', 'fftn', 'ifftn', 'hfft', 'irfft', 'irfftn'}
+
+# pocketfft is used whenever SCIPY_ARRAY_API is not set,
+# or x is a NumPy array or array-like.
+# When SCIPY_ARRAY_API is set, we try to use xp.fft for CuPy arrays,
+# PyTorch arrays and other array API standard supporting objects.
+# If xp.fft does not exist, we attempt to convert to np and back to use pocketfft.
+
+def _execute_1D(func_str, pocketfft_func, x, n, axis, norm, overwrite_x, workers, plan):
+    xp = array_namespace(x)
+
+    if is_numpy(xp):
+        x = np.asarray(x)
+        return pocketfft_func(x, n=n, axis=axis, norm=norm,
+                              overwrite_x=overwrite_x, workers=workers, plan=plan)
+
+    norm = _validate_fft_args(workers, plan, norm)
+    if hasattr(xp, 'fft'):
+        xp_func = getattr(xp.fft, func_str)
+        if func_str in complex_funcs:
+            try:
+                res = xp_func(x, n=n, axis=axis, norm=norm)
+            except: # backends may require complex input  # noqa: E722
+                x = xp_float_to_complex(x, xp)
+                res = xp_func(x, n=n, axis=axis, norm=norm)
+            return res
+        return xp_func(x, n=n, axis=axis, norm=norm)
+
+    x = np.asarray(x)
+    y = pocketfft_func(x, n=n, axis=axis, norm=norm)
+    return xp.asarray(y)
+
+
+def _execute_nD(func_str, pocketfft_func, x, s, axes, norm, overwrite_x, workers, plan):
+    xp = array_namespace(x)
+
+    if is_numpy(xp):
+        x = np.asarray(x)
+        return pocketfft_func(x, s=s, axes=axes, norm=norm,
+                              overwrite_x=overwrite_x, workers=workers, plan=plan)
+
+    norm = _validate_fft_args(workers, plan, norm)
+    if hasattr(xp, 'fft'):
+        xp_func = getattr(xp.fft, func_str)
+        if func_str in complex_funcs:
+            try:
+                res = xp_func(x, s=s, axes=axes, norm=norm)
+            except: # backends may require complex input  # noqa: E722
+                x = xp_float_to_complex(x, xp)
+                res = xp_func(x, s=s, axes=axes, norm=norm)
+            return res
+        return xp_func(x, s=s, axes=axes, norm=norm)
+
+    x = np.asarray(x)
+    y = pocketfft_func(x, s=s, axes=axes, norm=norm)
+    return xp.asarray(y)
+
+
+def fft(x, n=None, axis=-1, norm=None,
+        overwrite_x=False, workers=None, *, plan=None):
+    return _execute_1D('fft', _pocketfft.fft, x, n=n, axis=axis, norm=norm,
+                       overwrite_x=overwrite_x, workers=workers, plan=plan)
+
+
+def ifft(x, n=None, axis=-1, norm=None, overwrite_x=False, workers=None, *,
+         plan=None):
+    return _execute_1D('ifft', _pocketfft.ifft, x, n=n, axis=axis, norm=norm,
+                       overwrite_x=overwrite_x, workers=workers, plan=plan)
+
+
+def rfft(x, n=None, axis=-1, norm=None,
+         overwrite_x=False, workers=None, *, plan=None):
+    return _execute_1D('rfft', _pocketfft.rfft, x, n=n, axis=axis, norm=norm,
+                       overwrite_x=overwrite_x, workers=workers, plan=plan)
+
+
+def irfft(x, n=None, axis=-1, norm=None,
+          overwrite_x=False, workers=None, *, plan=None):
+    return _execute_1D('irfft', _pocketfft.irfft, x, n=n, axis=axis, norm=norm,
+                       overwrite_x=overwrite_x, workers=workers, plan=plan)
+
+
+def hfft(x, n=None, axis=-1, norm=None,
+         overwrite_x=False, workers=None, *, plan=None):
+    return _execute_1D('hfft', _pocketfft.hfft, x, n=n, axis=axis, norm=norm,
+                       overwrite_x=overwrite_x, workers=workers, plan=plan)
+
+
+def ihfft(x, n=None, axis=-1, norm=None,
+          overwrite_x=False, workers=None, *, plan=None):
+    return _execute_1D('ihfft', _pocketfft.ihfft, x, n=n, axis=axis, norm=norm,
+                       overwrite_x=overwrite_x, workers=workers, plan=plan)
+
+
+def fftn(x, s=None, axes=None, norm=None,
+         overwrite_x=False, workers=None, *, plan=None):
+    return _execute_nD('fftn', _pocketfft.fftn, x, s=s, axes=axes, norm=norm,
+                       overwrite_x=overwrite_x, workers=workers, plan=plan)
+
+
+
+def ifftn(x, s=None, axes=None, norm=None,
+          overwrite_x=False, workers=None, *, plan=None):
+    return _execute_nD('ifftn', _pocketfft.ifftn, x, s=s, axes=axes, norm=norm,
+                       overwrite_x=overwrite_x, workers=workers, plan=plan)
+
+
+def fft2(x, s=None, axes=(-2, -1), norm=None,
+         overwrite_x=False, workers=None, *, plan=None):
+    return fftn(x, s, axes, norm, overwrite_x, workers, plan=plan)
+
+
+def ifft2(x, s=None, axes=(-2, -1), norm=None,
+          overwrite_x=False, workers=None, *, plan=None):
+    return ifftn(x, s, axes, norm, overwrite_x, workers, plan=plan)
+
+
+def rfftn(x, s=None, axes=None, norm=None,
+          overwrite_x=False, workers=None, *, plan=None):
+    return _execute_nD('rfftn', _pocketfft.rfftn, x, s=s, axes=axes, norm=norm,
+                       overwrite_x=overwrite_x, workers=workers, plan=plan)
+
+
+def rfft2(x, s=None, axes=(-2, -1), norm=None,
+         overwrite_x=False, workers=None, *, plan=None):
+    return rfftn(x, s, axes, norm, overwrite_x, workers, plan=plan)
+
+
+def irfftn(x, s=None, axes=None, norm=None,
+           overwrite_x=False, workers=None, *, plan=None):
+    return _execute_nD('irfftn', _pocketfft.irfftn, x, s=s, axes=axes, norm=norm,
+                       overwrite_x=overwrite_x, workers=workers, plan=plan)
+
+
+def irfft2(x, s=None, axes=(-2, -1), norm=None,
+           overwrite_x=False, workers=None, *, plan=None):
+    return irfftn(x, s, axes, norm, overwrite_x, workers, plan=plan)
+
+
+def _swap_direction(norm):
+    if norm in (None, 'backward'):
+        norm = 'forward'
+    elif norm == 'forward':
+        norm = 'backward'
+    elif norm != 'ortho':
+        raise ValueError(f'Invalid norm value {norm}; should be "backward", '
+                         '"ortho", or "forward".')
+    return norm
+
+
+def hfftn(x, s=None, axes=None, norm=None,
+          overwrite_x=False, workers=None, *, plan=None):
+    xp = array_namespace(x)
+    if is_numpy(xp):
+        x = np.asarray(x)
+        return _pocketfft.hfftn(x, s, axes, norm, overwrite_x, workers, plan=plan)
+    if is_complex(x, xp):
+        x = xp.conj(x)
+    return irfftn(x, s, axes, _swap_direction(norm),
+                  overwrite_x, workers, plan=plan)
+
+
+def hfft2(x, s=None, axes=(-2, -1), norm=None,
+          overwrite_x=False, workers=None, *, plan=None):
+    return hfftn(x, s, axes, norm, overwrite_x, workers, plan=plan)
+
+
+def ihfftn(x, s=None, axes=None, norm=None,
+           overwrite_x=False, workers=None, *, plan=None):
+    xp = array_namespace(x)
+    if is_numpy(xp):
+        x = np.asarray(x)
+        return _pocketfft.ihfftn(x, s, axes, norm, overwrite_x, workers, plan=plan)
+    return xp.conj(rfftn(x, s, axes, _swap_direction(norm),
+                         overwrite_x, workers, plan=plan))
+
+def ihfft2(x, s=None, axes=(-2, -1), norm=None,
+           overwrite_x=False, workers=None, *, plan=None):
+    return ihfftn(x, s, axes, norm, overwrite_x, workers, plan=plan)
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/_debug_backends.py b/.venv/lib/python3.11/site-packages/scipy/fft/_debug_backends.py
new file mode 100644
index 0000000000000000000000000000000000000000..c9647c5d6ceddc73b97d95f562662ada02c1ae74
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/_debug_backends.py
@@ -0,0 +1,22 @@
+import numpy as np
+
+class NumPyBackend:
+    """Backend that uses numpy.fft"""
+    __ua_domain__ = "numpy.scipy.fft"
+
+    @staticmethod
+    def __ua_function__(method, args, kwargs):
+        kwargs.pop("overwrite_x", None)
+
+        fn = getattr(np.fft, method.__name__, None)
+        return (NotImplemented if fn is None
+                else fn(*args, **kwargs))
+
+
+class EchoBackend:
+    """Backend that just prints the __ua_function__ arguments"""
+    __ua_domain__ = "numpy.scipy.fft"
+
+    @staticmethod
+    def __ua_function__(method, args, kwargs):
+        print(method, args, kwargs, sep='\n')
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/_fftlog.py b/.venv/lib/python3.11/site-packages/scipy/fft/_fftlog.py
new file mode 100644
index 0000000000000000000000000000000000000000..7a01e2cc4a2734fdf9c11e33faf5bdd8bbfd8367
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/_fftlog.py
@@ -0,0 +1,227 @@
+"""Fast Hankel transforms using the FFTLog algorithm.
+
+The implementation closely follows the Fortran code of Hamilton (2000).
+
+added: 14/11/2020 Nicolas Tessore 
+"""
+
+from ._basic import _dispatch
+from scipy._lib.uarray import Dispatchable
+from ._fftlog_backend import fhtoffset
+from scipy._lib._array_api import xp_capabilities
+
+import numpy as np
+
+__all__ = ['fht', 'ifht', 'fhtoffset']
+
+
+@xp_capabilities(allow_dask_compute=True)
+@_dispatch
+def fht(a, dln, mu, offset=0.0, bias=0.0):
+    r'''Compute the fast Hankel transform.
+
+    Computes the discrete Hankel transform of a logarithmically spaced periodic
+    sequence using the FFTLog algorithm [1]_, [2]_.
+
+    Parameters
+    ----------
+    a : array_like (..., n)
+        Real periodic input array, uniformly logarithmically spaced.  For
+        multidimensional input, the transform is performed over the last axis.
+    dln : float
+        Uniform logarithmic spacing of the input array.
+    mu : float
+        Order of the Hankel transform, any positive or negative real number.
+    offset : float, optional
+        Offset of the uniform logarithmic spacing of the output array.
+    bias : float, optional
+        Exponent of power law bias, any positive or negative real number.
+
+    Returns
+    -------
+    A : array_like (..., n)
+        The transformed output array, which is real, periodic, uniformly
+        logarithmically spaced, and of the same shape as the input array.
+
+    See Also
+    --------
+    ifht : The inverse of `fht`.
+    fhtoffset : Return an optimal offset for `fht`.
+
+    Notes
+    -----
+    This function computes a discrete version of the Hankel transform
+
+    .. math::
+
+        A(k) = \int_{0}^{\infty} \! a(r) \, J_\mu(kr) \, k \, dr \;,
+
+    where :math:`J_\mu` is the Bessel function of order :math:`\mu`.  The index
+    :math:`\mu` may be any real number, positive or negative.  Note that the
+    numerical Hankel transform uses an integrand of :math:`k \, dr`, while the
+    mathematical Hankel transform is commonly defined using :math:`r \, dr`.
+
+    The input array `a` is a periodic sequence of length :math:`n`, uniformly
+    logarithmically spaced with spacing `dln`,
+
+    .. math::
+
+        a_j = a(r_j) \;, \quad
+        r_j = r_c \exp[(j-j_c) \, \mathtt{dln}]
+
+    centred about the point :math:`r_c`.  Note that the central index
+    :math:`j_c = (n-1)/2` is half-integral if :math:`n` is even, so that
+    :math:`r_c` falls between two input elements.  Similarly, the output
+    array `A` is a periodic sequence of length :math:`n`, also uniformly
+    logarithmically spaced with spacing `dln`
+
+    .. math::
+
+       A_j = A(k_j) \;, \quad
+       k_j = k_c \exp[(j-j_c) \, \mathtt{dln}]
+
+    centred about the point :math:`k_c`.
+
+    The centre points :math:`r_c` and :math:`k_c` of the periodic intervals may
+    be chosen arbitrarily, but it would be usual to choose the product
+    :math:`k_c r_c = k_j r_{n-1-j} = k_{n-1-j} r_j` to be unity.  This can be
+    changed using the `offset` parameter, which controls the logarithmic offset
+    :math:`\log(k_c) = \mathtt{offset} - \log(r_c)` of the output array.
+    Choosing an optimal value for `offset` may reduce ringing of the discrete
+    Hankel transform.
+
+    If the `bias` parameter is nonzero, this function computes a discrete
+    version of the biased Hankel transform
+
+    .. math::
+
+        A(k) = \int_{0}^{\infty} \! a_q(r) \, (kr)^q \, J_\mu(kr) \, k \, dr
+
+    where :math:`q` is the value of `bias`, and a power law bias
+    :math:`a_q(r) = a(r) \, (kr)^{-q}` is applied to the input sequence.
+    Biasing the transform can help approximate the continuous transform of
+    :math:`a(r)` if there is a value :math:`q` such that :math:`a_q(r)` is
+    close to a periodic sequence, in which case the resulting :math:`A(k)` will
+    be close to the continuous transform.
+
+    References
+    ----------
+    .. [1] Talman J. D., 1978, J. Comp. Phys., 29, 35
+    .. [2] Hamilton A. J. S., 2000, MNRAS, 312, 257 (astro-ph/9905191)
+
+    Examples
+    --------
+
+    This example is the adapted version of ``fftlogtest.f`` which is provided
+    in [2]_. It evaluates the integral
+
+    .. math::
+
+        \int^\infty_0 r^{\mu+1} \exp(-r^2/2) J_\mu(kr) k dr
+        = k^{\mu+1} \exp(-k^2/2) .
+
+    >>> import numpy as np
+    >>> from scipy import fft
+    >>> import matplotlib.pyplot as plt
+
+    Parameters for the transform.
+
+    >>> mu = 0.0                     # Order mu of Bessel function
+    >>> r = np.logspace(-7, 1, 128)  # Input evaluation points
+    >>> dln = np.log(r[1]/r[0])      # Step size
+    >>> offset = fft.fhtoffset(dln, initial=-6*np.log(10), mu=mu)
+    >>> k = np.exp(offset)/r[::-1]   # Output evaluation points
+
+    Define the analytical function.
+
+    >>> def f(x, mu):
+    ...     """Analytical function: x^(mu+1) exp(-x^2/2)."""
+    ...     return x**(mu + 1)*np.exp(-x**2/2)
+
+    Evaluate the function at ``r`` and compute the corresponding values at
+    ``k`` using FFTLog.
+
+    >>> a_r = f(r, mu)
+    >>> fht = fft.fht(a_r, dln, mu=mu, offset=offset)
+
+    For this example we can actually compute the analytical response (which in
+    this case is the same as the input function) for comparison and compute the
+    relative error.
+
+    >>> a_k = f(k, mu)
+    >>> rel_err = abs((fht-a_k)/a_k)
+
+    Plot the result.
+
+    >>> figargs = {'sharex': True, 'sharey': True, 'constrained_layout': True}
+    >>> fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4), **figargs)
+    >>> ax1.set_title(r'$r^{\mu+1}\ \exp(-r^2/2)$')
+    >>> ax1.loglog(r, a_r, 'k', lw=2)
+    >>> ax1.set_xlabel('r')
+    >>> ax2.set_title(r'$k^{\mu+1} \exp(-k^2/2)$')
+    >>> ax2.loglog(k, a_k, 'k', lw=2, label='Analytical')
+    >>> ax2.loglog(k, fht, 'C3--', lw=2, label='FFTLog')
+    >>> ax2.set_xlabel('k')
+    >>> ax2.legend(loc=3, framealpha=1)
+    >>> ax2.set_ylim([1e-10, 1e1])
+    >>> ax2b = ax2.twinx()
+    >>> ax2b.loglog(k, rel_err, 'C0', label='Rel. Error (-)')
+    >>> ax2b.set_ylabel('Rel. Error (-)', color='C0')
+    >>> ax2b.tick_params(axis='y', labelcolor='C0')
+    >>> ax2b.legend(loc=4, framealpha=1)
+    >>> ax2b.set_ylim([1e-9, 1e-3])
+    >>> plt.show()
+
+    '''
+    return (Dispatchable(a, np.ndarray),)
+
+
+@xp_capabilities(allow_dask_compute=True)
+@_dispatch
+def ifht(A, dln, mu, offset=0.0, bias=0.0):
+    r"""Compute the inverse fast Hankel transform.
+
+    Computes the discrete inverse Hankel transform of a logarithmically spaced
+    periodic sequence. This is the inverse operation to `fht`.
+
+    Parameters
+    ----------
+    A : array_like (..., n)
+        Real periodic input array, uniformly logarithmically spaced.  For
+        multidimensional input, the transform is performed over the last axis.
+    dln : float
+        Uniform logarithmic spacing of the input array.
+    mu : float
+        Order of the Hankel transform, any positive or negative real number.
+    offset : float, optional
+        Offset of the uniform logarithmic spacing of the output array.
+    bias : float, optional
+        Exponent of power law bias, any positive or negative real number.
+
+    Returns
+    -------
+    a : array_like (..., n)
+        The transformed output array, which is real, periodic, uniformly
+        logarithmically spaced, and of the same shape as the input array.
+
+    See Also
+    --------
+    fht : Definition of the fast Hankel transform.
+    fhtoffset : Return an optimal offset for `ifht`.
+
+    Notes
+    -----
+    This function computes a discrete version of the Hankel transform
+
+    .. math::
+
+        a(r) = \int_{0}^{\infty} \! A(k) \, J_\mu(kr) \, r \, dk \;,
+
+    where :math:`J_\mu` is the Bessel function of order :math:`\mu`.  The index
+    :math:`\mu` may be any real number, positive or negative. Note that the
+    numerical inverse Hankel transform uses an integrand of :math:`r \, dk`, while the
+    mathematical inverse Hankel transform is commonly defined using :math:`k \, dk`.
+
+    See `fht` for further details.
+    """
+    return (Dispatchable(A, np.ndarray),)
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/_fftlog_backend.py b/.venv/lib/python3.11/site-packages/scipy/fft/_fftlog_backend.py
new file mode 100644
index 0000000000000000000000000000000000000000..d361708ea96c0bb590ec023e775e9bcfdad71a16
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/_fftlog_backend.py
@@ -0,0 +1,201 @@
+import numpy as np
+from warnings import warn
+from ._basic import rfft, irfft
+from ..special import loggamma, poch
+
+from scipy._lib._array_api import array_namespace, xp_capabilities
+
+__all__ = ['fht', 'ifht', 'fhtoffset']
+
+# constants
+LN_2 = np.log(2)
+
+
+def fht(a, dln, mu, offset=0.0, bias=0.0):
+    xp = array_namespace(a)
+    a = xp.asarray(a)
+
+    # size of transform
+    n = a.shape[-1]
+
+    # bias input array
+    if bias != 0:
+        # a_q(r) = a(r) (r/r_c)^{-q}
+        j_c = (n-1)/2
+        j = xp.arange(n, dtype=xp.float64)
+        a = a * xp.exp(-bias*(j - j_c)*dln)
+
+    # compute FHT coefficients
+    u = xp.asarray(fhtcoeff(n, dln, mu, offset=offset, bias=bias))
+
+    # transform
+    A = _fhtq(a, u, xp=xp)
+
+    # bias output array
+    if bias != 0:
+        # A(k) = A_q(k) (k/k_c)^{-q} (k_c r_c)^{-q}
+        A *= xp.exp(-bias*((j - j_c)*dln + offset))
+
+    return A
+
+
+def ifht(A, dln, mu, offset=0.0, bias=0.0):
+    xp = array_namespace(A)
+    A = xp.asarray(A)
+
+    # size of transform
+    n = A.shape[-1]
+
+    # bias input array
+    if bias != 0:
+        # A_q(k) = A(k) (k/k_c)^{q} (k_c r_c)^{q}
+        j_c = (n-1)/2
+        j = xp.arange(n, dtype=xp.float64)
+        A = A * xp.exp(bias*((j - j_c)*dln + offset))
+
+    # compute FHT coefficients
+    u = xp.asarray(fhtcoeff(n, dln, mu, offset=offset, bias=bias, inverse=True))
+
+    # transform
+    a = _fhtq(A, u, inverse=True, xp=xp)
+
+    # bias output array
+    if bias != 0:
+        # a(r) = a_q(r) (r/r_c)^{q}
+        a /= xp.exp(-bias*(j - j_c)*dln)
+
+    return a
+
+
+def fhtcoeff(n, dln, mu, offset=0.0, bias=0.0, inverse=False):
+    """Compute the coefficient array for a fast Hankel transform."""
+    lnkr, q = offset, bias
+
+    # Hankel transform coefficients
+    # u_m = (kr)^{-i 2m pi/(n dlnr)} U_mu(q + i 2m pi/(n dlnr))
+    # with U_mu(x) = 2^x Gamma((mu+1+x)/2)/Gamma((mu+1-x)/2)
+    xp = (mu+1+q)/2
+    xm = (mu+1-q)/2
+    y = np.linspace(0, np.pi*(n//2)/(n*dln), n//2+1)
+    u = np.empty(n//2+1, dtype=complex)
+    v = np.empty(n//2+1, dtype=complex)
+    u.imag[:] = y
+    u.real[:] = xm
+    loggamma(u, out=v)
+    u.real[:] = xp
+    loggamma(u, out=u)
+    y *= 2*(LN_2 - lnkr)
+    u.real -= v.real
+    u.real += LN_2*q
+    u.imag += v.imag
+    u.imag += y
+    np.exp(u, out=u)
+
+    # fix last coefficient to be real
+    if n % 2 == 0:
+        u.imag[-1] = 0
+
+    # deal with special cases
+    if not np.isfinite(u[0]):
+        # write u_0 = 2^q Gamma(xp)/Gamma(xm) = 2^q poch(xm, xp-xm)
+        # poch() handles special cases for negative integers correctly
+        u[0] = 2**q * poch(xm, xp-xm)
+        # the coefficient may be inf or 0, meaning the transform or the
+        # inverse transform, respectively, is singular
+
+    # check for singular transform or singular inverse transform
+    if np.isinf(u[0]) and not inverse:
+        warn('singular transform; consider changing the bias', stacklevel=3)
+        # fix coefficient to obtain (potentially correct) transform anyway
+        u = np.copy(u)
+        u[0] = 0
+    elif u[0] == 0 and inverse:
+        warn('singular inverse transform; consider changing the bias', stacklevel=3)
+        # fix coefficient to obtain (potentially correct) inverse anyway
+        u = np.copy(u)
+        u[0] = np.inf
+
+    return u
+
+
+@xp_capabilities(out_of_scope=True)
+def fhtoffset(dln, mu, initial=0.0, bias=0.0):
+    """Return optimal offset for a fast Hankel transform.
+
+    Returns an offset close to `initial` that fulfils the low-ringing
+    condition of [1]_ for the fast Hankel transform `fht` with logarithmic
+    spacing `dln`, order `mu` and bias `bias`.
+
+    Parameters
+    ----------
+    dln : float
+        Uniform logarithmic spacing of the transform.
+    mu : float
+        Order of the Hankel transform, any positive or negative real number.
+    initial : float, optional
+        Initial value for the offset. Returns the closest value that fulfils
+        the low-ringing condition.
+    bias : float, optional
+        Exponent of power law bias, any positive or negative real number.
+
+    Returns
+    -------
+    offset : float
+        Optimal offset of the uniform logarithmic spacing of the transform that
+        fulfils a low-ringing condition.
+
+    Examples
+    --------
+    >>> from scipy.fft import fhtoffset
+    >>> dln = 0.1
+    >>> mu = 2.0
+    >>> initial = 0.5
+    >>> bias = 0.0
+    >>> offset = fhtoffset(dln, mu, initial, bias)
+    >>> offset
+    0.5454581477676637
+
+    See Also
+    --------
+    fht : Definition of the fast Hankel transform.
+
+    References
+    ----------
+    .. [1] Hamilton A. J. S., 2000, MNRAS, 312, 257 (astro-ph/9905191)
+
+    """
+
+    lnkr, q = initial, bias
+
+    xp = (mu+1+q)/2
+    xm = (mu+1-q)/2
+    y = np.pi/(2*dln)
+    zp = loggamma(xp + 1j*y)
+    zm = loggamma(xm + 1j*y)
+    arg = (LN_2 - lnkr)/dln + (zp.imag + zm.imag)/np.pi
+    return lnkr + (arg - np.round(arg))*dln
+
+
+def _fhtq(a, u, inverse=False, *, xp=None):
+    """Compute the biased fast Hankel transform.
+
+    This is the basic FFTLog routine.
+    """
+    if xp is None:
+        xp = np
+
+    # size of transform
+    n = a.shape[-1]
+
+    # biased fast Hankel transform via real FFT
+    A = rfft(a, axis=-1)
+    if not inverse:
+        # forward transform
+        A *= u
+    else:
+        # backward transform
+        A /= xp.conj(u)
+    A = irfft(A, n, axis=-1)
+    A = xp.flip(A, axis=-1)
+
+    return A
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/_helper.py b/.venv/lib/python3.11/site-packages/scipy/fft/_helper.py
new file mode 100644
index 0000000000000000000000000000000000000000..29896a30520ed7ce3e3b6eb080ee50a656194734
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/_helper.py
@@ -0,0 +1,355 @@
+from functools import update_wrapper, lru_cache
+import inspect
+
+from ._pocketfft import helper as _helper
+
+import numpy as np
+from scipy._lib._array_api import array_namespace
+from scipy._lib._array_api import xp_capabilities
+
+
+_init_nd_shape_and_axes = _helper._init_nd_shape_and_axes
+
+
+def next_fast_len(target, real=False):
+    """Find the next fast size of input data to ``fft``, for zero-padding, etc.
+
+    SciPy's FFT algorithms gain their speed by a recursive divide and conquer
+    strategy. This relies on efficient functions for small prime factors of the
+    input length. Thus, the transforms are fastest when using composites of the
+    prime factors handled by the fft implementation. If there are efficient
+    functions for all radices <= `n`, then the result will be a number `x`
+    >= ``target`` with only prime factors < `n`. (Also known as `n`-smooth
+    numbers)
+
+    Parameters
+    ----------
+    target : int
+        Length to start searching from. Must be a positive integer.
+    real : bool, optional
+        True if the FFT involves real input or output (e.g., `rfft` or `hfft`
+        but not `fft`). Defaults to False.
+
+    Returns
+    -------
+    out : int
+        The smallest fast length greater than or equal to ``target``.
+
+    Notes
+    -----
+    The result of this function may change in future as performance
+    considerations change, for example, if new prime factors are added.
+
+    Calling `fft` or `ifft` with real input data performs an ``'R2C'``
+    transform internally.
+
+    Examples
+    --------
+    On a particular machine, an FFT of prime length takes 11.4 ms:
+
+    >>> from scipy import fft
+    >>> import numpy as np
+    >>> rng = np.random.default_rng()
+    >>> min_len = 93059  # prime length is worst case for speed
+    >>> a = rng.standard_normal(min_len)
+    >>> b = fft.fft(a)
+
+    Zero-padding to the next regular length reduces computation time to
+    1.6 ms, a speedup of 7.3 times:
+
+    >>> fft.next_fast_len(min_len, real=True)
+    93312
+    >>> b = fft.fft(a, 93312)
+
+    Rounding up to the next power of 2 is not optimal, taking 3.0 ms to
+    compute; 1.9 times longer than the size given by ``next_fast_len``:
+
+    >>> b = fft.fft(a, 131072)
+
+    """
+    pass
+
+
+# Directly wrap the c-function good_size but take the docstring etc., from the
+# next_fast_len function above
+_sig = inspect.signature(next_fast_len)
+next_fast_len = update_wrapper(lru_cache(_helper.good_size), next_fast_len)
+next_fast_len = xp_capabilities(out_of_scope=True)(next_fast_len)
+next_fast_len.__wrapped__ = _helper.good_size
+next_fast_len.__signature__ = _sig
+
+
+def prev_fast_len(target, real=False):
+    """Find the previous fast size of input data to ``fft``.
+    Useful for discarding a minimal number of samples before FFT.
+
+    SciPy's FFT algorithms gain their speed by a recursive divide and conquer
+    strategy. This relies on efficient functions for small prime factors of the
+    input length. Thus, the transforms are fastest when using composites of the
+    prime factors handled by the fft implementation. If there are efficient
+    functions for all radices <= `n`, then the result will be a number `x`
+    <= ``target`` with only prime factors <= `n`. (Also known as `n`-smooth
+    numbers)
+
+    Parameters
+    ----------
+    target : int
+        Maximum length to search until. Must be a positive integer.
+    real : bool, optional
+        True if the FFT involves real input or output (e.g., `rfft` or `hfft`
+        but not `fft`). Defaults to False.
+
+    Returns
+    -------
+    out : int
+        The largest fast length less than or equal to ``target``.
+
+    Notes
+    -----
+    The result of this function may change in future as performance
+    considerations change, for example, if new prime factors are added.
+
+    Calling `fft` or `ifft` with real input data performs an ``'R2C'``
+    transform internally.
+
+    In the current implementation, prev_fast_len assumes radices of
+    2,3,5,7,11 for complex FFT and 2,3,5 for real FFT.
+
+    Examples
+    --------
+    On a particular machine, an FFT of prime length takes 16.2 ms:
+
+    >>> from scipy import fft
+    >>> import numpy as np
+    >>> rng = np.random.default_rng()
+    >>> max_len = 93059  # prime length is worst case for speed
+    >>> a = rng.standard_normal(max_len)
+    >>> b = fft.fft(a)
+
+    Performing FFT on the maximum fast length less than max_len
+    reduces the computation time to 1.5 ms, a speedup of 10.5 times:
+
+    >>> fft.prev_fast_len(max_len, real=True)
+    92160
+    >>> c = fft.fft(a[:92160]) # discard last 899 samples
+
+    """
+    pass
+
+
+# Directly wrap the c-function prev_good_size but take the docstring etc.,
+# from the prev_fast_len function above
+_sig_prev_fast_len = inspect.signature(prev_fast_len)
+prev_fast_len = update_wrapper(lru_cache()(_helper.prev_good_size), prev_fast_len)
+prev_fast_len = xp_capabilities(out_of_scope=True)(prev_fast_len)
+prev_fast_len.__wrapped__ = _helper.prev_good_size
+prev_fast_len.__signature__ = _sig_prev_fast_len
+
+
+@xp_capabilities()
+def fftfreq(n, d=1.0, *, xp=None, device=None):
+    """Return the Discrete Fourier Transform sample frequencies.
+
+    The returned float array `f` contains the frequency bin centers in cycles
+    per unit of the sample spacing (with zero at the start).  For instance, if
+    the sample spacing is in seconds, then the frequency unit is cycles/second.
+
+    Given a window length `n` and a sample spacing `d`::
+
+      f = [0, 1, ...,   n/2-1,     -n/2, ..., -1] / (d*n)   if n is even
+      f = [0, 1, ..., (n-1)/2, -(n-1)/2, ..., -1] / (d*n)   if n is odd
+
+    Parameters
+    ----------
+    n : int
+        Window length.
+    d : scalar, optional
+        Sample spacing (inverse of the sampling rate). Defaults to 1.
+    xp : array_namespace, optional
+        The namespace for the return array. Default is None, where NumPy is used.
+    device : device, optional
+        The device for the return array.
+        Only valid when `xp.fft.fftfreq` implements the device parameter.
+
+    Returns
+    -------
+    f : ndarray
+        Array of length `n` containing the sample frequencies.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> import scipy.fft
+    >>> signal = np.array([-2, 8, 6, 4, 1, 0, 3, 5], dtype=float)
+    >>> fourier = scipy.fft.fft(signal)
+    >>> n = signal.size
+    >>> timestep = 0.1
+    >>> freq = scipy.fft.fftfreq(n, d=timestep)
+    >>> freq
+    array([ 0.  ,  1.25,  2.5 , ..., -3.75, -2.5 , -1.25])
+
+    """
+    xp = np if xp is None else xp
+    # numpy does not yet support the `device` keyword
+    # `xp.__name__ != 'numpy'` should be removed when numpy is compatible
+    if hasattr(xp, 'fft') and xp.__name__ != 'numpy':
+        return xp.fft.fftfreq(n, d=d, device=device)
+    if device is not None:
+        raise ValueError('device parameter is not supported for input array type')
+    return np.fft.fftfreq(n, d=d)
+
+
+@xp_capabilities()
+def rfftfreq(n, d=1.0, *, xp=None, device=None):
+    """Return the Discrete Fourier Transform sample frequencies
+    (for usage with rfft, irfft).
+
+    The returned float array `f` contains the frequency bin centers in cycles
+    per unit of the sample spacing (with zero at the start).  For instance, if
+    the sample spacing is in seconds, then the frequency unit is cycles/second.
+
+    Given a window length `n` and a sample spacing `d`::
+
+      f = [0, 1, ...,     n/2-1,     n/2] / (d*n)   if n is even
+      f = [0, 1, ..., (n-1)/2-1, (n-1)/2] / (d*n)   if n is odd
+
+    Unlike `fftfreq` (but like `scipy.fftpack.rfftfreq`)
+    the Nyquist frequency component is considered to be positive.
+
+    Parameters
+    ----------
+    n : int
+        Window length.
+    d : scalar, optional
+        Sample spacing (inverse of the sampling rate). Defaults to 1.
+    xp : array_namespace, optional
+        The namespace for the return array. Default is None, where NumPy is used.
+    device : device, optional
+        The device for the return array.
+        Only valid when `xp.fft.rfftfreq` implements the device parameter.
+
+    Returns
+    -------
+    f : ndarray
+        Array of length ``n//2 + 1`` containing the sample frequencies.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> import scipy.fft
+    >>> signal = np.array([-2, 8, 6, 4, 1, 0, 3, 5, -3, 4], dtype=float)
+    >>> fourier = scipy.fft.rfft(signal)
+    >>> n = signal.size
+    >>> sample_rate = 100
+    >>> freq = scipy.fft.fftfreq(n, d=1./sample_rate)
+    >>> freq
+    array([  0.,  10.,  20., ..., -30., -20., -10.])
+    >>> freq = scipy.fft.rfftfreq(n, d=1./sample_rate)
+    >>> freq
+    array([  0.,  10.,  20.,  30.,  40.,  50.])
+
+    """
+    xp = np if xp is None else xp
+    # numpy does not yet support the `device` keyword
+    # `xp.__name__ != 'numpy'` should be removed when numpy is compatible
+    if hasattr(xp, 'fft') and xp.__name__ != 'numpy':
+        return xp.fft.rfftfreq(n, d=d, device=device)
+    if device is not None:
+        raise ValueError('device parameter is not supported for input array type')
+    return np.fft.rfftfreq(n, d=d)
+
+
+@xp_capabilities()
+def fftshift(x, axes=None):
+    """Shift the zero-frequency component to the center of the spectrum.
+
+    This function swaps half-spaces for all axes listed (defaults to all).
+    Note that ``y[0]`` is the Nyquist component only if ``len(x)`` is even.
+
+    Parameters
+    ----------
+    x : array_like
+        Input array.
+    axes : int or shape tuple, optional
+        Axes over which to shift.  Default is None, which shifts all axes.
+
+    Returns
+    -------
+    y : ndarray
+        The shifted array.
+
+    See Also
+    --------
+    ifftshift : The inverse of `fftshift`.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> freqs = np.fft.fftfreq(10, 0.1)
+    >>> freqs
+    array([ 0.,  1.,  2., ..., -3., -2., -1.])
+    >>> np.fft.fftshift(freqs)
+    array([-5., -4., -3., -2., -1.,  0.,  1.,  2.,  3.,  4.])
+
+    Shift the zero-frequency component only along the second axis:
+
+    >>> freqs = np.fft.fftfreq(9, d=1./9).reshape(3, 3)
+    >>> freqs
+    array([[ 0.,  1.,  2.],
+           [ 3.,  4., -4.],
+           [-3., -2., -1.]])
+    >>> np.fft.fftshift(freqs, axes=(1,))
+    array([[ 2.,  0.,  1.],
+           [-4.,  3.,  4.],
+           [-1., -3., -2.]])
+
+    """
+    xp = array_namespace(x)
+    if hasattr(xp, 'fft'):
+        return xp.fft.fftshift(x, axes=axes)
+    x = np.asarray(x)
+    y = np.fft.fftshift(x, axes=axes)
+    return xp.asarray(y)
+
+
+@xp_capabilities()
+def ifftshift(x, axes=None):
+    """The inverse of `fftshift`. Although identical for even-length `x`, the
+    functions differ by one sample for odd-length `x`.
+
+    Parameters
+    ----------
+    x : array_like
+        Input array.
+    axes : int or shape tuple, optional
+        Axes over which to calculate.  Defaults to None, which shifts all axes.
+
+    Returns
+    -------
+    y : ndarray
+        The shifted array.
+
+    See Also
+    --------
+    fftshift : Shift zero-frequency component to the center of the spectrum.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> freqs = np.fft.fftfreq(9, d=1./9).reshape(3, 3)
+    >>> freqs
+    array([[ 0.,  1.,  2.],
+           [ 3.,  4., -4.],
+           [-3., -2., -1.]])
+    >>> np.fft.ifftshift(np.fft.fftshift(freqs))
+    array([[ 0.,  1.,  2.],
+           [ 3.,  4., -4.],
+           [-3., -2., -1.]])
+
+    """
+    xp = array_namespace(x)
+    if hasattr(xp, 'fft'):
+        return xp.fft.ifftshift(x, axes=axes)
+    x = np.asarray(x)
+    y = np.fft.ifftshift(x, axes=axes)
+    return xp.asarray(y)
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/LICENSE.md b/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/LICENSE.md
new file mode 100644
index 0000000000000000000000000000000000000000..1b5163d8435976c24988afbd39ded304947178cb
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/LICENSE.md
@@ -0,0 +1,25 @@
+Copyright (C) 2010-2019 Max-Planck-Society
+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 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.
+* Neither the name of the copyright holder nor the names of its contributors may
+  be used to endorse or promote products derived from this software without
+  specific prior written permission.
+
+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.
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/__init__.py b/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..0671484c9a0780df353b9b783813b6fa7492d38d
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/__init__.py
@@ -0,0 +1,9 @@
+""" FFT backend using pypocketfft """
+
+from .basic import *
+from .realtransforms import *
+from .helper import *
+
+from scipy._lib._testutils import PytestTester
+test = PytestTester(__name__)
+del PytestTester
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/basic.py b/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/basic.py
new file mode 100644
index 0000000000000000000000000000000000000000..bd2d0d33958021c431171b72f72c37363ac98e03
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/basic.py
@@ -0,0 +1,251 @@
+"""
+Discrete Fourier Transforms - basic.py
+"""
+import numpy as np
+import functools
+from . import pypocketfft as pfft
+from .helper import (_asfarray, _init_nd_shape_and_axes, _datacopied,
+                     _fix_shape, _fix_shape_1d, _normalization,
+                     _workers)
+
+def c2c(forward, x, n=None, axis=-1, norm=None, overwrite_x=False,
+        workers=None, *, plan=None):
+    """ Return discrete Fourier transform of real or complex sequence. """
+    if plan is not None:
+        raise NotImplementedError('Passing a precomputed plan is not yet '
+                                  'supported by scipy.fft functions')
+    tmp = _asfarray(x)
+    overwrite_x = overwrite_x or _datacopied(tmp, x)
+    norm = _normalization(norm, forward)
+    workers = _workers(workers)
+
+    if n is not None:
+        tmp, copied = _fix_shape_1d(tmp, n, axis)
+        overwrite_x = overwrite_x or copied
+    elif tmp.shape[axis] < 1:
+        message = f"invalid number of data points ({tmp.shape[axis]}) specified"
+        raise ValueError(message)
+
+    out = (tmp if overwrite_x and tmp.dtype.kind == 'c' else None)
+
+    return pfft.c2c(tmp, (axis,), forward, norm, out, workers)
+
+
+fft = functools.partial(c2c, True)
+fft.__name__ = 'fft'
+ifft = functools.partial(c2c, False)
+ifft.__name__ = 'ifft'
+
+
+def r2c(forward, x, n=None, axis=-1, norm=None, overwrite_x=False,
+        workers=None, *, plan=None):
+    """
+    Discrete Fourier transform of a real sequence.
+    """
+    if plan is not None:
+        raise NotImplementedError('Passing a precomputed plan is not yet '
+                                  'supported by scipy.fft functions')
+    tmp = _asfarray(x)
+    norm = _normalization(norm, forward)
+    workers = _workers(workers)
+
+    if not np.isrealobj(tmp):
+        raise TypeError("x must be a real sequence")
+
+    if n is not None:
+        tmp, _ = _fix_shape_1d(tmp, n, axis)
+    elif tmp.shape[axis] < 1:
+        raise ValueError(f"invalid number of data points ({tmp.shape[axis]}) specified")
+
+    # Note: overwrite_x is not utilised
+    return pfft.r2c(tmp, (axis,), forward, norm, None, workers)
+
+
+rfft = functools.partial(r2c, True)
+rfft.__name__ = 'rfft'
+ihfft = functools.partial(r2c, False)
+ihfft.__name__ = 'ihfft'
+
+
+def c2r(forward, x, n=None, axis=-1, norm=None, overwrite_x=False,
+        workers=None, *, plan=None):
+    """
+    Return inverse discrete Fourier transform of real sequence x.
+    """
+    if plan is not None:
+        raise NotImplementedError('Passing a precomputed plan is not yet '
+                                  'supported by scipy.fft functions')
+    tmp = _asfarray(x)
+    norm = _normalization(norm, forward)
+    workers = _workers(workers)
+
+    # TODO: Optimize for hermitian and real?
+    if np.isrealobj(tmp):
+        tmp = tmp + 0.j
+
+    # Last axis utilizes hermitian symmetry
+    if n is None:
+        n = (tmp.shape[axis] - 1) * 2
+        if n < 1:
+            raise ValueError(f"Invalid number of data points ({n}) specified")
+    else:
+        tmp, _ = _fix_shape_1d(tmp, (n//2) + 1, axis)
+
+    # Note: overwrite_x is not utilized
+    return pfft.c2r(tmp, (axis,), n, forward, norm, None, workers)
+
+
+hfft = functools.partial(c2r, True)
+hfft.__name__ = 'hfft'
+irfft = functools.partial(c2r, False)
+irfft.__name__ = 'irfft'
+
+
+def hfft2(x, s=None, axes=(-2,-1), norm=None, overwrite_x=False, workers=None,
+          *, plan=None):
+    """
+    2-D discrete Fourier transform of a Hermitian sequence
+    """
+    if plan is not None:
+        raise NotImplementedError('Passing a precomputed plan is not yet '
+                                  'supported by scipy.fft functions')
+    return hfftn(x, s, axes, norm, overwrite_x, workers)
+
+
+def ihfft2(x, s=None, axes=(-2,-1), norm=None, overwrite_x=False, workers=None,
+           *, plan=None):
+    """
+    2-D discrete inverse Fourier transform of a Hermitian sequence
+    """
+    if plan is not None:
+        raise NotImplementedError('Passing a precomputed plan is not yet '
+                                  'supported by scipy.fft functions')
+    return ihfftn(x, s, axes, norm, overwrite_x, workers)
+
+
+def c2cn(forward, x, s=None, axes=None, norm=None, overwrite_x=False,
+         workers=None, *, plan=None):
+    """
+    Return multidimensional discrete Fourier transform.
+    """
+    if plan is not None:
+        raise NotImplementedError('Passing a precomputed plan is not yet '
+                                  'supported by scipy.fft functions')
+    tmp = _asfarray(x)
+
+    shape, axes = _init_nd_shape_and_axes(tmp, s, axes)
+    overwrite_x = overwrite_x or _datacopied(tmp, x)
+    workers = _workers(workers)
+
+    if len(axes) == 0:
+        return x
+
+    tmp, copied = _fix_shape(tmp, shape, axes)
+    overwrite_x = overwrite_x or copied
+
+    norm = _normalization(norm, forward)
+    out = (tmp if overwrite_x and tmp.dtype.kind == 'c' else None)
+
+    return pfft.c2c(tmp, axes, forward, norm, out, workers)
+
+
+fftn = functools.partial(c2cn, True)
+fftn.__name__ = 'fftn'
+ifftn = functools.partial(c2cn, False)
+ifftn.__name__ = 'ifftn'
+
+def r2cn(forward, x, s=None, axes=None, norm=None, overwrite_x=False,
+         workers=None, *, plan=None):
+    """Return multidimensional discrete Fourier transform of real input"""
+    if plan is not None:
+        raise NotImplementedError('Passing a precomputed plan is not yet '
+                                  'supported by scipy.fft functions')
+    tmp = _asfarray(x)
+
+    if not np.isrealobj(tmp):
+        raise TypeError("x must be a real sequence")
+
+    shape, axes = _init_nd_shape_and_axes(tmp, s, axes)
+    tmp, _ = _fix_shape(tmp, shape, axes)
+    norm = _normalization(norm, forward)
+    workers = _workers(workers)
+
+    if len(axes) == 0:
+        raise ValueError("at least 1 axis must be transformed")
+
+    # Note: overwrite_x is not utilized
+    return pfft.r2c(tmp, axes, forward, norm, None, workers)
+
+
+rfftn = functools.partial(r2cn, True)
+rfftn.__name__ = 'rfftn'
+ihfftn = functools.partial(r2cn, False)
+ihfftn.__name__ = 'ihfftn'
+
+
+def c2rn(forward, x, s=None, axes=None, norm=None, overwrite_x=False,
+         workers=None, *, plan=None):
+    """Multidimensional inverse discrete fourier transform with real output"""
+    if plan is not None:
+        raise NotImplementedError('Passing a precomputed plan is not yet '
+                                  'supported by scipy.fft functions')
+    tmp = _asfarray(x)
+
+    # TODO: Optimize for hermitian and real?
+    if np.isrealobj(tmp):
+        tmp = tmp + 0.j
+
+    noshape = s is None
+    shape, axes = _init_nd_shape_and_axes(tmp, s, axes)
+
+    if len(axes) == 0:
+        raise ValueError("at least 1 axis must be transformed")
+
+    shape = list(shape)
+    if noshape:
+        shape[-1] = (x.shape[axes[-1]] - 1) * 2
+
+    norm = _normalization(norm, forward)
+    workers = _workers(workers)
+
+    # Last axis utilizes hermitian symmetry
+    lastsize = shape[-1]
+    shape[-1] = (shape[-1] // 2) + 1
+
+    tmp, _ = tuple(_fix_shape(tmp, shape, axes))
+
+    # Note: overwrite_x is not utilized
+    return pfft.c2r(tmp, axes, lastsize, forward, norm, None, workers)
+
+
+hfftn = functools.partial(c2rn, True)
+hfftn.__name__ = 'hfftn'
+irfftn = functools.partial(c2rn, False)
+irfftn.__name__ = 'irfftn'
+
+
+def r2r_fftpack(forward, x, n=None, axis=-1, norm=None, overwrite_x=False):
+    """FFT of a real sequence, returning fftpack half complex format"""
+    tmp = _asfarray(x)
+    overwrite_x = overwrite_x or _datacopied(tmp, x)
+    norm = _normalization(norm, forward)
+    workers = _workers(None)
+
+    if tmp.dtype.kind == 'c':
+        raise TypeError('x must be a real sequence')
+
+    if n is not None:
+        tmp, copied = _fix_shape_1d(tmp, n, axis)
+        overwrite_x = overwrite_x or copied
+    elif tmp.shape[axis] < 1:
+        raise ValueError(f"invalid number of data points ({tmp.shape[axis]}) specified")
+
+    out = (tmp if overwrite_x else None)
+
+    return pfft.r2r_fftpack(tmp, (axis,), forward, forward, norm, out, workers)
+
+
+rfft_fftpack = functools.partial(r2r_fftpack, True)
+rfft_fftpack.__name__ = 'rfft_fftpack'
+irfft_fftpack = functools.partial(r2r_fftpack, False)
+irfft_fftpack.__name__ = 'irfft_fftpack'
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/helper.py b/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/helper.py
new file mode 100644
index 0000000000000000000000000000000000000000..81e4827a88bf8162ca6fe830f1df1ad1bcfd30d6
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/helper.py
@@ -0,0 +1,252 @@
+from numbers import Number
+import operator
+import os
+import threading
+import contextlib
+
+import numpy as np
+
+from scipy._lib._util import copy_if_needed
+from scipy._lib._array_api import xp_capabilities
+
+# good_size is exposed (and used) from this import
+from .pypocketfft import good_size, prev_good_size
+
+
+__all__ = ['good_size', 'prev_good_size', 'set_workers', 'get_workers']
+
+_config = threading.local()
+_cpu_count = os.cpu_count()
+
+
+def _iterable_of_int(x, name=None):
+    """Convert ``x`` to an iterable sequence of int
+
+    Parameters
+    ----------
+    x : value, or sequence of values, convertible to int
+    name : str, optional
+        Name of the argument being converted, only used in the error message
+
+    Returns
+    -------
+    y : ``List[int]``
+    """
+    if isinstance(x, Number):
+        x = (x,)
+
+    try:
+        x = [operator.index(a) for a in x]
+    except TypeError as e:
+        name = name or "value"
+        raise ValueError(f"{name} must be a scalar or iterable of integers") from e
+
+    return x
+
+
+def _init_nd_shape_and_axes(x, shape, axes):
+    """
+    Handle shape and axes arguments for N-D transforms.
+
+    Returns the shape and axes in a standard form, taking into account negative
+    values and checking for various potential errors.
+
+    Parameters
+    ----------
+    x : ndarray
+        The input array.
+    shape : int or array_like of ints or None
+        The shape of the result. If both `shape` and `axes` (see below) are
+        None, `shape` is ``x.shape``; if `shape` is None but `axes` is
+        not None, then `shape` is ``numpy.take(x.shape, axes, axis=0)``.
+        If `shape` is -1, the size of the corresponding dimension of `x` is
+        used.
+    axes : int or array_like of ints or None
+        Axes along which the calculation is computed.
+        The default is over all axes.
+        Negative indices are automatically converted to their positive
+        counterparts.
+
+    Returns
+    -------
+    shape : tuple
+        The shape of the result as a tuple of integers.
+    axes : list
+        Axes along which the calculation is computed, as a list of integers.
+    """
+    noshape = shape is None
+    noaxes = axes is None
+
+    if not noaxes:
+        axes = _iterable_of_int(axes, 'axes')
+        axes = [a + x.ndim if a < 0 else a for a in axes]
+
+        if any(a >= x.ndim or a < 0 for a in axes):
+            raise ValueError("axes exceeds dimensionality of input")
+        if len(set(axes)) != len(axes):
+            raise ValueError("all axes must be unique")
+
+    if not noshape:
+        shape = _iterable_of_int(shape, 'shape')
+
+        if axes and len(axes) != len(shape):
+            raise ValueError("when given, axes and shape arguments"
+                             " have to be of the same length")
+        if noaxes:
+            if len(shape) > x.ndim:
+                raise ValueError("shape requires more axes than are present")
+            axes = range(x.ndim - len(shape), x.ndim)
+
+        shape = [x.shape[a] if s == -1 else s for s, a in zip(shape, axes)]
+    elif noaxes:
+        shape = list(x.shape)
+        axes = range(x.ndim)
+    else:
+        shape = [x.shape[a] for a in axes]
+
+    if any(s < 1 for s in shape):
+        raise ValueError(
+            f"invalid number of data points ({shape}) specified")
+
+    return tuple(shape), list(axes)
+
+
+def _asfarray(x):
+    """
+    Convert to array with floating or complex dtype.
+
+    float16 values are also promoted to float32.
+    """
+    if not hasattr(x, "dtype"):
+        x = np.asarray(x)
+
+    if x.dtype == np.float16:
+        return np.asarray(x, np.float32)
+    elif x.dtype.kind not in 'fc':
+        return np.asarray(x, np.float64)
+
+    # Require native byte order
+    dtype = x.dtype.newbyteorder('=')
+    # Always align input
+    copy = True if not x.flags['ALIGNED'] else copy_if_needed
+    return np.array(x, dtype=dtype, copy=copy)
+
+def _datacopied(arr, original):
+    """
+    Strict check for `arr` not sharing any data with `original`,
+    under the assumption that arr = asarray(original)
+    """
+    if arr is original:
+        return False
+    if not isinstance(original, np.ndarray) and hasattr(original, '__array__'):
+        return False
+    return arr.base is None
+
+
+def _fix_shape(x, shape, axes):
+    """Internal auxiliary function for _raw_fft, _raw_fftnd."""
+    must_copy = False
+
+    # Build an nd slice with the dimensions to be read from x
+    index = [slice(None)]*x.ndim
+    for n, ax in zip(shape, axes):
+        if x.shape[ax] >= n:
+            index[ax] = slice(0, n)
+        else:
+            index[ax] = slice(0, x.shape[ax])
+            must_copy = True
+
+    index = tuple(index)
+
+    if not must_copy:
+        return x[index], False
+
+    s = list(x.shape)
+    for n, axis in zip(shape, axes):
+        s[axis] = n
+
+    z = np.zeros(s, x.dtype)
+    z[index] = x[index]
+    return z, True
+
+
+def _fix_shape_1d(x, n, axis):
+    if n < 1:
+        raise ValueError(
+            f"invalid number of data points ({n}) specified")
+
+    return _fix_shape(x, (n,), (axis,))
+
+
+_NORM_MAP = {None: 0, 'backward': 0, 'ortho': 1, 'forward': 2}
+
+
+def _normalization(norm, forward):
+    """Returns the pypocketfft normalization mode from the norm argument"""
+    try:
+        inorm = _NORM_MAP[norm]
+        return inorm if forward else (2 - inorm)
+    except KeyError:
+        raise ValueError(
+            f'Invalid norm value {norm!r}, should '
+            'be "backward", "ortho" or "forward"') from None
+
+
+def _workers(workers):
+    if workers is None:
+        return getattr(_config, 'default_workers', 1)
+
+    if workers < 0:
+        if workers >= -_cpu_count:
+            workers += 1 + _cpu_count
+        else:
+            raise ValueError(f"workers value out of range; got {workers}, must not be"
+                             f" less than {-_cpu_count}")
+    elif workers == 0:
+        raise ValueError("workers must not be zero")
+
+    return workers
+
+
+@xp_capabilities(out_of_scope=True)
+@contextlib.contextmanager
+def set_workers(workers):
+    """Context manager for the default number of workers used in `scipy.fft`
+
+    Parameters
+    ----------
+    workers : int
+        The default number of workers to use
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy import fft, signal
+    >>> rng = np.random.default_rng()
+    >>> x = rng.standard_normal((128, 64))
+    >>> with fft.set_workers(4):
+    ...     y = signal.fftconvolve(x, x)
+
+    """
+    old_workers = get_workers()
+    _config.default_workers = _workers(operator.index(workers))
+    try:
+        yield
+    finally:
+        _config.default_workers = old_workers
+
+
+@xp_capabilities(out_of_scope=True)
+def get_workers():
+    """Returns the default number of workers within the current context
+
+    Examples
+    --------
+    >>> from scipy import fft
+    >>> fft.get_workers()
+    1
+    >>> with fft.set_workers(4):
+    ...     fft.get_workers()
+    4
+    """
+    return getattr(_config, 'default_workers', 1)
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/pypocketfft.cpython-311-x86_64-linux-gnu.so b/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/pypocketfft.cpython-311-x86_64-linux-gnu.so
new file mode 100644
index 0000000000000000000000000000000000000000..d9bc182fc5b68ee0c602bba861e97f650bbb4ce8
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/pypocketfft.cpython-311-x86_64-linux-gnu.so
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:31d812b267667ac86e8479efe62b70353290105a0f5139b5233fd8ea38d65e32
+size 1223848
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/realtransforms.py b/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/realtransforms.py
new file mode 100644
index 0000000000000000000000000000000000000000..5a0c616742305444d51258e650344c060129dfab
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/realtransforms.py
@@ -0,0 +1,109 @@
+import numpy as np
+from . import pypocketfft as pfft
+from .helper import (_asfarray, _init_nd_shape_and_axes, _datacopied,
+                     _fix_shape, _fix_shape_1d, _normalization, _workers)
+import functools
+
+
+def _r2r(forward, transform, x, type=2, n=None, axis=-1, norm=None,
+         overwrite_x=False, workers=None, orthogonalize=None):
+    """Forward or backward 1-D DCT/DST
+
+    Parameters
+    ----------
+    forward : bool
+        Transform direction (determines type and normalisation)
+    transform : {pypocketfft.dct, pypocketfft.dst}
+        The transform to perform
+    """
+    tmp = _asfarray(x)
+    overwrite_x = overwrite_x or _datacopied(tmp, x)
+    norm = _normalization(norm, forward)
+    workers = _workers(workers)
+
+    if not forward:
+        if type == 2:
+            type = 3
+        elif type == 3:
+            type = 2
+
+    if n is not None:
+        tmp, copied = _fix_shape_1d(tmp, n, axis)
+        overwrite_x = overwrite_x or copied
+    elif tmp.shape[axis] < 1:
+        raise ValueError(f"invalid number of data points ({tmp.shape[axis]}) specified")
+
+    out = (tmp if overwrite_x else None)
+
+    # For complex input, transform real and imaginary components separably
+    if np.iscomplexobj(x):
+        out = np.empty_like(tmp) if out is None else out
+        transform(tmp.real, type, (axis,), norm, out.real, workers)
+        transform(tmp.imag, type, (axis,), norm, out.imag, workers)
+        return out
+
+    return transform(tmp, type, (axis,), norm, out, workers, orthogonalize)
+
+
+dct = functools.partial(_r2r, True, pfft.dct)
+dct.__name__ = 'dct'
+idct = functools.partial(_r2r, False, pfft.dct)
+idct.__name__ = 'idct'
+
+dst = functools.partial(_r2r, True, pfft.dst)
+dst.__name__ = 'dst'
+idst = functools.partial(_r2r, False, pfft.dst)
+idst.__name__ = 'idst'
+
+
+def _r2rn(forward, transform, x, type=2, s=None, axes=None, norm=None,
+          overwrite_x=False, workers=None, orthogonalize=None):
+    """Forward or backward nd DCT/DST
+
+    Parameters
+    ----------
+    forward : bool
+        Transform direction (determines type and normalisation)
+    transform : {pypocketfft.dct, pypocketfft.dst}
+        The transform to perform
+    """
+    tmp = _asfarray(x)
+
+    shape, axes = _init_nd_shape_and_axes(tmp, s, axes)
+    overwrite_x = overwrite_x or _datacopied(tmp, x)
+
+    if len(axes) == 0:
+        return x
+
+    tmp, copied = _fix_shape(tmp, shape, axes)
+    overwrite_x = overwrite_x or copied
+
+    if not forward:
+        if type == 2:
+            type = 3
+        elif type == 3:
+            type = 2
+
+    norm = _normalization(norm, forward)
+    workers = _workers(workers)
+    out = (tmp if overwrite_x else None)
+
+    # For complex input, transform real and imaginary components separably
+    if np.iscomplexobj(x):
+        out = np.empty_like(tmp) if out is None else out
+        transform(tmp.real, type, axes, norm, out.real, workers)
+        transform(tmp.imag, type, axes, norm, out.imag, workers)
+        return out
+
+    return transform(tmp, type, axes, norm, out, workers, orthogonalize)
+
+
+dctn = functools.partial(_r2rn, True, pfft.dct)
+dctn.__name__ = 'dctn'
+idctn = functools.partial(_r2rn, False, pfft.dct)
+idctn.__name__ = 'idctn'
+
+dstn = functools.partial(_r2rn, True, pfft.dst)
+dstn.__name__ = 'dstn'
+idstn = functools.partial(_r2rn, False, pfft.dst)
+idstn.__name__ = 'idstn'
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/tests/__init__.py b/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/tests/test_basic.py b/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/tests/test_basic.py
new file mode 100644
index 0000000000000000000000000000000000000000..4ed1abe818ce1769f480dadebc80e588babaff87
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/tests/test_basic.py
@@ -0,0 +1,1011 @@
+# Created by Pearu Peterson, September 2002
+
+from numpy.testing import (assert_, assert_equal, assert_array_almost_equal,
+                           assert_array_almost_equal_nulp, assert_array_less,
+                           assert_allclose)
+import pytest
+from pytest import raises as assert_raises
+from scipy.fft._pocketfft import (ifft, fft, fftn, ifftn,
+                                  rfft, irfft, rfftn, irfftn,
+                                  hfft, ihfft, hfftn, ihfftn)
+
+from numpy import (arange, array, asarray, zeros, dot, exp, pi,
+                   swapaxes, cdouble)
+import numpy as np
+import numpy.fft
+from numpy.random import rand
+
+# "large" composite numbers supported by FFT._PYPOCKETFFT
+LARGE_COMPOSITE_SIZES = [
+    2**13,
+    2**5 * 3**5,
+    2**3 * 3**3 * 5**2,
+]
+SMALL_COMPOSITE_SIZES = [
+    2,
+    2*3*5,
+    2*2*3*3,
+]
+# prime
+LARGE_PRIME_SIZES = [
+    2011
+]
+SMALL_PRIME_SIZES = [
+    29
+]
+
+
+def _assert_close_in_norm(x, y, rtol, size, rdt):
+    # helper function for testing
+    err_msg = f"size: {size}  rdt: {rdt}"
+    assert_array_less(np.linalg.norm(x - y), rtol*np.linalg.norm(x), err_msg)
+
+
+def random(size):
+    return rand(*size)
+
+def swap_byteorder(arr):
+    """Returns the same array with swapped byteorder"""
+    dtype = arr.dtype.newbyteorder('S')
+    return arr.astype(dtype)
+
+def direct_dft(x):
+    x = asarray(x)
+    n = len(x)
+    y = zeros(n, dtype=cdouble)
+    w = -arange(n)*(2j*pi/n)
+    for i in range(n):
+        y[i] = dot(exp(i*w), x)
+    return y
+
+
+def direct_idft(x):
+    x = asarray(x)
+    n = len(x)
+    y = zeros(n, dtype=cdouble)
+    w = arange(n)*(2j*pi/n)
+    for i in range(n):
+        y[i] = dot(exp(i*w), x)/n
+    return y
+
+
+def direct_dftn(x):
+    x = asarray(x)
+    for axis in range(x.ndim):
+        x = fft(x, axis=axis)
+    return x
+
+
+def direct_idftn(x):
+    x = asarray(x)
+    for axis in range(x.ndim):
+        x = ifft(x, axis=axis)
+    return x
+
+
+def direct_rdft(x):
+    x = asarray(x)
+    n = len(x)
+    w = -arange(n)*(2j*pi/n)
+    y = zeros(n//2+1, dtype=cdouble)
+    for i in range(n//2+1):
+        y[i] = dot(exp(i*w), x)
+    return y
+
+
+def direct_irdft(x, n):
+    x = asarray(x)
+    x1 = zeros(n, dtype=cdouble)
+    for i in range(n//2+1):
+        x1[i] = x[i]
+        if i > 0 and 2*i < n:
+            x1[n-i] = np.conj(x[i])
+    return direct_idft(x1).real
+
+
+def direct_rdftn(x):
+    return fftn(rfft(x), axes=range(x.ndim - 1))
+
+
+class _TestFFTBase:
+    def setup_method(self):
+        self.cdt = None
+        self.rdt = None
+        np.random.seed(1234)
+
+    def test_definition(self):
+        x = np.array([1,2,3,4+1j,1,2,3,4+2j], dtype=self.cdt)
+        y = fft(x)
+        assert_equal(y.dtype, self.cdt)
+        y1 = direct_dft(x)
+        assert_array_almost_equal(y,y1)
+        x = np.array([1,2,3,4+0j,5], dtype=self.cdt)
+        assert_array_almost_equal(fft(x),direct_dft(x))
+
+    def test_n_argument_real(self):
+        x1 = np.array([1,2,3,4], dtype=self.rdt)
+        x2 = np.array([1,2,3,4], dtype=self.rdt)
+        y = fft([x1,x2],n=4)
+        assert_equal(y.dtype, self.cdt)
+        assert_equal(y.shape,(2,4))
+        assert_array_almost_equal(y[0],direct_dft(x1))
+        assert_array_almost_equal(y[1],direct_dft(x2))
+
+    def _test_n_argument_complex(self):
+        x1 = np.array([1,2,3,4+1j], dtype=self.cdt)
+        x2 = np.array([1,2,3,4+1j], dtype=self.cdt)
+        y = fft([x1,x2],n=4)
+        assert_equal(y.dtype, self.cdt)
+        assert_equal(y.shape,(2,4))
+        assert_array_almost_equal(y[0],direct_dft(x1))
+        assert_array_almost_equal(y[1],direct_dft(x2))
+
+    def test_djbfft(self):
+        for i in range(2,14):
+            n = 2**i
+            x = np.arange(n)
+            y = fft(x.astype(complex))
+            y2 = numpy.fft.fft(x)
+            assert_array_almost_equal(y,y2)
+            y = fft(x)
+            assert_array_almost_equal(y,y2)
+
+    def test_invalid_sizes(self):
+        assert_raises(ValueError, fft, [])
+        assert_raises(ValueError, fft, [[1,1],[2,2]], -5)
+
+
+class TestLongDoubleFFT(_TestFFTBase):
+    def setup_method(self):
+        self.cdt = np.clongdouble
+        self.rdt = np.longdouble
+
+
+class TestDoubleFFT(_TestFFTBase):
+    def setup_method(self):
+        self.cdt = np.cdouble
+        self.rdt = np.float64
+
+
+class TestSingleFFT(_TestFFTBase):
+    def setup_method(self):
+        self.cdt = np.complex64
+        self.rdt = np.float32
+
+
+class TestFloat16FFT:
+
+    def test_1_argument_real(self):
+        x1 = np.array([1, 2, 3, 4], dtype=np.float16)
+        y = fft(x1, n=4)
+        assert_equal(y.dtype, np.complex64)
+        assert_equal(y.shape, (4, ))
+        assert_array_almost_equal(y, direct_dft(x1.astype(np.float32)))
+
+    def test_n_argument_real(self):
+        x1 = np.array([1, 2, 3, 4], dtype=np.float16)
+        x2 = np.array([1, 2, 3, 4], dtype=np.float16)
+        y = fft([x1, x2], n=4)
+        assert_equal(y.dtype, np.complex64)
+        assert_equal(y.shape, (2, 4))
+        assert_array_almost_equal(y[0], direct_dft(x1.astype(np.float32)))
+        assert_array_almost_equal(y[1], direct_dft(x2.astype(np.float32)))
+
+
+class _TestIFFTBase:
+    def setup_method(self):
+        np.random.seed(1234)
+
+    def test_definition(self):
+        x = np.array([1,2,3,4+1j,1,2,3,4+2j], self.cdt)
+        y = ifft(x)
+        y1 = direct_idft(x)
+        assert_equal(y.dtype, self.cdt)
+        assert_array_almost_equal(y,y1)
+
+        x = np.array([1,2,3,4+0j,5], self.cdt)
+        assert_array_almost_equal(ifft(x),direct_idft(x))
+
+    def test_definition_real(self):
+        x = np.array([1,2,3,4,1,2,3,4], self.rdt)
+        y = ifft(x)
+        assert_equal(y.dtype, self.cdt)
+        y1 = direct_idft(x)
+        assert_array_almost_equal(y,y1)
+
+        x = np.array([1,2,3,4,5], dtype=self.rdt)
+        assert_equal(y.dtype, self.cdt)
+        assert_array_almost_equal(ifft(x),direct_idft(x))
+
+    def test_djbfft(self):
+        for i in range(2,14):
+            n = 2**i
+            x = np.arange(n)
+            y = ifft(x.astype(self.cdt))
+            y2 = numpy.fft.ifft(x.astype(self.cdt))
+            assert_allclose(y,y2, rtol=self.rtol, atol=self.atol)
+            y = ifft(x)
+            assert_allclose(y,y2, rtol=self.rtol, atol=self.atol)
+
+    def test_random_complex(self):
+        for size in [1,51,111,100,200,64,128,256,1024]:
+            x = random([size]).astype(self.cdt)
+            x = random([size]).astype(self.cdt) + 1j*x
+            y1 = ifft(fft(x))
+            y2 = fft(ifft(x))
+            assert_equal(y1.dtype, self.cdt)
+            assert_equal(y2.dtype, self.cdt)
+            assert_array_almost_equal(y1, x)
+            assert_array_almost_equal(y2, x)
+
+    def test_random_real(self):
+        for size in [1,51,111,100,200,64,128,256,1024]:
+            x = random([size]).astype(self.rdt)
+            y1 = ifft(fft(x))
+            y2 = fft(ifft(x))
+            assert_equal(y1.dtype, self.cdt)
+            assert_equal(y2.dtype, self.cdt)
+            assert_array_almost_equal(y1, x)
+            assert_array_almost_equal(y2, x)
+
+    def test_size_accuracy(self):
+        # Sanity check for the accuracy for prime and non-prime sized inputs
+        for size in LARGE_COMPOSITE_SIZES + LARGE_PRIME_SIZES:
+            np.random.seed(1234)
+            x = np.random.rand(size).astype(self.rdt)
+            y = ifft(fft(x))
+            _assert_close_in_norm(x, y, self.rtol, size, self.rdt)
+            y = fft(ifft(x))
+            _assert_close_in_norm(x, y, self.rtol, size, self.rdt)
+
+            x = (x + 1j*np.random.rand(size)).astype(self.cdt)
+            y = ifft(fft(x))
+            _assert_close_in_norm(x, y, self.rtol, size, self.rdt)
+            y = fft(ifft(x))
+            _assert_close_in_norm(x, y, self.rtol, size, self.rdt)
+
+    def test_invalid_sizes(self):
+        assert_raises(ValueError, ifft, [])
+        assert_raises(ValueError, ifft, [[1,1],[2,2]], -5)
+
+
+@pytest.mark.skipif(np.longdouble is np.float64,
+                    reason="Long double is aliased to double")
+class TestLongDoubleIFFT(_TestIFFTBase):
+    def setup_method(self):
+        self.cdt = np.clongdouble
+        self.rdt = np.longdouble
+        self.rtol = 1e-10
+        self.atol = 1e-10
+
+
+class TestDoubleIFFT(_TestIFFTBase):
+    def setup_method(self):
+        self.cdt = np.complex128
+        self.rdt = np.float64
+        self.rtol = 1e-10
+        self.atol = 1e-10
+
+
+class TestSingleIFFT(_TestIFFTBase):
+    def setup_method(self):
+        self.cdt = np.complex64
+        self.rdt = np.float32
+        self.rtol = 1e-5
+        self.atol = 1e-4
+
+
+class _TestRFFTBase:
+    def setup_method(self):
+        np.random.seed(1234)
+
+    def test_definition(self):
+        for t in [[1, 2, 3, 4, 1, 2, 3, 4], [1, 2, 3, 4, 1, 2, 3, 4, 5]]:
+            x = np.array(t, dtype=self.rdt)
+            y = rfft(x)
+            y1 = direct_rdft(x)
+            assert_array_almost_equal(y,y1)
+            assert_equal(y.dtype, self.cdt)
+
+    def test_djbfft(self):
+        for i in range(2,14):
+            n = 2**i
+            x = np.arange(n)
+            y1 = np.fft.rfft(x)
+            y = rfft(x)
+            assert_array_almost_equal(y,y1)
+
+    def test_invalid_sizes(self):
+        assert_raises(ValueError, rfft, [])
+        assert_raises(ValueError, rfft, [[1,1],[2,2]], -5)
+
+    def test_complex_input(self):
+        x = np.zeros(10, dtype=self.cdt)
+        with assert_raises(TypeError, match="x must be a real sequence"):
+            rfft(x)
+
+    # See gh-5790
+    class MockSeries:
+        def __init__(self, data):
+            self.data = np.asarray(data)
+
+        def __getattr__(self, item):
+            try:
+                return getattr(self.data, item)
+            except AttributeError as e:
+                raise AttributeError("'MockSeries' object "
+                                      f"has no attribute '{item}'") from e
+
+    def test_non_ndarray_with_dtype(self):
+        x = np.array([1., 2., 3., 4., 5.])
+        xs = _TestRFFTBase.MockSeries(x)
+
+        expected = [1, 2, 3, 4, 5]
+        rfft(xs)
+
+        # Data should not have been overwritten
+        assert_equal(x, expected)
+        assert_equal(xs.data, expected)
+
+@pytest.mark.skipif(np.longdouble is np.float64,
+                    reason="Long double is aliased to double")
+class TestRFFTLongDouble(_TestRFFTBase):
+    def setup_method(self):
+        self.cdt = np.clongdouble
+        self.rdt = np.longdouble
+
+
+class TestRFFTDouble(_TestRFFTBase):
+    def setup_method(self):
+        self.cdt = np.complex128
+        self.rdt = np.float64
+
+
+class TestRFFTSingle(_TestRFFTBase):
+    def setup_method(self):
+        self.cdt = np.complex64
+        self.rdt = np.float32
+
+
+class _TestIRFFTBase:
+    def setup_method(self):
+        np.random.seed(1234)
+
+    def test_definition(self):
+        x1 = [1,2+3j,4+1j,1+2j,3+4j]
+        x1_1 = [1,2+3j,4+1j,2+3j,4,2-3j,4-1j,2-3j]
+        x1 = x1_1[:5]
+        x2_1 = [1,2+3j,4+1j,2+3j,4+5j,4-5j,2-3j,4-1j,2-3j]
+        x2 = x2_1[:5]
+
+        def _test(x, xr):
+            y = irfft(np.array(x, dtype=self.cdt), n=len(xr))
+            y1 = direct_irdft(x, len(xr))
+            assert_equal(y.dtype, self.rdt)
+            assert_array_almost_equal(y,y1, decimal=self.ndec)
+            assert_array_almost_equal(y,ifft(xr), decimal=self.ndec)
+
+        _test(x1, x1_1)
+        _test(x2, x2_1)
+
+    def test_djbfft(self):
+        for i in range(2,14):
+            n = 2**i
+            x = np.arange(-1, n, 2) + 1j * np.arange(0, n+1, 2)
+            x[0] = 0
+            if n % 2 == 0:
+                x[-1] = np.real(x[-1])
+            y1 = np.fft.irfft(x)
+            y = irfft(x)
+            assert_array_almost_equal(y,y1)
+
+    def test_random_real(self):
+        for size in [1,51,111,100,200,64,128,256,1024]:
+            x = random([size]).astype(self.rdt)
+            y1 = irfft(rfft(x), n=size)
+            y2 = rfft(irfft(x, n=(size*2-1)))
+            assert_equal(y1.dtype, self.rdt)
+            assert_equal(y2.dtype, self.cdt)
+            assert_array_almost_equal(y1, x, decimal=self.ndec, err_msg=f"size={size}")
+            assert_array_almost_equal(y2, x, decimal=self.ndec, err_msg=f"size={size}")
+
+    def test_size_accuracy(self):
+        # Sanity check for the accuracy for prime and non-prime sized inputs
+        if self.rdt == np.float32:
+            rtol = 1e-5
+        elif self.rdt == np.float64:
+            rtol = 1e-10
+
+        for size in LARGE_COMPOSITE_SIZES + LARGE_PRIME_SIZES:
+            np.random.seed(1234)
+            x = np.random.rand(size).astype(self.rdt)
+            y = irfft(rfft(x), len(x))
+            _assert_close_in_norm(x, y, rtol, size, self.rdt)
+            y = rfft(irfft(x, 2 * len(x) - 1))
+            _assert_close_in_norm(x, y, rtol, size, self.rdt)
+
+    def test_invalid_sizes(self):
+        assert_raises(ValueError, irfft, [])
+        assert_raises(ValueError, irfft, [[1,1],[2,2]], -5)
+
+
+# self.ndec is bogus; we should have a assert_array_approx_equal for number of
+# significant digits
+
+@pytest.mark.skipif(np.longdouble is np.float64,
+                    reason="Long double is aliased to double")
+class TestIRFFTLongDouble(_TestIRFFTBase):
+    def setup_method(self):
+        self.cdt = np.complex128
+        self.rdt = np.float64
+        self.ndec = 14
+
+
+class TestIRFFTDouble(_TestIRFFTBase):
+    def setup_method(self):
+        self.cdt = np.complex128
+        self.rdt = np.float64
+        self.ndec = 14
+
+
+class TestIRFFTSingle(_TestIRFFTBase):
+    def setup_method(self):
+        self.cdt = np.complex64
+        self.rdt = np.float32
+        self.ndec = 5
+
+
+class TestFftnSingle:
+    def setup_method(self):
+        np.random.seed(1234)
+
+    def test_definition(self):
+        x = [[1, 2, 3],
+             [4, 5, 6],
+             [7, 8, 9]]
+        y = fftn(np.array(x, np.float32))
+        assert_(y.dtype == np.complex64,
+                msg="double precision output with single precision")
+
+        y_r = np.array(fftn(x), np.complex64)
+        assert_array_almost_equal_nulp(y, y_r)
+
+    @pytest.mark.parametrize('size', SMALL_COMPOSITE_SIZES + SMALL_PRIME_SIZES)
+    def test_size_accuracy_small(self, size):
+        rng = np.random.default_rng(1234)
+        x = rng.random((size, size)) + 1j * rng.random((size, size))
+        y1 = fftn(x.real.astype(np.float32))
+        y2 = fftn(x.real.astype(np.float64)).astype(np.complex64)
+
+        assert_equal(y1.dtype, np.complex64)
+        assert_array_almost_equal_nulp(y1, y2, 2000)
+
+    @pytest.mark.parametrize('size', LARGE_COMPOSITE_SIZES + LARGE_PRIME_SIZES)
+    def test_size_accuracy_large(self, size):
+        rng = np.random.default_rng(1234)
+        x = rng.random((size, 3)) + 1j * rng.random((size, 3))
+        y1 = fftn(x.real.astype(np.float32))
+        y2 = fftn(x.real.astype(np.float64)).astype(np.complex64)
+
+        assert_equal(y1.dtype, np.complex64)
+        assert_array_almost_equal_nulp(y1, y2, 2000)
+
+    def test_definition_float16(self):
+        x = [[1, 2, 3],
+             [4, 5, 6],
+             [7, 8, 9]]
+        y = fftn(np.array(x, np.float16))
+        assert_equal(y.dtype, np.complex64)
+        y_r = np.array(fftn(x), np.complex64)
+        assert_array_almost_equal_nulp(y, y_r)
+
+    @pytest.mark.parametrize('size', SMALL_COMPOSITE_SIZES + SMALL_PRIME_SIZES)
+    def test_float16_input_small(self, size):
+        rng = np.random.default_rng(1234)
+        x = rng.random((size, size)) + 1j*rng.random((size, size))
+        y1 = fftn(x.real.astype(np.float16))
+        y2 = fftn(x.real.astype(np.float64)).astype(np.complex64)
+
+        assert_equal(y1.dtype, np.complex64)
+        assert_array_almost_equal_nulp(y1, y2, 5e5)
+
+    @pytest.mark.parametrize('size', LARGE_COMPOSITE_SIZES + LARGE_PRIME_SIZES)
+    def test_float16_input_large(self, size):
+        rng = np.random.default_rng(1234)
+        x = rng.random((size, 3)) + 1j*rng.random((size, 3))
+        y1 = fftn(x.real.astype(np.float16))
+        y2 = fftn(x.real.astype(np.float64)).astype(np.complex64)
+
+        assert_equal(y1.dtype, np.complex64)
+        assert_array_almost_equal_nulp(y1, y2, 2e6)
+
+
+class TestFftn:
+    def setup_method(self):
+        np.random.seed(1234)
+
+    def test_definition(self):
+        x = [[1, 2, 3],
+             [4, 5, 6],
+             [7, 8, 9]]
+        y = fftn(x)
+        assert_array_almost_equal(y, direct_dftn(x))
+
+        x = random((20, 26))
+        assert_array_almost_equal(fftn(x), direct_dftn(x))
+
+        x = random((5, 4, 3, 20))
+        assert_array_almost_equal(fftn(x), direct_dftn(x))
+
+    def test_axes_argument(self):
+        # plane == ji_plane, x== kji_space
+        plane1 = [[1, 2, 3],
+                  [4, 5, 6],
+                  [7, 8, 9]]
+        plane2 = [[10, 11, 12],
+                  [13, 14, 15],
+                  [16, 17, 18]]
+        plane3 = [[19, 20, 21],
+                  [22, 23, 24],
+                  [25, 26, 27]]
+        ki_plane1 = [[1, 2, 3],
+                     [10, 11, 12],
+                     [19, 20, 21]]
+        ki_plane2 = [[4, 5, 6],
+                     [13, 14, 15],
+                     [22, 23, 24]]
+        ki_plane3 = [[7, 8, 9],
+                     [16, 17, 18],
+                     [25, 26, 27]]
+        jk_plane1 = [[1, 10, 19],
+                     [4, 13, 22],
+                     [7, 16, 25]]
+        jk_plane2 = [[2, 11, 20],
+                     [5, 14, 23],
+                     [8, 17, 26]]
+        jk_plane3 = [[3, 12, 21],
+                     [6, 15, 24],
+                     [9, 18, 27]]
+        kj_plane1 = [[1, 4, 7],
+                     [10, 13, 16], [19, 22, 25]]
+        kj_plane2 = [[2, 5, 8],
+                     [11, 14, 17], [20, 23, 26]]
+        kj_plane3 = [[3, 6, 9],
+                     [12, 15, 18], [21, 24, 27]]
+        ij_plane1 = [[1, 4, 7],
+                     [2, 5, 8],
+                     [3, 6, 9]]
+        ij_plane2 = [[10, 13, 16],
+                     [11, 14, 17],
+                     [12, 15, 18]]
+        ij_plane3 = [[19, 22, 25],
+                     [20, 23, 26],
+                     [21, 24, 27]]
+        ik_plane1 = [[1, 10, 19],
+                     [2, 11, 20],
+                     [3, 12, 21]]
+        ik_plane2 = [[4, 13, 22],
+                     [5, 14, 23],
+                     [6, 15, 24]]
+        ik_plane3 = [[7, 16, 25],
+                     [8, 17, 26],
+                     [9, 18, 27]]
+        ijk_space = [jk_plane1, jk_plane2, jk_plane3]
+        ikj_space = [kj_plane1, kj_plane2, kj_plane3]
+        jik_space = [ik_plane1, ik_plane2, ik_plane3]
+        jki_space = [ki_plane1, ki_plane2, ki_plane3]
+        kij_space = [ij_plane1, ij_plane2, ij_plane3]
+        x = array([plane1, plane2, plane3])
+
+        assert_array_almost_equal(fftn(x),
+                                  fftn(x, axes=(-3, -2, -1)))  # kji_space
+        assert_array_almost_equal(fftn(x), fftn(x, axes=(0, 1, 2)))
+        assert_array_almost_equal(fftn(x, axes=(0, 2)), fftn(x, axes=(0, -1)))
+        y = fftn(x, axes=(2, 1, 0))  # ijk_space
+        assert_array_almost_equal(swapaxes(y, -1, -3), fftn(ijk_space))
+        y = fftn(x, axes=(2, 0, 1))  # ikj_space
+        assert_array_almost_equal(swapaxes(swapaxes(y, -1, -3), -1, -2),
+                                  fftn(ikj_space))
+        y = fftn(x, axes=(1, 2, 0))  # jik_space
+        assert_array_almost_equal(swapaxes(swapaxes(y, -1, -3), -3, -2),
+                                  fftn(jik_space))
+        y = fftn(x, axes=(1, 0, 2))  # jki_space
+        assert_array_almost_equal(swapaxes(y, -2, -3), fftn(jki_space))
+        y = fftn(x, axes=(0, 2, 1))  # kij_space
+        assert_array_almost_equal(swapaxes(y, -2, -1), fftn(kij_space))
+
+        y = fftn(x, axes=(-2, -1))  # ji_plane
+        assert_array_almost_equal(fftn(plane1), y[0])
+        assert_array_almost_equal(fftn(plane2), y[1])
+        assert_array_almost_equal(fftn(plane3), y[2])
+
+        y = fftn(x, axes=(1, 2))  # ji_plane
+        assert_array_almost_equal(fftn(plane1), y[0])
+        assert_array_almost_equal(fftn(plane2), y[1])
+        assert_array_almost_equal(fftn(plane3), y[2])
+
+        y = fftn(x, axes=(-3, -2))  # kj_plane
+        assert_array_almost_equal(fftn(x[:, :, 0]), y[:, :, 0])
+        assert_array_almost_equal(fftn(x[:, :, 1]), y[:, :, 1])
+        assert_array_almost_equal(fftn(x[:, :, 2]), y[:, :, 2])
+
+        y = fftn(x, axes=(-3, -1))  # ki_plane
+        assert_array_almost_equal(fftn(x[:, 0, :]), y[:, 0, :])
+        assert_array_almost_equal(fftn(x[:, 1, :]), y[:, 1, :])
+        assert_array_almost_equal(fftn(x[:, 2, :]), y[:, 2, :])
+
+        y = fftn(x, axes=(-1, -2))  # ij_plane
+        assert_array_almost_equal(fftn(ij_plane1), swapaxes(y[0], -2, -1))
+        assert_array_almost_equal(fftn(ij_plane2), swapaxes(y[1], -2, -1))
+        assert_array_almost_equal(fftn(ij_plane3), swapaxes(y[2], -2, -1))
+
+        y = fftn(x, axes=(-1, -3))  # ik_plane
+        assert_array_almost_equal(fftn(ik_plane1),
+                                  swapaxes(y[:, 0, :], -1, -2))
+        assert_array_almost_equal(fftn(ik_plane2),
+                                  swapaxes(y[:, 1, :], -1, -2))
+        assert_array_almost_equal(fftn(ik_plane3),
+                                  swapaxes(y[:, 2, :], -1, -2))
+
+        y = fftn(x, axes=(-2, -3))  # jk_plane
+        assert_array_almost_equal(fftn(jk_plane1),
+                                  swapaxes(y[:, :, 0], -1, -2))
+        assert_array_almost_equal(fftn(jk_plane2),
+                                  swapaxes(y[:, :, 1], -1, -2))
+        assert_array_almost_equal(fftn(jk_plane3),
+                                  swapaxes(y[:, :, 2], -1, -2))
+
+        y = fftn(x, axes=(-1,))  # i_line
+        for i in range(3):
+            for j in range(3):
+                assert_array_almost_equal(fft(x[i, j, :]), y[i, j, :])
+        y = fftn(x, axes=(-2,))  # j_line
+        for i in range(3):
+            for j in range(3):
+                assert_array_almost_equal(fft(x[i, :, j]), y[i, :, j])
+        y = fftn(x, axes=(0,))  # k_line
+        for i in range(3):
+            for j in range(3):
+                assert_array_almost_equal(fft(x[:, i, j]), y[:, i, j])
+
+        y = fftn(x, axes=())  # point
+        assert_array_almost_equal(y, x)
+
+    def test_shape_argument(self):
+        small_x = [[1, 2, 3],
+                   [4, 5, 6]]
+        large_x1 = [[1, 2, 3, 0],
+                    [4, 5, 6, 0],
+                    [0, 0, 0, 0],
+                    [0, 0, 0, 0]]
+
+        y = fftn(small_x, s=(4, 4))
+        assert_array_almost_equal(y, fftn(large_x1))
+
+        y = fftn(small_x, s=(3, 4))
+        assert_array_almost_equal(y, fftn(large_x1[:-1]))
+
+    def test_shape_axes_argument(self):
+        small_x = [[1, 2, 3],
+                   [4, 5, 6],
+                   [7, 8, 9]]
+        large_x1 = array([[1, 2, 3, 0],
+                          [4, 5, 6, 0],
+                          [7, 8, 9, 0],
+                          [0, 0, 0, 0]])
+        y = fftn(small_x, s=(4, 4), axes=(-2, -1))
+        assert_array_almost_equal(y, fftn(large_x1))
+        y = fftn(small_x, s=(4, 4), axes=(-1, -2))
+
+        assert_array_almost_equal(y, swapaxes(
+            fftn(swapaxes(large_x1, -1, -2)), -1, -2))
+
+    def test_shape_axes_argument2(self):
+        # Change shape of the last axis
+        x = numpy.random.random((10, 5, 3, 7))
+        y = fftn(x, axes=(-1,), s=(8,))
+        assert_array_almost_equal(y, fft(x, axis=-1, n=8))
+
+        # Change shape of an arbitrary axis which is not the last one
+        x = numpy.random.random((10, 5, 3, 7))
+        y = fftn(x, axes=(-2,), s=(8,))
+        assert_array_almost_equal(y, fft(x, axis=-2, n=8))
+
+        # Change shape of axes: cf #244, where shape and axes were mixed up
+        x = numpy.random.random((4, 4, 2))
+        y = fftn(x, axes=(-3, -2), s=(8, 8))
+        assert_array_almost_equal(y,
+                                  numpy.fft.fftn(x, axes=(-3, -2), s=(8, 8)))
+
+    def test_shape_argument_more(self):
+        x = zeros((4, 4, 2))
+        with assert_raises(ValueError,
+                           match="shape requires more axes than are present"):
+            fftn(x, s=(8, 8, 2, 1))
+
+    def test_invalid_sizes(self):
+        with assert_raises(ValueError,
+                           match="invalid number of data points"
+                           r" \(\[1, 0\]\) specified"):
+            fftn([[]])
+
+        with assert_raises(ValueError,
+                           match="invalid number of data points"
+                           r" \(\[4, -3\]\) specified"):
+            fftn([[1, 1], [2, 2]], (4, -3))
+
+    def test_no_axes(self):
+        x = numpy.random.random((2,2,2))
+        assert_allclose(fftn(x, axes=[]), x, atol=1e-7)
+
+    def test_regression_244(self):
+        """FFT returns wrong result with axes parameter."""
+        # fftn (and hence fft2) used to break when both axes and shape were used
+        x = numpy.ones((4, 4, 2))
+        y = fftn(x, s=(8, 8), axes=(-3, -2))
+        y_r = numpy.fft.fftn(x, s=(8, 8), axes=(-3, -2))
+        assert_allclose(y, y_r)
+
+
+class TestIfftn:
+    dtype = None
+    cdtype = None
+
+    def setup_method(self):
+        np.random.seed(1234)
+
+    @pytest.mark.parametrize('dtype,cdtype,maxnlp',
+                             [(np.float64, np.complex128, 2000),
+                              (np.float32, np.complex64, 3500)])
+    def test_definition(self, dtype, cdtype, maxnlp):
+        rng = np.random.default_rng(1234)
+        x = np.array([[1, 2, 3],
+                      [4, 5, 6],
+                      [7, 8, 9]], dtype=dtype)
+        y = ifftn(x)
+        assert_equal(y.dtype, cdtype)
+        assert_array_almost_equal_nulp(y, direct_idftn(x), maxnlp)
+
+        x = rng.random((20, 26))
+        assert_array_almost_equal_nulp(ifftn(x), direct_idftn(x), maxnlp)
+
+        x = rng.random((5, 4, 3, 20))
+        assert_array_almost_equal_nulp(ifftn(x), direct_idftn(x), maxnlp)
+
+    @pytest.mark.parametrize('maxnlp', [2000, 3500])
+    @pytest.mark.parametrize('size', [1, 2, 51, 32, 64, 92])
+    def test_random_complex(self, maxnlp, size):
+        rng = np.random.default_rng(1234)
+        x = rng.random([size, size]) + 1j * rng.random([size, size])
+        assert_array_almost_equal_nulp(ifftn(fftn(x)), x, maxnlp)
+        assert_array_almost_equal_nulp(fftn(ifftn(x)), x, maxnlp)
+
+    def test_invalid_sizes(self):
+        with assert_raises(ValueError,
+                           match="invalid number of data points"
+                           r" \(\[1, 0\]\) specified"):
+            ifftn([[]])
+
+        with assert_raises(ValueError,
+                           match="invalid number of data points"
+                           r" \(\[4, -3\]\) specified"):
+            ifftn([[1, 1], [2, 2]], (4, -3))
+
+    def test_no_axes(self):
+        x = numpy.random.random((2,2,2))
+        assert_allclose(ifftn(x, axes=[]), x, atol=1e-7)
+
+class TestRfftn:
+    dtype = None
+    cdtype = None
+
+    def setup_method(self):
+        np.random.seed(1234)
+
+    @pytest.mark.parametrize('dtype,cdtype,maxnlp',
+                             [(np.float64, np.complex128, 2000),
+                              (np.float32, np.complex64, 3500)])
+    def test_definition(self, dtype, cdtype, maxnlp):
+        rng = np.random.default_rng(1234)
+        x = np.array([[1, 2, 3],
+                      [4, 5, 6],
+                      [7, 8, 9]], dtype=dtype)
+        y = rfftn(x)
+        assert_equal(y.dtype, cdtype)
+        assert_array_almost_equal_nulp(y, direct_rdftn(x), maxnlp)
+
+        x = rng.random((20, 26))
+        assert_array_almost_equal_nulp(rfftn(x), direct_rdftn(x), maxnlp)
+
+        x = rng.random((5, 4, 3, 20))
+        assert_array_almost_equal_nulp(rfftn(x), direct_rdftn(x), maxnlp)
+
+    @pytest.mark.parametrize('size', [1, 2, 51, 32, 64, 92])
+    def test_random(self, size):
+        rng = np.random.default_rng(1234)
+        x = rng.random([size, size])
+        assert_allclose(irfftn(rfftn(x), x.shape), x, atol=1e-10)
+
+    @pytest.mark.parametrize('func', [rfftn, irfftn])
+    def test_invalid_sizes(self, func):
+        with assert_raises(ValueError,
+                           match="invalid number of data points"
+                           r" \(\[1, 0\]\) specified"):
+            func([[]])
+
+        with assert_raises(ValueError,
+                           match="invalid number of data points"
+                           r" \(\[4, -3\]\) specified"):
+            func([[1, 1], [2, 2]], (4, -3))
+
+    @pytest.mark.parametrize('func', [rfftn, irfftn])
+    def test_no_axes(self, func):
+        with assert_raises(ValueError,
+                           match="at least 1 axis must be transformed"):
+            func([], axes=[])
+
+    def test_complex_input(self):
+        with assert_raises(TypeError, match="x must be a real sequence"):
+            rfftn(np.zeros(10, dtype=np.complex64))
+
+
+class FakeArray:
+    def __init__(self, data):
+        self._data = data
+        self.__array_interface__ = data.__array_interface__
+
+
+class FakeArray2:
+    def __init__(self, data):
+        self._data = data
+
+    def __array__(self, dtype=None, copy=None):
+        return self._data
+
+# TODO: Is this test actually valuable? The behavior it's testing shouldn't be
+# relied upon by users except for overwrite_x = False
+class TestOverwrite:
+    """Check input overwrite behavior of the FFT functions."""
+
+    real_dtypes = [np.float32, np.float64, np.longdouble]
+    dtypes = real_dtypes + [np.complex64, np.complex128, np.clongdouble]
+    fftsizes = [8, 16, 32]
+
+    def _check(self, x, routine, fftsize, axis, overwrite_x, should_overwrite):
+        x2 = x.copy()
+        for fake in [lambda x: x, FakeArray, FakeArray2]:
+            routine(fake(x2), fftsize, axis, overwrite_x=overwrite_x)
+
+            sig = (f"{routine.__name__}({x.dtype}{x.shape!r}, {fftsize!r}, "
+                   f"axis={axis!r}, overwrite_x={overwrite_x!r})")
+            if not should_overwrite:
+                assert_equal(x2, x, err_msg=f"spurious overwrite in {sig}")
+
+    def _check_1d(self, routine, dtype, shape, axis, overwritable_dtypes,
+                  fftsize, overwrite_x):
+        np.random.seed(1234)
+        if np.issubdtype(dtype, np.complexfloating):
+            data = np.random.randn(*shape) + 1j*np.random.randn(*shape)
+        else:
+            data = np.random.randn(*shape)
+        data = data.astype(dtype)
+
+        should_overwrite = (overwrite_x
+                            and dtype in overwritable_dtypes
+                            and fftsize <= shape[axis])
+        self._check(data, routine, fftsize, axis,
+                    overwrite_x=overwrite_x,
+                    should_overwrite=should_overwrite)
+
+    @pytest.mark.parametrize('dtype', dtypes)
+    @pytest.mark.parametrize('fftsize', fftsizes)
+    @pytest.mark.parametrize('overwrite_x', [True, False])
+    @pytest.mark.parametrize('shape,axes', [((16,), -1),
+                                            ((16, 2), 0),
+                                            ((2, 16), 1)])
+    def test_fft_ifft(self, dtype, fftsize, overwrite_x, shape, axes):
+        overwritable = (np.clongdouble, np.complex128, np.complex64)
+        self._check_1d(fft, dtype, shape, axes, overwritable,
+                       fftsize, overwrite_x)
+        self._check_1d(ifft, dtype, shape, axes, overwritable,
+                       fftsize, overwrite_x)
+
+    @pytest.mark.parametrize('dtype', real_dtypes)
+    @pytest.mark.parametrize('fftsize', fftsizes)
+    @pytest.mark.parametrize('overwrite_x', [True, False])
+    @pytest.mark.parametrize('shape,axes', [((16,), -1),
+                                            ((16, 2), 0),
+                                            ((2, 16), 1)])
+    def test_rfft_irfft(self, dtype, fftsize, overwrite_x, shape, axes):
+        overwritable = self.real_dtypes
+        self._check_1d(irfft, dtype, shape, axes, overwritable,
+                       fftsize, overwrite_x)
+        self._check_1d(rfft, dtype, shape, axes, overwritable,
+                       fftsize, overwrite_x)
+
+    def _check_nd_one(self, routine, dtype, shape, axes, overwritable_dtypes,
+                      overwrite_x):
+        np.random.seed(1234)
+        if np.issubdtype(dtype, np.complexfloating):
+            data = np.random.randn(*shape) + 1j*np.random.randn(*shape)
+        else:
+            data = np.random.randn(*shape)
+        data = data.astype(dtype)
+
+        def fftshape_iter(shp):
+            if len(shp) <= 0:
+                yield ()
+            else:
+                for j in (shp[0]//2, shp[0], shp[0]*2):
+                    for rest in fftshape_iter(shp[1:]):
+                        yield (j,) + rest
+
+        def part_shape(shape, axes):
+            if axes is None:
+                return shape
+            else:
+                return tuple(np.take(shape, axes))
+
+        def should_overwrite(data, shape, axes):
+            s = part_shape(data.shape, axes)
+            return (overwrite_x and
+                    np.prod(shape) <= np.prod(s)
+                    and dtype in overwritable_dtypes)
+
+        for fftshape in fftshape_iter(part_shape(shape, axes)):
+            self._check(data, routine, fftshape, axes,
+                        overwrite_x=overwrite_x,
+                        should_overwrite=should_overwrite(data, fftshape, axes))
+            if data.ndim > 1:
+                # check fortran order
+                self._check(data.T, routine, fftshape, axes,
+                            overwrite_x=overwrite_x,
+                            should_overwrite=should_overwrite(
+                                data.T, fftshape, axes))
+
+    @pytest.mark.parametrize('dtype', dtypes)
+    @pytest.mark.parametrize('overwrite_x', [True, False])
+    @pytest.mark.parametrize('shape,axes', [((16,), None),
+                                            ((16,), (0,)),
+                                            ((16, 2), (0,)),
+                                            ((2, 16), (1,)),
+                                            ((8, 16), None),
+                                            ((8, 16), (0, 1)),
+                                            ((8, 16, 2), (0, 1)),
+                                            ((8, 16, 2), (1, 2)),
+                                            ((8, 16, 2), (0,)),
+                                            ((8, 16, 2), (1,)),
+                                            ((8, 16, 2), (2,)),
+                                            ((8, 16, 2), None),
+                                            ((8, 16, 2), (0, 1, 2))])
+    def test_fftn_ifftn(self, dtype, overwrite_x, shape, axes):
+        overwritable = (np.clongdouble, np.complex128, np.complex64)
+        self._check_nd_one(fftn, dtype, shape, axes, overwritable,
+                           overwrite_x)
+        self._check_nd_one(ifftn, dtype, shape, axes, overwritable,
+                           overwrite_x)
+
+
+@pytest.mark.parametrize('func', [fft, ifft, fftn, ifftn,
+                                 rfft, irfft, rfftn, irfftn])
+def test_invalid_norm(func):
+    x = np.arange(10, dtype=float)
+    with assert_raises(ValueError,
+                       match='Invalid norm value \'o\', should be'
+                             ' "backward", "ortho" or "forward"'):
+        func(x, norm='o')
+
+
+@pytest.mark.parametrize('func', [fft, ifft, fftn, ifftn,
+                                   irfft, irfftn, hfft, hfftn])
+def test_swapped_byte_order_complex(func):
+    rng = np.random.RandomState(1234)
+    x = rng.rand(10) + 1j * rng.rand(10)
+    assert_allclose(func(swap_byteorder(x)), func(x))
+
+
+@pytest.mark.parametrize('func', [ihfft, ihfftn, rfft, rfftn])
+def test_swapped_byte_order_real(func):
+    rng = np.random.RandomState(1234)
+    x = rng.rand(10)
+    assert_allclose(func(swap_byteorder(x)), func(x))
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/tests/test_real_transforms.py b/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/tests/test_real_transforms.py
new file mode 100644
index 0000000000000000000000000000000000000000..16477375397e05ccbf7b87f84d66b1d450dcb3bd
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/_pocketfft/tests/test_real_transforms.py
@@ -0,0 +1,506 @@
+from os.path import join, dirname
+from collections.abc import Callable
+from threading import Lock
+
+import numpy as np
+from numpy.testing import (
+    assert_array_almost_equal, assert_equal, assert_allclose)
+import pytest
+from pytest import raises as assert_raises
+
+from scipy.fft._pocketfft.realtransforms import (
+    dct, idct, dst, idst, dctn, idctn, dstn, idstn)
+
+fftpack_test_dir = join(dirname(__file__), '..', '..', '..', 'fftpack', 'tests')
+
+MDATA_COUNT = 8
+FFTWDATA_COUNT = 14
+
+
+def is_longdouble_binary_compatible():
+    try:
+        one = np.frombuffer(
+            b'\x00\x00\x00\x00\x00\x00\x00\x80\xff\x3f\x00\x00\x00\x00\x00\x00',
+            dtype=' decimal
+dec_map: DecMapType = {
+    # DCT
+    (dct, np.float64, 1): 13,
+    (dct, np.float32, 1): 6,
+
+    (dct, np.float64, 2): 14,
+    (dct, np.float32, 2): 5,
+
+    (dct, np.float64, 3): 14,
+    (dct, np.float32, 3): 5,
+
+    (dct, np.float64, 4): 13,
+    (dct, np.float32, 4): 6,
+
+    # IDCT
+    (idct, np.float64, 1): 14,
+    (idct, np.float32, 1): 6,
+
+    (idct, np.float64, 2): 14,
+    (idct, np.float32, 2): 5,
+
+    (idct, np.float64, 3): 14,
+    (idct, np.float32, 3): 5,
+
+    (idct, np.float64, 4): 14,
+    (idct, np.float32, 4): 6,
+
+    # DST
+    (dst, np.float64, 1): 13,
+    (dst, np.float32, 1): 6,
+
+    (dst, np.float64, 2): 14,
+    (dst, np.float32, 2): 6,
+
+    (dst, np.float64, 3): 14,
+    (dst, np.float32, 3): 7,
+
+    (dst, np.float64, 4): 13,
+    (dst, np.float32, 4): 5,
+
+    # IDST
+    (idst, np.float64, 1): 14,
+    (idst, np.float32, 1): 6,
+
+    (idst, np.float64, 2): 14,
+    (idst, np.float32, 2): 6,
+
+    (idst, np.float64, 3): 14,
+    (idst, np.float32, 3): 6,
+
+    (idst, np.float64, 4): 14,
+    (idst, np.float32, 4): 6,
+}
+
+for k,v in dec_map.copy().items():
+    if k[1] == np.float64:
+        dec_map[(k[0], np.longdouble, k[2])] = v
+    elif k[1] == np.float32:
+        dec_map[(k[0], int, k[2])] = v
+
+
+@pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
+@pytest.mark.parametrize('type', [1, 2, 3, 4])
+class TestDCT:
+    def test_definition(self, rdt, type, fftwdata_size,
+                        reference_data, ref_lock):
+        with ref_lock:
+            x, yr, dt = fftw_dct_ref(type, fftwdata_size, rdt, reference_data)
+        y = dct(x, type=type)
+        assert_equal(y.dtype, dt)
+        dec = dec_map[(dct, rdt, type)]
+        assert_allclose(y, yr, rtol=0., atol=np.max(yr)*10**(-dec))
+
+    @pytest.mark.parametrize('size', [7, 8, 9, 16, 32, 64])
+    def test_axis(self, rdt, type, size):
+        nt = 2
+        dec = dec_map[(dct, rdt, type)]
+        x = np.random.randn(nt, size)
+        y = dct(x, type=type)
+        for j in range(nt):
+            assert_array_almost_equal(y[j], dct(x[j], type=type),
+                                      decimal=dec)
+
+        x = x.T
+        y = dct(x, axis=0, type=type)
+        for j in range(nt):
+            assert_array_almost_equal(y[:,j], dct(x[:,j], type=type),
+                                      decimal=dec)
+
+
+@pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
+def test_dct1_definition_ortho(rdt, mdata_x):
+    # Test orthornomal mode.
+    dec = dec_map[(dct, rdt, 1)]
+    x = np.array(mdata_x, dtype=rdt)
+    dt = np.result_type(np.float32, rdt)
+    y = dct(x, norm='ortho', type=1)
+    y2 = naive_dct1(x, norm='ortho')
+    assert_equal(y.dtype, dt)
+    assert_allclose(y, y2, rtol=0., atol=np.max(y2)*10**(-dec))
+
+
+@pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
+def test_dct2_definition_matlab(mdata_xy, rdt):
+    # Test correspondence with matlab (orthornomal mode).
+    dt = np.result_type(np.float32, rdt)
+    x = np.array(mdata_xy[0], dtype=dt)
+
+    yr = mdata_xy[1]
+    y = dct(x, norm="ortho", type=2)
+    dec = dec_map[(dct, rdt, 2)]
+    assert_equal(y.dtype, dt)
+    assert_array_almost_equal(y, yr, decimal=dec)
+
+
+@pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
+def test_dct3_definition_ortho(mdata_x, rdt):
+    # Test orthornomal mode.
+    x = np.array(mdata_x, dtype=rdt)
+    dt = np.result_type(np.float32, rdt)
+    y = dct(x, norm='ortho', type=2)
+    xi = dct(y, norm="ortho", type=3)
+    dec = dec_map[(dct, rdt, 3)]
+    assert_equal(xi.dtype, dt)
+    assert_array_almost_equal(xi, x, decimal=dec)
+
+
+@pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
+def test_dct4_definition_ortho(mdata_x, rdt):
+    # Test orthornomal mode.
+    x = np.array(mdata_x, dtype=rdt)
+    dt = np.result_type(np.float32, rdt)
+    y = dct(x, norm='ortho', type=4)
+    y2 = naive_dct4(x, norm='ortho')
+    dec = dec_map[(dct, rdt, 4)]
+    assert_equal(y.dtype, dt)
+    assert_allclose(y, y2, rtol=0., atol=np.max(y2)*10**(-dec))
+
+
+@pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
+@pytest.mark.parametrize('type', [1, 2, 3, 4])
+def test_idct_definition(fftwdata_size, rdt, type, reference_data, ref_lock):
+    with ref_lock:
+        xr, yr, dt = fftw_dct_ref(type, fftwdata_size, rdt, reference_data)
+    x = idct(yr, type=type)
+    dec = dec_map[(idct, rdt, type)]
+    assert_equal(x.dtype, dt)
+    assert_allclose(x, xr, rtol=0., atol=np.max(xr)*10**(-dec))
+
+
+@pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
+@pytest.mark.parametrize('type', [1, 2, 3, 4])
+def test_definition(fftwdata_size, rdt, type, reference_data, ref_lock):
+    with ref_lock:
+        xr, yr, dt = fftw_dst_ref(type, fftwdata_size, rdt, reference_data)
+    y = dst(xr, type=type)
+    dec = dec_map[(dst, rdt, type)]
+    assert_equal(y.dtype, dt)
+    assert_allclose(y, yr, rtol=0., atol=np.max(yr)*10**(-dec))
+
+
+@pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
+def test_dst1_definition_ortho(rdt, mdata_x):
+    # Test orthornomal mode.
+    dec = dec_map[(dst, rdt, 1)]
+    x = np.array(mdata_x, dtype=rdt)
+    dt = np.result_type(np.float32, rdt)
+    y = dst(x, norm='ortho', type=1)
+    y2 = naive_dst1(x, norm='ortho')
+    assert_equal(y.dtype, dt)
+    assert_allclose(y, y2, rtol=0., atol=np.max(y2)*10**(-dec))
+
+
+@pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
+def test_dst4_definition_ortho(rdt, mdata_x):
+    # Test orthornomal mode.
+    dec = dec_map[(dst, rdt, 4)]
+    x = np.array(mdata_x, dtype=rdt)
+    dt = np.result_type(np.float32, rdt)
+    y = dst(x, norm='ortho', type=4)
+    y2 = naive_dst4(x, norm='ortho')
+    assert_equal(y.dtype, dt)
+    assert_array_almost_equal(y, y2, decimal=dec)
+
+
+@pytest.mark.parametrize('rdt', [np.longdouble, np.float64, np.float32, int])
+@pytest.mark.parametrize('type', [1, 2, 3, 4])
+def test_idst_definition(fftwdata_size, rdt, type, reference_data, ref_lock):
+    with ref_lock:
+        xr, yr, dt = fftw_dst_ref(type, fftwdata_size, rdt, reference_data)
+    x = idst(yr, type=type)
+    dec = dec_map[(idst, rdt, type)]
+    assert_equal(x.dtype, dt)
+    assert_allclose(x, xr, rtol=0., atol=np.max(xr)*10**(-dec))
+
+
+@pytest.mark.parametrize('routine', [dct, dst, idct, idst])
+@pytest.mark.parametrize('dtype', [np.float32, np.float64, np.longdouble])
+@pytest.mark.parametrize('shape, axis', [
+    ((16,), -1), ((16, 2), 0), ((2, 16), 1)
+])
+@pytest.mark.parametrize('type', [1, 2, 3, 4])
+@pytest.mark.parametrize('overwrite_x', [True, False])
+@pytest.mark.parametrize('norm', [None, 'ortho'])
+def test_overwrite(routine, dtype, shape, axis, type, norm, overwrite_x):
+    # Check input overwrite behavior
+    np.random.seed(1234)
+    if np.issubdtype(dtype, np.complexfloating):
+        x = np.random.randn(*shape) + 1j*np.random.randn(*shape)
+    else:
+        x = np.random.randn(*shape)
+    x = x.astype(dtype)
+    x2 = x.copy()
+    routine(x2, type, None, axis, norm, overwrite_x=overwrite_x)
+
+    sig = (f"{routine.__name__}({x.dtype}{x.shape!r}, {None!r}, axis={axis!r}, "
+           f"overwrite_x={overwrite_x!r})")
+    if not overwrite_x:
+        assert_equal(x2, x, err_msg=f"spurious overwrite in {sig}")
+
+
+class Test_DCTN_IDCTN:
+    dec = 14
+    dct_type = [1, 2, 3, 4]
+    norms = [None, 'backward', 'ortho', 'forward']
+    rstate = np.random.RandomState(1234)
+    shape = (32, 16)
+    data = rstate.randn(*shape)
+
+    @pytest.mark.parametrize('fforward,finverse', [(dctn, idctn),
+                                                   (dstn, idstn)])
+    @pytest.mark.parametrize('axes', [None,
+                                      1, (1,), [1],
+                                      0, (0,), [0],
+                                      (0, 1), [0, 1],
+                                      (-2, -1), [-2, -1]])
+    @pytest.mark.parametrize('dct_type', dct_type)
+    @pytest.mark.parametrize('norm', ['ortho'])
+    def test_axes_round_trip(self, fforward, finverse, axes, dct_type, norm):
+        tmp = fforward(self.data, type=dct_type, axes=axes, norm=norm)
+        tmp = finverse(tmp, type=dct_type, axes=axes, norm=norm)
+        assert_array_almost_equal(self.data, tmp, decimal=12)
+
+    @pytest.mark.parametrize('funcn,func', [(dctn, dct), (dstn, dst)])
+    @pytest.mark.parametrize('dct_type', dct_type)
+    @pytest.mark.parametrize('norm', norms)
+    def test_dctn_vs_2d_reference(self, funcn, func, dct_type, norm):
+        y1 = funcn(self.data, type=dct_type, axes=None, norm=norm)
+        y2 = ref_2d(func, self.data, type=dct_type, norm=norm)
+        assert_array_almost_equal(y1, y2, decimal=11)
+
+    @pytest.mark.parametrize('funcn,func', [(idctn, idct), (idstn, idst)])
+    @pytest.mark.parametrize('dct_type', dct_type)
+    @pytest.mark.parametrize('norm', norms)
+    def test_idctn_vs_2d_reference(self, funcn, func, dct_type, norm):
+        fdata = dctn(self.data, type=dct_type, norm=norm)
+        y1 = funcn(fdata, type=dct_type, norm=norm)
+        y2 = ref_2d(func, fdata, type=dct_type, norm=norm)
+        assert_array_almost_equal(y1, y2, decimal=11)
+
+    @pytest.mark.parametrize('fforward,finverse', [(dctn, idctn),
+                                                   (dstn, idstn)])
+    def test_axes_and_shape(self, fforward, finverse):
+        with assert_raises(ValueError,
+                           match="when given, axes and shape arguments"
+                           " have to be of the same length"):
+            fforward(self.data, s=self.data.shape[0], axes=(0, 1))
+
+        with assert_raises(ValueError,
+                           match="when given, axes and shape arguments"
+                           " have to be of the same length"):
+            fforward(self.data, s=self.data.shape, axes=0)
+
+    @pytest.mark.parametrize('fforward', [dctn, dstn])
+    def test_shape(self, fforward):
+        tmp = fforward(self.data, s=(128, 128), axes=None)
+        assert_equal(tmp.shape, (128, 128))
+
+    @pytest.mark.parametrize('fforward,finverse', [(dctn, idctn),
+                                                   (dstn, idstn)])
+    @pytest.mark.parametrize('axes', [1, (1,), [1],
+                                      0, (0,), [0]])
+    def test_shape_is_none_with_axes(self, fforward, finverse, axes):
+        tmp = fforward(self.data, s=None, axes=axes, norm='ortho')
+        tmp = finverse(tmp, s=None, axes=axes, norm='ortho')
+        assert_array_almost_equal(self.data, tmp, decimal=self.dec)
+
+
+@pytest.mark.parametrize('func', [dct, dctn, idct, idctn,
+                                  dst, dstn, idst, idstn])
+def test_swapped_byte_order(func):
+    rng = np.random.RandomState(1234)
+    x = rng.rand(10)
+    swapped_dt = x.dtype.newbyteorder('S')
+    assert_allclose(func(x.astype(swapped_dt)), func(x))
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/_realtransforms.py b/.venv/lib/python3.11/site-packages/scipy/fft/_realtransforms.py
new file mode 100644
index 0000000000000000000000000000000000000000..c18da398fa2b7f7449022c4874e0694d9a34e984
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/_realtransforms.py
@@ -0,0 +1,717 @@
+from ._basic import _dispatch
+from scipy._lib.uarray import Dispatchable
+from scipy._lib._array_api import xp_capabilities
+import numpy as np
+
+__all__ = ['dct', 'idct', 'dst', 'idst', 'dctn', 'idctn', 'dstn', 'idstn']
+
+
+@xp_capabilities(cpu_only=True, allow_dask_compute=True)
+@_dispatch
+def dctn(x, type=2, s=None, axes=None, norm=None, overwrite_x=False,
+         workers=None, *, orthogonalize=None):
+    """
+    Return multidimensional Discrete Cosine Transform along the specified axes.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DCT (see Notes). Default type is 2.
+    s : int or array_like of ints or None, optional
+        The shape of the result. If both `s` and `axes` (see below) are None,
+        `s` is ``x.shape``; if `s` is None but `axes` is not None, then `s` is
+        ``numpy.take(x.shape, axes, axis=0)``.
+        If ``s[i] > x.shape[i]``, the ith dimension of the input is padded with zeros.
+        If ``s[i] < x.shape[i]``, the ith dimension of the input is truncated to length
+        ``s[i]``.
+        If any element of `s` is -1, the size of the corresponding dimension of
+        `x` is used.
+    axes : int or array_like of ints or None, optional
+        Axes over which the DCT is computed. If not given, the last ``len(s)``
+        axes are used, or all axes if `s` is also not specified.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see Notes). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    orthogonalize : bool, optional
+        Whether to use the orthogonalized DCT variant (see Notes).
+        Defaults to ``True`` when ``norm="ortho"`` and ``False`` otherwise.
+
+        .. versionadded:: 1.8.0
+
+    Returns
+    -------
+    y : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    idctn : Inverse multidimensional DCT
+
+    Notes
+    -----
+    For full details of the DCT types and normalization modes, as well as
+    references, see `dct`.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.fft import dctn, idctn
+    >>> rng = np.random.default_rng()
+    >>> y = rng.standard_normal((16, 16))
+    >>> np.allclose(y, idctn(dctn(y)))
+    True
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(cpu_only=True, allow_dask_compute=True)
+@_dispatch
+def idctn(x, type=2, s=None, axes=None, norm=None, overwrite_x=False,
+          workers=None, orthogonalize=None):
+    """
+    Return multidimensional Inverse Discrete Cosine Transform along the specified axes.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DCT (see Notes). Default type is 2.
+    s : int or array_like of ints or None, optional
+        The shape of the result.  If both `s` and `axes` (see below) are
+        None, `s` is ``x.shape``; if `s` is None but `axes` is
+        not None, then `s` is ``numpy.take(x.shape, axes, axis=0)``.
+        If ``s[i] > x.shape[i]``, the ith dimension of the input is padded with zeros.
+        If ``s[i] < x.shape[i]``, the ith dimension of the input is truncated to length
+        ``s[i]``.
+        If any element of `s` is -1, the size of the corresponding dimension of
+        `x` is used.
+    axes : int or array_like of ints or None, optional
+        Axes over which the IDCT is computed. If not given, the last ``len(s)``
+        axes are used, or all axes if `s` is also not specified.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see Notes). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    orthogonalize : bool, optional
+        Whether to use the orthogonalized IDCT variant (see Notes).
+        Defaults to ``True`` when ``norm="ortho"`` and ``False`` otherwise.
+
+        .. versionadded:: 1.8.0
+
+    Returns
+    -------
+    y : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    dctn : multidimensional DCT
+
+    Notes
+    -----
+    For full details of the IDCT types and normalization modes, as well as
+    references, see `idct`.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.fft import dctn, idctn
+    >>> rng = np.random.default_rng()
+    >>> y = rng.standard_normal((16, 16))
+    >>> np.allclose(y, idctn(dctn(y)))
+    True
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(cpu_only=True, allow_dask_compute=True)
+@_dispatch
+def dstn(x, type=2, s=None, axes=None, norm=None, overwrite_x=False,
+         workers=None, orthogonalize=None):
+    """
+    Return multidimensional Discrete Sine Transform along the specified axes.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DST (see Notes). Default type is 2.
+    s : int or array_like of ints or None, optional
+        The shape of the result.  If both `s` and `axes` (see below) are None,
+        `s` is ``x.shape``; if `s` is None but `axes` is not None, then `s` is
+        ``numpy.take(x.shape, axes, axis=0)``.
+        If ``s[i] > x.shape[i]``, the ith dimension of the input is padded with zeros.
+        If ``s[i] < x.shape[i]``, the ith dimension of the input is truncated to length
+        ``s[i]``.
+        If any element of `shape` is -1, the size of the corresponding dimension
+        of `x` is used.
+    axes : int or array_like of ints or None, optional
+        Axes over which the DST is computed. If not given, the last ``len(s)``
+        axes are used, or all axes if `s` is also not specified.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see Notes). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    orthogonalize : bool, optional
+        Whether to use the orthogonalized DST variant (see Notes).
+        Defaults to ``True`` when ``norm="ortho"`` and ``False`` otherwise.
+
+        .. versionadded:: 1.8.0
+
+    Returns
+    -------
+    y : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    idstn : Inverse multidimensional DST
+
+    Notes
+    -----
+    For full details of the DST types and normalization modes, as well as
+    references, see `dst`.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.fft import dstn, idstn
+    >>> rng = np.random.default_rng()
+    >>> y = rng.standard_normal((16, 16))
+    >>> np.allclose(y, idstn(dstn(y)))
+    True
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(cpu_only=True, allow_dask_compute=True)
+@_dispatch
+def idstn(x, type=2, s=None, axes=None, norm=None, overwrite_x=False,
+          workers=None, orthogonalize=None):
+    """
+    Return multidimensional Inverse Discrete Sine Transform along the specified axes.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DST (see Notes). Default type is 2.
+    s : int or array_like of ints or None, optional
+        The shape of the result.  If both `s` and `axes` (see below) are None,
+        `s` is ``x.shape``; if `s` is None but `axes` is not None, then `s` is
+        ``numpy.take(x.shape, axes, axis=0)``.
+        If ``s[i] > x.shape[i]``, the ith dimension of the input is padded with zeros.
+        If ``s[i] < x.shape[i]``, the ith dimension of the input is truncated to length
+        ``s[i]``.
+        If any element of `s` is -1, the size of the corresponding dimension of
+        `x` is used.
+    axes : int or array_like of ints or None, optional
+        Axes over which the IDST is computed. If not given, the last ``len(s)``
+        axes are used, or all axes if `s` is also not specified.
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see Notes). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    orthogonalize : bool, optional
+        Whether to use the orthogonalized IDST variant (see Notes).
+        Defaults to ``True`` when ``norm="ortho"`` and ``False`` otherwise.
+
+        .. versionadded:: 1.8.0
+
+    Returns
+    -------
+    y : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    dstn : multidimensional DST
+
+    Notes
+    -----
+    For full details of the IDST types and normalization modes, as well as
+    references, see `idst`.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.fft import dstn, idstn
+    >>> rng = np.random.default_rng()
+    >>> y = rng.standard_normal((16, 16))
+    >>> np.allclose(y, idstn(dstn(y)))
+    True
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(cpu_only=True, allow_dask_compute=True,
+                 skip_backends=[('jax.numpy', 'XXX large tolerance violations')])
+@_dispatch
+def dct(x, type=2, n=None, axis=-1, norm=None, overwrite_x=False, workers=None,
+        orthogonalize=None):
+    r"""Return the Discrete Cosine Transform of arbitrary type sequence x.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DCT (see Notes). Default type is 2.
+    n : int, optional
+        Length of the transform.  If ``n < x.shape[axis]``, `x` is
+        truncated.  If ``n > x.shape[axis]``, `x` is zero-padded. The
+        default results in ``n = x.shape[axis]``.
+    axis : int, optional
+        Axis along which the dct is computed; the default is over the
+        last axis (i.e., ``axis=-1``).
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see Notes). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    orthogonalize : bool, optional
+        Whether to use the orthogonalized DCT variant (see Notes).
+        Defaults to ``True`` when ``norm="ortho"`` and ``False`` otherwise.
+
+        .. versionadded:: 1.8.0
+
+    Returns
+    -------
+    y : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    idct : Inverse DCT
+
+    Notes
+    -----
+    For a single dimension array ``x``, ``dct(x, norm='ortho')`` is equal to
+    MATLAB ``dct(x)``.
+
+    .. warning:: For ``type in {1, 2, 3}``, ``norm="ortho"`` breaks the direct
+                 correspondence with the direct Fourier transform. To recover
+                 it you must specify ``orthogonalize=False``.
+
+    For ``norm="ortho"`` both the `dct` and `idct` are scaled by the same
+    overall factor in both directions. By default, the transform is also
+    orthogonalized which for types 1, 2 and 3 means the transform definition is
+    modified to give orthogonality of the DCT matrix (see below).
+
+    For ``norm="backward"``, there is no scaling on `dct` and the `idct` is
+    scaled by ``1/N`` where ``N`` is the "logical" size of the DCT. For
+    ``norm="forward"`` the ``1/N`` normalization is applied to the forward
+    `dct` instead and the `idct` is unnormalized.
+
+    There are, theoretically, 8 types of the DCT, only the first 4 types are
+    implemented in SciPy.'The' DCT generally refers to DCT type 2, and 'the'
+    Inverse DCT generally refers to DCT type 3.
+
+    **Type I**
+
+    There are several definitions of the DCT-I; we use the following
+    (for ``norm="backward"``)
+
+    .. math::
+
+       y_k = x_0 + (-1)^k x_{N-1} + 2 \sum_{n=1}^{N-2} x_n \cos\left(
+       \frac{\pi k n}{N-1} \right)
+
+    If ``orthogonalize=True``, ``x[0]`` and ``x[N-1]`` are multiplied by a
+    scaling factor of :math:`\sqrt{2}`, and ``y[0]`` and ``y[N-1]`` are divided
+    by :math:`\sqrt{2}`. When combined with ``norm="ortho"``, this makes the
+    corresponding matrix of coefficients orthonormal (``O @ O.T = np.eye(N)``).
+
+    .. note::
+       The DCT-I is only supported for input size > 1.
+
+    **Type II**
+
+    There are several definitions of the DCT-II; we use the following
+    (for ``norm="backward"``)
+
+    .. math::
+
+       y_k = 2 \sum_{n=0}^{N-1} x_n \cos\left(\frac{\pi k(2n+1)}{2N} \right)
+
+    If ``orthogonalize=True``, ``y[0]`` is divided by :math:`\sqrt{2}` which,
+    when combined with ``norm="ortho"``, makes the corresponding matrix of
+    coefficients orthonormal (``O @ O.T = np.eye(N)``).
+
+    **Type III**
+
+    There are several definitions, we use the following (for
+    ``norm="backward"``)
+
+    .. math::
+
+       y_k = x_0 + 2 \sum_{n=1}^{N-1} x_n \cos\left(\frac{\pi(2k+1)n}{2N}\right)
+
+    If ``orthogonalize=True``, ``x[0]`` terms are multiplied by
+    :math:`\sqrt{2}` which, when combined with ``norm="ortho"``, makes the
+    corresponding matrix of coefficients orthonormal (``O @ O.T = np.eye(N)``).
+
+    The (unnormalized) DCT-III is the inverse of the (unnormalized) DCT-II, up
+    to a factor `2N`. The orthonormalized DCT-III is exactly the inverse of
+    the orthonormalized DCT-II.
+
+    **Type IV**
+
+    There are several definitions of the DCT-IV; we use the following
+    (for ``norm="backward"``)
+
+    .. math::
+
+       y_k = 2 \sum_{n=0}^{N-1} x_n \cos\left(\frac{\pi(2k+1)(2n+1)}{4N} \right)
+
+    ``orthogonalize`` has no effect here, as the DCT-IV matrix is already
+    orthogonal up to a scale factor of ``2N``.
+
+    References
+    ----------
+    .. [1] 'A Fast Cosine Transform in One and Two Dimensions', by J.
+           Makhoul, `IEEE Transactions on acoustics, speech and signal
+           processing` vol. 28(1), pp. 27-34,
+           :doi:`10.1109/TASSP.1980.1163351` (1980).
+    .. [2] Wikipedia, "Discrete cosine transform",
+           https://en.wikipedia.org/wiki/Discrete_cosine_transform
+
+    Examples
+    --------
+    The Type 1 DCT is equivalent to the FFT (though faster) for real,
+    even-symmetrical inputs. The output is also real and even-symmetrical.
+    Half of the FFT input is used to generate half of the FFT output:
+
+    >>> from scipy.fft import fft, dct
+    >>> import numpy as np
+    >>> fft(np.array([4., 3., 5., 10., 5., 3.])).real
+    array([ 30.,  -8.,   6.,  -2.,   6.,  -8.])
+    >>> dct(np.array([4., 3., 5., 10.]), 1)
+    array([ 30.,  -8.,   6.,  -2.])
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(cpu_only=True, allow_dask_compute=True)
+@_dispatch
+def idct(x, type=2, n=None, axis=-1, norm=None, overwrite_x=False,
+         workers=None, orthogonalize=None):
+    """
+    Return the Inverse Discrete Cosine Transform of an arbitrary type sequence.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DCT (see Notes). Default type is 2.
+    n : int, optional
+        Length of the transform.  If ``n < x.shape[axis]``, `x` is
+        truncated.  If ``n > x.shape[axis]``, `x` is zero-padded. The
+        default results in ``n = x.shape[axis]``.
+    axis : int, optional
+        Axis along which the idct is computed; the default is over the
+        last axis (i.e., ``axis=-1``).
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see Notes). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    orthogonalize : bool, optional
+        Whether to use the orthogonalized IDCT variant (see Notes).
+        Defaults to ``True`` when ``norm="ortho"`` and ``False`` otherwise.
+
+        .. versionadded:: 1.8.0
+
+    Returns
+    -------
+    idct : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    dct : Forward DCT
+
+    Notes
+    -----
+    For a single dimension array `x`, ``idct(x, norm='ortho')`` is equal to
+    MATLAB ``idct(x)``.
+
+    .. warning:: For ``type in {1, 2, 3}``, ``norm="ortho"`` breaks the direct
+                 correspondence with the inverse direct Fourier transform. To
+                 recover it you must specify ``orthogonalize=False``.
+
+    For ``norm="ortho"`` both the `dct` and `idct` are scaled by the same
+    overall factor in both directions. By default, the transform is also
+    orthogonalized which for types 1, 2 and 3 means the transform definition is
+    modified to give orthogonality of the IDCT matrix (see `dct` for the full
+    definitions).
+
+    'The' IDCT is the IDCT-II, which is the same as the normalized DCT-III.
+
+    The IDCT is equivalent to a normal DCT except for the normalization and
+    type. DCT type 1 and 4 are their own inverse and DCTs 2 and 3 are each
+    other's inverses.
+
+    Examples
+    --------
+    The Type 1 DCT is equivalent to the DFT for real, even-symmetrical
+    inputs. The output is also real and even-symmetrical. Half of the IFFT
+    input is used to generate half of the IFFT output:
+
+    >>> from scipy.fft import ifft, idct
+    >>> import numpy as np
+    >>> ifft(np.array([ 30.,  -8.,   6.,  -2.,   6.,  -8.])).real
+    array([  4.,   3.,   5.,  10.,   5.,   3.])
+    >>> idct(np.array([ 30.,  -8.,   6.,  -2.]), 1)
+    array([  4.,   3.,   5.,  10.])
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(cpu_only=True, allow_dask_compute=True,
+                 skip_backends=[('jax.numpy', 'XXX large tolerance violations')])
+@_dispatch
+def dst(x, type=2, n=None, axis=-1, norm=None, overwrite_x=False, workers=None,
+        orthogonalize=None):
+    r"""
+    Return the Discrete Sine Transform of arbitrary type sequence x.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DST (see Notes). Default type is 2.
+    n : int, optional
+        Length of the transform. If ``n < x.shape[axis]``, `x` is
+        truncated.  If ``n > x.shape[axis]``, `x` is zero-padded. The
+        default results in ``n = x.shape[axis]``.
+    axis : int, optional
+        Axis along which the dst is computed; the default is over the
+        last axis (i.e., ``axis=-1``).
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see Notes). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    orthogonalize : bool, optional
+        Whether to use the orthogonalized DST variant (see Notes).
+        Defaults to ``True`` when ``norm="ortho"`` and ``False`` otherwise.
+
+        .. versionadded:: 1.8.0
+
+    Returns
+    -------
+    dst : ndarray of reals
+        The transformed input array.
+
+    See Also
+    --------
+    idst : Inverse DST
+
+    Notes
+    -----
+    .. warning:: For ``type in {2, 3}``, ``norm="ortho"`` breaks the direct
+                 correspondence with the direct Fourier transform. To recover
+                 it you must specify ``orthogonalize=False``.
+
+    For ``norm="ortho"`` both the `dst` and `idst` are scaled by the same
+    overall factor in both directions. By default, the transform is also
+    orthogonalized which for types 2 and 3 means the transform definition is
+    modified to give orthogonality of the DST matrix (see below).
+
+    For ``norm="backward"``, there is no scaling on the `dst` and the `idst` is
+    scaled by ``1/N`` where ``N`` is the "logical" size of the DST.
+
+    There are, theoretically, 8 types of the DST for different combinations of
+    even/odd boundary conditions and boundary off sets [1]_, only the first
+    4 types are implemented in SciPy.
+
+    **Type I**
+
+    There are several definitions of the DST-I; we use the following for
+    ``norm="backward"``. DST-I assumes the input is odd around :math:`n=-1` and
+    :math:`n=N`.
+
+    .. math::
+
+        y_k = 2 \sum_{n=0}^{N-1} x_n \sin\left(\frac{\pi(k+1)(n+1)}{N+1}\right)
+
+    Note that the DST-I is only supported for input size > 1.
+    The (unnormalized) DST-I is its own inverse, up to a factor :math:`2(N+1)`.
+    The orthonormalized DST-I is exactly its own inverse.
+
+    ``orthogonalize`` has no effect here, as the DST-I matrix is already
+    orthogonal up to a scale factor of ``2N``.
+
+    **Type II**
+
+    There are several definitions of the DST-II; we use the following for
+    ``norm="backward"``. DST-II assumes the input is odd around :math:`n=-1/2` and
+    :math:`n=N-1/2`; the output is odd around :math:`k=-1` and even around :math:`k=N-1`
+
+    .. math::
+
+        y_k = 2 \sum_{n=0}^{N-1} x_n \sin\left(\frac{\pi(k+1)(2n+1)}{2N}\right)
+
+    If ``orthogonalize=True``, ``y[-1]`` is divided :math:`\sqrt{2}` which, when
+    combined with ``norm="ortho"``, makes the corresponding matrix of
+    coefficients orthonormal (``O @ O.T = np.eye(N)``).
+
+    **Type III**
+
+    There are several definitions of the DST-III, we use the following (for
+    ``norm="backward"``). DST-III assumes the input is odd around :math:`n=-1` and
+    even around :math:`n=N-1`
+
+    .. math::
+
+        y_k = (-1)^k x_{N-1} + 2 \sum_{n=0}^{N-2} x_n \sin\left(
+        \frac{\pi(2k+1)(n+1)}{2N}\right)
+
+    If ``orthogonalize=True``, ``x[-1]`` is multiplied by :math:`\sqrt{2}`
+    which, when combined with ``norm="ortho"``, makes the corresponding matrix
+    of coefficients orthonormal (``O @ O.T = np.eye(N)``).
+
+    The (unnormalized) DST-III is the inverse of the (unnormalized) DST-II, up
+    to a factor :math:`2N`. The orthonormalized DST-III is exactly the inverse of the
+    orthonormalized DST-II.
+
+    **Type IV**
+
+    There are several definitions of the DST-IV, we use the following (for
+    ``norm="backward"``). DST-IV assumes the input is odd around :math:`n=-0.5` and
+    even around :math:`n=N-0.5`
+
+    .. math::
+
+        y_k = 2 \sum_{n=0}^{N-1} x_n \sin\left(\frac{\pi(2k+1)(2n+1)}{4N}\right)
+
+    ``orthogonalize`` has no effect here, as the DST-IV matrix is already
+    orthogonal up to a scale factor of ``2N``.
+
+    The (unnormalized) DST-IV is its own inverse, up to a factor :math:`2N`. The
+    orthonormalized DST-IV is exactly its own inverse.
+
+    Examples
+    --------
+    Compute the DST of a simple 1D array:
+
+    >>> import numpy as np
+    >>> from scipy.fft import dst
+    >>> x = np.array([1, -1, 1, -1])
+    >>> dst(x, type=2)
+    array([0., 0., 0., 8.])
+
+    This computes the Discrete Sine Transform (DST) of type-II for the input array. 
+    The output contains the transformed values corresponding to the given input sequence
+
+    References
+    ----------
+    .. [1] Wikipedia, "Discrete sine transform",
+           https://en.wikipedia.org/wiki/Discrete_sine_transform
+
+    """
+    return (Dispatchable(x, np.ndarray),)
+
+
+@xp_capabilities(cpu_only=True, allow_dask_compute=True)
+@_dispatch
+def idst(x, type=2, n=None, axis=-1, norm=None, overwrite_x=False,
+         workers=None, orthogonalize=None):
+    """
+    Return the Inverse Discrete Sine Transform of an arbitrary type sequence.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DST (see Notes). Default type is 2.
+    n : int, optional
+        Length of the transform. If ``n < x.shape[axis]``, `x` is
+        truncated.  If ``n > x.shape[axis]``, `x` is zero-padded. The
+        default results in ``n = x.shape[axis]``.
+    axis : int, optional
+        Axis along which the idst is computed; the default is over the
+        last axis (i.e., ``axis=-1``).
+    norm : {"backward", "ortho", "forward"}, optional
+        Normalization mode (see Notes). Default is "backward".
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+    workers : int, optional
+        Maximum number of workers to use for parallel computation. If negative,
+        the value wraps around from ``os.cpu_count()``.
+        See :func:`~scipy.fft.fft` for more details.
+    orthogonalize : bool, optional
+        Whether to use the orthogonalized IDST variant (see Notes).
+        Defaults to ``True`` when ``norm="ortho"`` and ``False`` otherwise.
+
+        .. versionadded:: 1.8.0
+
+    Returns
+    -------
+    idst : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    dst : Forward DST
+
+    Notes
+    -----
+    .. warning:: For ``type in {2, 3}``, ``norm="ortho"`` breaks the direct
+                 correspondence with the inverse direct Fourier transform.
+
+    For ``norm="ortho"`` both the `dst` and `idst` are scaled by the same
+    overall factor in both directions. By default, the transform is also
+    orthogonalized which for types 2 and 3 means the transform definition is
+    modified to give orthogonality of the DST matrix (see `dst` for the full
+    definitions).
+
+    'The' IDST is the IDST-II, which is the same as the normalized DST-III.
+
+    The IDST is equivalent to a normal DST except for the normalization and
+    type. DST type 1 and 4 are their own inverse and DSTs 2 and 3 are each
+    other's inverses.
+
+    """
+    return (Dispatchable(x, np.ndarray),)
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/_realtransforms_backend.py b/.venv/lib/python3.11/site-packages/scipy/fft/_realtransforms_backend.py
new file mode 100644
index 0000000000000000000000000000000000000000..2042453733bec54860974cc1e20ba908e8c9b94d
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/_realtransforms_backend.py
@@ -0,0 +1,63 @@
+from scipy._lib._array_api import array_namespace
+import numpy as np
+from . import _pocketfft
+
+__all__ = ['dct', 'idct', 'dst', 'idst', 'dctn', 'idctn', 'dstn', 'idstn']
+
+
+def _execute(pocketfft_func, x, type, s, axes, norm, 
+             overwrite_x, workers, orthogonalize):
+    xp = array_namespace(x)
+    x = np.asarray(x)
+    y = pocketfft_func(x, type, s, axes, norm,
+                       overwrite_x=overwrite_x, workers=workers,
+                       orthogonalize=orthogonalize)
+    return xp.asarray(y)
+
+
+def dctn(x, type=2, s=None, axes=None, norm=None,
+         overwrite_x=False, workers=None, *, orthogonalize=None):
+    return _execute(_pocketfft.dctn, x, type, s, axes, norm, 
+                    overwrite_x, workers, orthogonalize)
+
+
+def idctn(x, type=2, s=None, axes=None, norm=None,
+          overwrite_x=False, workers=None, *, orthogonalize=None):
+    return _execute(_pocketfft.idctn, x, type, s, axes, norm, 
+                    overwrite_x, workers, orthogonalize)
+
+
+def dstn(x, type=2, s=None, axes=None, norm=None,
+         overwrite_x=False, workers=None, orthogonalize=None):
+    return _execute(_pocketfft.dstn, x, type, s, axes, norm, 
+                    overwrite_x, workers, orthogonalize)
+
+
+def idstn(x, type=2, s=None, axes=None, norm=None,
+          overwrite_x=False, workers=None, *, orthogonalize=None):
+    return _execute(_pocketfft.idstn, x, type, s, axes, norm, 
+                    overwrite_x, workers, orthogonalize)
+
+
+def dct(x, type=2, n=None, axis=-1, norm=None,
+        overwrite_x=False, workers=None, orthogonalize=None):
+    return _execute(_pocketfft.dct, x, type, n, axis, norm, 
+                    overwrite_x, workers, orthogonalize)
+
+
+def idct(x, type=2, n=None, axis=-1, norm=None,
+         overwrite_x=False, workers=None, orthogonalize=None):
+    return _execute(_pocketfft.idct, x, type, n, axis, norm, 
+                    overwrite_x, workers, orthogonalize)
+
+
+def dst(x, type=2, n=None, axis=-1, norm=None,
+        overwrite_x=False, workers=None, orthogonalize=None):
+    return _execute(_pocketfft.dst, x, type, n, axis, norm, 
+                    overwrite_x, workers, orthogonalize)
+
+
+def idst(x, type=2, n=None, axis=-1, norm=None,
+         overwrite_x=False, workers=None, orthogonalize=None):
+    return _execute(_pocketfft.idst, x, type, n, axis, norm, 
+                    overwrite_x, workers, orthogonalize)
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/tests/__init__.py b/.venv/lib/python3.11/site-packages/scipy/fft/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/tests/mock_backend.py b/.venv/lib/python3.11/site-packages/scipy/fft/tests/mock_backend.py
new file mode 100644
index 0000000000000000000000000000000000000000..48a7d2b3b50501b84f7c18e366ad2e66782b4ab6
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/tests/mock_backend.py
@@ -0,0 +1,96 @@
+import numpy as np
+import scipy.fft
+import threading
+
+class _MockFunction:
+    def __init__(self, return_value = None):
+        self.number_calls = threading.local()
+        self.return_value = return_value
+        self.last_args = threading.local()
+
+    def __call__(self, *args, **kwargs):
+        if not hasattr(self.number_calls, 'c'):
+            self.number_calls.c = 0
+
+        self.number_calls.c += 1
+        self.last_args.l = (args, kwargs)
+        return self.return_value
+
+
+fft = _MockFunction(np.random.random(10))
+fft2 = _MockFunction(np.random.random(10))
+fftn = _MockFunction(np.random.random(10))
+
+ifft = _MockFunction(np.random.random(10))
+ifft2 = _MockFunction(np.random.random(10))
+ifftn = _MockFunction(np.random.random(10))
+
+rfft = _MockFunction(np.random.random(10))
+rfft2 = _MockFunction(np.random.random(10))
+rfftn = _MockFunction(np.random.random(10))
+
+irfft = _MockFunction(np.random.random(10))
+irfft2 = _MockFunction(np.random.random(10))
+irfftn = _MockFunction(np.random.random(10))
+
+hfft = _MockFunction(np.random.random(10))
+hfft2 = _MockFunction(np.random.random(10))
+hfftn = _MockFunction(np.random.random(10))
+
+ihfft = _MockFunction(np.random.random(10))
+ihfft2 = _MockFunction(np.random.random(10))
+ihfftn = _MockFunction(np.random.random(10))
+
+dct = _MockFunction(np.random.random(10))
+idct = _MockFunction(np.random.random(10))
+dctn = _MockFunction(np.random.random(10))
+idctn = _MockFunction(np.random.random(10))
+
+dst = _MockFunction(np.random.random(10))
+idst = _MockFunction(np.random.random(10))
+dstn = _MockFunction(np.random.random(10))
+idstn = _MockFunction(np.random.random(10))
+
+fht = _MockFunction(np.random.random(10))
+ifht = _MockFunction(np.random.random(10))
+
+
+__ua_domain__ = "numpy.scipy.fft"
+
+
+_implements = {
+    scipy.fft.fft: fft,
+    scipy.fft.fft2: fft2,
+    scipy.fft.fftn: fftn,
+    scipy.fft.ifft: ifft,
+    scipy.fft.ifft2: ifft2,
+    scipy.fft.ifftn: ifftn,
+    scipy.fft.rfft: rfft,
+    scipy.fft.rfft2: rfft2,
+    scipy.fft.rfftn: rfftn,
+    scipy.fft.irfft: irfft,
+    scipy.fft.irfft2: irfft2,
+    scipy.fft.irfftn: irfftn,
+    scipy.fft.hfft: hfft,
+    scipy.fft.hfft2: hfft2,
+    scipy.fft.hfftn: hfftn,
+    scipy.fft.ihfft: ihfft,
+    scipy.fft.ihfft2: ihfft2,
+    scipy.fft.ihfftn: ihfftn,
+    scipy.fft.dct: dct,
+    scipy.fft.idct: idct,
+    scipy.fft.dctn: dctn,
+    scipy.fft.idctn: idctn,
+    scipy.fft.dst: dst,
+    scipy.fft.idst: idst,
+    scipy.fft.dstn: dstn,
+    scipy.fft.idstn: idstn,
+    scipy.fft.fht: fht,
+    scipy.fft.ifht: ifht
+}
+
+
+def __ua_function__(method, args, kwargs):
+    fn = _implements.get(method)
+    return (fn(*args, **kwargs) if fn is not None
+            else NotImplemented)
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/tests/test_backend.py b/.venv/lib/python3.11/site-packages/scipy/fft/tests/test_backend.py
new file mode 100644
index 0000000000000000000000000000000000000000..933e9c0302d46faf33b3dc015d58996e3e46c058
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/tests/test_backend.py
@@ -0,0 +1,98 @@
+from functools import partial
+
+import numpy as np
+import scipy.fft
+from scipy.fft import _fftlog, _pocketfft, set_backend
+from scipy.fft.tests import mock_backend
+
+from numpy.testing import assert_allclose, assert_equal
+import pytest
+
+fnames = ('fft', 'fft2', 'fftn',
+          'ifft', 'ifft2', 'ifftn',
+          'rfft', 'rfft2', 'rfftn',
+          'irfft', 'irfft2', 'irfftn',
+          'dct', 'idct', 'dctn', 'idctn',
+          'dst', 'idst', 'dstn', 'idstn',
+          'fht', 'ifht')
+
+np_funcs = (np.fft.fft, np.fft.fft2, np.fft.fftn,
+            np.fft.ifft, np.fft.ifft2, np.fft.ifftn,
+            np.fft.rfft, np.fft.rfft2, np.fft.rfftn,
+            np.fft.irfft, np.fft.irfft2, np.fft.irfftn,
+            np.fft.hfft, _pocketfft.hfft2, _pocketfft.hfftn,  # np has no hfftn
+            np.fft.ihfft, _pocketfft.ihfft2, _pocketfft.ihfftn,
+            _pocketfft.dct, _pocketfft.idct, _pocketfft.dctn, _pocketfft.idctn,
+            _pocketfft.dst, _pocketfft.idst, _pocketfft.dstn, _pocketfft.idstn,
+            # must provide required kwargs for fht, ifht
+            partial(_fftlog.fht, dln=2, mu=0.5),
+            partial(_fftlog.ifht, dln=2, mu=0.5))
+
+funcs = (scipy.fft.fft, scipy.fft.fft2, scipy.fft.fftn,
+         scipy.fft.ifft, scipy.fft.ifft2, scipy.fft.ifftn,
+         scipy.fft.rfft, scipy.fft.rfft2, scipy.fft.rfftn,
+         scipy.fft.irfft, scipy.fft.irfft2, scipy.fft.irfftn,
+         scipy.fft.hfft, scipy.fft.hfft2, scipy.fft.hfftn,
+         scipy.fft.ihfft, scipy.fft.ihfft2, scipy.fft.ihfftn,
+         scipy.fft.dct, scipy.fft.idct, scipy.fft.dctn, scipy.fft.idctn,
+         scipy.fft.dst, scipy.fft.idst, scipy.fft.dstn, scipy.fft.idstn,
+         # must provide required kwargs for fht, ifht
+         partial(scipy.fft.fht, dln=2, mu=0.5),
+         partial(scipy.fft.ifht, dln=2, mu=0.5))
+
+mocks = (mock_backend.fft, mock_backend.fft2, mock_backend.fftn,
+         mock_backend.ifft, mock_backend.ifft2, mock_backend.ifftn,
+         mock_backend.rfft, mock_backend.rfft2, mock_backend.rfftn,
+         mock_backend.irfft, mock_backend.irfft2, mock_backend.irfftn,
+         mock_backend.hfft, mock_backend.hfft2, mock_backend.hfftn,
+         mock_backend.ihfft, mock_backend.ihfft2, mock_backend.ihfftn,
+         mock_backend.dct, mock_backend.idct,
+         mock_backend.dctn, mock_backend.idctn,
+         mock_backend.dst, mock_backend.idst,
+         mock_backend.dstn, mock_backend.idstn,
+         mock_backend.fht, mock_backend.ifht)
+
+
+@pytest.mark.parametrize("func, np_func, mock", zip(funcs, np_funcs, mocks))
+def test_backend_call(func, np_func, mock):
+    x = np.arange(20).reshape((10,2))
+    answer = np_func(x.astype(np.float64))
+    assert_allclose(func(x), answer, atol=1e-10)
+
+    with set_backend(mock_backend, only=True):
+        mock.number_calls.c = 0
+        y = func(x)
+        assert_equal(y, mock.return_value)
+        assert_equal(mock.number_calls.c, 1)
+
+    assert_allclose(func(x), answer, atol=1e-10)
+
+
+plan_funcs = (scipy.fft.fft, scipy.fft.fft2, scipy.fft.fftn,
+              scipy.fft.ifft, scipy.fft.ifft2, scipy.fft.ifftn,
+              scipy.fft.rfft, scipy.fft.rfft2, scipy.fft.rfftn,
+              scipy.fft.irfft, scipy.fft.irfft2, scipy.fft.irfftn,
+              scipy.fft.hfft, scipy.fft.hfft2, scipy.fft.hfftn,
+              scipy.fft.ihfft, scipy.fft.ihfft2, scipy.fft.ihfftn)
+
+plan_mocks = (mock_backend.fft, mock_backend.fft2, mock_backend.fftn,
+              mock_backend.ifft, mock_backend.ifft2, mock_backend.ifftn,
+              mock_backend.rfft, mock_backend.rfft2, mock_backend.rfftn,
+              mock_backend.irfft, mock_backend.irfft2, mock_backend.irfftn,
+              mock_backend.hfft, mock_backend.hfft2, mock_backend.hfftn,
+              mock_backend.ihfft, mock_backend.ihfft2, mock_backend.ihfftn)
+
+
+@pytest.mark.parametrize("func, mock", zip(plan_funcs, plan_mocks))
+def test_backend_plan(func, mock):
+    x = np.arange(20).reshape((10, 2))
+
+    with pytest.raises(NotImplementedError, match='precomputed plan'):
+        func(x, plan='foo')
+
+    with set_backend(mock_backend, only=True):
+        mock.number_calls.c = 0
+        y = func(x, plan='foo')
+        assert_equal(y, mock.return_value)
+        assert_equal(mock.number_calls.c, 1)
+        assert_equal(mock.last_args.l[1]['plan'], 'foo')
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/tests/test_basic.py b/.venv/lib/python3.11/site-packages/scipy/fft/tests/test_basic.py
new file mode 100644
index 0000000000000000000000000000000000000000..8d25baba624de6a639b815cc6bf15cddf5cdac69
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/tests/test_basic.py
@@ -0,0 +1,549 @@
+import queue
+import threading
+import multiprocessing
+import numpy as np
+import pytest
+from numpy.random import random
+from numpy.testing import assert_array_almost_equal, assert_allclose
+from pytest import raises as assert_raises
+import scipy.fft as fft
+from scipy._lib._array_api import (
+    is_numpy, xp_size, xp_assert_close, xp_assert_equal, make_xp_test_case,
+    make_xp_pytest_param
+)
+
+lazy_xp_modules = [fft]
+skip_xp_backends = pytest.mark.skip_xp_backends
+
+
+# Expected input dtypes. Note that `scipy.fft` is more flexible for numpy,
+# but for C2C transforms like `fft.fft`, the array API standard only mandates
+# that complex dtypes should work, float32/float64 aren't guaranteed to.
+def get_expected_input_dtype(func, xp):
+    # use __name__ so that `lazy_xp_function` doesn't break things
+    if func.__name__ in ["fft", "fftn", "fft2", "ifft", "ifftn", "ifft2", "hfft",
+                         "hfftn", "hfft2", "irfft", "irfftn", "irfft2"]:
+        dtype = xp.complex128
+    elif func.__name__ in ["rfft", "rfftn", "rfft2", "ihfft", "ihfftn", "ihfft2"]:
+        dtype = xp.float64
+    else:
+        raise ValueError(f'Unknown FFT function: {func}')
+
+    return dtype
+
+
+def fft1(x):
+    L = len(x)
+    phase = -2j*np.pi*(np.arange(L)/float(L))
+    phase = np.arange(L).reshape(-1, 1) * phase
+    return np.sum(x*np.exp(phase), axis=1)
+
+
+class TestFFT:
+    @make_xp_test_case(fft.ifft, fft.fft, fft.rfft, fft.irfft)
+    def test_identity(self, xp):
+        maxlen = 512
+        x = xp.asarray(random(maxlen) + 1j*random(maxlen))
+        xr = xp.asarray(random(maxlen))
+        # Check some powers of 2 and some primes
+        for i in [1, 2, 16, 128, 512, 53, 149, 281, 397]:
+            xp_assert_close(fft.ifft(fft.fft(x[0:i])), x[0:i])
+            xp_assert_close(fft.irfft(fft.rfft(xr[0:i]), i), xr[0:i])
+
+    @skip_xp_backends(np_only=True, reason='significant overhead for some backends')
+    def test_identity_extensive(self, xp):
+        maxlen = 512
+        x = xp.asarray(random(maxlen) + 1j*random(maxlen))
+        xr = xp.asarray(random(maxlen))
+        for i in range(1, maxlen):
+            xp_assert_close(fft.ifft(fft.fft(x[0:i])), x[0:i])
+            xp_assert_close(fft.irfft(fft.rfft(xr[0:i]), i), xr[0:i])
+
+    @make_xp_test_case(fft.fft)
+    def test_fft(self, xp):
+        x = random(30) + 1j*random(30)
+        expect = xp.asarray(fft1(x))
+        x = xp.asarray(x)
+        xp_assert_close(fft.fft(x), expect)
+        xp_assert_close(fft.fft(x, norm="backward"), expect)
+        xp_assert_close(fft.fft(x, norm="ortho"),
+                        expect / xp.sqrt(xp.asarray(30, dtype=xp.float64)),)
+        xp_assert_close(fft.fft(x, norm="forward"), expect / 30)
+
+    @skip_xp_backends(np_only=True, reason='some backends allow `n=0`')
+    def test_fft_n(self, xp):
+        x = xp.asarray([1, 2, 3], dtype=xp.complex128)
+        assert_raises(ValueError, fft.fft, x, 0)
+
+    @make_xp_test_case(fft.fft, fft.ifft)
+    def test_ifft(self, xp):
+        x = xp.asarray(random(30) + 1j*random(30))
+        xp_assert_close(fft.ifft(fft.fft(x)), x)
+        for norm in ["backward", "ortho", "forward"]:
+            xp_assert_close(fft.ifft(fft.fft(x, norm=norm), norm=norm), x)
+
+    @make_xp_test_case(fft.fft, fft.fft2)
+    def test_fft2(self, xp):
+        x = xp.asarray(random((30, 20)) + 1j*random((30, 20)))
+        expect = fft.fft(fft.fft(x, axis=1), axis=0)
+        xp_assert_close(fft.fft2(x), expect)
+        xp_assert_close(fft.fft2(x, norm="backward"), expect)
+        xp_assert_close(fft.fft2(x, norm="ortho"),
+                        expect / xp.sqrt(xp.asarray(30 * 20, dtype=xp.float64)))
+        xp_assert_close(fft.fft2(x, norm="forward"), expect / (30 * 20))
+
+    @make_xp_test_case(fft.ifft, fft.ifft2)
+    def test_ifft2(self, xp):
+        x = xp.asarray(random((30, 20)) + 1j*random((30, 20)))
+        expect = fft.ifft(fft.ifft(x, axis=1), axis=0)
+        xp_assert_close(fft.ifft2(x), expect)
+        xp_assert_close(fft.ifft2(x, norm="backward"), expect)
+        xp_assert_close(fft.ifft2(x, norm="ortho"),
+                        expect * xp.sqrt(xp.asarray(30 * 20, dtype=xp.float64)))
+        xp_assert_close(fft.ifft2(x, norm="forward"), expect * (30 * 20))
+
+    @make_xp_test_case(fft.fft, fft.fftn)
+    def test_fftn(self, xp):
+        x = xp.asarray(random((30, 20, 10)) + 1j*random((30, 20, 10)))
+        expect = fft.fft(fft.fft(fft.fft(x, axis=2), axis=1), axis=0)
+        xp_assert_close(fft.fftn(x), expect)
+        xp_assert_close(fft.fftn(x, norm="backward"), expect)
+        xp_assert_close(fft.fftn(x, norm="ortho"),
+                        expect / xp.sqrt(xp.asarray(30 * 20 * 10, dtype=xp.float64)))
+        xp_assert_close(fft.fftn(x, norm="forward"), expect / (30 * 20 * 10))
+
+    @make_xp_test_case(fft.ifft, fft.ifftn)
+    def test_ifftn(self, xp):
+        x = xp.asarray(random((30, 20, 10)) + 1j*random((30, 20, 10)))
+        expect = fft.ifft(fft.ifft(fft.ifft(x, axis=2), axis=1), axis=0)
+        xp_assert_close(fft.ifftn(x), expect, rtol=1e-7)
+        xp_assert_close(fft.ifftn(x, norm="backward"), expect, rtol=1e-7)
+        xp_assert_close(
+            fft.ifftn(x, norm="ortho"),
+            fft.ifftn(x) * xp.sqrt(xp.asarray(30 * 20 * 10, dtype=xp.float64))
+        )
+        xp_assert_close(fft.ifftn(x, norm="forward"),
+                        expect * (30 * 20 * 10),
+                        rtol=1e-7)
+
+    @make_xp_test_case(fft.fft, fft.rfft)
+    def test_rfft(self, xp):
+        x = xp.asarray(random(29), dtype=xp.float64)
+        for n in [xp_size(x), 2*xp_size(x)]:
+            for norm in [None, "backward", "ortho", "forward"]:
+                xp_assert_close(fft.rfft(x, n=n, norm=norm),
+                                fft.fft(xp.asarray(x, dtype=xp.complex128),
+                                        n=n, norm=norm)[:(n//2 + 1)])
+            xp_assert_close(
+                fft.rfft(x, n=n, norm="ortho"),
+                fft.rfft(x, n=n) / xp.sqrt(xp.asarray(n, dtype=xp.float64))
+            )
+
+    @make_xp_test_case(fft.irfft, fft.rfft)
+    def test_irfft(self, xp):
+        x = xp.asarray(random(30))
+        xp_assert_close(fft.irfft(fft.rfft(x)), x)
+        for norm in ["backward", "ortho", "forward"]:
+            xp_assert_close(fft.irfft(fft.rfft(x, norm=norm), norm=norm), x)
+
+    @make_xp_test_case(fft.rfft2)
+    def test_rfft2(self, xp):
+        x = xp.asarray(random((30, 20)), dtype=xp.float64)
+        expect = fft.fft2(xp.asarray(x, dtype=xp.complex128))[:, :11]
+        xp_assert_close(fft.rfft2(x), expect)
+        xp_assert_close(fft.rfft2(x, norm="backward"), expect)
+        xp_assert_close(fft.rfft2(x, norm="ortho"),
+                        expect / xp.sqrt(xp.asarray(30 * 20, dtype=xp.float64)))
+        xp_assert_close(fft.rfft2(x, norm="forward"), expect / (30 * 20))
+
+    @make_xp_test_case(fft.rfft2, fft.irfft2)
+    def test_irfft2(self, xp):
+        x = xp.asarray(random((30, 20)))
+        xp_assert_close(fft.irfft2(fft.rfft2(x)), x)
+        for norm in ["backward", "ortho", "forward"]:
+            xp_assert_close(fft.irfft2(fft.rfft2(x, norm=norm), norm=norm), x)
+
+    @make_xp_test_case(fft.fftn, fft.rfftn)
+    def test_rfftn(self, xp):
+        x = xp.asarray(random((30, 20, 10)), dtype=xp.float64)
+        expect = fft.fftn(xp.asarray(x, dtype=xp.complex128))[:, :, :6]
+        xp_assert_close(fft.rfftn(x), expect)
+        xp_assert_close(fft.rfftn(x, norm="backward"), expect)
+        xp_assert_close(fft.rfftn(x, norm="ortho"),
+                        expect / xp.sqrt(xp.asarray(30 * 20 * 10, dtype=xp.float64)))
+        xp_assert_close(fft.rfftn(x, norm="forward"), expect / (30 * 20 * 10))
+
+    @make_xp_test_case(fft.irfftn, fft.rfftn)
+    def test_irfftn(self, xp):
+        x = xp.asarray(random((30, 20, 10)))
+        xp_assert_close(fft.irfftn(fft.rfftn(x)), x)
+        for norm in ["backward", "ortho", "forward"]:
+            xp_assert_close(fft.irfftn(fft.rfftn(x, norm=norm), norm=norm), x)
+
+    @make_xp_test_case(fft.hfft, fft.fft)
+    def test_hfft(self, xp):
+        x = random(14) + 1j*random(14)
+        x_herm = np.concatenate((random(1), x, random(1)))
+        x = np.concatenate((x_herm, x[::-1].conj()))
+        x = xp.asarray(x)
+        x_herm = xp.asarray(x_herm)
+        expect = xp.real(fft.fft(x))
+        xp_assert_close(fft.hfft(x_herm), expect)
+        xp_assert_close(fft.hfft(x_herm, norm="backward"), expect)
+        xp_assert_close(fft.hfft(x_herm, norm="ortho"),
+                        expect / xp.sqrt(xp.asarray(30, dtype=xp.float64)))
+        xp_assert_close(fft.hfft(x_herm, norm="forward"), expect / 30)
+
+    @make_xp_test_case(fft.hfft, fft.ihfft)
+    def test_ihfft(self, xp):
+        x = random(14) + 1j*random(14)
+        x_herm = np.concatenate((random(1), x, random(1)))
+        x = np.concatenate((x_herm, x[::-1].conj()))
+        x = xp.asarray(x)
+        x_herm = xp.asarray(x_herm)
+        xp_assert_close(fft.ihfft(fft.hfft(x_herm)), x_herm)
+        for norm in ["backward", "ortho", "forward"]:
+            xp_assert_close(fft.ihfft(fft.hfft(x_herm, norm=norm), norm=norm), x_herm)
+
+    @make_xp_test_case(fft.hfft2, fft.ihfft2)
+    def test_hfft2(self, xp):
+        x = xp.asarray(random((30, 20)))
+        xp_assert_close(fft.hfft2(fft.ihfft2(x)), x)
+        for norm in ["backward", "ortho", "forward"]:
+            xp_assert_close(fft.hfft2(fft.ihfft2(x, norm=norm), norm=norm), x)
+
+    @make_xp_test_case(fft.ifft2)
+    def test_ihfft2(self, xp):
+        x = xp.asarray(random((30, 20)), dtype=xp.float64)
+        expect = fft.ifft2(xp.asarray(x, dtype=xp.complex128))[:, :11]
+        xp_assert_close(fft.ihfft2(x), expect)
+        xp_assert_close(fft.ihfft2(x, norm="backward"), expect)
+        xp_assert_close(
+            fft.ihfft2(x, norm="ortho"),
+            expect * xp.sqrt(xp.asarray(30 * 20, dtype=xp.float64))
+        )
+        xp_assert_close(fft.ihfft2(x, norm="forward"), expect * (30 * 20))
+
+    @make_xp_test_case(fft.hfftn, fft.ihfftn)
+    def test_hfftn(self, xp):
+        x = xp.asarray(random((30, 20, 10)))
+        xp_assert_close(fft.hfftn(fft.ihfftn(x)), x)
+        for norm in ["backward", "ortho", "forward"]:
+            xp_assert_close(fft.hfftn(fft.ihfftn(x, norm=norm), norm=norm), x)
+
+    @make_xp_test_case(fft.ifftn, fft.ihfftn)
+    def test_ihfftn(self, xp):
+        x = xp.asarray(random((30, 20, 10)), dtype=xp.float64)
+        expect = fft.ifftn(xp.asarray(x, dtype=xp.complex128))[:, :, :6]
+        xp_assert_close(expect, fft.ihfftn(x))
+        xp_assert_close(expect, fft.ihfftn(x, norm="backward"))
+        xp_assert_close(
+            fft.ihfftn(x, norm="ortho"),
+            expect * xp.sqrt(xp.asarray(30 * 20 * 10, dtype=xp.float64))
+        )
+        xp_assert_close(fft.ihfftn(x, norm="forward"), expect * (30 * 20 * 10))
+
+    def _check_axes(self, op, xp):
+        dtype = get_expected_input_dtype(op, xp)
+        x = xp.asarray(random((30, 20, 10)), dtype=dtype)
+        axes = [(0, 1, 2), (0, 2, 1), (1, 0, 2), (1, 2, 0), (2, 0, 1), (2, 1, 0)]
+
+        for a in axes:
+            op_tr = op(xp.permute_dims(x, axes=a))
+            tr_op = xp.permute_dims(op(x, axes=a), axes=a)
+            xp_assert_close(op_tr, tr_op)
+
+    @pytest.mark.parametrize("op", [make_xp_pytest_param(fft.fftn),
+                                    make_xp_pytest_param(fft.ifftn),
+                                    make_xp_pytest_param(fft.rfftn),
+                                    make_xp_pytest_param(fft.irfftn)])
+    def test_axes_standard(self, op, xp):
+        self._check_axes(op, xp)
+
+    @pytest.mark.parametrize("op", [make_xp_pytest_param(fft.hfftn),
+                                    make_xp_pytest_param(fft.ihfftn)])
+    def test_axes_non_standard(self, op, xp):
+        self._check_axes(op, xp)
+
+    @pytest.mark.parametrize("op", [make_xp_pytest_param(fft.fftn),
+                                    make_xp_pytest_param(fft.ifftn),
+                                    make_xp_pytest_param(fft.rfftn),
+                                    make_xp_pytest_param(fft.irfftn)])
+    def test_axes_subset_with_shape_standard(self, op, xp):
+        dtype = get_expected_input_dtype(op, xp)
+        x = xp.asarray(random((16, 8, 4)), dtype=dtype)
+        axes = [(0, 1, 2), (0, 2, 1), (1, 2, 0)]
+
+        for a in axes:
+            # different shape on the first two axes
+            shape = tuple([2*x.shape[ax] if ax in a[:2] else x.shape[ax]
+                           for ax in range(x.ndim)])
+            # transform only the first two axes
+            op_tr = op(xp.permute_dims(x, axes=a),
+                       s=shape[:2], axes=(0, 1))
+            tr_op = xp.permute_dims(op(x, s=shape[:2], axes=a[:2]),
+                                         axes=a)
+            xp_assert_close(op_tr, tr_op)
+
+    @pytest.mark.parametrize("op", [make_xp_pytest_param(fft.fft2),
+                                    make_xp_pytest_param(fft.ifft2),
+                                    make_xp_pytest_param(fft.rfft2),
+                                    make_xp_pytest_param(fft.irfft2),
+                                    make_xp_pytest_param(fft.hfft2),
+                                    make_xp_pytest_param(fft.ihfft2),
+                                    make_xp_pytest_param(fft.hfftn),
+                                    make_xp_pytest_param(fft.ihfftn)])
+    def test_axes_subset_with_shape_non_standard(self, op, xp):
+        dtype = get_expected_input_dtype(op, xp)
+        x = xp.asarray(random((16, 8, 4)), dtype=dtype)
+        axes = [(0, 1, 2), (0, 2, 1), (1, 2, 0)]
+
+        for a in axes:
+            # different shape on the first two axes
+            shape = tuple([2*x.shape[ax] if ax in a[:2] else x.shape[ax]
+                           for ax in range(x.ndim)])
+            # transform only the first two axes
+            op_tr = op(xp.permute_dims(x, axes=a), s=shape[:2], axes=(0, 1))
+            tr_op = xp.permute_dims(op(x, s=shape[:2], axes=a[:2]), axes=a)
+            xp_assert_close(op_tr, tr_op)
+
+    @make_xp_test_case(fft.rfft, fft.irfft, fft.ihfft, fft.hfft, fft.fft, fft.ifft)
+    def test_all_1d_norm_preserving(self, xp):
+        # verify that round-trip transforms are norm-preserving
+        x = xp.asarray(random(30), dtype=xp.float64)
+
+        x_norm = xp.linalg.vector_norm(x)
+        n = xp_size(x) * 2
+        func_pairs = [(fft.rfft, fft.irfft),
+                      # hfft: order so the first function takes x.size samples
+                      #       (necessary for comparison to x_norm above)
+                      (fft.ihfft, fft.hfft),
+                      # functions that expect complex dtypes at the end
+                      (fft.fft, fft.ifft),
+                      ]
+        for forw, back in func_pairs:
+            if forw == fft.fft:
+                x = xp.asarray(x, dtype=xp.complex128)
+                x_norm = xp.linalg.vector_norm(x)
+            for n in [xp_size(x), 2*xp_size(x)]:
+                for norm in ['backward', 'ortho', 'forward']:
+                    tmp = forw(x, n=n, norm=norm)
+                    tmp = back(tmp, n=n, norm=norm)
+                    xp_assert_close(xp.linalg.vector_norm(tmp), x_norm)
+
+    @pytest.mark.parametrize("dtype", [np.float16, np.longdouble])
+    def test_dtypes_nonstandard(self, dtype):
+        x = random(30).astype(dtype)
+        out_dtypes = {np.float16: np.complex64, np.longdouble: np.clongdouble}
+        x_complex = x.astype(out_dtypes[dtype])
+
+        res_fft = fft.ifft(fft.fft(x))
+        res_rfft = fft.irfft(fft.rfft(x))
+        res_hfft = fft.hfft(fft.ihfft(x), x.shape[0])
+        # Check both numerical results and exact dtype matches
+        assert_array_almost_equal(res_fft, x_complex)
+        assert_array_almost_equal(res_rfft, x)
+        assert_array_almost_equal(res_hfft, x)
+        assert res_fft.dtype == x_complex.dtype
+        assert res_rfft.dtype == np.result_type(np.float32, x.dtype)
+        assert res_hfft.dtype == np.result_type(np.float32, x.dtype)
+
+    @make_xp_test_case(fft.irfft, fft.rfft)
+    @pytest.mark.parametrize("dtype", ["float32", "float64"])
+    def test_dtypes_real(self, dtype, xp):
+        x = xp.asarray(random(30), dtype=getattr(xp, dtype))
+
+        res_rfft = fft.irfft(fft.rfft(x))
+        res_hfft = fft.hfft(fft.ihfft(x), x.shape[0])
+        # Check both numerical results and exact dtype matches
+        xp_assert_close(res_rfft, x)
+        xp_assert_close(res_hfft, x)
+
+    @make_xp_test_case(fft.fft, fft.ifft)
+    @pytest.mark.parametrize("dtype", ["complex64", "complex128"])
+    def test_dtypes_complex(self, dtype, xp):
+        rng = np.random.default_rng(1234)
+        x = xp.asarray(rng.random(30), dtype=getattr(xp, dtype))
+
+        res_fft = fft.ifft(fft.fft(x))
+        # Check both numerical results and exact dtype matches
+        xp_assert_close(res_fft, x)
+
+    @pytest.mark.parametrize("op", [fft.fft, fft.ifft,
+                                    fft.fft2, fft.ifft2,
+                                    fft.fftn, fft.ifftn,
+                                    fft.rfft, fft.irfft,
+                                    fft.rfft2, fft.irfft2,
+                                    fft.rfftn, fft.irfftn,
+                                    fft.hfft, fft.ihfft,
+                                    fft.hfft2, fft.ihfft2,
+                                    fft.hfftn, fft.ihfftn,])
+    def test_array_like(self, op):
+        x = [[[1.0, 1.0], [1.0, 1.0]],
+             [[1.0, 1.0], [1.0, 1.0]],
+             [[1.0, 1.0], [1.0, 1.0]]]
+        xp_assert_close(op(x), op(np.asarray(x)))
+
+
+@pytest.mark.parametrize(
+        "dtype",
+        [np.float32, np.float64, np.longdouble,
+         np.complex64, np.complex128, np.clongdouble])
+@pytest.mark.parametrize("order", ["F", 'non-contiguous'])
+@pytest.mark.parametrize(
+        "fft",
+        [fft.fft, fft.fft2, fft.fftn, fft.ifft, fft.ifft2, fft.ifftn])
+def test_fft_with_order(dtype, order, fft):
+    # Check that FFT/IFFT produces identical results for C, Fortran and
+    # non contiguous arrays
+    rng = np.random.RandomState(42)
+    X = rng.rand(8, 7, 13).astype(dtype, copy=False)
+    if order == 'F':
+        Y = np.asfortranarray(X)
+    else:
+        # Make a non contiguous array
+        Y = X[::-1]
+        X = np.ascontiguousarray(X[::-1])
+
+    if fft.__name__.endswith('fft'):
+        for axis in range(3):
+            X_res = fft(X, axis=axis)
+            Y_res = fft(Y, axis=axis)
+            assert_array_almost_equal(X_res, Y_res)
+    elif fft.__name__.endswith(('fft2', 'fftn')):
+        axes = [(0, 1), (1, 2), (0, 2)]
+        if fft.__name__.endswith('fftn'):
+            axes.extend([(0,), (1,), (2,), None])
+        for ax in axes:
+            X_res = fft(X, axes=ax)
+            Y_res = fft(Y, axes=ax)
+            assert_array_almost_equal(X_res, Y_res)
+    else:
+        raise ValueError
+
+
+@skip_xp_backends(cpu_only=True)
+class TestFFTThreadSafe:
+    threads = 16
+    input_shape = (800, 200)
+
+    def _test_mtsame(self, func, *args, xp=None):
+        def worker(args, q):
+            q.put(func(*args))
+
+        q = queue.Queue()
+        expected = func(*args)
+
+        # Spin off a bunch of threads to call the same function simultaneously
+        t = [threading.Thread(target=worker, args=(args, q))
+             for i in range(self.threads)]
+        [x.start() for x in t]
+
+        [x.join() for x in t]
+
+        # Make sure all threads returned the correct value
+        for i in range(self.threads):
+            xp_assert_equal(
+                q.get(timeout=5), expected,
+                err_msg='Function returned wrong value in multithreaded context'
+            )
+
+    @make_xp_test_case(fft.fft)
+    def test_fft(self, xp):
+        a = xp.ones(self.input_shape, dtype=xp.complex128)
+        self._test_mtsame(fft.fft, a, xp=xp)
+
+    @make_xp_test_case(fft.ifft)
+    def test_ifft(self, xp):
+        a = xp.full(self.input_shape, 1+0j)
+        self._test_mtsame(fft.ifft, a, xp=xp)
+
+    @make_xp_test_case(fft.rfft)
+    def test_rfft(self, xp):
+        a = xp.ones(self.input_shape)
+        self._test_mtsame(fft.rfft, a, xp=xp)
+
+    @make_xp_test_case(fft.irfft)
+    def test_irfft(self, xp):
+        a = xp.full(self.input_shape, 1+0j)
+        self._test_mtsame(fft.irfft, a, xp=xp)
+
+    @make_xp_test_case(fft.hfft)
+    def test_hfft(self, xp):
+        a = xp.ones(self.input_shape, dtype=xp.complex64)
+        self._test_mtsame(fft.hfft, a, xp=xp)
+
+    @make_xp_test_case(fft.ihfft)
+    def test_ihfft(self, xp):
+        a = xp.ones(self.input_shape)
+        self._test_mtsame(fft.ihfft, a, xp=xp)
+
+
+@pytest.mark.parametrize("func", [fft.fft, fft.ifft, fft.rfft, fft.irfft])
+def test_multiprocess(func):
+    # Test that fft still works after fork (gh-10422)
+
+    with multiprocessing.Pool(2) as p:
+        res = p.map(func, [np.ones(100) for _ in range(4)])
+
+    expect = func(np.ones(100))
+    for x in res:
+        assert_allclose(x, expect)
+
+
+@make_xp_test_case(fft.irfftn)
+class TestIRFFTN:
+
+    def test_not_last_axis_success(self, xp):
+        ar, ai = np.random.random((2, 16, 8, 32))
+        a = ar + 1j*ai
+        a = xp.asarray(a)
+
+        axes = (-2,)
+
+        # Should not raise error
+        fft.irfftn(a, axes=axes)
+
+
+@pytest.mark.parametrize("func", [make_xp_pytest_param(fft.fft),
+                                  make_xp_pytest_param(fft.ifft),
+                                  make_xp_pytest_param(fft.rfft),
+                                  make_xp_pytest_param(fft.irfft),
+                                  make_xp_pytest_param(fft.fftn),
+                                  make_xp_pytest_param(fft.ifftn),
+                                  make_xp_pytest_param(fft.rfftn),
+                                  make_xp_pytest_param(fft.irfftn),
+                                  make_xp_pytest_param(fft.hfft),
+                                  make_xp_pytest_param(fft.ihfft)])
+def test_non_standard_params(func, xp):
+    # use __name__ so that `lazy_xp_function` doesn't break things
+    if func.__name__ in ["rfft", "rfftn", "ihfft"]:
+        dtype = xp.float64
+    else:
+        dtype = xp.complex128
+
+    x = xp.asarray([1, 2, 3], dtype=dtype)
+    # func(x) should not raise an exception
+    func(x)
+
+    if is_numpy(xp):
+        func(x, workers=2)
+    else:
+        assert_raises(ValueError, func, x, workers=2)
+
+    # `plan` param is not tested since SciPy does not use it currently
+    # but should be tested if it comes into use
+
+
+@pytest.mark.parametrize("dtype", ['float32', 'float64'])
+@pytest.mark.parametrize("func", [make_xp_pytest_param(fft.fft),
+                                  make_xp_pytest_param(fft.ifft),
+                                  make_xp_pytest_param(fft.irfft),
+                                  make_xp_pytest_param(fft.fftn),
+                                  make_xp_pytest_param(fft.ifftn),
+                                  make_xp_pytest_param(fft.irfftn),
+                                  make_xp_pytest_param(fft.hfft)])
+def test_real_input(func, dtype, xp):
+    x = xp.asarray([1, 2, 3], dtype=getattr(xp, dtype))
+    # func(x) should not raise an exception
+    func(x)
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/tests/test_fftlog.py b/.venv/lib/python3.11/site-packages/scipy/fft/tests/test_fftlog.py
new file mode 100644
index 0000000000000000000000000000000000000000..636d3fa8ffec108971385371213c4680b6a74035
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/tests/test_fftlog.py
@@ -0,0 +1,214 @@
+import warnings
+import math
+
+import numpy as np
+import pytest
+
+from scipy.fft._fftlog import fht, ifht, fhtoffset
+from scipy.special import poch
+
+from scipy._lib._array_api import xp_assert_close, xp_assert_less
+
+skip_xp_backends = pytest.mark.skip_xp_backends
+
+
+def test_fht_agrees_with_fftlog(xp):
+    # check that fht numerically agrees with the output from Fortran FFTLog,
+    # the results were generated with the provided `fftlogtest` program,
+    # after fixing how the k array is generated (divide range by n-1, not n)
+
+    # test function, analytical Hankel transform is of the same form
+    def f(r, mu):
+        return r**(mu+1)*np.exp(-r**2/2)
+
+    r = np.logspace(-4, 4, 16)
+
+    dln = math.log(r[1]/r[0])
+    mu = 0.3
+    offset = 0.0
+    bias = 0.0
+
+    a = xp.asarray(f(r, mu))
+
+    # test 1: compute as given
+    ours = fht(a, dln, mu, offset=offset, bias=bias)
+    theirs = [-0.1159922613593045E-02, +0.1625822618458832E-02,
+              -0.1949518286432330E-02, +0.3789220182554077E-02,
+              +0.5093959119952945E-03, +0.2785387803618774E-01,
+              +0.9944952700848897E-01, +0.4599202164586588E+00,
+              +0.3157462160881342E+00, -0.8201236844404755E-03,
+              -0.7834031308271878E-03, +0.3931444945110708E-03,
+              -0.2697710625194777E-03, +0.3568398050238820E-03,
+              -0.5554454827797206E-03, +0.8286331026468585E-03]
+    theirs = xp.asarray(theirs, dtype=xp.float64)
+    xp_assert_close(ours, theirs)
+
+    # test 2: change to optimal offset
+    offset = fhtoffset(dln, mu, bias=bias)
+    ours = fht(a, dln, mu, offset=offset, bias=bias)
+    theirs = [+0.4353768523152057E-04, -0.9197045663594285E-05,
+              +0.3150140927838524E-03, +0.9149121960963704E-03,
+              +0.5808089753959363E-02, +0.2548065256377240E-01,
+              +0.1339477692089897E+00, +0.4821530509479356E+00,
+              +0.2659899781579785E+00, -0.1116475278448113E-01,
+              +0.1791441617592385E-02, -0.4181810476548056E-03,
+              +0.1314963536765343E-03, -0.5422057743066297E-04,
+              +0.3208681804170443E-04, -0.2696849476008234E-04]
+    theirs = xp.asarray(theirs, dtype=xp.float64)
+    xp_assert_close(ours, theirs)
+
+    # test 3: positive bias
+    bias = 0.8
+    offset = fhtoffset(dln, mu, bias=bias)
+    # offset is a np.float64, which array-api-strict disallows
+    # even if it's technically a subclass of float
+    offset = float(offset)
+
+    ours = fht(a, dln, mu, offset=offset, bias=bias)
+    theirs = [-7.3436673558316850E+00, +0.1710271207817100E+00,
+              +0.1065374386206564E+00, -0.5121739602708132E-01,
+              +0.2636649319269470E-01, +0.1697209218849693E-01,
+              +0.1250215614723183E+00, +0.4739583261486729E+00,
+              +0.2841149874912028E+00, -0.8312764741645729E-02,
+              +0.1024233505508988E-02, -0.1644902767389120E-03,
+              +0.3305775476926270E-04, -0.7786993194882709E-05,
+              +0.1962258449520547E-05, -0.8977895734909250E-06]
+    theirs = xp.asarray(theirs, dtype=xp.float64)
+    xp_assert_close(ours, theirs)
+
+    # test 4: negative bias
+    bias = -0.8
+    offset = fhtoffset(dln, mu, bias=bias)
+    offset = float(offset)
+
+    ours = fht(a, dln, mu, offset=offset, bias=bias)
+    theirs = [+0.8985777068568745E-05, +0.4074898209936099E-04,
+              +0.2123969254700955E-03, +0.1009558244834628E-02,
+              +0.5131386375222176E-02, +0.2461678673516286E-01,
+              +0.1235812845384476E+00, +0.4719570096404403E+00,
+              +0.2893487490631317E+00, -0.1686570611318716E-01,
+              +0.2231398155172505E-01, -0.1480742256379873E-01,
+              +0.1692387813500801E+00, +0.3097490354365797E+00,
+              +2.7593607182401860E+00, 10.5251075070045800E+00]
+    theirs = xp.asarray(theirs, dtype=xp.float64)
+    xp_assert_close(ours, theirs)
+
+
+@pytest.mark.parametrize('optimal', [True, False])
+@pytest.mark.parametrize('offset', [0.0, 1.0, -1.0])
+@pytest.mark.parametrize('bias', [0, 0.1, -0.1])
+@pytest.mark.parametrize('n', [64, 63])
+def test_fht_identity(n, bias, offset, optimal, xp):
+    rng = np.random.RandomState(3491349965)
+
+    a = xp.asarray(rng.standard_normal(n))
+    dln = rng.uniform(-1, 1)
+    mu = rng.uniform(-2, 2)
+
+    if optimal:
+        offset = fhtoffset(dln, mu, initial=offset, bias=bias)
+        # offset is a np.float64, which array-api-strict disallows
+        # even if it's technically a subclass of float
+        offset = float(offset)
+
+    A = fht(a, dln, mu, offset=offset, bias=bias)
+    a_ = ifht(A, dln, mu, offset=offset, bias=bias)
+
+    xp_assert_close(a_, a, rtol=1.5e-7)
+
+
+
+
+def test_fht_special_cases(xp):
+    rng = np.random.RandomState(3491349965)
+
+    a = xp.asarray(rng.standard_normal(64))
+    dln = rng.uniform(-1, 1)
+
+    # let x = (mu+1+q)/2, y = (mu+1-q)/2, M = {0, -1, -2, ...}
+
+    # case 1: x in M, y in M => well-defined transform
+    mu, bias = -4.0, 1.0
+    with warnings.catch_warnings(record=True) as record:
+        fht(a, dln, mu, bias=bias)
+        assert not record, 'fht warned about a well-defined transform'
+
+    # case 2: x not in M, y in M => well-defined transform
+    mu, bias = -2.5, 0.5
+    with warnings.catch_warnings(record=True) as record:
+        fht(a, dln, mu, bias=bias)
+        assert not record, 'fht warned about a well-defined transform'
+
+    # with fht_lock:
+    # case 3: x in M, y not in M => singular transform
+    mu, bias = -3.5, 0.5
+    with pytest.warns(Warning) as record:
+        fht(a, dln, mu, bias=bias)
+        assert record, 'fht did not warn about a singular transform'
+
+    # with fht_lock:
+    # case 4: x not in M, y in M => singular inverse transform
+    mu, bias = -2.5, 0.5
+    with pytest.warns(Warning) as record:
+        ifht(a, dln, mu, bias=bias)
+        assert record, 'ifht did not warn about a singular transform'
+
+
+@pytest.mark.parametrize('n', [64, 63])
+def test_fht_exact(n, xp):
+    rng = np.random.RandomState(3491349965)
+
+    # for a(r) a power law r^\gamma, the fast Hankel transform produces the
+    # exact continuous Hankel transform if biased with q = \gamma
+
+    mu = rng.uniform(0, 3)
+
+    # convergence of HT: -1-mu < gamma < 1/2
+    gamma = rng.uniform(-1-mu, 1/2)
+
+    r = np.logspace(-2, 2, n)
+    a = xp.asarray(r**gamma)
+
+    dln = math.log(r[1]/r[0])
+
+    offset = fhtoffset(dln, mu, initial=0.0, bias=gamma)
+    # offset is a np.float64, which array-api-strict disallows
+    # even if it's technically a subclass of float
+    offset = float(offset)
+
+    A = fht(a, dln, mu, offset=offset, bias=gamma)
+
+    k = np.exp(offset)/r[::-1]
+
+    # analytical result
+    At = xp.asarray((2/k)**gamma * poch((mu+1-gamma)/2, gamma))
+
+    xp_assert_close(A, At)
+
+@skip_xp_backends(np_only=True,
+                  reason='array-likes only supported for NumPy backend')
+@pytest.mark.parametrize("op", [fht, ifht])
+def test_array_like(xp, op):
+    x = [[[1.0, 1.0], [1.0, 1.0]],
+         [[1.0, 1.0], [1.0, 1.0]],
+         [[1.0, 1.0], [1.0, 1.0]]]
+    xp_assert_close(op(x, 1.0, 2.0), op(xp.asarray(x), 1.0, 2.0))
+
+@pytest.mark.parametrize('n', [128, 129])
+def test_gh_21661(xp, n):
+    one = xp.asarray(1.0)
+    mu = 0.0
+    r = np.logspace(-7, 1, n)
+    dln = math.log(r[1] / r[0])
+    offset = fhtoffset(dln, initial=-6 * np.log(10), mu=mu)
+    r = xp.asarray(r, dtype=one.dtype)
+    k = math.exp(offset) / xp.flip(r, axis=-1)
+
+    def f(x, mu):
+        return x**(mu + 1)*xp.exp(-x**2/2)
+
+    a_r = f(r, mu)
+    fht_val = fht(a_r, dln, mu=mu, offset=offset)
+    a_k = f(k, mu)
+    rel_err = xp.max(xp.abs((fht_val - a_k) / a_k))
+    xp_assert_less(rel_err, xp.asarray(7.28e+16)[()])
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/tests/test_helper.py b/.venv/lib/python3.11/site-packages/scipy/fft/tests/test_helper.py
new file mode 100644
index 0000000000000000000000000000000000000000..b0427966c256ab7b919f75f4ae773a4b18be818a
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/tests/test_helper.py
@@ -0,0 +1,555 @@
+"""Includes test functions for fftpack.helper module
+
+Copied from fftpack.helper by Pearu Peterson, October 2005
+Modified for Array API, 2023
+
+"""
+from scipy.fft._helper import next_fast_len, prev_fast_len, _init_nd_shape_and_axes
+from numpy.testing import assert_equal
+from pytest import raises as assert_raises
+import pytest
+import numpy as np
+import sys
+from scipy._lib._array_api import xp_assert_close, xp_device
+from scipy import fft
+
+skip_xp_backends = pytest.mark.skip_xp_backends
+
+_5_smooth_numbers = [
+    2, 3, 4, 5, 6, 8, 9, 10,
+    2 * 3 * 5,
+    2**3 * 3**5,
+    2**3 * 3**3 * 5**2,
+]
+
+
+def test_next_fast_len():
+    for n in _5_smooth_numbers:
+        assert_equal(next_fast_len(n), n)
+
+
+def _assert_n_smooth(x, n):
+    x_orig = x
+    if n < 2:
+        assert False
+
+    while True:
+        q, r = divmod(x, 2)
+        if r != 0:
+            break
+        x = q
+
+    for d in range(3, n+1, 2):
+        while True:
+            q, r = divmod(x, d)
+            if r != 0:
+                break
+            x = q
+
+    assert x == 1, \
+           f'x={x_orig} is not {n}-smooth, remainder={x}'
+
+
+class TestNextFastLen:
+
+    def test_next_fast_len(self):
+        np.random.seed(1234)
+
+        def nums():
+            yield from range(1, 1000)
+            yield 2**5 * 3**5 * 4**5 + 1
+
+        for n in nums():
+            m = next_fast_len(n)
+            _assert_n_smooth(m, 11)
+            assert m == next_fast_len(n, False)
+
+            m = next_fast_len(n, True)
+            _assert_n_smooth(m, 5)
+
+    def test_np_integers(self):
+        ITYPES = [np.int16, np.int32, np.int64, np.uint16, np.uint32, np.uint64]
+        for ityp in ITYPES:
+            x = ityp(12345)
+            testN = next_fast_len(x)
+            assert_equal(testN, next_fast_len(int(x)))
+
+    def testnext_fast_len_small(self):
+        hams = {
+            1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 8, 8: 8, 14: 15, 15: 15,
+            16: 16, 17: 18, 1021: 1024, 1536: 1536, 51200000: 51200000
+        }
+        for x, y in hams.items():
+            assert_equal(next_fast_len(x, True), y)
+
+    @pytest.mark.xfail(sys.maxsize < 2**32,
+                       reason="Hamming Numbers too large for 32-bit",
+                       raises=ValueError, strict=True)
+    def testnext_fast_len_big(self):
+        hams = {
+            510183360: 510183360, 510183360 + 1: 512000000,
+            511000000: 512000000,
+            854296875: 854296875, 854296875 + 1: 859963392,
+            196608000000: 196608000000, 196608000000 + 1: 196830000000,
+            8789062500000: 8789062500000, 8789062500000 + 1: 8796093022208,
+            206391214080000: 206391214080000,
+            206391214080000 + 1: 206624260800000,
+            470184984576000: 470184984576000,
+            470184984576000 + 1: 470715894135000,
+            7222041363087360: 7222041363087360,
+            7222041363087360 + 1: 7230196133913600,
+            # power of 5    5**23
+            11920928955078125: 11920928955078125,
+            11920928955078125 - 1: 11920928955078125,
+            # power of 3    3**34
+            16677181699666569: 16677181699666569,
+            16677181699666569 - 1: 16677181699666569,
+            # power of 2   2**54
+            18014398509481984: 18014398509481984,
+            18014398509481984 - 1: 18014398509481984,
+            # above this, int(ceil(n)) == int(ceil(n+1))
+            19200000000000000: 19200000000000000,
+            19200000000000000 + 1: 19221679687500000,
+            288230376151711744: 288230376151711744,
+            288230376151711744 + 1: 288325195312500000,
+            288325195312500000 - 1: 288325195312500000,
+            288325195312500000: 288325195312500000,
+            288325195312500000 + 1: 288555831593533440,
+        }
+        for x, y in hams.items():
+            assert_equal(next_fast_len(x, True), y)
+
+    def test_keyword_args(self, xp):
+        assert next_fast_len(11, real=True) == 12
+        assert next_fast_len(target=7, real=False) == 7
+
+
+class TestPrevFastLen:
+
+    def test_prev_fast_len(self):
+        np.random.seed(1234)
+
+        def nums():
+            yield from range(1, 1000)
+            yield 2**5 * 3**5 * 4**5 + 1
+
+        for n in nums():
+            m = prev_fast_len(n)
+            _assert_n_smooth(m, 11)
+            assert m == prev_fast_len(n, False)
+
+            m = prev_fast_len(n, True)
+            _assert_n_smooth(m, 5)
+
+    def test_np_integers(self):
+        ITYPES = [np.int16, np.int32, np.int64, np.uint16, np.uint32,
+                    np.uint64]
+        for ityp in ITYPES:
+            x = ityp(12345)
+            testN = prev_fast_len(x)
+            assert_equal(testN, prev_fast_len(int(x)))
+
+            testN = prev_fast_len(x, real=True)
+            assert_equal(testN, prev_fast_len(int(x), real=True))
+
+    def testprev_fast_len_small(self):
+        hams = {
+            1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 6, 8: 8, 14: 12, 15: 15,
+            16: 16, 17: 16, 1021: 1000, 1536: 1536, 51200000: 51200000
+        }
+        for x, y in hams.items():
+            assert_equal(prev_fast_len(x, True), y)
+
+        hams = {
+            1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10,
+            11: 11, 12: 12, 13: 12, 14: 14, 15: 15, 16: 16, 17: 16, 18: 18,
+            19: 18, 20: 20, 21: 21, 22: 22, 120: 120, 121: 121, 122: 121,
+            1021: 1008, 1536: 1536, 51200000: 51200000
+        }
+        for x, y in hams.items():
+            assert_equal(prev_fast_len(x, False), y)
+
+    @pytest.mark.xfail(sys.maxsize < 2**32,
+                       reason="Hamming Numbers too large for 32-bit",
+                       raises=ValueError, strict=True)
+    def testprev_fast_len_big(self):
+        hams = {
+            # 2**6 * 3**13 * 5**1
+            510183360: 510183360,
+            510183360 + 1: 510183360,
+            510183360 - 1: 509607936,  # 2**21 * 3**5
+            # 2**6 * 5**6 * 7**1 * 73**1
+            511000000: 510183360,
+            511000000 + 1: 510183360,
+            511000000 - 1: 510183360,  # 2**6 * 3**13 * 5**1
+            # 3**7 * 5**8
+            854296875: 854296875,
+            854296875 + 1: 854296875,
+            854296875 - 1: 850305600,  # 2**6 * 3**12 * 5**2
+            # 2**22 * 3**1 * 5**6
+            196608000000: 196608000000,
+            196608000000 + 1: 196608000000,
+            196608000000 - 1: 195910410240,  # 2**13 * 3**14 * 5**1
+            # 2**5 * 3**2 * 5**15
+            8789062500000: 8789062500000,
+            8789062500000 + 1: 8789062500000,
+            8789062500000 - 1: 8748000000000,  # 2**11 * 3**7 * 5**9
+            # 2**24 * 3**9 * 5**4
+            206391214080000: 206391214080000,
+            206391214080000 + 1: 206391214080000,
+            206391214080000 - 1: 206158430208000,  # 2**39 * 3**1 * 5**3
+            # 2**18 * 3**15 * 5**3
+            470184984576000: 470184984576000,
+            470184984576000 + 1: 470184984576000,
+            470184984576000 - 1: 469654673817600,  # 2**33 * 3**7 **5**2
+            # 2**25 * 3**16 * 5**1
+            7222041363087360: 7222041363087360,
+            7222041363087360 + 1: 7222041363087360,
+            7222041363087360 - 1: 7213895789838336,  # 2**40 * 3**8
+            # power of 5    5**23
+            11920928955078125: 11920928955078125,
+            11920928955078125 + 1: 11920928955078125,
+            11920928955078125 - 1: 11901557422080000,  # 2**14 * 3**19 * 5**4
+            # power of 3    3**34
+            16677181699666569: 16677181699666569,
+            16677181699666569 + 1: 16677181699666569,
+            16677181699666569 - 1: 16607531250000000,  # 2**7 * 3**12 * 5**12
+            # power of 2   2**54
+            18014398509481984: 18014398509481984,
+            18014398509481984 + 1: 18014398509481984,
+            18014398509481984 - 1: 18000000000000000,  # 2**16 * 3**2 * 5**15
+            # 2**20 * 3**1 * 5**14
+            19200000000000000: 19200000000000000,
+            19200000000000000 + 1: 19200000000000000,
+            19200000000000000 - 1: 19131876000000000,  # 2**11 * 3**14 * 5**9
+            # 2**58
+            288230376151711744: 288230376151711744,
+            288230376151711744 + 1: 288230376151711744,
+            288230376151711744 - 1: 288000000000000000,  # 2**20 * 3**2 * 5**15
+            # 2**5 * 3**10 * 5**16
+            288325195312500000: 288325195312500000,
+            288325195312500000 + 1: 288325195312500000,
+            288325195312500000 - 1: 288230376151711744,  # 2**58
+        }
+        for x, y in hams.items():
+            assert_equal(prev_fast_len(x, True), y)
+
+    def test_keyword_args(self):
+        assert prev_fast_len(11, real=True) == 10
+        assert prev_fast_len(target=7, real=False) == 7
+
+
+@skip_xp_backends(cpu_only=True)
+class Test_init_nd_shape_and_axes:
+
+    def test_py_0d_defaults(self, xp):
+        x = xp.asarray(4)
+        shape = None
+        axes = None
+
+        shape_expected = ()
+        axes_expected = []
+
+        shape_res, axes_res = _init_nd_shape_and_axes(x, shape, axes)
+
+        assert shape_res == shape_expected
+        assert axes_res == axes_expected
+
+    def test_xp_0d_defaults(self, xp):
+        x = xp.asarray(7.)
+        shape = None
+        axes = None
+
+        shape_expected = ()
+        axes_expected = []
+
+        shape_res, axes_res = _init_nd_shape_and_axes(x, shape, axes)
+
+        assert shape_res == shape_expected
+        assert axes_res == axes_expected
+
+    def test_py_1d_defaults(self, xp):
+        x = xp.asarray([1, 2, 3])
+        shape = None
+        axes = None
+
+        shape_expected = (3,)
+        axes_expected = [0]
+
+        shape_res, axes_res = _init_nd_shape_and_axes(x, shape, axes)
+
+        assert shape_res == shape_expected
+        assert axes_res == axes_expected
+
+    def test_xp_1d_defaults(self, xp):
+        x = xp.arange(0, 1, .1)
+        shape = None
+        axes = None
+
+        shape_expected = (10,)
+        axes_expected = [0]
+
+        shape_res, axes_res = _init_nd_shape_and_axes(x, shape, axes)
+
+        assert shape_res == shape_expected
+        assert axes_res == axes_expected
+
+    def test_py_2d_defaults(self, xp):
+        x = xp.asarray([[1, 2, 3, 4],
+                        [5, 6, 7, 8]])
+        shape = None
+        axes = None
+
+        shape_expected = (2, 4)
+        axes_expected = [0, 1]
+
+        shape_res, axes_res = _init_nd_shape_and_axes(x, shape, axes)
+
+        assert shape_res == shape_expected
+        assert axes_res == axes_expected
+
+    def test_xp_2d_defaults(self, xp):
+        x = xp.arange(0, 1, .1)
+        x = xp.reshape(x, (5, 2))
+        shape = None
+        axes = None
+
+        shape_expected = (5, 2)
+        axes_expected = [0, 1]
+
+        shape_res, axes_res = _init_nd_shape_and_axes(x, shape, axes)
+
+        assert shape_res == shape_expected
+        assert axes_res == axes_expected
+
+    def test_xp_5d_defaults(self, xp):
+        x = xp.zeros([6, 2, 5, 3, 4])
+        shape = None
+        axes = None
+
+        shape_expected = (6, 2, 5, 3, 4)
+        axes_expected = [0, 1, 2, 3, 4]
+
+        shape_res, axes_res = _init_nd_shape_and_axes(x, shape, axes)
+
+        assert shape_res == shape_expected
+        assert axes_res == axes_expected
+
+    def test_xp_5d_set_shape(self, xp):
+        x = xp.zeros([6, 2, 5, 3, 4])
+        shape = [10, -1, -1, 1, 4]
+        axes = None
+
+        shape_expected = (10, 2, 5, 1, 4)
+        axes_expected = [0, 1, 2, 3, 4]
+
+        shape_res, axes_res = _init_nd_shape_and_axes(x, shape, axes)
+
+        assert shape_res == shape_expected
+        assert axes_res == axes_expected
+
+    def test_xp_5d_set_axes(self, xp):
+        x = xp.zeros([6, 2, 5, 3, 4])
+        shape = None
+        axes = [4, 1, 2]
+
+        shape_expected = (4, 2, 5)
+        axes_expected = [4, 1, 2]
+
+        shape_res, axes_res = _init_nd_shape_and_axes(x, shape, axes)
+
+        assert shape_res == shape_expected
+        assert axes_res == axes_expected
+
+    def test_xp_5d_set_shape_axes(self, xp):
+        x = xp.zeros([6, 2, 5, 3, 4])
+        shape = [10, -1, 2]
+        axes = [1, 0, 3]
+
+        shape_expected = (10, 6, 2)
+        axes_expected = [1, 0, 3]
+
+        shape_res, axes_res = _init_nd_shape_and_axes(x, shape, axes)
+
+        assert shape_res == shape_expected
+        assert axes_res == axes_expected
+
+    def test_shape_axes_subset(self, xp):
+        x = xp.zeros((2, 3, 4, 5))
+        shape, axes = _init_nd_shape_and_axes(x, shape=(5, 5, 5), axes=None)
+
+        assert shape == (5, 5, 5)
+        assert axes == [1, 2, 3]
+
+    def test_errors(self, xp):
+        x = xp.zeros(1)
+        with assert_raises(ValueError, match="axes must be a scalar or "
+                           "iterable of integers"):
+            _init_nd_shape_and_axes(x, shape=None, axes=[[1, 2], [3, 4]])
+
+        with assert_raises(ValueError, match="axes must be a scalar or "
+                           "iterable of integers"):
+            _init_nd_shape_and_axes(x, shape=None, axes=[1., 2., 3., 4.])
+
+        with assert_raises(ValueError,
+                           match="axes exceeds dimensionality of input"):
+            _init_nd_shape_and_axes(x, shape=None, axes=[1])
+
+        with assert_raises(ValueError,
+                           match="axes exceeds dimensionality of input"):
+            _init_nd_shape_and_axes(x, shape=None, axes=[-2])
+
+        with assert_raises(ValueError,
+                           match="all axes must be unique"):
+            _init_nd_shape_and_axes(x, shape=None, axes=[0, 0])
+
+        with assert_raises(ValueError, match="shape must be a scalar or "
+                           "iterable of integers"):
+            _init_nd_shape_and_axes(x, shape=[[1, 2], [3, 4]], axes=None)
+
+        with assert_raises(ValueError, match="shape must be a scalar or "
+                           "iterable of integers"):
+            _init_nd_shape_and_axes(x, shape=[1., 2., 3., 4.], axes=None)
+
+        with assert_raises(ValueError,
+                           match="when given, axes and shape arguments"
+                           " have to be of the same length"):
+            _init_nd_shape_and_axes(xp.zeros([1, 1, 1, 1]),
+                                    shape=[1, 2, 3], axes=[1])
+
+        with assert_raises(ValueError,
+                           match="invalid number of data points"
+                           r" \(\[0\]\) specified"):
+            _init_nd_shape_and_axes(x, shape=[0], axes=None)
+
+        with assert_raises(ValueError,
+                           match="invalid number of data points"
+                           r" \(\[-2\]\) specified"):
+            _init_nd_shape_and_axes(x, shape=-2, axes=None)
+
+
+class TestFFTShift:
+
+    def test_definition(self, xp):
+        x = xp.asarray([0., 1, 2, 3, 4, -4, -3, -2, -1])
+        y = xp.asarray([-4., -3, -2, -1, 0, 1, 2, 3, 4])
+        xp_assert_close(fft.fftshift(x), y)
+        xp_assert_close(fft.ifftshift(y), x)
+        x = xp.asarray([0., 1, 2, 3, 4, -5, -4, -3, -2, -1])
+        y = xp.asarray([-5., -4, -3, -2, -1, 0, 1, 2, 3, 4])
+        xp_assert_close(fft.fftshift(x), y)
+        xp_assert_close(fft.ifftshift(y), x)
+
+    def test_inverse(self, xp):
+        for n in [1, 4, 9, 100, 211]:
+            x = xp.asarray(np.random.random((n,)))
+            xp_assert_close(fft.ifftshift(fft.fftshift(x)), x)
+
+    def test_axes_keyword(self, xp):
+        freqs = xp.asarray([[0., 1, 2], [3, 4, -4], [-3, -2, -1]])
+        shifted = xp.asarray([[-1., -3, -2], [2, 0, 1], [-4, 3, 4]])
+        xp_assert_close(fft.fftshift(freqs, axes=(0, 1)), shifted)
+        xp_assert_close(fft.fftshift(freqs, axes=0), fft.fftshift(freqs, axes=(0,)))
+        xp_assert_close(fft.ifftshift(shifted, axes=(0, 1)), freqs)
+        xp_assert_close(fft.ifftshift(shifted, axes=0),
+                        fft.ifftshift(shifted, axes=(0,)))
+        xp_assert_close(fft.fftshift(freqs), shifted)
+        xp_assert_close(fft.ifftshift(shifted), freqs)
+
+    def test_uneven_dims(self, xp):
+        """ Test 2D input, which has uneven dimension sizes """
+        freqs = xp.asarray([
+            [0, 1],
+            [2, 3],
+            [4, 5]
+        ], dtype=xp.float64)
+
+        # shift in dimension 0
+        shift_dim0 = xp.asarray([
+            [4, 5],
+            [0, 1],
+            [2, 3]
+        ], dtype=xp.float64)
+        xp_assert_close(fft.fftshift(freqs, axes=0), shift_dim0)
+        xp_assert_close(fft.ifftshift(shift_dim0, axes=0), freqs)
+        xp_assert_close(fft.fftshift(freqs, axes=(0,)), shift_dim0)
+        xp_assert_close(fft.ifftshift(shift_dim0, axes=[0]), freqs)
+
+        # shift in dimension 1
+        shift_dim1 = xp.asarray([
+            [1, 0],
+            [3, 2],
+            [5, 4]
+        ], dtype=xp.float64)
+        xp_assert_close(fft.fftshift(freqs, axes=1), shift_dim1)
+        xp_assert_close(fft.ifftshift(shift_dim1, axes=1), freqs)
+
+        # shift in both dimensions
+        shift_dim_both = xp.asarray([
+            [5, 4],
+            [1, 0],
+            [3, 2]
+        ], dtype=xp.float64)
+        xp_assert_close(fft.fftshift(freqs, axes=(0, 1)), shift_dim_both)
+        xp_assert_close(fft.ifftshift(shift_dim_both, axes=(0, 1)), freqs)
+        xp_assert_close(fft.fftshift(freqs, axes=[0, 1]), shift_dim_both)
+        xp_assert_close(fft.ifftshift(shift_dim_both, axes=[0, 1]), freqs)
+
+        # axes=None (default) shift in all dimensions
+        xp_assert_close(fft.fftshift(freqs, axes=None), shift_dim_both)
+        xp_assert_close(fft.ifftshift(shift_dim_both, axes=None), freqs)
+        xp_assert_close(fft.fftshift(freqs), shift_dim_both)
+        xp_assert_close(fft.ifftshift(shift_dim_both), freqs)
+
+
+class TestFFTFreq:
+    def test_definition(self, xp):
+        x = xp.asarray([0, 1, 2, 3, 4, -4, -3, -2, -1], dtype=xp.float64)
+        x2 = xp.asarray([0, 1, 2, 3, 4, -5, -4, -3, -2, -1], dtype=xp.float64)
+
+        # default dtype varies across backends
+
+        y = 9 * fft.fftfreq(9, xp=xp)
+        xp_assert_close(y, x, check_dtype=False, check_namespace=True)
+
+        y = 9 * xp.pi * fft.fftfreq(9, xp.pi, xp=xp)
+        xp_assert_close(y, x, check_dtype=False)
+
+        y = 10 * fft.fftfreq(10, xp=xp)
+        xp_assert_close(y, x2, check_dtype=False)
+
+        y = 10 * xp.pi * fft.fftfreq(10, xp.pi, xp=xp)
+        xp_assert_close(y, x2, check_dtype=False)
+
+    def test_device(self, xp, devices):
+        for d in devices:
+            y = fft.fftfreq(9, xp=xp, device=d)
+            x = xp.empty(0, device=d)
+            assert xp_device(y) == xp_device(x)
+
+
+class TestRFFTFreq:
+
+    def test_definition(self, xp):
+        x = xp.asarray([0, 1, 2, 3, 4], dtype=xp.float64)
+        x2 = xp.asarray([0, 1, 2, 3, 4, 5], dtype=xp.float64)
+
+        # default dtype varies across backends
+
+        y = 9 * fft.rfftfreq(9, xp=xp)
+        xp_assert_close(y, x, check_dtype=False, check_namespace=True)
+
+        y = 9 * xp.pi * fft.rfftfreq(9, xp.pi, xp=xp)
+        xp_assert_close(y, x, check_dtype=False)
+
+        y = 10 * fft.rfftfreq(10, xp=xp)
+        xp_assert_close(y, x2, check_dtype=False)
+
+        y = 10 * xp.pi * fft.rfftfreq(10, xp.pi, xp=xp)
+        xp_assert_close(y, x2, check_dtype=False)
+
+    def test_device(self, xp, devices):
+        for d in devices:
+            y = fft.rfftfreq(9, xp=xp, device=d)
+            x = xp.empty(0, device=d)
+            assert xp_device(y) == xp_device(x)
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/tests/test_multithreading.py b/.venv/lib/python3.11/site-packages/scipy/fft/tests/test_multithreading.py
new file mode 100644
index 0000000000000000000000000000000000000000..1a6b71b830211f8bcbe56e97ff71098be75021c8
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/tests/test_multithreading.py
@@ -0,0 +1,84 @@
+from scipy import fft
+import numpy as np
+import pytest
+from numpy.testing import assert_allclose
+import multiprocessing
+import os
+
+
+@pytest.fixture(scope='module')
+def x():
+    return np.random.randn(512, 128)  # Must be large enough to qualify for mt
+
+
+@pytest.mark.parametrize("func", [
+    fft.fft, fft.ifft, fft.fft2, fft.ifft2, fft.fftn, fft.ifftn,
+    fft.rfft, fft.irfft, fft.rfft2, fft.irfft2, fft.rfftn, fft.irfftn,
+    fft.hfft, fft.ihfft, fft.hfft2, fft.ihfft2, fft.hfftn, fft.ihfftn,
+    fft.dct, fft.idct, fft.dctn, fft.idctn,
+    fft.dst, fft.idst, fft.dstn, fft.idstn,
+])
+@pytest.mark.parametrize("workers", [2, -1])
+def test_threaded_same(x, func, workers):
+    expected = func(x, workers=1)
+    actual = func(x, workers=workers)
+    assert_allclose(actual, expected)
+
+
+def _mt_fft(x):
+    return fft.fft(x, workers=2)
+
+
+@pytest.mark.slow
+def test_mixed_threads_processes(x):
+    # Test that the fft threadpool is safe to use before & after fork
+
+    expect = fft.fft(x, workers=2)
+
+    with multiprocessing.Pool(2) as p:
+        res = p.map(_mt_fft, [x for _ in range(4)])
+
+    for r in res:
+        assert_allclose(r, expect)
+
+    fft.fft(x, workers=2)
+
+
+def test_invalid_workers(x):
+    cpus = os.cpu_count()
+
+    fft.ifft([1], workers=-cpus)
+
+    with pytest.raises(ValueError, match='workers must not be zero'):
+        fft.fft(x, workers=0)
+
+    with pytest.raises(ValueError, match='workers value out of range'):
+        fft.ifft(x, workers=-cpus-1)
+
+
+def test_set_get_workers():
+    cpus = os.cpu_count()
+    assert fft.get_workers() == 1
+    with fft.set_workers(4):
+        assert fft.get_workers() == 4
+
+        with fft.set_workers(-1):
+            assert fft.get_workers() == cpus
+
+        assert fft.get_workers() == 4
+
+    assert fft.get_workers() == 1
+
+    with fft.set_workers(-cpus):
+        assert fft.get_workers() == 1
+
+
+def test_set_workers_invalid():
+
+    with pytest.raises(ValueError, match='workers must not be zero'):
+        with fft.set_workers(0):
+            pass
+
+    with pytest.raises(ValueError, match='workers value out of range'):
+        with fft.set_workers(-os.cpu_count()-1):
+            pass
diff --git a/.venv/lib/python3.11/site-packages/scipy/fft/tests/test_real_transforms.py b/.venv/lib/python3.11/site-packages/scipy/fft/tests/test_real_transforms.py
new file mode 100644
index 0000000000000000000000000000000000000000..3add13d58ae70a567f3f4bf7b73dda72a15a47ae
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fft/tests/test_real_transforms.py
@@ -0,0 +1,249 @@
+import numpy as np
+from numpy.testing import assert_allclose, assert_array_equal
+import pytest
+import math
+
+from scipy.fft import dct, idct, dctn, idctn, dst, idst, dstn, idstn
+import scipy.fft as fft
+from scipy import fftpack
+from scipy._lib._array_api import (xp_copy, xp_assert_close, make_xp_test_case,
+                                   make_xp_pytest_param)
+import scipy._lib.array_api_extra as xpx
+
+skip_xp_backends = pytest.mark.skip_xp_backends
+
+SQRT_2 = math.sqrt(2)
+
+# scipy.fft wraps the fftpack versions but with normalized inverse transforms.
+# So, the forward transforms and definitions are already thoroughly tested in
+# fftpack/test_real_transforms.py
+
+
+@pytest.mark.parametrize("forward, backward", [make_xp_pytest_param(dct, idct),
+                                               make_xp_pytest_param(dst, idst)])
+@pytest.mark.parametrize("type", [1, 2, 3, 4])
+@pytest.mark.parametrize("n", [2, 3, 4, 5, 10, 16])
+@pytest.mark.parametrize("axis", [0, 1])
+@pytest.mark.parametrize("norm", [None, 'backward', 'ortho', 'forward'])
+@pytest.mark.parametrize("orthogonalize", [False, True])
+def test_identity_1d(forward, backward, type, n, axis, norm, orthogonalize, xp):
+    # Test the identity f^-1(f(x)) == x
+    x = xp.asarray(np.random.rand(n, n))
+
+    y = forward(x, type, axis=axis, norm=norm, orthogonalize=orthogonalize)
+    z = backward(y, type, axis=axis, norm=norm, orthogonalize=orthogonalize)
+    xp_assert_close(z, x)
+
+    pad = [(0, 0)] * 2
+    pad[axis] = (0, 4)
+
+    y2 = xp.asarray(np.pad(np.asarray(y), pad, mode='edge'))
+    z2 = backward(y2, type, n, axis, norm, orthogonalize=orthogonalize)
+    xp_assert_close(z2, x)
+
+
+@skip_xp_backends(np_only=True,
+                  reason='`overwrite_x` only supported for NumPy backend.')
+@pytest.mark.parametrize("forward, backward", [(dct, idct), (dst, idst)])
+@pytest.mark.parametrize("type", [1, 2, 3, 4])
+@pytest.mark.parametrize("dtype", [np.float16, np.float32, np.float64,
+                                   np.complex64, np.complex128])
+@pytest.mark.parametrize("axis", [0, 1])
+@pytest.mark.parametrize("norm", [None, 'backward', 'ortho', 'forward'])
+@pytest.mark.parametrize("overwrite_x", [True, False])
+def test_identity_1d_overwrite(forward, backward, type, dtype, axis, norm,
+                               overwrite_x, xp):
+    # Test the identity f^-1(f(x)) == x
+    x = np.random.rand(7, 8).astype(dtype)
+    x_orig = x.copy()
+
+    y = forward(x, type, axis=axis, norm=norm, overwrite_x=overwrite_x)
+    y_orig = y.copy()
+    z = backward(y, type, axis=axis, norm=norm, overwrite_x=overwrite_x)
+    if not overwrite_x:
+        assert_allclose(z, x, rtol=1e-6, atol=1e-6)
+        assert_array_equal(x, x_orig)
+        assert_array_equal(y, y_orig)
+    else:
+        assert_allclose(z, x_orig, rtol=1e-6, atol=1e-6)
+
+
+@pytest.mark.parametrize("forward, backward", [make_xp_pytest_param(dctn, idctn),
+                                               make_xp_pytest_param(dstn, idstn)])
+@pytest.mark.parametrize("type", [1, 2, 3, 4])
+@pytest.mark.parametrize("shape, axes",
+                         [
+                             ((4, 4), 0),
+                             ((4, 4), 1),
+                             ((4, 4), None),
+                             ((4, 4), (0, 1)),
+                             ((10, 12), None),
+                             ((10, 12), (0, 1)),
+                             ((4, 5, 6), None),
+                             ((4, 5, 6), 1),
+                             ((4, 5, 6), (0, 2)),
+                         ])
+@pytest.mark.parametrize("norm", [None, 'backward', 'ortho', 'forward'])
+@pytest.mark.parametrize("orthogonalize", [False, True])
+def test_identity_nd(forward, backward, type, shape, axes, norm,
+                     orthogonalize, xp):
+    # Test the identity f^-1(f(x)) == x
+
+    x = xp.asarray(np.random.random(shape))
+
+    if axes is not None:
+        shape = np.take(shape, axes)
+
+    y = forward(x, type, axes=axes, norm=norm, orthogonalize=orthogonalize)
+    z = backward(y, type, axes=axes, norm=norm, orthogonalize=orthogonalize)
+    xp_assert_close(z, x)
+
+    if axes is None:
+        pad = [(0, 4)] * x.ndim
+    elif isinstance(axes, int):
+        pad = [(0, 0)] * x.ndim
+        pad[axes] = (0, 4)
+    else:
+        pad = [(0, 0)] * x.ndim
+
+        for a in axes:
+            pad[a] = (0, 4)
+
+    # TODO write an array-agnostic pad
+    y2 = xp.asarray(np.pad(np.asarray(y), pad, mode='edge'))
+    z2 = backward(y2, type, shape, axes, norm, orthogonalize=orthogonalize)
+    xp_assert_close(z2, x)
+
+
+@skip_xp_backends(np_only=True,
+                  reason='`overwrite_x` only supported for NumPy backend.')
+@pytest.mark.parametrize("forward, backward", [(dctn, idctn), (dstn, idstn)])
+@pytest.mark.parametrize("type", [1, 2, 3, 4])
+@pytest.mark.parametrize("shape, axes",
+                         [
+                             ((4, 5), 0),
+                             ((4, 5), 1),
+                             ((4, 5), None),
+                         ])
+@pytest.mark.parametrize("dtype", [np.float16, np.float32, np.float64,
+                                   np.complex64, np.complex128])
+@pytest.mark.parametrize("norm", [None, 'backward', 'ortho', 'forward'])
+@pytest.mark.parametrize("overwrite_x", [False, True])
+def test_identity_nd_overwrite(forward, backward, type, shape, axes, dtype,
+                               norm, overwrite_x, xp):
+    # Test the identity f^-1(f(x)) == x
+
+    x = np.random.random(shape).astype(dtype)
+    x_orig = x.copy()
+
+    if axes is not None:
+        shape = np.take(shape, axes)
+
+    y = forward(x, type, axes=axes, norm=norm)
+    y_orig = y.copy()
+    z = backward(y, type, axes=axes, norm=norm)
+    if overwrite_x:
+        assert_allclose(z, x_orig, rtol=1e-6, atol=1e-6)
+    else:
+        assert_allclose(z, x, rtol=1e-6, atol=1e-6)
+        assert_array_equal(x, x_orig)
+        assert_array_equal(y, y_orig)
+
+
+@pytest.mark.parametrize("func", [make_xp_pytest_param(dct),
+                                  make_xp_pytest_param(dst),
+                                  make_xp_pytest_param(dctn),
+                                  make_xp_pytest_param(dstn)])
+@pytest.mark.parametrize("type", [1, 2, 3, 4])
+@pytest.mark.parametrize("norm", [None, 'backward', 'ortho', 'forward'])
+def test_fftpack_equivalience(func, type, norm, xp):
+    x = np.random.rand(8, 16)
+    fftpack_res = xp.asarray(getattr(fftpack, func.__name__)(x, type, norm=norm))
+    x = xp.asarray(x)
+    fft_res = getattr(fft, func.__name__)(x, type, norm=norm)
+
+    xp_assert_close(fft_res, fftpack_res)
+
+
+@pytest.mark.parametrize("func", [make_xp_pytest_param(dct),
+                                  make_xp_pytest_param(dst),
+                                  make_xp_pytest_param(dctn),
+                                  make_xp_pytest_param(dstn)])
+@pytest.mark.parametrize("type", [1, 2, 3, 4])
+def test_orthogonalize_default(func, type, xp):
+    # Test orthogonalize is the default when norm="ortho", but not otherwise
+    x = xp.asarray(np.random.rand(100))
+
+    for norm, ortho in [
+            ("forward", False),
+            ("backward", False),
+            ("ortho", True),
+    ]:
+        a = func(x, type=type, norm=norm, orthogonalize=ortho)
+        b = func(x, type=type, norm=norm)
+        xp_assert_close(a, b)
+
+
+@pytest.mark.parametrize("norm", ["backward", "ortho", "forward"])
+@pytest.mark.parametrize("func, type", [make_xp_pytest_param(dct, 4),
+                                        make_xp_pytest_param(dst, 1),
+                                        make_xp_pytest_param(dst, 4)])
+def test_orthogonalize_noop(func, type, norm, xp):
+    # Transforms where orthogonalize is a no-op
+    x = xp.asarray(np.random.rand(100))
+    y1 = func(x, type=type, norm=norm, orthogonalize=True)
+    y2 = func(x, type=type, norm=norm, orthogonalize=False)
+    xp_assert_close(y1, y2)
+
+
+@make_xp_test_case(dct)
+@pytest.mark.parametrize("norm", ["backward", "ortho", "forward"])
+def test_orthogonalize_dct1(norm, xp):
+    x = xp.asarray(np.random.rand(100))
+
+    x2 = xp_copy(x, xp=xp)
+    xpx.at(x2, 0).multiply(SQRT_2)
+    xpx.at(x2, -1).multiply(SQRT_2)
+
+    y1 = dct(x, type=1, norm=norm, orthogonalize=True)
+    y2 = dct(x2, type=1, norm=norm, orthogonalize=False)
+
+    xpx.at(y2, 0).divide(SQRT_2)
+    xpx.at(y2, -1).divide(SQRT_2)
+    xp_assert_close(y1, y2)
+
+
+
+@pytest.mark.parametrize("norm", ["backward", "ortho", "forward"])
+@pytest.mark.parametrize("func", [make_xp_pytest_param(dct),
+                                  make_xp_pytest_param(dst)])
+def test_orthogonalize_dcst2(func, norm, xp):
+    x = xp.asarray(np.random.rand(100))
+    y1 = func(x, type=2, norm=norm, orthogonalize=True)
+    y2 = func(x, type=2, norm=norm, orthogonalize=False)
+
+    xpx.at(y2, 0 if func.__name__ == "dct" else -1).divide(SQRT_2)
+    xp_assert_close(y1, y2)
+
+
+@pytest.mark.parametrize("norm", ["backward", "ortho", "forward"])
+@pytest.mark.parametrize("func", [make_xp_pytest_param(dct),
+                                  make_xp_pytest_param(dst)])
+def test_orthogonalize_dcst3(func, norm, xp):
+    x = xp.asarray(np.random.rand(100))
+    x2 = xp_copy(x, xp=xp)
+    xpx.at(x2, 0 if func.__name__ == "dct" else -1).multiply(SQRT_2)
+
+    y1 = func(x, type=3, norm=norm, orthogonalize=True)
+    y2 = func(x2, type=3, norm=norm, orthogonalize=False)
+    xp_assert_close(y1, y2)
+
+
+@skip_xp_backends(np_only=True,
+                  reason='array-likes only supported for NumPy backend')
+@pytest.mark.parametrize("func", [dct, idct, dctn, idctn, dst, idst, dstn, idstn])
+def test_array_like(xp, func):
+    x = [[[1.0, 1.0], [1.0, 1.0]],
+         [[1.0, 1.0], [1.0, 1.0]],
+         [[1.0, 1.0], [1.0, 1.0]]]
+    xp_assert_close(func(x), func(xp.asarray(x)))
diff --git a/.venv/lib/python3.11/site-packages/scipy/fftpack/__init__.py b/.venv/lib/python3.11/site-packages/scipy/fftpack/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..10f4b39e48e2d6c0b042582ca65f572bde6ba575
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fftpack/__init__.py
@@ -0,0 +1,103 @@
+"""
+=========================================================
+Legacy discrete Fourier transforms (:mod:`scipy.fftpack`)
+=========================================================
+
+.. legacy::
+
+   New code should use :mod:`scipy.fft`.
+
+Fast Fourier Transforms (FFTs)
+==============================
+
+.. autosummary::
+   :toctree: generated/
+
+   fft - Fast (discrete) Fourier Transform (FFT)
+   ifft - Inverse FFT
+   fft2 - 2-D FFT
+   ifft2 - 2-D inverse FFT
+   fftn - N-D FFT
+   ifftn - N-D inverse FFT
+   rfft - FFT of strictly real-valued sequence
+   irfft - Inverse of rfft
+   dct - Discrete cosine transform
+   idct - Inverse discrete cosine transform
+   dctn - N-D Discrete cosine transform
+   idctn - N-D Inverse discrete cosine transform
+   dst - Discrete sine transform
+   idst - Inverse discrete sine transform
+   dstn - N-D Discrete sine transform
+   idstn - N-D Inverse discrete sine transform
+
+Differential and pseudo-differential operators
+==============================================
+
+.. autosummary::
+   :toctree: generated/
+
+   diff - Differentiation and integration of periodic sequences
+   tilbert - Tilbert transform:         cs_diff(x,h,h)
+   itilbert - Inverse Tilbert transform: sc_diff(x,h,h)
+   hilbert - Hilbert transform:         cs_diff(x,inf,inf)
+   ihilbert - Inverse Hilbert transform: sc_diff(x,inf,inf)
+   cs_diff - cosh/sinh pseudo-derivative of periodic sequences
+   sc_diff - sinh/cosh pseudo-derivative of periodic sequences
+   ss_diff - sinh/sinh pseudo-derivative of periodic sequences
+   cc_diff - cosh/cosh pseudo-derivative of periodic sequences
+   shift - Shift periodic sequences
+
+Helper functions
+================
+
+.. autosummary::
+   :toctree: generated/
+
+   fftshift - Shift the zero-frequency component to the center of the spectrum
+   ifftshift - The inverse of `fftshift`
+   fftfreq - Return the Discrete Fourier Transform sample frequencies
+   rfftfreq - DFT sample frequencies (for usage with rfft, irfft)
+   next_fast_len - Find the optimal length to zero-pad an FFT for speed
+
+Note that ``fftshift``, ``ifftshift`` and ``fftfreq`` are numpy functions
+exposed by ``fftpack``; importing them from ``numpy`` should be preferred.
+
+Convolutions (:mod:`scipy.fftpack.convolve`)
+============================================
+
+.. module:: scipy.fftpack.convolve
+
+.. autosummary::
+   :toctree: generated/
+
+   convolve
+   convolve_z
+   init_convolution_kernel
+   destroy_convolve_cache
+
+"""
+
+
+__all__ = ['fft','ifft','fftn','ifftn','rfft','irfft',
+           'fft2','ifft2',
+           'diff',
+           'tilbert','itilbert','hilbert','ihilbert',
+           'sc_diff','cs_diff','cc_diff','ss_diff',
+           'shift',
+           'fftfreq', 'rfftfreq',
+           'fftshift', 'ifftshift',
+           'next_fast_len',
+           'dct', 'idct', 'dst', 'idst', 'dctn', 'idctn', 'dstn', 'idstn'
+           ]
+
+from ._basic import *
+from ._pseudo_diffs import *
+from ._helper import *
+from ._realtransforms import *
+
+# Deprecated namespaces, to be removed in v2.0.0
+from . import basic, helper, pseudo_diffs, realtransforms
+
+from scipy._lib._testutils import PytestTester
+test = PytestTester(__name__)
+del PytestTester
diff --git a/.venv/lib/python3.11/site-packages/scipy/fftpack/_basic.py b/.venv/lib/python3.11/site-packages/scipy/fftpack/_basic.py
new file mode 100644
index 0000000000000000000000000000000000000000..59c85ae4b364464a66489ef221f7f7ac45624694
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fftpack/_basic.py
@@ -0,0 +1,428 @@
+"""
+Discrete Fourier Transforms - _basic.py
+"""
+# Created by Pearu Peterson, August,September 2002
+__all__ = ['fft','ifft','fftn','ifftn','rfft','irfft',
+           'fft2','ifft2']
+
+from scipy.fft import _pocketfft
+from ._helper import _good_shape
+
+
+def fft(x, n=None, axis=-1, overwrite_x=False):
+    """
+    Return discrete Fourier transform of real or complex sequence.
+
+    The returned complex array contains ``y(0), y(1),..., y(n-1)``, where
+
+    ``y(j) = (x * exp(-2*pi*sqrt(-1)*j*np.arange(n)/n)).sum()``.
+
+    Parameters
+    ----------
+    x : array_like
+        Array to Fourier transform.
+    n : int, optional
+        Length of the Fourier transform. If ``n < x.shape[axis]``, `x` is
+        truncated. If ``n > x.shape[axis]``, `x` is zero-padded. The
+        default results in ``n = x.shape[axis]``.
+    axis : int, optional
+        Axis along which the fft's are computed; the default is over the
+        last axis (i.e., ``axis=-1``).
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+
+    Returns
+    -------
+    z : complex ndarray
+        with the elements::
+
+            [y(0),y(1),..,y(n/2),y(1-n/2),...,y(-1)]        if n is even
+            [y(0),y(1),..,y((n-1)/2),y(-(n-1)/2),...,y(-1)]  if n is odd
+
+        where::
+
+            y(j) = sum[k=0..n-1] x[k] * exp(-sqrt(-1)*j*k* 2*pi/n), j = 0..n-1
+
+    See Also
+    --------
+    ifft : Inverse FFT
+    rfft : FFT of a real sequence
+
+    Notes
+    -----
+    The packing of the result is "standard": If ``A = fft(a, n)``, then
+    ``A[0]`` contains the zero-frequency term, ``A[1:n/2]`` contains the
+    positive-frequency terms, and ``A[n/2:]`` contains the negative-frequency
+    terms, in order of decreasingly negative frequency. So ,for an 8-point
+    transform, the frequencies of the result are [0, 1, 2, 3, -4, -3, -2, -1].
+    To rearrange the fft output so that the zero-frequency component is
+    centered, like [-4, -3, -2, -1,  0,  1,  2,  3], use `fftshift`.
+
+    Both single and double precision routines are implemented. Half precision
+    inputs will be converted to single precision. Non-floating-point inputs
+    will be converted to double precision. Long-double precision inputs are
+    not supported.
+
+    This function is most efficient when `n` is a power of two, and least
+    efficient when `n` is prime.
+
+    Note that if ``x`` is real-valued, then ``A[j] == A[n-j].conjugate()``.
+    If ``x`` is real-valued and ``n`` is even, then ``A[n/2]`` is real.
+
+    If the data type of `x` is real, a "real FFT" algorithm is automatically
+    used, which roughly halves the computation time. To increase efficiency
+    a little further, use `rfft`, which does the same calculation, but only
+    outputs half of the symmetrical spectrum. If the data is both real and
+    symmetrical, the `dct` can again double the efficiency by generating
+    half of the spectrum from half of the signal.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.fftpack import fft, ifft
+    >>> x = np.arange(5)
+    >>> np.allclose(fft(ifft(x)), x, atol=1e-15)  # within numerical accuracy.
+    True
+
+    """
+    return _pocketfft.fft(x, n, axis, None, overwrite_x)
+
+
+def ifft(x, n=None, axis=-1, overwrite_x=False):
+    """
+    Return discrete inverse Fourier transform of real or complex sequence.
+
+    The returned complex array contains ``y(0), y(1),..., y(n-1)``, where
+
+    ``y(j) = (x * exp(2*pi*sqrt(-1)*j*np.arange(n)/n)).mean()``.
+
+    Parameters
+    ----------
+    x : array_like
+        Transformed data to invert.
+    n : int, optional
+        Length of the inverse Fourier transform.  If ``n < x.shape[axis]``,
+        `x` is truncated. If ``n > x.shape[axis]``, `x` is zero-padded.
+        The default results in ``n = x.shape[axis]``.
+    axis : int, optional
+        Axis along which the ifft's are computed; the default is over the
+        last axis (i.e., ``axis=-1``).
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+
+    Returns
+    -------
+    ifft : ndarray of floats
+        The inverse discrete Fourier transform.
+
+    See Also
+    --------
+    fft : Forward FFT
+
+    Notes
+    -----
+    Both single and double precision routines are implemented. Half precision
+    inputs will be converted to single precision. Non-floating-point inputs
+    will be converted to double precision. Long-double precision inputs are
+    not supported.
+
+    This function is most efficient when `n` is a power of two, and least
+    efficient when `n` is prime.
+
+    If the data type of `x` is real, a "real IFFT" algorithm is automatically
+    used, which roughly halves the computation time.
+
+    Examples
+    --------
+    >>> from scipy.fftpack import fft, ifft
+    >>> import numpy as np
+    >>> x = np.arange(5)
+    >>> np.allclose(ifft(fft(x)), x, atol=1e-15)  # within numerical accuracy.
+    True
+
+    """
+    return _pocketfft.ifft(x, n, axis, None, overwrite_x)
+
+
+def rfft(x, n=None, axis=-1, overwrite_x=False):
+    """
+    Discrete Fourier transform of a real sequence.
+
+    Parameters
+    ----------
+    x : array_like, real-valued
+        The data to transform.
+    n : int, optional
+        Defines the length of the Fourier transform. If `n` is not specified
+        (the default) then ``n = x.shape[axis]``. If ``n < x.shape[axis]``,
+        `x` is truncated, if ``n > x.shape[axis]``, `x` is zero-padded.
+    axis : int, optional
+        The axis along which the transform is applied. The default is the
+        last axis.
+    overwrite_x : bool, optional
+        If set to true, the contents of `x` can be overwritten. Default is
+        False.
+
+    Returns
+    -------
+    z : real ndarray
+        The returned real array contains::
+
+          [y(0),Re(y(1)),Im(y(1)),...,Re(y(n/2))]              if n is even
+          [y(0),Re(y(1)),Im(y(1)),...,Re(y(n/2)),Im(y(n/2))]   if n is odd
+
+        where::
+
+          y(j) = sum[k=0..n-1] x[k] * exp(-sqrt(-1)*j*k*2*pi/n)
+          j = 0..n-1
+
+    See Also
+    --------
+    fft, irfft, scipy.fft.rfft
+
+    Notes
+    -----
+    Within numerical accuracy, ``y == rfft(irfft(y))``.
+
+    Both single and double precision routines are implemented. Half precision
+    inputs will be converted to single precision. Non-floating-point inputs
+    will be converted to double precision. Long-double precision inputs are
+    not supported.
+
+    To get an output with a complex datatype, consider using the newer
+    function `scipy.fft.rfft`.
+
+    Examples
+    --------
+    >>> from scipy.fftpack import fft, rfft
+    >>> a = [9, -9, 1, 3]
+    >>> fft(a)
+    array([  4. +0.j,   8.+12.j,  16. +0.j,   8.-12.j])
+    >>> rfft(a)
+    array([  4.,   8.,  12.,  16.])
+
+    """
+    return _pocketfft.rfft_fftpack(x, n, axis, None, overwrite_x)
+
+
+def irfft(x, n=None, axis=-1, overwrite_x=False):
+    """
+    Return inverse discrete Fourier transform of real sequence x.
+
+    The contents of `x` are interpreted as the output of the `rfft`
+    function.
+
+    Parameters
+    ----------
+    x : array_like
+        Transformed data to invert.
+    n : int, optional
+        Length of the inverse Fourier transform.
+        If n < x.shape[axis], x is truncated.
+        If n > x.shape[axis], x is zero-padded.
+        The default results in n = x.shape[axis].
+    axis : int, optional
+        Axis along which the ifft's are computed; the default is over
+        the last axis (i.e., axis=-1).
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+
+    Returns
+    -------
+    irfft : ndarray of floats
+        The inverse discrete Fourier transform.
+
+    See Also
+    --------
+    rfft, ifft, scipy.fft.irfft
+
+    Notes
+    -----
+    The returned real array contains::
+
+        [y(0),y(1),...,y(n-1)]
+
+    where for n is even::
+
+        y(j) = 1/n (sum[k=1..n/2-1] (x[2*k-1]+sqrt(-1)*x[2*k])
+                                     * exp(sqrt(-1)*j*k* 2*pi/n)
+                    + c.c. + x[0] + (-1)**(j) x[n-1])
+
+    and for n is odd::
+
+        y(j) = 1/n (sum[k=1..(n-1)/2] (x[2*k-1]+sqrt(-1)*x[2*k])
+                                     * exp(sqrt(-1)*j*k* 2*pi/n)
+                    + c.c. + x[0])
+
+    c.c. denotes complex conjugate of preceding expression.
+
+    For details on input parameters, see `rfft`.
+
+    To process (conjugate-symmetric) frequency-domain data with a complex
+    datatype, consider using the newer function `scipy.fft.irfft`.
+
+    Examples
+    --------
+    >>> from scipy.fftpack import rfft, irfft
+    >>> a = [1.0, 2.0, 3.0, 4.0, 5.0]
+    >>> irfft(a)
+    array([ 2.6       , -3.16405192,  1.24398433, -1.14955713,  1.46962473])
+    >>> irfft(rfft(a))
+    array([1., 2., 3., 4., 5.])
+
+    """
+    return _pocketfft.irfft_fftpack(x, n, axis, None, overwrite_x)
+
+
+def fftn(x, shape=None, axes=None, overwrite_x=False):
+    """
+    Return multidimensional discrete Fourier transform.
+
+    The returned array contains::
+
+      y[j_1,..,j_d] = sum[k_1=0..n_1-1, ..., k_d=0..n_d-1]
+         x[k_1,..,k_d] * prod[i=1..d] exp(-sqrt(-1)*2*pi/n_i * j_i * k_i)
+
+    where d = len(x.shape) and n = x.shape.
+
+    Parameters
+    ----------
+    x : array_like
+        The (N-D) array to transform.
+    shape : int or array_like of ints or None, optional
+        The shape of the result. If both `shape` and `axes` (see below) are
+        None, `shape` is ``x.shape``; if `shape` is None but `axes` is
+        not None, then `shape` is ``numpy.take(x.shape, axes, axis=0)``.
+        If ``shape[i] > x.shape[i]``, the ith dimension is padded with zeros.
+        If ``shape[i] < x.shape[i]``, the ith dimension is truncated to
+        length ``shape[i]``.
+        If any element of `shape` is -1, the size of the corresponding
+        dimension of `x` is used.
+    axes : int or array_like of ints or None, optional
+        The axes of `x` (`y` if `shape` is not None) along which the
+        transform is applied.
+        The default is over all axes.
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed. Default is False.
+
+    Returns
+    -------
+    y : complex-valued N-D NumPy array
+        The (N-D) DFT of the input array.
+
+    See Also
+    --------
+    ifftn
+
+    Notes
+    -----
+    If ``x`` is real-valued, then
+    ``y[..., j_i, ...] == y[..., n_i-j_i, ...].conjugate()``.
+
+    Both single and double precision routines are implemented. Half precision
+    inputs will be converted to single precision. Non-floating-point inputs
+    will be converted to double precision. Long-double precision inputs are
+    not supported.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.fftpack import fftn, ifftn
+    >>> y = (-np.arange(16), 8 - np.arange(16), np.arange(16))
+    >>> np.allclose(y, fftn(ifftn(y)))
+    True
+
+    """
+    shape = _good_shape(x, shape, axes)
+    return _pocketfft.fftn(x, shape, axes, None, overwrite_x)
+
+
+def ifftn(x, shape=None, axes=None, overwrite_x=False):
+    """
+    Return inverse multidimensional discrete Fourier transform.
+
+    The sequence can be of an arbitrary type.
+
+    The returned array contains::
+
+      y[j_1,..,j_d] = 1/p * sum[k_1=0..n_1-1, ..., k_d=0..n_d-1]
+         x[k_1,..,k_d] * prod[i=1..d] exp(sqrt(-1)*2*pi/n_i * j_i * k_i)
+
+    where ``d = len(x.shape)``, ``n = x.shape``, and ``p = prod[i=1..d] n_i``.
+
+    For description of parameters see `fftn`.
+
+    See Also
+    --------
+    fftn : for detailed information.
+
+    Examples
+    --------
+    >>> from scipy.fftpack import fftn, ifftn
+    >>> import numpy as np
+    >>> y = (-np.arange(16), 8 - np.arange(16), np.arange(16))
+    >>> np.allclose(y, ifftn(fftn(y)))
+    True
+
+    """
+    shape = _good_shape(x, shape, axes)
+    return _pocketfft.ifftn(x, shape, axes, None, overwrite_x)
+
+
+def fft2(x, shape=None, axes=(-2,-1), overwrite_x=False):
+    """
+    2-D discrete Fourier transform.
+
+    Return the 2-D discrete Fourier transform of the 2-D argument
+    `x`.
+
+    See Also
+    --------
+    fftn : for detailed information.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.fftpack import fft2, ifft2
+    >>> y = np.mgrid[:5, :5][0]
+    >>> y
+    array([[0, 0, 0, 0, 0],
+           [1, 1, 1, 1, 1],
+           [2, 2, 2, 2, 2],
+           [3, 3, 3, 3, 3],
+           [4, 4, 4, 4, 4]])
+    >>> np.allclose(y, ifft2(fft2(y)))
+    True
+    """
+    return fftn(x,shape,axes,overwrite_x)
+
+
+def ifft2(x, shape=None, axes=(-2,-1), overwrite_x=False):
+    """
+    2-D discrete inverse Fourier transform of real or complex sequence.
+
+    Return inverse 2-D discrete Fourier transform of
+    arbitrary type sequence x.
+
+    See `ifft` for more information.
+
+    See Also
+    --------
+    fft2, ifft
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.fftpack import fft2, ifft2
+    >>> y = np.mgrid[:5, :5][0]
+    >>> y
+    array([[0, 0, 0, 0, 0],
+           [1, 1, 1, 1, 1],
+           [2, 2, 2, 2, 2],
+           [3, 3, 3, 3, 3],
+           [4, 4, 4, 4, 4]])
+    >>> np.allclose(y, fft2(ifft2(y)))
+    True
+
+    """
+    return ifftn(x,shape,axes,overwrite_x)
diff --git a/.venv/lib/python3.11/site-packages/scipy/fftpack/_helper.py b/.venv/lib/python3.11/site-packages/scipy/fftpack/_helper.py
new file mode 100644
index 0000000000000000000000000000000000000000..7892543732906dcd86b4c1aa9c1f249af701c137
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fftpack/_helper.py
@@ -0,0 +1,115 @@
+import operator
+
+import numpy as np
+from numpy.fft import fftshift, ifftshift, fftfreq
+
+import scipy.fft._pocketfft.helper as _helper
+
+__all__ = ['fftshift', 'ifftshift', 'fftfreq', 'rfftfreq', 'next_fast_len']
+
+
+def rfftfreq(n, d=1.0):
+    """DFT sample frequencies (for usage with rfft, irfft).
+
+    The returned float array contains the frequency bins in
+    cycles/unit (with zero at the start) given a window length `n` and a
+    sample spacing `d`::
+
+      f = [0,1,1,2,2,...,n/2-1,n/2-1,n/2]/(d*n)   if n is even
+      f = [0,1,1,2,2,...,n/2-1,n/2-1,n/2,n/2]/(d*n)   if n is odd
+
+    Parameters
+    ----------
+    n : int
+        Window length.
+    d : scalar, optional
+        Sample spacing. Default is 1.
+
+    Returns
+    -------
+    out : ndarray
+        The array of length `n`, containing the sample frequencies.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy import fftpack
+    >>> sig = np.array([-2, 8, 6, 4, 1, 0, 3, 5], dtype=float)
+    >>> sig_fft = fftpack.rfft(sig)
+    >>> n = sig_fft.size
+    >>> timestep = 0.1
+    >>> freq = fftpack.rfftfreq(n, d=timestep)
+    >>> freq
+    array([ 0.  ,  1.25,  1.25,  2.5 ,  2.5 ,  3.75,  3.75,  5.  ])
+
+    """
+    n = operator.index(n)
+    if n < 0:
+        raise ValueError(f"n = {n} is not valid. "
+                         "n must be a nonnegative integer.")
+
+    return (np.arange(1, n + 1, dtype=int) // 2) / float(n * d)
+
+
+def next_fast_len(target):
+    """
+    Find the next fast size of input data to `fft`, for zero-padding, etc.
+
+    SciPy's FFTPACK has efficient functions for radix {2, 3, 4, 5}, so this
+    returns the next composite of the prime factors 2, 3, and 5 which is
+    greater than or equal to `target`. (These are also known as 5-smooth
+    numbers, regular numbers, or Hamming numbers.)
+
+    Parameters
+    ----------
+    target : int
+        Length to start searching from. Must be a positive integer.
+
+    Returns
+    -------
+    out : int
+        The first 5-smooth number greater than or equal to `target`.
+
+    Notes
+    -----
+    .. versionadded:: 0.18.0
+
+    Examples
+    --------
+    On a particular machine, an FFT of prime length takes 133 ms:
+
+    >>> from scipy import fftpack
+    >>> import numpy as np
+    >>> rng = np.random.default_rng()
+    >>> min_len = 10007  # prime length is worst case for speed
+    >>> a = rng.standard_normal(min_len)
+    >>> b = fftpack.fft(a)
+
+    Zero-padding to the next 5-smooth length reduces computation time to
+    211 us, a speedup of 630 times:
+
+    >>> fftpack.next_fast_len(min_len)
+    10125
+    >>> b = fftpack.fft(a, 10125)
+
+    Rounding up to the next power of 2 is not optimal, taking 367 us to
+    compute, 1.7 times as long as the 5-smooth size:
+
+    >>> b = fftpack.fft(a, 16384)
+
+    """
+    # Real transforms use regular sizes so this is backwards compatible
+    return _helper.good_size(target, True)
+
+
+def _good_shape(x, shape, axes):
+    """Ensure that shape argument is valid for scipy.fftpack
+
+    scipy.fftpack does not support len(shape) < x.ndim when axes is not given.
+    """
+    if shape is not None and axes is None:
+        shape = _helper._iterable_of_int(shape, 'shape')
+        if len(shape) != np.ndim(x):
+            raise ValueError("when given, axes and shape arguments"
+                             " have to be of the same length")
+    return shape
diff --git a/.venv/lib/python3.11/site-packages/scipy/fftpack/_pseudo_diffs.py b/.venv/lib/python3.11/site-packages/scipy/fftpack/_pseudo_diffs.py
new file mode 100644
index 0000000000000000000000000000000000000000..6dbcc8d3979b35c1497266fea34cf565cd3d11d7
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fftpack/_pseudo_diffs.py
@@ -0,0 +1,554 @@
+"""
+Differential and pseudo-differential operators.
+"""
+# Created by Pearu Peterson, September 2002
+
+__all__ = ['diff',
+           'tilbert','itilbert','hilbert','ihilbert',
+           'cs_diff','cc_diff','sc_diff','ss_diff',
+           'shift']
+
+import threading
+
+from numpy import pi, asarray, sin, cos, sinh, cosh, tanh, iscomplexobj
+from . import convolve
+
+from scipy.fft._pocketfft.helper import _datacopied
+
+
+_cache = threading.local()
+
+
+def diff(x,order=1,period=None, _cache=_cache):
+    """
+    Return kth derivative (or integral) of a periodic sequence x.
+
+    If x_j and y_j are Fourier coefficients of periodic functions x
+    and y, respectively, then::
+
+      y_j = pow(sqrt(-1)*j*2*pi/period, order) * x_j
+      y_0 = 0 if order is not 0.
+
+    Parameters
+    ----------
+    x : array_like
+        Input array.
+    order : int, optional
+        The order of differentiation. Default order is 1. If order is
+        negative, then integration is carried out under the assumption
+        that ``x_0 == 0``.
+    period : float, optional
+        The assumed period of the sequence. Default is ``2*pi``.
+
+    Notes
+    -----
+    If ``sum(x, axis=0) = 0`` then ``diff(diff(x, k), -k) == x`` (within
+    numerical accuracy).
+
+    For odd order and even ``len(x)``, the Nyquist mode is taken zero.
+
+    """
+    if isinstance(_cache, threading.local):
+        if not hasattr(_cache, 'diff_cache'):
+            _cache.diff_cache = {}
+        _cache = _cache.diff_cache
+
+    tmp = asarray(x)
+    if order == 0:
+        return tmp
+    if iscomplexobj(tmp):
+        return diff(tmp.real, order, period, _cache)+1j*diff(
+            tmp.imag, order, period, _cache)
+    if period is not None:
+        c = 2*pi/period
+    else:
+        c = 1.0
+    n = len(x)
+    omega = _cache.get((n,order,c))
+    if omega is None:
+        if len(_cache) > 20:
+            while _cache:
+                _cache.popitem()
+
+        def kernel(k,order=order,c=c):
+            if k:
+                return pow(c*k,order)
+            return 0
+        omega = convolve.init_convolution_kernel(n,kernel,d=order,
+                                                 zero_nyquist=1)
+        _cache[(n,order,c)] = omega
+    overwrite_x = _datacopied(tmp, x)
+    return convolve.convolve(tmp,omega,swap_real_imag=order % 2,
+                             overwrite_x=overwrite_x)
+
+
+def tilbert(x, h, period=None, _cache=_cache):
+    """
+    Return h-Tilbert transform of a periodic sequence x.
+
+    If x_j and y_j are Fourier coefficients of periodic functions x
+    and y, respectively, then::
+
+        y_j = sqrt(-1)*coth(j*h*2*pi/period) * x_j
+        y_0 = 0
+
+    Parameters
+    ----------
+    x : array_like
+        The input array to transform.
+    h : float
+        Defines the parameter of the Tilbert transform.
+    period : float, optional
+        The assumed period of the sequence. Default period is ``2*pi``.
+
+    Returns
+    -------
+    tilbert : ndarray
+        The result of the transform.
+
+    Notes
+    -----
+    If ``sum(x, axis=0) == 0`` and ``n = len(x)`` is odd, then
+    ``tilbert(itilbert(x)) == x``.
+
+    If ``2 * pi * h / period`` is approximately 10 or larger, then
+    numerically ``tilbert == hilbert``
+    (theoretically oo-Tilbert == Hilbert).
+
+    For even ``len(x)``, the Nyquist mode of ``x`` is taken zero.
+
+    """
+    if isinstance(_cache, threading.local):
+        if not hasattr(_cache, 'tilbert_cache'):
+            _cache.tilbert_cache = {}
+        _cache = _cache.tilbert_cache
+
+    tmp = asarray(x)
+    if iscomplexobj(tmp):
+        return tilbert(tmp.real, h, period, _cache) + \
+               1j * tilbert(tmp.imag, h, period, _cache)
+
+    if period is not None:
+        h = h * 2 * pi / period
+
+    n = len(x)
+    omega = _cache.get((n, h))
+    if omega is None:
+        if len(_cache) > 20:
+            while _cache:
+                _cache.popitem()
+
+        def kernel(k, h=h):
+            if k:
+                return 1.0/tanh(h*k)
+
+            return 0
+
+        omega = convolve.init_convolution_kernel(n, kernel, d=1)
+        _cache[(n,h)] = omega
+
+    overwrite_x = _datacopied(tmp, x)
+    return convolve.convolve(tmp,omega,swap_real_imag=1,overwrite_x=overwrite_x)
+
+
+def itilbert(x,h,period=None, _cache=_cache):
+    """
+    Return inverse h-Tilbert transform of a periodic sequence x.
+
+    If ``x_j`` and ``y_j`` are Fourier coefficients of periodic functions x
+    and y, respectively, then::
+
+      y_j = -sqrt(-1)*tanh(j*h*2*pi/period) * x_j
+      y_0 = 0
+
+    For more details, see `tilbert`.
+
+    """
+    if isinstance(_cache, threading.local):
+        if not hasattr(_cache, 'itilbert_cache'):
+            _cache.itilbert_cache = {}
+        _cache = _cache.itilbert_cache
+
+    tmp = asarray(x)
+    if iscomplexobj(tmp):
+        return itilbert(tmp.real, h, period, _cache) + \
+               1j*itilbert(tmp.imag, h, period, _cache)
+    if period is not None:
+        h = h*2*pi/period
+    n = len(x)
+    omega = _cache.get((n,h))
+    if omega is None:
+        if len(_cache) > 20:
+            while _cache:
+                _cache.popitem()
+
+        def kernel(k,h=h):
+            if k:
+                return -tanh(h*k)
+            return 0
+        omega = convolve.init_convolution_kernel(n,kernel,d=1)
+        _cache[(n,h)] = omega
+    overwrite_x = _datacopied(tmp, x)
+    return convolve.convolve(tmp,omega,swap_real_imag=1,overwrite_x=overwrite_x)
+
+
+def hilbert(x, _cache=_cache):
+    """
+    Return Hilbert transform of a periodic sequence x.
+
+    If x_j and y_j are Fourier coefficients of periodic functions x
+    and y, respectively, then::
+
+      y_j = sqrt(-1)*sign(j) * x_j
+      y_0 = 0
+
+    Parameters
+    ----------
+    x : array_like
+        The input array, should be periodic.
+    _cache : dict, optional
+        Dictionary that contains the kernel used to do a convolution with.
+
+    Returns
+    -------
+    y : ndarray
+        The transformed input.
+
+    See Also
+    --------
+    scipy.signal.hilbert : Compute the analytic signal, using the Hilbert
+                           transform.
+
+    Notes
+    -----
+    If ``sum(x, axis=0) == 0`` then ``hilbert(ihilbert(x)) == x``.
+
+    For even len(x), the Nyquist mode of x is taken zero.
+
+    The sign of the returned transform does not have a factor -1 that is more
+    often than not found in the definition of the Hilbert transform. Note also
+    that `scipy.signal.hilbert` does have an extra -1 factor compared to this
+    function.
+
+    """
+    if isinstance(_cache, threading.local):
+        if not hasattr(_cache, 'hilbert_cache'):
+            _cache.hilbert_cache = {}
+        _cache = _cache.hilbert_cache
+
+    tmp = asarray(x)
+    if iscomplexobj(tmp):
+        return hilbert(tmp.real, _cache) + 1j * hilbert(tmp.imag, _cache)
+    n = len(x)
+    omega = _cache.get(n)
+    if omega is None:
+        if len(_cache) > 20:
+            while _cache:
+                _cache.popitem()
+
+        def kernel(k):
+            if k > 0:
+                return 1.0
+            elif k < 0:
+                return -1.0
+            return 0.0
+        omega = convolve.init_convolution_kernel(n,kernel,d=1)
+        _cache[n] = omega
+    overwrite_x = _datacopied(tmp, x)
+    return convolve.convolve(tmp,omega,swap_real_imag=1,overwrite_x=overwrite_x)
+
+
+def ihilbert(x, _cache=_cache):
+    """
+    Return inverse Hilbert transform of a periodic sequence x.
+
+    If ``x_j`` and ``y_j`` are Fourier coefficients of periodic functions x
+    and y, respectively, then::
+
+      y_j = -sqrt(-1)*sign(j) * x_j
+      y_0 = 0
+
+    """
+    if isinstance(_cache, threading.local):
+        if not hasattr(_cache, 'ihilbert_cache'):
+            _cache.ihilbert_cache = {}
+        _cache = _cache.ihilbert_cache
+    return -hilbert(x, _cache)
+
+
+def cs_diff(x, a, b, period=None, _cache=_cache):
+    """
+    Return (a,b)-cosh/sinh pseudo-derivative of a periodic sequence.
+
+    If ``x_j`` and ``y_j`` are Fourier coefficients of periodic functions x
+    and y, respectively, then::
+
+      y_j = -sqrt(-1)*cosh(j*a*2*pi/period)/sinh(j*b*2*pi/period) * x_j
+      y_0 = 0
+
+    Parameters
+    ----------
+    x : array_like
+        The array to take the pseudo-derivative from.
+    a, b : float
+        Defines the parameters of the cosh/sinh pseudo-differential
+        operator.
+    period : float, optional
+        The period of the sequence. Default period is ``2*pi``.
+
+    Returns
+    -------
+    cs_diff : ndarray
+        Pseudo-derivative of periodic sequence `x`.
+
+    Notes
+    -----
+    For even len(`x`), the Nyquist mode of `x` is taken as zero.
+
+    """
+    if isinstance(_cache, threading.local):
+        if not hasattr(_cache, 'cs_diff_cache'):
+            _cache.cs_diff_cache = {}
+        _cache = _cache.cs_diff_cache
+
+    tmp = asarray(x)
+    if iscomplexobj(tmp):
+        return cs_diff(tmp.real, a, b, period, _cache) + \
+               1j*cs_diff(tmp.imag, a, b, period, _cache)
+    if period is not None:
+        a = a*2*pi/period
+        b = b*2*pi/period
+    n = len(x)
+    omega = _cache.get((n,a,b))
+    if omega is None:
+        if len(_cache) > 20:
+            while _cache:
+                _cache.popitem()
+
+        def kernel(k,a=a,b=b):
+            if k:
+                return -cosh(a*k)/sinh(b*k)
+            return 0
+        omega = convolve.init_convolution_kernel(n,kernel,d=1)
+        _cache[(n,a,b)] = omega
+    overwrite_x = _datacopied(tmp, x)
+    return convolve.convolve(tmp,omega,swap_real_imag=1,overwrite_x=overwrite_x)
+
+
+def sc_diff(x, a, b, period=None, _cache=_cache):
+    """
+    Return (a,b)-sinh/cosh pseudo-derivative of a periodic sequence x.
+
+    If x_j and y_j are Fourier coefficients of periodic functions x
+    and y, respectively, then::
+
+      y_j = sqrt(-1)*sinh(j*a*2*pi/period)/cosh(j*b*2*pi/period) * x_j
+      y_0 = 0
+
+    Parameters
+    ----------
+    x : array_like
+        Input array.
+    a,b : float
+        Defines the parameters of the sinh/cosh pseudo-differential
+        operator.
+    period : float, optional
+        The period of the sequence x. Default is 2*pi.
+
+    Notes
+    -----
+    ``sc_diff(cs_diff(x,a,b),b,a) == x``
+    For even ``len(x)``, the Nyquist mode of x is taken as zero.
+
+    """
+    if isinstance(_cache, threading.local):
+        if not hasattr(_cache, 'sc_diff_cache'):
+            _cache.sc_diff_cache = {}
+        _cache = _cache.sc_diff_cache
+
+    tmp = asarray(x)
+    if iscomplexobj(tmp):
+        return sc_diff(tmp.real, a, b, period, _cache) + \
+               1j * sc_diff(tmp.imag, a, b, period, _cache)
+    if period is not None:
+        a = a*2*pi/period
+        b = b*2*pi/period
+    n = len(x)
+    omega = _cache.get((n,a,b))
+    if omega is None:
+        if len(_cache) > 20:
+            while _cache:
+                _cache.popitem()
+
+        def kernel(k,a=a,b=b):
+            if k:
+                return sinh(a*k)/cosh(b*k)
+            return 0
+        omega = convolve.init_convolution_kernel(n,kernel,d=1)
+        _cache[(n,a,b)] = omega
+    overwrite_x = _datacopied(tmp, x)
+    return convolve.convolve(tmp,omega,swap_real_imag=1,overwrite_x=overwrite_x)
+
+
+def ss_diff(x, a, b, period=None, _cache=_cache):
+    """
+    Return (a,b)-sinh/sinh pseudo-derivative of a periodic sequence x.
+
+    If x_j and y_j are Fourier coefficients of periodic functions x
+    and y, respectively, then::
+
+      y_j = sinh(j*a*2*pi/period)/sinh(j*b*2*pi/period) * x_j
+      y_0 = a/b * x_0
+
+    Parameters
+    ----------
+    x : array_like
+        The array to take the pseudo-derivative from.
+    a,b
+        Defines the parameters of the sinh/sinh pseudo-differential
+        operator.
+    period : float, optional
+        The period of the sequence x. Default is ``2*pi``.
+
+    Notes
+    -----
+    ``ss_diff(ss_diff(x,a,b),b,a) == x``
+
+    """
+    if isinstance(_cache, threading.local):
+        if not hasattr(_cache, 'ss_diff_cache'):
+            _cache.ss_diff_cache = {}
+        _cache = _cache.ss_diff_cache
+
+    tmp = asarray(x)
+    if iscomplexobj(tmp):
+        return ss_diff(tmp.real, a, b, period, _cache) + \
+               1j*ss_diff(tmp.imag, a, b, period, _cache)
+    if period is not None:
+        a = a*2*pi/period
+        b = b*2*pi/period
+    n = len(x)
+    omega = _cache.get((n,a,b))
+    if omega is None:
+        if len(_cache) > 20:
+            while _cache:
+                _cache.popitem()
+
+        def kernel(k,a=a,b=b):
+            if k:
+                return sinh(a*k)/sinh(b*k)
+            return float(a)/b
+        omega = convolve.init_convolution_kernel(n,kernel)
+        _cache[(n,a,b)] = omega
+    overwrite_x = _datacopied(tmp, x)
+    return convolve.convolve(tmp,omega,overwrite_x=overwrite_x)
+
+
+def cc_diff(x, a, b, period=None, _cache=_cache):
+    """
+    Return (a,b)-cosh/cosh pseudo-derivative of a periodic sequence.
+
+    If x_j and y_j are Fourier coefficients of periodic functions x
+    and y, respectively, then::
+
+      y_j = cosh(j*a*2*pi/period)/cosh(j*b*2*pi/period) * x_j
+
+    Parameters
+    ----------
+    x : array_like
+        The array to take the pseudo-derivative from.
+    a,b : float
+        Defines the parameters of the sinh/sinh pseudo-differential
+        operator.
+    period : float, optional
+        The period of the sequence x. Default is ``2*pi``.
+
+    Returns
+    -------
+    cc_diff : ndarray
+        Pseudo-derivative of periodic sequence `x`.
+
+    Notes
+    -----
+    ``cc_diff(cc_diff(x,a,b),b,a) == x``
+
+    """
+    if isinstance(_cache, threading.local):
+        if not hasattr(_cache, 'cc_diff_cache'):
+            _cache.cc_diff_cache = {}
+        _cache = _cache.cc_diff_cache
+
+    tmp = asarray(x)
+    if iscomplexobj(tmp):
+        return cc_diff(tmp.real, a, b, period, _cache) + \
+               1j * cc_diff(tmp.imag, a, b, period, _cache)
+    if period is not None:
+        a = a*2*pi/period
+        b = b*2*pi/period
+    n = len(x)
+    omega = _cache.get((n,a,b))
+    if omega is None:
+        if len(_cache) > 20:
+            while _cache:
+                _cache.popitem()
+
+        def kernel(k,a=a,b=b):
+            return cosh(a*k)/cosh(b*k)
+        omega = convolve.init_convolution_kernel(n,kernel)
+        _cache[(n,a,b)] = omega
+    overwrite_x = _datacopied(tmp, x)
+    return convolve.convolve(tmp,omega,overwrite_x=overwrite_x)
+
+
+def shift(x, a, period=None, _cache=_cache):
+    """
+    Shift periodic sequence x by a: y(u) = x(u+a).
+
+    If x_j and y_j are Fourier coefficients of periodic functions x
+    and y, respectively, then::
+
+          y_j = exp(j*a*2*pi/period*sqrt(-1)) * x_f
+
+    Parameters
+    ----------
+    x : array_like
+        The array to take the pseudo-derivative from.
+    a : float
+        Defines the parameters of the sinh/sinh pseudo-differential
+    period : float, optional
+        The period of the sequences x and y. Default period is ``2*pi``.
+    """
+    if isinstance(_cache, threading.local):
+        if not hasattr(_cache, 'shift_cache'):
+            _cache.shift_cache = {}
+        _cache = _cache.shift_cache
+
+    tmp = asarray(x)
+    if iscomplexobj(tmp):
+        return shift(tmp.real, a, period, _cache) + 1j * shift(
+            tmp.imag, a, period, _cache)
+    if period is not None:
+        a = a*2*pi/period
+    n = len(x)
+    omega = _cache.get((n,a))
+    if omega is None:
+        if len(_cache) > 20:
+            while _cache:
+                _cache.popitem()
+
+        def kernel_real(k,a=a):
+            return cos(a*k)
+
+        def kernel_imag(k,a=a):
+            return sin(a*k)
+        omega_real = convolve.init_convolution_kernel(n,kernel_real,d=0,
+                                                      zero_nyquist=0)
+        omega_imag = convolve.init_convolution_kernel(n,kernel_imag,d=1,
+                                                      zero_nyquist=0)
+        _cache[(n,a)] = omega_real,omega_imag
+    else:
+        omega_real,omega_imag = omega
+    overwrite_x = _datacopied(tmp, x)
+    return convolve.convolve_z(tmp,omega_real,omega_imag,
+                               overwrite_x=overwrite_x)
+
diff --git a/.venv/lib/python3.11/site-packages/scipy/fftpack/_realtransforms.py b/.venv/lib/python3.11/site-packages/scipy/fftpack/_realtransforms.py
new file mode 100644
index 0000000000000000000000000000000000000000..ad71d517b0ac829ab71850bf67f7dc38636161f2
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fftpack/_realtransforms.py
@@ -0,0 +1,598 @@
+"""
+Real spectrum transforms (DCT, DST, MDCT)
+"""
+
+__all__ = ['dct', 'idct', 'dst', 'idst', 'dctn', 'idctn', 'dstn', 'idstn']
+
+from scipy.fft import _pocketfft
+from ._helper import _good_shape
+
+_inverse_typemap = {1: 1, 2: 3, 3: 2, 4: 4}
+
+
+def dctn(x, type=2, shape=None, axes=None, norm=None, overwrite_x=False):
+    """
+    Return multidimensional Discrete Cosine Transform along the specified axes.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DCT (see Notes). Default type is 2.
+    shape : int or array_like of ints or None, optional
+        The shape of the result. If both `shape` and `axes` (see below) are
+        None, `shape` is ``x.shape``; if `shape` is None but `axes` is
+        not None, then `shape` is ``numpy.take(x.shape, axes, axis=0)``.
+        If ``shape[i] > x.shape[i]``, the ith dimension is padded with zeros.
+        If ``shape[i] < x.shape[i]``, the ith dimension is truncated to
+        length ``shape[i]``.
+        If any element of `shape` is -1, the size of the corresponding
+        dimension of `x` is used.
+    axes : int or array_like of ints or None, optional
+        Axes along which the DCT is computed.
+        The default is over all axes.
+    norm : {None, 'ortho'}, optional
+        Normalization mode (see Notes). Default is None.
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+
+    Returns
+    -------
+    y : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    idctn : Inverse multidimensional DCT
+
+    Notes
+    -----
+    For full details of the DCT types and normalization modes, as well as
+    references, see `dct`.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.fftpack import dctn, idctn
+    >>> rng = np.random.default_rng()
+    >>> y = rng.standard_normal((16, 16))
+    >>> np.allclose(y, idctn(dctn(y, norm='ortho'), norm='ortho'))
+    True
+
+    """
+    shape = _good_shape(x, shape, axes)
+    return _pocketfft.dctn(x, type, shape, axes, norm, overwrite_x)
+
+
+def idctn(x, type=2, shape=None, axes=None, norm=None, overwrite_x=False):
+    """
+    Return multidimensional Discrete Cosine Transform along the specified axes.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DCT (see Notes). Default type is 2.
+    shape : int or array_like of ints or None, optional
+        The shape of the result.  If both `shape` and `axes` (see below) are
+        None, `shape` is ``x.shape``; if `shape` is None but `axes` is
+        not None, then `shape` is ``numpy.take(x.shape, axes, axis=0)``.
+        If ``shape[i] > x.shape[i]``, the ith dimension is padded with zeros.
+        If ``shape[i] < x.shape[i]``, the ith dimension is truncated to
+        length ``shape[i]``.
+        If any element of `shape` is -1, the size of the corresponding
+        dimension of `x` is used.
+    axes : int or array_like of ints or None, optional
+        Axes along which the IDCT is computed.
+        The default is over all axes.
+    norm : {None, 'ortho'}, optional
+        Normalization mode (see Notes). Default is None.
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+
+    Returns
+    -------
+    y : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    dctn : multidimensional DCT
+
+    Notes
+    -----
+    For full details of the IDCT types and normalization modes, as well as
+    references, see `idct`.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.fftpack import dctn, idctn
+    >>> rng = np.random.default_rng()
+    >>> y = rng.standard_normal((16, 16))
+    >>> np.allclose(y, idctn(dctn(y, norm='ortho'), norm='ortho'))
+    True
+
+    """
+    type = _inverse_typemap[type]
+    shape = _good_shape(x, shape, axes)
+    return _pocketfft.dctn(x, type, shape, axes, norm, overwrite_x)
+
+
+def dstn(x, type=2, shape=None, axes=None, norm=None, overwrite_x=False):
+    """
+    Return multidimensional Discrete Sine Transform along the specified axes.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DST (see Notes). Default type is 2.
+    shape : int or array_like of ints or None, optional
+        The shape of the result.  If both `shape` and `axes` (see below) are
+        None, `shape` is ``x.shape``; if `shape` is None but `axes` is
+        not None, then `shape` is ``numpy.take(x.shape, axes, axis=0)``.
+        If ``shape[i] > x.shape[i]``, the ith dimension is padded with zeros.
+        If ``shape[i] < x.shape[i]``, the ith dimension is truncated to
+        length ``shape[i]``.
+        If any element of `shape` is -1, the size of the corresponding
+        dimension of `x` is used.
+    axes : int or array_like of ints or None, optional
+        Axes along which the DCT is computed.
+        The default is over all axes.
+    norm : {None, 'ortho'}, optional
+        Normalization mode (see Notes). Default is None.
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+
+    Returns
+    -------
+    y : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    idstn : Inverse multidimensional DST
+
+    Notes
+    -----
+    For full details of the DST types and normalization modes, as well as
+    references, see `dst`.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.fftpack import dstn, idstn
+    >>> rng = np.random.default_rng()
+    >>> y = rng.standard_normal((16, 16))
+    >>> np.allclose(y, idstn(dstn(y, norm='ortho'), norm='ortho'))
+    True
+
+    """
+    shape = _good_shape(x, shape, axes)
+    return _pocketfft.dstn(x, type, shape, axes, norm, overwrite_x)
+
+
+def idstn(x, type=2, shape=None, axes=None, norm=None, overwrite_x=False):
+    """
+    Return multidimensional Discrete Sine Transform along the specified axes.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DST (see Notes). Default type is 2.
+    shape : int or array_like of ints or None, optional
+        The shape of the result.  If both `shape` and `axes` (see below) are
+        None, `shape` is ``x.shape``; if `shape` is None but `axes` is
+        not None, then `shape` is ``numpy.take(x.shape, axes, axis=0)``.
+        If ``shape[i] > x.shape[i]``, the ith dimension is padded with zeros.
+        If ``shape[i] < x.shape[i]``, the ith dimension is truncated to
+        length ``shape[i]``.
+        If any element of `shape` is -1, the size of the corresponding
+        dimension of `x` is used.
+    axes : int or array_like of ints or None, optional
+        Axes along which the IDST is computed.
+        The default is over all axes.
+    norm : {None, 'ortho'}, optional
+        Normalization mode (see Notes). Default is None.
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+
+    Returns
+    -------
+    y : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    dstn : multidimensional DST
+
+    Notes
+    -----
+    For full details of the IDST types and normalization modes, as well as
+    references, see `idst`.
+
+    Examples
+    --------
+    >>> import numpy as np
+    >>> from scipy.fftpack import dstn, idstn
+    >>> rng = np.random.default_rng()
+    >>> y = rng.standard_normal((16, 16))
+    >>> np.allclose(y, idstn(dstn(y, norm='ortho'), norm='ortho'))
+    True
+
+    """
+    type = _inverse_typemap[type]
+    shape = _good_shape(x, shape, axes)
+    return _pocketfft.dstn(x, type, shape, axes, norm, overwrite_x)
+
+
+def dct(x, type=2, n=None, axis=-1, norm=None, overwrite_x=False):
+    r"""
+    Return the Discrete Cosine Transform of arbitrary type sequence x.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DCT (see Notes). Default type is 2.
+    n : int, optional
+        Length of the transform.  If ``n < x.shape[axis]``, `x` is
+        truncated.  If ``n > x.shape[axis]``, `x` is zero-padded. The
+        default results in ``n = x.shape[axis]``.
+    axis : int, optional
+        Axis along which the dct is computed; the default is over the
+        last axis (i.e., ``axis=-1``).
+    norm : {None, 'ortho'}, optional
+        Normalization mode (see Notes). Default is None.
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+
+    Returns
+    -------
+    y : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    idct : Inverse DCT
+
+    Notes
+    -----
+    For a single dimension array ``x``, ``dct(x, norm='ortho')`` is equal to
+    MATLAB ``dct(x)``.
+
+    There are, theoretically, 8 types of the DCT, only the first 4 types are
+    implemented in scipy. 'The' DCT generally refers to DCT type 2, and 'the'
+    Inverse DCT generally refers to DCT type 3.
+
+    **Type I**
+
+    There are several definitions of the DCT-I; we use the following
+    (for ``norm=None``)
+
+    .. math::
+
+       y_k = x_0 + (-1)^k x_{N-1} + 2 \sum_{n=1}^{N-2} x_n \cos\left(
+       \frac{\pi k n}{N-1} \right)
+
+    If ``norm='ortho'``, ``x[0]`` and ``x[N-1]`` are multiplied by a scaling
+    factor of :math:`\sqrt{2}`, and ``y[k]`` is multiplied by a scaling factor
+    ``f``
+
+    .. math::
+
+        f = \begin{cases}
+         \frac{1}{2}\sqrt{\frac{1}{N-1}} & \text{if }k=0\text{ or }N-1, \\
+         \frac{1}{2}\sqrt{\frac{2}{N-1}} & \text{otherwise} \end{cases}
+
+    .. versionadded:: 1.2.0
+       Orthonormalization in DCT-I.
+
+    .. note::
+       The DCT-I is only supported for input size > 1.
+
+    **Type II**
+
+    There are several definitions of the DCT-II; we use the following
+    (for ``norm=None``)
+
+    .. math::
+
+       y_k = 2 \sum_{n=0}^{N-1} x_n \cos\left(\frac{\pi k(2n+1)}{2N} \right)
+
+    If ``norm='ortho'``, ``y[k]`` is multiplied by a scaling factor ``f``
+
+    .. math::
+       f = \begin{cases}
+       \sqrt{\frac{1}{4N}} & \text{if }k=0, \\
+       \sqrt{\frac{1}{2N}} & \text{otherwise} \end{cases}
+
+    which makes the corresponding matrix of coefficients orthonormal
+    (``O @ O.T = np.eye(N)``).
+
+    **Type III**
+
+    There are several definitions, we use the following (for ``norm=None``)
+
+    .. math::
+
+       y_k = x_0 + 2 \sum_{n=1}^{N-1} x_n \cos\left(\frac{\pi(2k+1)n}{2N}\right)
+
+    or, for ``norm='ortho'``
+
+    .. math::
+
+       y_k = \frac{x_0}{\sqrt{N}} + \sqrt{\frac{2}{N}} \sum_{n=1}^{N-1} x_n
+       \cos\left(\frac{\pi(2k+1)n}{2N}\right)
+
+    The (unnormalized) DCT-III is the inverse of the (unnormalized) DCT-II, up
+    to a factor ``2N``. The orthonormalized DCT-III is exactly the inverse of
+    the orthonormalized DCT-II.
+
+    **Type IV**
+
+    There are several definitions of the DCT-IV; we use the following
+    (for ``norm=None``)
+
+    .. math::
+
+       y_k = 2 \sum_{n=0}^{N-1} x_n \cos\left(\frac{\pi(2k+1)(2n+1)}{4N} \right)
+
+    If ``norm='ortho'``, ``y[k]`` is multiplied by a scaling factor ``f``
+
+    .. math::
+
+        f = \frac{1}{\sqrt{2N}}
+
+    .. versionadded:: 1.2.0
+       Support for DCT-IV.
+
+    References
+    ----------
+    .. [1] 'A Fast Cosine Transform in One and Two Dimensions', by J.
+           Makhoul, `IEEE Transactions on acoustics, speech and signal
+           processing` vol. 28(1), pp. 27-34,
+           :doi:`10.1109/TASSP.1980.1163351` (1980).
+    .. [2] Wikipedia, "Discrete cosine transform",
+           https://en.wikipedia.org/wiki/Discrete_cosine_transform
+
+    Examples
+    --------
+    The Type 1 DCT is equivalent to the FFT (though faster) for real,
+    even-symmetrical inputs. The output is also real and even-symmetrical.
+    Half of the FFT input is used to generate half of the FFT output:
+
+    >>> from scipy.fftpack import fft, dct
+    >>> import numpy as np
+    >>> fft(np.array([4., 3., 5., 10., 5., 3.])).real
+    array([ 30.,  -8.,   6.,  -2.,   6.,  -8.])
+    >>> dct(np.array([4., 3., 5., 10.]), 1)
+    array([ 30.,  -8.,   6.,  -2.])
+
+    """
+    return _pocketfft.dct(x, type, n, axis, norm, overwrite_x)
+
+
+def idct(x, type=2, n=None, axis=-1, norm=None, overwrite_x=False):
+    """
+    Return the Inverse Discrete Cosine Transform of an arbitrary type sequence.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DCT (see Notes). Default type is 2.
+    n : int, optional
+        Length of the transform.  If ``n < x.shape[axis]``, `x` is
+        truncated.  If ``n > x.shape[axis]``, `x` is zero-padded. The
+        default results in ``n = x.shape[axis]``.
+    axis : int, optional
+        Axis along which the idct is computed; the default is over the
+        last axis (i.e., ``axis=-1``).
+    norm : {None, 'ortho'}, optional
+        Normalization mode (see Notes). Default is None.
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+
+    Returns
+    -------
+    idct : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    dct : Forward DCT
+
+    Notes
+    -----
+    For a single dimension array `x`, ``idct(x, norm='ortho')`` is equal to
+    MATLAB ``idct(x)``.
+
+    'The' IDCT is the IDCT of type 2, which is the same as DCT of type 3.
+
+    IDCT of type 1 is the DCT of type 1, IDCT of type 2 is the DCT of type
+    3, and IDCT of type 3 is the DCT of type 2. IDCT of type 4 is the DCT
+    of type 4. For the definition of these types, see `dct`.
+
+    Examples
+    --------
+    The Type 1 DCT is equivalent to the DFT for real, even-symmetrical
+    inputs. The output is also real and even-symmetrical. Half of the IFFT
+    input is used to generate half of the IFFT output:
+
+    >>> from scipy.fftpack import ifft, idct
+    >>> import numpy as np
+    >>> ifft(np.array([ 30.,  -8.,   6.,  -2.,   6.,  -8.])).real
+    array([  4.,   3.,   5.,  10.,   5.,   3.])
+    >>> idct(np.array([ 30.,  -8.,   6.,  -2.]), 1) / 6
+    array([  4.,   3.,   5.,  10.])
+
+    """
+    type = _inverse_typemap[type]
+    return _pocketfft.dct(x, type, n, axis, norm, overwrite_x)
+
+
+def dst(x, type=2, n=None, axis=-1, norm=None, overwrite_x=False):
+    r"""
+    Return the Discrete Sine Transform of arbitrary type sequence x.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DST (see Notes). Default type is 2.
+    n : int, optional
+        Length of the transform.  If ``n < x.shape[axis]``, `x` is
+        truncated.  If ``n > x.shape[axis]``, `x` is zero-padded. The
+        default results in ``n = x.shape[axis]``.
+    axis : int, optional
+        Axis along which the dst is computed; the default is over the
+        last axis (i.e., ``axis=-1``).
+    norm : {None, 'ortho'}, optional
+        Normalization mode (see Notes). Default is None.
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+
+    Returns
+    -------
+    dst : ndarray of reals
+        The transformed input array.
+
+    See Also
+    --------
+    idst : Inverse DST
+
+    Notes
+    -----
+    For a single dimension array ``x``.
+
+    There are, theoretically, 8 types of the DST for different combinations of
+    even/odd boundary conditions and boundary off sets [1]_, only the first
+    4 types are implemented in scipy.
+
+    **Type I**
+
+    There are several definitions of the DST-I; we use the following
+    for ``norm=None``. DST-I assumes the input is odd around `n=-1` and `n=N`.
+
+    .. math::
+
+        y_k = 2 \sum_{n=0}^{N-1} x_n \sin\left(\frac{\pi(k+1)(n+1)}{N+1}\right)
+
+    Note that the DST-I is only supported for input size > 1.
+    The (unnormalized) DST-I is its own inverse, up to a factor ``2(N+1)``.
+    The orthonormalized DST-I is exactly its own inverse.
+
+    **Type II**
+
+    There are several definitions of the DST-II; we use the following for
+    ``norm=None``. DST-II assumes the input is odd around `n=-1/2` and
+    `n=N-1/2`; the output is odd around :math:`k=-1` and even around `k=N-1`
+
+    .. math::
+
+        y_k = 2 \sum_{n=0}^{N-1} x_n \sin\left(\frac{\pi(k+1)(2n+1)}{2N}\right)
+
+    if ``norm='ortho'``, ``y[k]`` is multiplied by a scaling factor ``f``
+
+    .. math::
+
+        f = \begin{cases}
+        \sqrt{\frac{1}{4N}} & \text{if }k = 0, \\
+        \sqrt{\frac{1}{2N}} & \text{otherwise} \end{cases}
+
+    **Type III**
+
+    There are several definitions of the DST-III, we use the following (for
+    ``norm=None``). DST-III assumes the input is odd around `n=-1` and even
+    around `n=N-1`
+
+    .. math::
+
+        y_k = (-1)^k x_{N-1} + 2 \sum_{n=0}^{N-2} x_n \sin\left(
+        \frac{\pi(2k+1)(n+1)}{2N}\right)
+
+    The (unnormalized) DST-III is the inverse of the (unnormalized) DST-II, up
+    to a factor ``2N``. The orthonormalized DST-III is exactly the inverse of the
+    orthonormalized DST-II.
+
+    .. versionadded:: 0.11.0
+
+    **Type IV**
+
+    There are several definitions of the DST-IV, we use the following (for
+    ``norm=None``). DST-IV assumes the input is odd around `n=-0.5` and even
+    around `n=N-0.5`
+
+    .. math::
+
+        y_k = 2 \sum_{n=0}^{N-1} x_n \sin\left(\frac{\pi(2k+1)(2n+1)}{4N}\right)
+
+    The (unnormalized) DST-IV is its own inverse, up to a factor ``2N``. The
+    orthonormalized DST-IV is exactly its own inverse.
+
+    .. versionadded:: 1.2.0
+       Support for DST-IV.
+
+    References
+    ----------
+    .. [1] Wikipedia, "Discrete sine transform",
+           https://en.wikipedia.org/wiki/Discrete_sine_transform
+
+    """
+    return _pocketfft.dst(x, type, n, axis, norm, overwrite_x)
+
+
+def idst(x, type=2, n=None, axis=-1, norm=None, overwrite_x=False):
+    """
+    Return the Inverse Discrete Sine Transform of an arbitrary type sequence.
+
+    Parameters
+    ----------
+    x : array_like
+        The input array.
+    type : {1, 2, 3, 4}, optional
+        Type of the DST (see Notes). Default type is 2.
+    n : int, optional
+        Length of the transform.  If ``n < x.shape[axis]``, `x` is
+        truncated. If ``n > x.shape[axis]``, `x` is zero-padded. The
+        default results in ``n = x.shape[axis]``.
+    axis : int, optional
+        Axis along which the idst is computed; the default is over the
+        last axis (i.e., ``axis=-1``).
+    norm : {None, 'ortho'}, optional
+        Normalization mode (see Notes). Default is None.
+    overwrite_x : bool, optional
+        If True, the contents of `x` can be destroyed; the default is False.
+
+    Returns
+    -------
+    idst : ndarray of real
+        The transformed input array.
+
+    See Also
+    --------
+    dst : Forward DST
+
+    Notes
+    -----
+    'The' IDST is the IDST of type 2, which is the same as DST of type 3.
+
+    IDST of type 1 is the DST of type 1, IDST of type 2 is the DST of type
+    3, and IDST of type 3 is the DST of type 2. For the definition of these
+    types, see `dst`.
+
+    .. versionadded:: 0.11.0
+
+    """
+    type = _inverse_typemap[type]
+    return _pocketfft.dst(x, type, n, axis, norm, overwrite_x)
diff --git a/.venv/lib/python3.11/site-packages/scipy/fftpack/basic.py b/.venv/lib/python3.11/site-packages/scipy/fftpack/basic.py
new file mode 100644
index 0000000000000000000000000000000000000000..553f456fe1561c28928ecc4ebe2238459cc60443
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fftpack/basic.py
@@ -0,0 +1,20 @@
+# This file is not meant for public use and will be removed in SciPy v2.0.0.
+# Use the `scipy.fftpack` namespace for importing the functions
+# included below.
+
+from scipy._lib.deprecation import _sub_module_deprecation
+
+__all__ = [  # noqa: F822
+    'fft','ifft','fftn','ifftn','rfft','irfft',
+    'fft2','ifft2'
+]
+
+
+def __dir__():
+    return __all__
+
+
+def __getattr__(name):
+    return _sub_module_deprecation(sub_package="fftpack", module="basic",
+                                   private_modules=["_basic"], all=__all__,
+                                   attribute=name)
diff --git a/.venv/lib/python3.11/site-packages/scipy/fftpack/convolve.cpython-311-x86_64-linux-gnu.so b/.venv/lib/python3.11/site-packages/scipy/fftpack/convolve.cpython-311-x86_64-linux-gnu.so
new file mode 100644
index 0000000000000000000000000000000000000000..9ee721becedb8b5f384b3d5a4c42ecc4403092b0
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fftpack/convolve.cpython-311-x86_64-linux-gnu.so
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:09ee0f3f25a84c918270a67a3944fe171b5568c89ce36fd35c79df132eb7db09
+size 114040
diff --git a/.venv/lib/python3.11/site-packages/scipy/fftpack/helper.py b/.venv/lib/python3.11/site-packages/scipy/fftpack/helper.py
new file mode 100644
index 0000000000000000000000000000000000000000..fcc7000c215f8a7605a2a59b5767b27b2fcd969d
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fftpack/helper.py
@@ -0,0 +1,19 @@
+# This file is not meant for public use and will be removed in SciPy v2.0.0.
+# Use the `scipy.fftpack` namespace for importing the functions
+# included below.
+
+from scipy._lib.deprecation import _sub_module_deprecation
+
+__all__ = [  # noqa: F822
+    'fftshift', 'ifftshift', 'fftfreq', 'rfftfreq', 'next_fast_len'
+]
+
+
+def __dir__():
+    return __all__
+
+
+def __getattr__(name):
+    return _sub_module_deprecation(sub_package="fftpack", module="helper",
+                                   private_modules=["_helper"], all=__all__,
+                                   attribute=name)
diff --git a/.venv/lib/python3.11/site-packages/scipy/fftpack/pseudo_diffs.py b/.venv/lib/python3.11/site-packages/scipy/fftpack/pseudo_diffs.py
new file mode 100644
index 0000000000000000000000000000000000000000..ecf71ad3256d48d2131c8058072da724cb001af9
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fftpack/pseudo_diffs.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.fftpack` namespace for importing the functions
+# included below.
+
+from scipy._lib.deprecation import _sub_module_deprecation
+
+__all__ = [  # noqa: F822
+    'diff',
+    'tilbert', 'itilbert', 'hilbert', 'ihilbert',
+    'cs_diff', 'cc_diff', 'sc_diff', 'ss_diff',
+    'shift', 'convolve'
+]
+
+
+def __dir__():
+    return __all__
+
+
+def __getattr__(name):
+    return _sub_module_deprecation(sub_package="fftpack", module="pseudo_diffs",
+                                   private_modules=["_pseudo_diffs"], all=__all__,
+                                   attribute=name)
diff --git a/.venv/lib/python3.11/site-packages/scipy/fftpack/realtransforms.py b/.venv/lib/python3.11/site-packages/scipy/fftpack/realtransforms.py
new file mode 100644
index 0000000000000000000000000000000000000000..9a392198fccf213bc988a79058bd69515e39f510
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fftpack/realtransforms.py
@@ -0,0 +1,19 @@
+# This file is not meant for public use and will be removed in SciPy v2.0.0.
+# Use the `scipy.fftpack` namespace for importing the functions
+# included below.
+
+from scipy._lib.deprecation import _sub_module_deprecation
+
+__all__ = [  # noqa: F822
+    'dct', 'idct', 'dst', 'idst', 'dctn', 'idctn', 'dstn', 'idstn'
+]
+
+
+def __dir__():
+    return __all__
+
+
+def __getattr__(name):
+    return _sub_module_deprecation(sub_package="fftpack", module="realtransforms",
+                                   private_modules=["_realtransforms"], all=__all__,
+                                   attribute=name)
diff --git a/.venv/lib/python3.11/site-packages/scipy/fftpack/tests/__init__.py b/.venv/lib/python3.11/site-packages/scipy/fftpack/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/.venv/lib/python3.11/site-packages/scipy/fftpack/tests/fftw_double_ref.npz b/.venv/lib/python3.11/site-packages/scipy/fftpack/tests/fftw_double_ref.npz
new file mode 100644
index 0000000000000000000000000000000000000000..e1e3d620400746177b560b9193efce03c2841e99
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fftpack/tests/fftw_double_ref.npz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a60c649415b645223924d8342ccc5c097801c86901287a369e53fc9259f5ec4e
+size 162120
diff --git a/.venv/lib/python3.11/site-packages/scipy/fftpack/tests/fftw_longdouble_ref.npz b/.venv/lib/python3.11/site-packages/scipy/fftpack/tests/fftw_longdouble_ref.npz
new file mode 100644
index 0000000000000000000000000000000000000000..b1a646889c9889541e8d368c8c2d96520d183dc4
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fftpack/tests/fftw_longdouble_ref.npz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a406cbd4dad04d0c59dd38f54416fb49424c82229c1a074b6a44ec0cde2000e3
+size 296072
diff --git a/.venv/lib/python3.11/site-packages/scipy/fftpack/tests/fftw_single_ref.npz b/.venv/lib/python3.11/site-packages/scipy/fftpack/tests/fftw_single_ref.npz
new file mode 100644
index 0000000000000000000000000000000000000000..a42748dba14b7ff0d2f53ce4cd5a86a4f08e5d93
--- /dev/null
+++ b/.venv/lib/python3.11/site-packages/scipy/fftpack/tests/fftw_single_ref.npz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:276a9141318e6fc36e4ab6ff54a61b64054ef8849b660f17359e5f541b43c526
+size 95144