From fadc9cecfaede6ca8553e6ba4ab383ae543b5954 Mon Sep 17 00:00:00 2001 From: saudzahirr Date: Sun, 10 May 2026 16:49:16 +0500 Subject: [PATCH] Update --- .github/dependabot.yml | 14 + .github/workflows/code_style.yml | 36 + .github/workflows/code_test.yml | 99 ++ .github/workflows/codeql.yml | 101 ++ .github/workflows/dist_build.yml | 40 + .github/workflows/docs_build.yml | 52 + .github/workflows/publish_dist.yml | 56 + .gitignore | 225 ++- .python-version | 1 + .travis.yml | 25 - README | 120 -- README.md | 91 ++ ad/__init__.py | 2220 +++++++++++++--------------- ad/admath/__init__.py | 113 +- ad/admath/admath.py | 1944 ++++++++++++------------ ad/linalg/__init__.py | 7 +- ad/linalg/linalg.py | 723 ++++----- doc/conf.py | 203 --- docs/_static/favicon.ico | Bin 0 -> 9918 bytes docs/_static/logo.png | Bin 0 -> 25387 bytes docs/_static/lstsq_fit.png | Bin 0 -> 27183 bytes docs/api.md | 165 +++ docs/index.md | 50 + docs/installation.md | 117 ++ docs/quickstart.md | 273 ++++ docs/references.md | 16 + docs/theory.md | 156 ++ mkdocs.yml | 51 + pyproject.toml | 133 ++ setup.py | 67 - test_ad.py | 242 --- tests/test_core.py | 232 +++ tests/test_numpy.py | 55 + uv.lock | 960 ++++++++++++ 34 files changed, 5266 insertions(+), 3321 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/code_style.yml create mode 100644 .github/workflows/code_test.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/dist_build.yml create mode 100644 .github/workflows/docs_build.yml create mode 100644 .github/workflows/publish_dist.yml create mode 100644 .python-version delete mode 100644 .travis.yml delete mode 100644 README create mode 100644 README.md delete mode 100644 doc/conf.py create mode 100644 docs/_static/favicon.ico create mode 100644 docs/_static/logo.png create mode 100644 docs/_static/lstsq_fit.png create mode 100644 docs/api.md create mode 100644 docs/index.md create mode 100644 docs/installation.md create mode 100644 docs/quickstart.md create mode 100644 docs/references.md create mode 100644 docs/theory.md create mode 100644 mkdocs.yml create mode 100644 pyproject.toml delete mode 100644 setup.py delete mode 100644 test_ad.py create mode 100644 tests/test_core.py create mode 100644 tests/test_numpy.py create mode 100644 uv.lock diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..7e6c742 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "friday" + + - package-ecosystem: "uv" + directory: "/" + schedule: + interval: "weekly" + day: "friday" diff --git a/.github/workflows/code_style.yml b/.github/workflows/code_style.yml new file mode 100644 index 0000000..eb07849 --- /dev/null +++ b/.github/workflows/code_style.yml @@ -0,0 +1,36 @@ +name: Lint and Format + +on: + push: + branches: [ master ] + + pull_request: + branches: [ master ] + paths: [ '**/*.py' ] + + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}/${{ github.ref }} + cancel-in-progress: true + +jobs: + python-format: + name: Ruff Lint and Format + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v6 + + - name: Setup Ruff + uses: astral-sh/ruff-action@v3 + with: + args: --version + + - name: Enforce Lint + run: ruff check ad + + - name: Enforce Format + run: | + ruff format --diff ad diff --git a/.github/workflows/code_test.yml b/.github/workflows/code_test.yml new file mode 100644 index 0000000..5bddf4f --- /dev/null +++ b/.github/workflows/code_test.yml @@ -0,0 +1,99 @@ +name: Code Test + +on: + push: + branches: [ master ] + + pull_request: + branches: [ master ] + + schedule: + - cron: "0 0 * * 0" # Runs every Sunday at midnight UTC + + workflow_dispatch: + workflow_call: + outputs: + artifact_name: + value: ${{ jobs.fresh_build.outputs.artifact_name }} + artifact_run_id: + value: ${{ jobs.fresh_build.outputs.run_id }} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + setup: + runs-on: ubuntu-latest + + steps: + - name: Fetch Supported Python + id: fetch-versions + run: | + versions=$(curl -fsSL https://endoflife.date/api/v1/products/python/ \ + | jq '[ .result.releases[] | select(.isEol == false) | .label ]') + + echo "supported_versions=$(echo $versions |jq -c '.')" >> $GITHUB_OUTPUT + echo "bound_versions=$(echo $versions |jq -c '[.[0],.[-1]]')" >> $GITHUB_OUTPUT + + outputs: + supported_versions: ${{ steps.fetch-versions.outputs.supported_versions }} + bound_versions: ${{ steps.fetch-versions.outputs.bound_versions }} + + fresh_build: + uses: ./.github/workflows/dist_build.yml + + testing: + runs-on: ${{ matrix.os }} + + permissions: + id-token: write # for working of oidc of codecov + + strategy: + matrix: + os: [ ubuntu-latest ] + py-version: ${{ fromJson(needs.setup.outputs.supported_versions) }} + include: + - os: windows-latest + py-version: ${{ fromJson(needs.setup.outputs.bound_versions)[0] }} + - os: windows-latest + py-version: ${{ fromJson(needs.setup.outputs.bound_versions)[1] }} + - os: macos-latest + py-version: ${{ fromJson(needs.setup.outputs.bound_versions)[0] }} + - os: macos-latest + py-version: ${{ fromJson(needs.setup.outputs.bound_versions)[1] }} + + needs: + - fresh_build + - setup + + steps: + - uses: actions/checkout@v6 + + - name: Download Build Artifacts + uses: actions/download-artifact@v8 + with: + name: ${{ needs.fresh_build.outputs.artifact_name }} + run-id: ${{ needs.fresh_build.outputs.run_id }} + path: ./dist + + - uses: astral-sh/setup-uv@v7 + with: + python-version: ${{ matrix.py-version }} + + - name: Install Packages + shell: bash + run: | + uv sync --no-dev --group test + uv pip install -v ./dist/*.whl + + - name: Run Tests + run: uv run --no-sync pytest -v -n auto --cov --cov-report=xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v6 + with: + fail_ci_if_error: true + use_oidc: true + verbose: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..36471e6 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,101 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + schedule: + - cron: '33 5 * * 2' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dist_build.yml b/.github/workflows/dist_build.yml new file mode 100644 index 0000000..acc7c2c --- /dev/null +++ b/.github/workflows/dist_build.yml @@ -0,0 +1,40 @@ +name: Build Distribution + +on: + workflow_call: + outputs: + artifact_name: + value: ${{ jobs.build.outputs.artifact_name }} + run_id: + value: ${{ github.run_id }} + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup uv + uses: astral-sh/setup-uv@v7 + + - name: Build distribution + run: uv build -v --clear . + + - name: Set Artifact Name + id: artifact-name + run: | + echo "artifact_name=${{ github.run_id }}[${{ github.run_attempt }}]--build-artifacts" \ + | tee -a $GITHUB_OUTPUT + + - name: Upload Build Artifacts + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.artifact-name.outputs.artifact_name }} + path: dist + if-no-files-found: error + + outputs: + artifact_name: ${{ steps.artifact-name.outputs.artifact_name }} diff --git a/.github/workflows/docs_build.yml b/.github/workflows/docs_build.yml new file mode 100644 index 0000000..6d41b2d --- /dev/null +++ b/.github/workflows/docs_build.yml @@ -0,0 +1,52 @@ +name: Build Documentation +permissions: + pages: write + id-token: write + +on: + pull_request: + branches: [ master ] + workflow_call: + inputs: &build-docs-inputs + deploy: + type: boolean + required: false + default: false + + workflow_dispatch: + inputs: *build-docs-inputs + +concurrency: + group: docs-build + +jobs: + + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Setup uv + uses: astral-sh/setup-uv@v7 + + - name: Build Documentation + run: uv run --no-dev --group docs zensical build --clean --strict + + - name: Set Artifact Name + id: artifact-name + run: | + echo "artifact_name=${{ github.run_id }}[${{ github.run_attempt }}]--docs-artifacts" \ + | tee -a $GITHUB_OUTPUT + + - name: Upload Pages Artifacts + uses: actions/upload-pages-artifact@v5 + with: + name: ${{ steps.artifact-name.outputs.artifact_name }} + path: site/ + + - name: Deploy to GitHub Pages + if: ${{ inputs.deploy }} + uses: actions/deploy-pages@v5 + with: + artifact_name: ${{ steps.artifact-name.outputs.artifact_name }} diff --git a/.github/workflows/publish_dist.yml b/.github/workflows/publish_dist.yml new file mode 100644 index 0000000..9e7e944 --- /dev/null +++ b/.github/workflows/publish_dist.yml @@ -0,0 +1,56 @@ +name: Publish to PyPI + +on: + release: + types: [published] + +permissions: + id-token: write + +concurrency: + group: ${{ github.workflow }} + +jobs: + testing: + uses: ./.github/workflows/code_test.yml + + publish-to-pypi: + runs-on: ubuntu-latest + + environment: + name: pypi + + needs: testing + + steps: + + - &download_dist + name: Download Distributions + uses: actions/download-artifact@v8 + with: + name: ${{ needs.testing.outputs.artifact_name }} + run-id: ${{ needs.testing.outputs.artifact_run_id }} + path: ./dist + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + verbose: true + + - name: Smoke Test on PyPI + shell: bash + run: | + sleep 60 + pip install -v --index-url https://pypi.org/simple/ ad + python -c "import ad;print(ad.__version__)" + + publish_docs: + needs: publish-to-pypi + + permissions: + pages: write + id-token: write + + uses: ./.github/workflows/docs_build.yml + with: + deploy: true diff --git a/.gitignore b/.gitignore index 2f66383..b3ec7d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,220 @@ -*.pyc -dist -build +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg MANIFEST -doc/_build -*~ -doc/*.zip \ No newline at end of file + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +*.lcov +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi/* +!.pixi/config.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule* +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ +# Temporary file for partial code execution +tempCodeRunnerFile.py + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 77f758f..0000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -# Config file for automatic testing at travis-ci.org - -language: python - -python: - - "2.6" - - "2.7" - - "3.2" - - "3.3" - - "3.4" - -env: - - NONUMPY=false - - NONUMPY=true - -install: - - if [[ $TRAVIS_PYTHON_VERSION != "3.2" ]]; then pip install sphinx; fi - - if $NONUMPY; then pip uninstall -y numpy; fi - - python setup.py install - - pip freeze - -script: - - nosetests -vv test_ad.py - - cd doc - - if [[ $TRAVIS_PYTHON_VERSION != "3.2" ]]; then make html; fi diff --git a/README b/README deleted file mode 100644 index 21cb8dd..0000000 --- a/README +++ /dev/null @@ -1,120 +0,0 @@ -``ad`` Package Documentation -============================ - -.. image:: https://travis-ci.org/tisimst/ad.png?branch=master - -Overview --------- - -The ``ad`` package allows you to **easily** and **transparently** perform -**first and second-order automatic differentiation**. Advanced math -involving trigonometric, logarithmic, hyperbolic, etc. functions can also -be evaluated directly using the ``admath`` sub-module. - -**All base numeric types are supported** (``int``, ``float``, ``complex``, -etc.). This package is designed so that the underlying numeric types will -interact with each other *as they normally do* when performing any -calculations. Thus, this package acts more like a "wrapper" that simply helps -keep track of derivatives while **maintaining the original functionality** of -the numeric calculations. - -From the Wikipedia entry on `Automatic differentiation`_ (AD): - - "AD exploits the fact that every computer program, no matter how - complicated, executes a sequence of elementary arithmetic operations - (addition, subtraction, multiplication, division, etc.) and elementary - functions (exp, log, sin, cos, etc.). By applying the chain rule - repeatedly to these operations, derivatives of arbitrary order can be - computed automatically, and accurate to working precision." - -See the `package documentation`_ for details and examples. - -Main Features -------------- - -- **Transparent calculations with derivatives: no or little - modification of existing code** is needed, including when using - the `Numpy`_ module. - -- **Almost all mathematical operations** are supported, including - functions from the standard math_ module (sin, cos, exp, erf, - etc.) and cmath_ module (phase, polar, etc.) with additional convenience - trigonometric, hyperbolic, and logarithmic functions (csc, acoth, ln, etc.). - Comparison operators follow the **same rules as the underlying numeric - types**. - -- **Real and complex** arithmetic handled seamlessly. Treat objects as you - normally would using the `math`_ and `cmath`_ functions, but with their new - ``admath`` counterparts. - -- **Automatic gradient and hessian function generator** for optimization - studies using `scipy.optimize`_ routines with ``gh(your_func_here)``. - -- **Compatible Linear Algebra Routines** in the ``ad.linalg`` submodule, - similar to those found in NumPy's ``linalg`` submodule, that are not - dependent on LAPACK. There are currently: - - a. Decompositions - - 1. ``chol``: Cholesky Decomposition - 2. ``lu``: LU Decomposition - 3. ``qr``: QR Decomposition - - b. Solving equations and inverting matrices - - 1. ``solve``: General solver for linear systems of equations - 2. ``lstsq``: Least-squares solver for linear systems of equations - 3. ``inv``: Solve for the (multiplicative) inverse of a matrix - -Installation ------------- - -You have several easy, convenient options to install the ``ad`` package -(administrative privileges may be required): - -1. Download the package files below, unzip to any directory, and run - ``python setup.py install`` from the command-line. - -2. Simply copy the unzipped ``ad-XYZ`` directory to any other location - that python can find it and rename it ``ad``. - -3. If ``setuptools`` is installed, run ``easy_install --upgrade ad`` - from the command-line. - -4. If ``pip`` is installed, run ``pip install --upgrade ad`` from the - command-line. - -5. Download the *bleeding-edge* version on GitHub_ - -Contact -------- - -Please send **feature requests, bug reports, or feedback** to -`Abraham Lee`_. - -Acknowledgements ----------------- - -The author expresses his thanks to : - -- `Eric O. LEBIGOT (EOL)`_, author of the `uncertainties`_ package, for providing - code insight and inspiration -- Stephen Marks, professor at Pomona College, for useful feedback concerning - the interface with optimization routines in ``scipy.optimize``. -- Wendell Smith, for updating testing functionality and numerous other useful - function updates -- Jonathan Terhorst, for catching a bug that made derivatives of logarithmic - functions (base != e) give the wrong answers. -- GitHub user ``fhgd`` for catching a mis-calculation in ``admath.atan2`` - - -.. _NumPy: http://numpy.scipy.org/ -.. _math: http://docs.python.org/library/math.html -.. _cmath: http://docs.python.org/library/cmath.html -.. _Automatic differentiation: http://en.wikipedia.org/wiki/Automatic_differentiation -.. _Eric O. LEBIGOT (EOL): http://www.linkedin.com/pub/eric-lebigot/22/293/277 -.. _uncertainties: http://pypi.python.org/pypi/uncertainties -.. _scipy.optimize: http://docs.scipy.org/doc/scipy/reference/optimize.html -.. _Abraham Lee: mailto:tisimst@gmail.com -.. _package documentation: http://pythonhosted.org/ad -.. _GitHub: https://github.com/tisimst/ad diff --git a/README.md b/README.md new file mode 100644 index 0000000..b1987f8 --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# ad + +**Fast, transparent first- and second-order automatic differentiation for Python** + +[![Tests](https://github.com/eggzec/ad/actions/workflows/code_test.yml/badge.svg)](https://github.com/eggzec/ad/actions/workflows/code_test.yml) +[![Documentation](https://github.com/eggzec/ad/actions/workflows/docs_build.yml/badge.svg)](https://github.com/eggzec/ad/actions/workflows/docs_build.yml) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) + +[![codecov](https://codecov.io/github/eggzec/ad/graph/badge.svg)](https://codecov.io/github/eggzec/ad) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=eggzec_ad&metric=alert_status)](https://sonarcloud.io/project/overview?id=eggzec_ad) +[![License: BSD-3](https://img.shields.io/badge/License-BSD--3-blue.svg)](LICENSE) + +[![PyPI Downloads](https://img.shields.io/pypi/dm/ad.svg?label=PyPI%20downloads)](https://pypi.org/project/ad/) +[![Python versions](https://img.shields.io/pypi/pyversions/ad.svg)](https://pypi.org/project/ad/) + +## Overview + +The `ad` package allows you to easily and transparently perform first- and +second-order automatic differentiation. Advanced math involving trigonometric, +logarithmic, hyperbolic, and related functions can be evaluated directly using +the `ad.admath` submodule. + +All base numeric types are supported (`int`, `float`, `complex`, etc.). The +package is designed so underlying numeric types interact as they normally do +during calculations. In practice, `ad` behaves like a lightweight wrapper that +tracks derivatives while preserving standard numeric behavior. + +From the Wikipedia entry on +[Automatic differentiation](http://en.wikipedia.org/wiki/Automatic_differentiation): + +> "AD exploits the fact that every computer program, no matter how +> complicated, executes a sequence of elementary arithmetic operations +> (addition, subtraction, multiplication, division, etc.) and elementary +> functions (exp, log, sin, cos, etc.). By applying the chain rule repeatedly +> to these operations, derivatives of arbitrary order can be computed +> automatically, and accurate to working precision." + +See the +[package documentation](http://pythonhosted.org/ad) +for details and examples. + +## Main Features + +- Transparent calculations with derivatives, requiring little or no + modification to existing code (including NumPy-based code). +- Broad mathematical operation support, including most functions from + [`math`](http://docs.python.org/library/math.html) and + [`cmath`](http://docs.python.org/library/cmath.html), plus convenience + trigonometric, hyperbolic, and logarithmic helpers (`csc`, `acoth`, `ln`, + etc.). Comparison operators follow the same rules as the wrapped numeric + values. +- Seamless real and complex arithmetic through `ad.admath` counterparts. +- Automatic gradient and Hessian function generator for optimization workflows + with `scipy.optimize` via `gh(your_func_here)`. +- Linear algebra routines in `ad.linalg` similar to NumPy's `linalg`, without + LAPACK dependency. + +### Linear Algebra Routines + +**Decompositions** + +- `chol`: Cholesky decomposition +- `lu`: LU decomposition +- `qr`: QR decomposition + +**Solving equations and matrix inversion** + +- `solve`: General solver for linear systems +- `lstsq`: Least-squares solver for linear systems +- `inv`: Multiplicative inverse of a matrix + + +## Installation + +```bash +uv pip install ad +``` + +Requires Python 3.10+. See the +[full installation guide](https://eggzec.github.io/ad/installation/). + +## Documentation + +- [Theory](https://eggzec.github.io/ad/theory/) — mathematical background, hierarchical basis, algorithms +- [Quickstart](https://eggzec.github.io/ad/quickstart/) — runnable examples +- [API Reference](https://eggzec.github.io/ad/api/) — class and function signatures +- [References](https://eggzec.github.io/ad/references/) — literature citations + +## License + +BSD-3-Clause. diff --git a/ad/__init__.py b/ad/__init__.py index d49e01b..db04736 100644 --- a/ad/__init__.py +++ b/ad/__init__.py @@ -1,1222 +1,998 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Apr 11 12:52:09 2013 - -@author: tisimst -""" -import math -import cmath -import copy -from random import randint -from numbers import Number - -try: - import numpy - numpy_installed = True -except ImportError: - numpy_installed = False - -__version_info__ = (1, 3, 2) -__version__ = '.'.join(list(map(str, __version_info__))) - -__author__ = 'Abraham Lee' - -__all__ = ['adnumber', 'gh', 'jacobian'] - -CONSTANT_TYPES = Number - -def to_auto_diff(x): - """ - Transforms x into a automatically differentiated function (ADF), - unless it is already an ADF (or a subclass of it), in which case x is - returned unchanged. - - Raises an exception unless 'x' belongs to some specific classes of - objects that are known not to depend on ADF objects (which then cannot be - considered as constants). - """ - - if isinstance(x, ADF): - return x - - #! In Python 2.6+, numbers.Number could be used instead, here: - if isinstance(x, CONSTANT_TYPES): - # constants have no derivatives to define: - return ADF(x, {}, {}, {}) - - raise NotImplementedError( - 'Automatic differentiation not yet supported for {0:} objects'.format( - type(x)) - ) - -def _apply_chain_rule(ad_funcs, variables, lc_wrt_args, qc_wrt_args, - cp_wrt_args): - """ - This function applies the first and second-order chain rule to calculate - the derivatives with respect to original variables (i.e., objects created - with the ``adnumber(...)`` constructor). - - For reference: - - ``lc`` refers to "linear coefficients" or first-order terms - - ``qc`` refers to "quadratic coefficients" or pure second-order terms - - ``cp`` refers to "cross-product" second-order terms - - """ - num_funcs = len(ad_funcs) - - # Initial value (is updated below): - lc_wrt_vars = dict((var, 0.) for var in variables) - qc_wrt_vars = dict((var, 0.) for var in variables) - cp_wrt_vars = {} - for i,var1 in enumerate(variables): - for j,var2 in enumerate(variables): - if i1: - tmp = 2*cp_wrt_args*ad_funcs[0].d(var1)*ad_funcs[1].d(var1) - qc_wrt_vars[var1] += tmp - - elif j1: - tmp = cp_wrt_args*(ad_funcs[0].d(var1)*ad_funcs[1].d(var2) + \ - ad_funcs[0].d(var2)*ad_funcs[1].d(var1)) - cp_wrt_vars[(var1, var2)] += tmp - - return (lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - -def _floor(x): - """ - Return the floor of x as a float, the largest integer value less than or - equal to x. This is required for the "mod" function. - """ - if isinstance(x,ADF): - ad_funcs = [to_auto_diff(x)] - - x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: - f = _floor(x) - - ######################################## - - variables = ad_funcs[0]._get_variables(ad_funcs) - - if not variables or isinstance(f, bool): - return f - - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - - lc_wrt_args = [0.0] - qc_wrt_args = [0.0] - cp_wrt_args = 0.0 - - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. - - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: - return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - else: - return math.floor(x) - -class ADF(object): - """ - The ADF (Automatically Differentiated Function) class contains derivative - information about the results of a previous operation on any two objects - where at least one is an ADF or ADV object. - - An ADF object has class members '_lc', '_qc', and '_cp' to contain - first-order derivatives, second-order derivatives, and cross-product - derivatives, respectively, of all ADV objects in the ADF's lineage. When - requesting a cross-product term, either order of objects may be used since, - mathematically, they are equivalent. - - For example, if z = z(x, y), then:: - - 2 2 - d z d z - ----- = ----- - dx dy dy dx - - - Example - ------- - Initialize some ADV objects (tag not required, but useful):: - - >>> x = adnumber(1, tag='x') - >>> y = adnumber(2, tag='y') - - Now some basic math, showing the derivatives of the final result. Note that - if we don't supply an input to the derivative methods, a dictionary with - all derivatives wrt the subsequently used ADV objects is returned:: - - >>> z = x + y - >>> z.d() - {ad(1.0, x): 1.0, ad(2.0, y): 1.0} - >>> z.d2() - {ad(1.0, x): 0.0, ad(2.0, y): 0.0} - >>> z.d2c() - {(ad(1.0, x), ad(2.0, y)): 0.0} - - Let's take it a step further now and see if relationships hold:: - - >>> w = x*z # same as x*(x+y) = x**2 + x*y - >>> w.d(x) # dw/dx = 2*x+y = 2*(1) + (2) = 4 - 4.0 - >>> w.d2(x) # d2w/dx2 = 2 - 2.0 - >>> w.d2(y) # d2w/dy2 = 0 - 0.0 - >>> w.d2c(x, y) # d2w/dxdy = 1 - 1.0 - - For convenience, we can get the gradient and hessian if we supply the order - of the variables (useful in optimization routines):: - - >>> w.gradient([x, y]) - [4.0, 1.0] - >>> w.hessian([x, y]) - [[2.0, 1.0], [1.0, 0.0]] - - You'll note that these are constructed using lists and nested lists instead - of depending on numpy arrays, though if numpy is installed, they can look - much nicer and are a little easier to work with:: - - >>> import numpy as np - >>> np.array(w.hessian([x, y])) - array([[ 2., 1.], - [ 1., 0.]]) - - """ - - def __init__(self, value, lc, qc, cp, tag=None): - # I want to be able to perform complex derivatives, so "x" will - # assume whatever type of object is put into it. - self.x = value - self._lc = lc - self._qc = qc - self._cp = cp - self.tag = tag - - def __hash__(self): - return id(self) - - def trace_me(self): - """ - Make this object traceable in future derivative calculations (not - retroactive). - - Caution - ------- - When using ADF (i.e. dependent variable) objects as input to the - derivative class methods, the returning value may only be useful - with the ``d(...)`` and ``d2(...)`` methods. - - DO NOT MIX ADV AND ADF OBJECTS AS INPUTS TO THE ``d2c(...)`` METHOD - SINCE THE RESULT IS NOT LIKELY TO BE NUMERICALLY MEANINGFUL :) - - Example - ------- - :: - - >>> x = adnumber(2.1) - >>> y = x**2 - >>> y.d(y) # Dependent variables by default aren't traced - 0.0 - - # Initialize tracing - >>> y.trace_me() - >>> y.d(y) # Now we get an answer! - 1.0 - >>> z = 2*y/y**2 - >>> z.d(y) # Would have been 0.0 before trace activiation - -0.10283780934898525 - - # Check the chain rule - >>> z.d(y)*y.d(x) == z.d(x) # dz/dy * dy/dx == dz/dx - True - - """ - if self not in self._lc: - self._lc[self] = 1.0 - self._qc[self] = 0.0 - - @property - def real(self): - return self.x.real - - @property - def imag(self): - return self.x.imag - - def _to_general_representation(self, str_func): - """ - This provides the general representation of the underlying numeric - object, but assumes self.tag is a string object. - """ - if self.tag is None: - return 'ad({0:})'.format(str_func(self.x)) - else: - return 'ad({0:}, {1:})'.format(str_func(self.x), str(self.tag)) - - def __repr__(self): - return self._to_general_representation(repr) - - def __str__(self): - return self._to_general_representation(str) - - def d(self, x=None): - """ - Returns first derivative with respect to x (an AD object). - - Optional - -------- - x : AD object - Technically this can be any object, but to make it practically - useful, ``x`` should be a single object created using the - ``adnumber(...)`` constructor. If ``x=None``, then all associated - first derivatives are returned in the form of a ``dict`` object. - - Returns - ------- - df/dx : scalar - The derivative (if it exists), otherwise, zero. - - Examples - -------- - :: - >>> x = adnumber(2) - >>> y = 3 - >>> z = x**y - - >>> z.d() - {ad(2): 12.0} - - >>> z.d(x) - 12.0 - - >>> z.d(y) # derivative wrt y is zero since it's not an AD object - 0.0 - - See Also - -------- - d2, d2c, gradient, hessian - - """ - if x is not None: - if isinstance(x, ADF): - try: - tmp = self._lc[x] - except KeyError: - tmp = 0.0 - return tmp if tmp.imag else tmp.real - else: - return 0.0 - else: - return self._lc - - def d2(self, x=None): - """ - Returns pure second derivative with respect to x (an AD object). - - Optional - -------- - x : AD object - Technically this can be any object, but to make it practically - useful, ``x`` should be a single object created using the - ``adnumber(...)`` constructor. If ``x=None``, then all associated - second derivatives are returned in the form of a ``dict`` object. - - Returns - ------- - d2f/dx2 : scalar - The pure second derivative (if it exists), otherwise, zero. - - Examples - -------- - :: - >>> x = adnumber(2.5) - >>> y = 3 - >>> z = x**y - - >>> z.d2() - {ad(2): 15.0} - - >>> z.d2(x) - 15.0 - - >>> z.d2(y) # second deriv wrt y is zero since not an AD object - 0.0 - - See Also - -------- - d, d2c, gradient, hessian - - """ - if x is not None: - if isinstance(x, ADF): - try: - tmp = self._qc[x] - except KeyError: - tmp = 0.0 - return tmp if tmp.imag else tmp.real - else: - return 0.0 - else: - return self._qc - - def d2c(self, x=None, y=None): - """ - Returns cross-product second derivative with respect to two objects, x - and y (preferrably AD objects). If both inputs are ``None``, then a dict - containing all cross-product second derivatives is returned. This is - one-way only (i.e., if f = f(x, y) then **either** d2f/dxdy or d2f/dydx - will be in that dictionary and NOT BOTH). - - If only one of the inputs is ``None`` or if the cross-product - derivative doesn't exist, then zero is returned. - - If x and y are the same object, then the pure second-order derivative - is returned. - - Optional - -------- - x : AD object - Technically this can be any object, but to make it practically - useful, ``x`` should be a single object created using the - ``adnumber(...)`` constructor. - y : AD object - Same as ``x``. - - Returns - ------- - d2f/dxdy : scalar - The pure second derivative (if it exists), otherwise, zero. - - Examples - -------- - :: - >>> x = adnumber(2.5) - >>> y = adnumber(3) - >>> z = x**y - - >>> z.d2c() - {(ad(2.5), ad(3)): 33.06704268553368} - - >>> z.d2c(x, y) # either input order gives same result - 33.06704268553368 - - >>> z.d2c(y, y) # pure second deriv wrt y - 0.8395887053184748 - - See Also - -------- - d, d2, gradient, hessian - - """ - if (x is not None) and (y is not None): - if x is y: - tmp = self.d2(x) - else: - if isinstance(x, ADF) and isinstance(y, ADF): - try: - tmp = self._cp[(x, y)] - except KeyError: - try: - tmp = self._cp[(y, x)] - except KeyError: - tmp = 0.0 - else: - tmp = 0.0 - - return tmp if tmp.imag else tmp.real - - elif ((x is not None) and not (y is not None)) or \ - ((y is not None) and not (x is not None)): - return 0.0 - else: - return self._cp - - def gradient(self, variables): - """ - Returns the gradient, or Jacobian, (array of partial derivatives) of the - AD object given some input variables. The order of the inputs - determines the order of the returned list of values:: - - f.gradient([y, x, z]) --> [df/dy, df/dx, df/dz] - - Parameters - ---------- - variables : array-like - An array of objects (they don't have to be AD objects). If a partial - derivative doesn't exist, then zero will be returned. If a single - object is input, a single derivative will be returned as a list. - - Returns - ------- - grad : list - An list of partial derivatives - - Example - ------- - :: - >>> x = adnumber(2) - >>> y = adnumber(0.5) - >>> z = x**y - >>> z.gradient([x, y]) - [0.3535533905932738, 0.9802581434685472] - - >>> z.gradient([x, 3, 0.4, y, -19]) - [0.9802581434685472, 0.0, 0.0, 0.3535533905932738, 0.0] - - See Also - -------- - hessian, d, d2, d2c - """ - try: - grad = [self.d(v) for v in variables] - except TypeError: - grad = [self.d(variables)] - return grad - - def hessian(self, variables): - """ - Returns the hessian (2-d array of second partial derivatives) of the AD - object given some input variables. The output order is determined by the - input order:: - - f.hessian([y, x, z]) --> [[d2f/dy2, d2f/dydx, d2f/dydz], - [d2f/dxdy, d2f/dx2, d2f/dxdz], - [d2f/dzdy, d2f/dzdx, d2f/dz2]] - - Parameters - ---------- - variables : array-like - An array of objects (they don't have to be AD objects). If a partial - derivative doesn't exist, the result of that item is zero as - expected. If a single object is input, a single second derivative - will be returned as a nested list. - - Returns - ------- - hess : 2d-list - An nested list of second partial derivatives (pure and - cross-product) - - Example - ------- - :: - >>> x = adnumber(2) - >>> y = adnumber(0.5) - >>> z = x**y - - >>> z.hessian([x, y]) - [[-0.08838835, 1.33381153], - [ 1.33381153, 0.48045301]] - - >>> z.hessian([y, 3, 0.4, x, -19]) - - [[ 0.48045301, 0. , 0. , 1.33381153, 0. ], - [ 0. , 0. , 0. , 0. , 0. ], - [ 0. , 0. , 0. , 0. , 0. ], - [ 1.33381153, 0. , 0. , -0.08838835, 0. ], - [ 0. , 0. , 0. , 0. , 0. ]] - - See Also - -------- - gradient, d, d2, d2c - """ - try: - hess = [] - for v1 in variables: - hess.append([self.d2c(v1,v2) for v2 in variables]) - except TypeError: - hess = [[self.d2(variables)]] - return hess - - def sqrt(self): - """ - A convenience function equal to x**0.5. This is required for some - ``numpy`` functions like ``numpy.sqrt``, ``numpy.std``, etc. - """ - return self**0.5 - - def _get_variables(self, ad_funcs): - # List of involved variables (ADV objects): - variables = set() - for expr in ad_funcs: - variables |= set(expr._lc) - return variables - - def __add__(self, val): - ad_funcs = [self, to_auto_diff(val)] # list(map(to_auto_diff, (self, val))) - - x = ad_funcs[0].x - y = ad_funcs[1].x - - ######################################## - # Nominal value of the constructed ADF: - f = x + y - - ######################################## - variables = self._get_variables(ad_funcs) - - if not variables or isinstance(f, bool): - return f - - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - lc_wrt_args = [1., 1.] - qc_wrt_args = [0., 0.] - cp_wrt_args = 0. - - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. - lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( - ad_funcs, variables, lc_wrt_args, - qc_wrt_args, cp_wrt_args) - - # The function now returns an ADF object: - return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - - def __radd__(self, val): - """ - This method shouldn't need any modification if __add__ has - been defined - """ - return self + val - - def __mul__(self, val): - ad_funcs = [self, to_auto_diff(val)] # list(map(to_auto_diff, (self, val))) - - x = ad_funcs[0].x - y = ad_funcs[1].x - - ######################################## - # Nominal value of the constructed ADF: - f = x*y - - ######################################## - - variables = self._get_variables(ad_funcs) - - if not variables or isinstance(f, bool): - return f - - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - lc_wrt_args = [y, x] - qc_wrt_args = [0., 0.] - cp_wrt_args = 1. - - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. - lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( - ad_funcs, variables, lc_wrt_args, - qc_wrt_args, cp_wrt_args) - - - # The function now returns an ADF object: - return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - - def __rmul__(self, val): - """ - This method shouldn't need any modification if __mul__ has - been defined - """ - return self*val - - def __div__(self, val): - return self.__truediv__(val) - - def __truediv__(self, val): - ad_funcs = [self, to_auto_diff(val)] # list(map(to_auto_diff, (self, val))) - - x = ad_funcs[0].x - y = ad_funcs[1].x - - ######################################## - # Nominal value of the constructed ADF: - f = x/y - - ######################################## - - variables = self._get_variables(ad_funcs) - - if not variables or isinstance(f, bool): - return f - - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - lc_wrt_args = [1./y, -x/y**2] - qc_wrt_args = [0., 2*x/y**3] - cp_wrt_args = -1./y**2 - - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. - lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( - ad_funcs, variables, lc_wrt_args, - qc_wrt_args, cp_wrt_args) - - - # The function now returns an ADF object: - return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - - - def __rdiv__(self, val): - """ - This method shouldn't need any modification if __pow__ and __mul__ have - been defined - """ - return val*self**(-1) - - def __rtruediv__(self, val): - """ - This method shouldn't need any modification if __pow__ and __mul__ have - been defined - """ - return val*self**(-1) - - def __sub__(self, val): - """ - This method shouldn't need any modification if __add__ and __mul__ have - been defined - """ - return self + (-1*val) - - def __rsub__(self, val): - """ - This method shouldn't need any modification if __add__ and __mul__ have - been defined - """ - return -1*self + val - - def __pow__(self, val): - ad_funcs = [self, to_auto_diff(val)] # list(map(to_auto_diff, (self, val))) - - x = ad_funcs[0].x - y = ad_funcs[1].x - - ######################################## - # Nominal value of the constructed ADF: - f = x**y - - ######################################## - - variables = self._get_variables(ad_funcs) - - if not variables or isinstance(f, bool): - return f - - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - - if x.imag or y.imag: - if abs(x)>0 and ad_funcs[1].d(ad_funcs[1])!=0: - lc_wrt_args = [y*x**(y - 1), x**y*cmath.log(x)] - qc_wrt_args = [y*(y - 1)*x**(y - 2), x**y*(cmath.log(x))**2] - cp_wrt_args = x**(y - 1)*(y*cmath.log(x) + 1)/x - else: - lc_wrt_args = [y*x**(y - 1), 0.] - qc_wrt_args = [y*(y - 1)*x**(y - 2), 0.] - cp_wrt_args = 0. - else: - x = x.real - y = y.real - if x>0: - lc_wrt_args = [y*x**(y - 1), x**y*math.log(x)] - qc_wrt_args = [y*(y - 1)*x**(y - 2), x**y*(math.log(x))**2] - cp_wrt_args = x**y*(y*math.log(x) + 1)/x - else: - lc_wrt_args = [y*x**(y - 1), 0.] - qc_wrt_args = [y*(y - 1)*x**(y - 2), 0.] - cp_wrt_args = 0. - - - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. - lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( - ad_funcs, variables, lc_wrt_args, - qc_wrt_args, cp_wrt_args) - - - # The function now returns an ADF object: - return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - - def __rpow__(self,val): - return to_auto_diff(val)**self - - def __mod__(self, val): - return self - val*_floor(self/val) - - def __rmod__(self, val): - return val - self*_floor(val/self) - - def __neg__(self): - return -1*self - - def __pos__(self): - return self - - def __invert__(self): - return -(self+1) - - def __abs__(self): - ad_funcs = [self] # list(map(to_auto_diff, [self])) - - x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: - f = abs(x) - - ######################################## - - variables = self._get_variables(ad_funcs) - - if not variables or isinstance(f, bool): - return f - - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - - # catch the x=0 exception - try: - lc_wrt_args = [x/abs(x)] - except ZeroDivisionError: - lc_wrt_args = [0.0] - - qc_wrt_args = [0.0] - cp_wrt_args = 0.0 - - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. - lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( - ad_funcs, variables, lc_wrt_args, - qc_wrt_args, cp_wrt_args) - - - # The function now returns an ADF object: - return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - - def toInt(self): - """ - Converts the base number to an ``int`` object - """ - self.x = int(self.x) - return self - - def toFloat(self): - """ - Converts the base number to an ``float`` object - """ - self.x = float(self.x) - return self - - def toComplex(self): - """ - Converts the base number to an ``complex`` object - """ - self.x = complex(self.x) - return self - - # coercion follows the capabilities of the respective input types - def __int__(self): - return int(self.x) - - def __float__(self): - return float(self.x) - - def __complex__(self): - return complex(self.x) - - # let the respective numeric types take care of the comparison operators - def __eq__(self, val): - ad_funcs = [self, to_auto_diff(val)] # list(map(to_auto_diff, [self, val])) - return ad_funcs[0].x==ad_funcs[1].x - - def __ne__(self, val): - return not self==val - - def __lt__(self, val): - ad_funcs = [self, to_auto_diff(val)] # list(map(to_auto_diff, [self, val])) - return ad_funcs[0].xad_funcs[1].x - return not self<=val - - def __ge__(self, val): - return (self>val) or (self==val) - - def __nonzero__(self): - return type(self.x).__nonzero__(self.x) - -class ADV(ADF): - """ - A convenience class for distinguishing between FUNCTIONS (ADF) and VARIABLES - """ - def __init__(self, value, tag=None): - # The first derivative of a variable wrt itself is always 1.0 and - # the second is always 0.0 - super(ADV, self).__init__(value, {self:1.0}, {self:0.0}, {}, tag=tag) - -def adnumber(x, tag=None): - """ - Constructor of automatic differentiation (AD) variables, or numbers that - keep track of the derivatives of subsequent calculations. - - Parameters - ---------- - x : scalar or array-like - The nominal value(s) of the variable(s). Any numeric type or array is - supported. If ``x`` is another AD object, a fresh copy is returned that - contains all the derivatives of ``x``, but is not related to ``x`` in - any way. - - Optional - -------- - tag : str - A string identifier. If an array of values for ``x`` is input, the tag - applies to all the new AD objects. - - Returns - ------- - x_ad : an AD object - - Examples - -------- - - Creating an AD object (any numeric type can be input--int, float, complex, - etc.):: - - >>> from ad import adnumber - >>> x = adnumber(2) - >>> x - ad(2.0) - >>> x.d(x) # the derivative wrt itself is always 1.0 - 1.0 - - >>> y = adnumber(0.5, 'y') # tags are nice for tracking AD variables - >>> y - ad(0.5, y) - - Let's do some math:: - - >>> x*y - ad(1.0) - >>> x/y - ad(4.0) - - >>> z = x**y - >>> z - ad(1.41421356237) - - >>> z.d(x) - 0.3535533905932738 - >>> z.d2(x) - -0.08838834764831845 - >>> z.d2c(x, y) # z.d2c(y, x) returns the same - 1.333811534061821 - >>> z.d2c(y, y) # equivalent to z.d2(y) - 0.4804530139182014 - - # only derivatives wrt original variables are tracked, thus the - # derivative of z wrt itself is zero - >>> z.d(z) - 0.0 - - We can also use the exponential, logarithm, and trigonometric functions:: - - >>> from ad.admath import * # sin, exp, etc. math funcs - >>> z = sqrt(x)*sin(erf(y)/3) - >>> z - ad(0.24413683610889056) - >>> z.d() - {ad(0.5, y): 0.4080425982773223, ad(2.0): 0.06103420902722264} - >>> z.d2() - {ad(0.5, y): -0.42899113441354375, ad(2.0): -0.01525855225680566} - >>> z.d2c() - {(ad(0.5, y), ad(2.0)): 0.10201064956933058} - - We can also initialize multiple AD objects in the same constructor by - supplying a sequence of values--the ``tag`` keyword is applied to all the - new objects:: - - >>> x, y, z = adnumber([2, 0.5, (1+3j)], tag='group1') - >>> z - ad((1+3j), group1) - - If ``numpy`` is installed, the returned array can be converted to a - ``numpy.ndarray`` using the ``numpy.array(...)`` constructor:: - - >>> import numpy as np - >>> x = np.array(adnumber([2, 0.5, (1+3j)]) - - From here, many ``numpy`` operations can be performed (i.e., sum, max, - etc.), though I haven't performed extensive testing to know which functions - won't work. - - """ - try: - # If the input is a numpy array, return a numpy array, otherwise try to - # match the input type (numpy arrays are constructed differently using - # numpy.array(...) and the actual class type, numpy.ndarray(...), so we - # needed an exception). Other iterable types may need exceptions, but - # this should always work for list and tuple objects at least. - - if numpy_installed and isinstance(x, numpy.ndarray): - return numpy.array([adnumber(xi, tag) for xi in x]) - elif isinstance(x, (tuple, list)): - return type(x)([adnumber(xi, tag) for xi in x]) - else: - raise TypeError - - except TypeError: - if isinstance(x, ADF): - cp = copy.deepcopy(x) - return cp - elif isinstance(x, CONSTANT_TYPES): - return ADV(x, tag) - - raise NotImplementedError( - 'Automatic differentiation not yet supported for {0:} objects'.format( - type(x)) - ) - -adfloat = adnumber # for backwards compatibility - -def gh(func): - """ - Generates gradient (g) and hessian (h) functions of the input function - using automatic differentiation. This is primarily for use in conjunction - with the scipy.optimize package, though certainly not restricted there. - - NOTE: If NumPy is installed, the returned object from ``grad`` and ``hess`` - will be a NumPy array. Otherwise, a generic list (or nested list, for - ``hess``) will be returned. - - Parameters - ---------- - func : function - This function should be composed of pure python mathematics (i.e., it - shouldn't be used for calling an external executable since AD doesn't - work for that). - - Returns - ------- - grad : function - The AD-compatible gradient function of ``func`` - hess : function - The AD-compatible hessian function of ``func`` - - Examples - -------- - :: - - >>> def my_cool_function(x): - ... return (x[0]-10.0)**2 + (x[1]+5.0)**2 - ... - >>> grad, hess = gh(my_cool_function) - >>> x = [24, 17] - >>> grad(x) - [28.0, 44.0] - >>> hess(x) - [[2.0, 0.0], [0.0, 2.0]] - - >>> import numpy as np - >>> x_arr = np.array(x) - >>> grad(x_arr) - array([ 28., 44.]) - >>> hess(x_arr) - array([[ 2., 0.], - [ 0., 2.]]) - - """ - def grad(x, *args): - xa = adnumber(x) - if numpy_installed and isinstance(x, numpy.ndarray): - ans = func(xa, *args) - if isinstance(ans, numpy.ndarray): - return numpy.array(ans[0].gradient(list(xa))) - else: - return numpy.array(ans.gradient(list(xa))) - else: - try: - # first see if the input is an array-like object (list or tuple) - return func(xa, *args).gradient(xa) - except TypeError: - # if it's a scalar, then update to a list for the gradient call - return func(xa, *args).gradient([xa]) - - def hess(x, *args): - xa = adnumber(x) - if numpy_installed and isinstance(x, numpy.ndarray): - ans = func(xa, *args) - if isinstance(ans, numpy.ndarray): - return numpy.array(ans[0].hessian(list(xa))) - else: - return numpy.array(ans.hessian(list(xa))) - else: - try: - # first see if the input is an array-like object (list or tuple) - return func(xa, *args).hessian(xa) - except TypeError: - # if it's a scalar, then update to a list for the hessian call - return func(xa, *args).hessian([xa]) - - # customize the documentation with the input function name - for f, name in zip([grad, hess], ['gradient', 'hessian']): - f.__doc__ = 'The %s of %s, '%(name, func.__name__) - f.__doc__ += 'calculated using automatic\ndifferentiation.\n\n' - if func.__doc__ is not None and isinstance(func.__doc__, str): - f.__doc__ += 'Original documentation:\n'+func.__doc__ - - return grad, hess - -def jacobian(adfuns, advars): - """ - Calculate the Jacobian matrix - - Parameters - ---------- - adfuns : array - An array of AD objects (best when they are DEPENDENT AD variables). - advars : array - An array of AD objects (best when they are INDEPENDENT AD variables). - - Returns - ------- - jac : 2d-array - Each row is the gradient of each ``adfun`` with respect to each - ``advar``, all in the order specified for both. - - Example - ------- - :: - - >>> x, y, z = adnumber([1.0, 2.0, 3.0]) - >>> u, v, w = x + y + z, x*y/z, (z - x)**y - >>> jacobian([u, v, w], [x, y, z]) - [[ 1.0 , 1.0 , 1.0 ], - [ 0.666666, 0.333333, -0.222222], - [ -4.0 , 2.772589, 4.0 ]] - - """ - # Test the dependent variables to see if an array is given - try: - adfuns[0] - except (TypeError, AttributeError): # if only one dependent given - adfuns = [adfuns] - - # Test the independent variables to see if an array is given - try: - advars[0] - except (TypeError, AttributeError): - advars = [advars] - - # Now, loop through each dependent variable, iterating over the independent - # variables, collecting each derivative, if it exists - jac = [] - for adfun in adfuns: - if hasattr(adfun, 'gradient'): - jac.append(adfun.gradient(advars)) - else: - jac.append([0.0]*len(advars)) - - return jac - -if numpy_installed: - def d(a, b, out=None): - """ - Take a derivative of a with respect to b. - - This is a numpy ufunc, so the derivative will be broadcast over both a and b. - - a: scalar or array over which to take the derivative - b: scalar or array of variable(s) to take the derivative with respect to - - >>> x = adnumber(3) - >>> y = x**2 - >>> d(y, x) - array(6.0, dtype=object) - - >>> import numpy as np - >>> from ad.admath import exp - >>> x = adnumber(np.linspace(0,2,5)) - >>> y = x**2 - >>> d(y, x) - array([0.0, 1.0, 2.0, 3.0, 4.0], dtype=object) - """ - it = numpy.nditer([a, b, out], - flags = ['buffered', 'refs_ok'], - op_flags = [['readonly'], ['readonly'], - ['writeonly', 'allocate', 'no_broadcast']]) - for y, x, deriv in it: - (v1,), (v2,) = y.flat, x.flat - deriv[...] = v1.d(v2) - return it.operands[2] - - def d2(a, b, out=None): - """ - Take a second derivative of a with respect to b. - - This is a numpy ufunc, so the derivative will be broadcast over both a and b. - - See d() and adnumber.d2() for more details. - """ - it = numpy.nditer([a, b, out], - flags = ['buffered', 'refs_ok'], - op_flags = [['readonly'], ['readonly'], - ['writeonly', 'allocate', 'no_broadcast']]) - for y, x, deriv in it: - (v1,), (v2,) = y.flat, x.flat - deriv[...] = v1.d2(v2) - return it.operands[2] +""" +Created on Thu Apr 11 12:52:09 2013 + +@author: tisimst +""" + +import cmath +import copy +import math +from numbers import Number + + +try: + import numpy + + numpy_installed = True +except ImportError: + numpy_installed = False + +__version_info__ = (1, 3, 2) +__version__ = ".".join(list(map(str, __version_info__))) + +__author__ = "Abraham Lee" + +__all__ = ["adnumber", "gh", "jacobian"] + +CONSTANT_TYPES = Number + + +def to_auto_diff(x: object) -> "ADF": + """ + Transform ``x`` into an automatically differentiated function (ADF). + + If ``x`` is already an ADF (or a subclass), it is returned unchanged. + + Raises an exception unless ``x`` belongs to some specific classes of + objects that are known not to depend on ADF objects (which then cannot be + considered as constants). + + Returns + ------- + ADF + ``x`` wrapped as an ADF (or returned as-is if already one). + """ + if isinstance(x, ADF): + return x + + if isinstance(x, CONSTANT_TYPES): + return ADF(x, {}, {}, {}) + + raise NotImplementedError( + f"Automatic differentiation not yet supported for {type(x)} objects" + ) + + +def _apply_chain_rule( + ad_funcs: list, + variables: set, + lc_wrt_args: list, + qc_wrt_args: list, + cp_wrt_args: float, +) -> tuple[dict, dict, dict]: + """ + Apply the first and second-order chain rule. + + Calculates the derivatives with respect to original variables (i.e., + objects created with the ``adnumber(...)`` constructor). + + For reference: + + - ``lc`` refers to "linear coefficients" or first-order terms + - ``qc`` refers to "quadratic coefficients" or pure second-order terms + - ``cp`` refers to "cross-product" second-order terms + + Returns + ------- + tuple[dict, dict, dict] + Tuple ``(lc_wrt_vars, qc_wrt_vars, cp_wrt_vars)`` of derivative maps. + """ + num_funcs = len(ad_funcs) + + lc_wrt_vars = dict((var, 0.0) for var in variables) + qc_wrt_vars = dict((var, 0.0) for var in variables) + cp_wrt_vars = {} + for i, var1 in enumerate(variables): + for j, var2 in enumerate(variables): + if i < j: + cp_wrt_vars[var1, var2] = 0.0 + + for j, var1 in enumerate(variables): + for k, var2 in enumerate(variables): + for f, dh, d2h in zip( + ad_funcs, lc_wrt_args, qc_wrt_args, strict=False + ): + if j == k: + fdv1 = f.d(var1) + lc_wrt_vars[var1] += dh * fdv1 + + qc_wrt_vars[var1] += dh * f.d2(var1) + d2h * fdv1**2 + + elif j < k: + tmp = dh * f.d2c(var1, var2) + d2h * f.d(var1) * f.d(var2) + cp_wrt_vars[var1, var2] += tmp + + if j == k and num_funcs > 1: + tmp = ( + 2 * cp_wrt_args * ad_funcs[0].d(var1) * ad_funcs[1].d(var1) + ) + qc_wrt_vars[var1] += tmp + + elif j < k and num_funcs > 1: + tmp = cp_wrt_args * ( + ad_funcs[0].d(var1) * ad_funcs[1].d(var2) + + ad_funcs[0].d(var2) * ad_funcs[1].d(var1) + ) + cp_wrt_vars[var1, var2] += tmp + + return (lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) + + +def _floor(x: object) -> float: + """ + Return the floor of ``x`` as a float. + + The largest integer value less than or equal to ``x``. This is required + for the ``mod`` function. + + Returns + ------- + float + Floor value of ``x`` (or an :class:`ADF` when ``x`` is one). + """ + if isinstance(x, ADF): + ad_funcs = [to_auto_diff(x)] + + x = ad_funcs[0].x + + f = _floor(x) + + variables = ad_funcs[0]._get_variables(ad_funcs) + + if not variables or isinstance(f, bool): + return f + + lc_wrt_args = [0.0] + qc_wrt_args = [0.0] + cp_wrt_args = 0.0 + + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) + + return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) + return math.floor(x) + + +class _ADFArithmetic: + """ + Mixin providing the arithmetic dunder methods for :class:`ADF`. + + Splitting these into a mixin keeps the :class:`ADF` class itself within + the public-method limit while preserving the public API. + """ + + def __add__(self, val: object) -> "ADF": + ad_funcs = [self, to_auto_diff(val)] + + x = ad_funcs[0].x + y = ad_funcs[1].x + + f = x + y + + variables = self._get_variables(ad_funcs) + + if not variables or isinstance(f, bool): + return f + + lc_wrt_args = [1.0, 1.0] + qc_wrt_args = [0.0, 0.0] + cp_wrt_args = 0.0 + + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) + + return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) + + def __radd__(self, val: object) -> "ADF": + """Return ``val + self``. + + Returns + ------- + ADF + Result of ``val + self``. + """ + return self + val + + def __mul__(self, val: object) -> "ADF": + ad_funcs = [self, to_auto_diff(val)] + + x = ad_funcs[0].x + y = ad_funcs[1].x + + f = x * y + + variables = self._get_variables(ad_funcs) + + if not variables or isinstance(f, bool): + return f + + lc_wrt_args = [y, x] + qc_wrt_args = [0.0, 0.0] + cp_wrt_args = 1.0 + + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) + + return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) + + def __rmul__(self, val: object) -> "ADF": + """Return ``val * self``. + + Returns + ------- + ADF + Result of ``val * self``. + """ + return self * val + + def __truediv__(self, val: object) -> "ADF": + ad_funcs = [self, to_auto_diff(val)] + + x = ad_funcs[0].x + y = ad_funcs[1].x + + f = x / y + + variables = self._get_variables(ad_funcs) + + if not variables or isinstance(f, bool): + return f + + lc_wrt_args = [1.0 / y, -x / y**2] + qc_wrt_args = [0.0, 2 * x / y**3] + cp_wrt_args = -1.0 / y**2 + + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) + + return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) + + def __rtruediv__(self, val: object) -> "ADF": + """Return ``val / self``. + + Returns + ------- + ADF + Result of ``val * self ** -1``. + """ + return val * self ** (-1) + + def __sub__(self, val: object) -> "ADF": + """Return ``self - val``. + + Returns + ------- + ADF + Result of ``self + (-1 * val)``. + """ + return self + (-1 * val) + + def __rsub__(self, val: object) -> "ADF": + """Return ``val - self``. + + Returns + ------- + ADF + Result of ``-1 * self + val``. + """ + return -1 * self + val + + def __pow__(self, val: object) -> "ADF": + ad_funcs = [self, to_auto_diff(val)] + + x = ad_funcs[0].x + y = ad_funcs[1].x + + f = x**y + + variables = self._get_variables(ad_funcs) + + if not variables or isinstance(f, bool): + return f + + if x.imag or y.imag: + if abs(x) > 0 and ad_funcs[1].d(ad_funcs[1]) != 0: + lc_wrt_args = [y * x ** (y - 1), x**y * cmath.log(x)] + qc_wrt_args = [ + y * (y - 1) * x ** (y - 2), + x**y * (cmath.log(x)) ** 2, + ] + cp_wrt_args = x ** (y - 1) * (y * cmath.log(x) + 1) / x + else: + lc_wrt_args = [y * x ** (y - 1), 0.0] + qc_wrt_args = [y * (y - 1) * x ** (y - 2), 0.0] + cp_wrt_args = 0.0 + else: + x = x.real + y = y.real + if x > 0: + lc_wrt_args = [y * x ** (y - 1), x**y * math.log(x)] + qc_wrt_args = [ + y * (y - 1) * x ** (y - 2), + x**y * (math.log(x)) ** 2, + ] + cp_wrt_args = x**y * (y * math.log(x) + 1) / x + else: + lc_wrt_args = [y * x ** (y - 1), 0.0] + qc_wrt_args = [y * (y - 1) * x ** (y - 2), 0.0] + cp_wrt_args = 0.0 + + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) + + return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) + + def __rpow__(self, val: object) -> "ADF": + return to_auto_diff(val) ** self + + def __mod__(self, val: object) -> "ADF": + return self - val * _floor(self / val) + + def __rmod__(self, val: object) -> "ADF": + return val - self * _floor(val / self) + + def __neg__(self) -> "ADF": + return -1 * self + + def __pos__(self) -> "ADF": + return self + + def __invert__(self) -> "ADF": + return -(self + 1) + + def __abs__(self) -> "ADF": + ad_funcs = [self] + + x = ad_funcs[0].x + + f = abs(x) + + variables = self._get_variables(ad_funcs) + + if not variables or isinstance(f, bool): + return f + + try: + lc_wrt_args = [x / abs(x)] + except ZeroDivisionError: + lc_wrt_args = [0.0] + + qc_wrt_args = [0.0] + cp_wrt_args = 0.0 + + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) + + return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) + + +class _ADFComparison: + """ + Mixin providing the comparison dunder methods for :class:`ADF`. + """ + + __hash__ = object.__hash__ + + def __eq__(self, val: object) -> bool: + ad_funcs = [self, to_auto_diff(val)] + return ad_funcs[0].x == ad_funcs[1].x + + def __ne__(self, val: object) -> bool: + return not self == val + + def __lt__(self, val: object) -> bool: + ad_funcs = [self, to_auto_diff(val)] + return ad_funcs[0].x < ad_funcs[1].x + + def __le__(self, val: object) -> bool: + return (self < val) or (self == val) + + def __gt__(self, val: object) -> bool: + return not self <= val + + def __ge__(self, val: object) -> bool: + return (self > val) or (self == val) + + +class ADF(_ADFArithmetic, _ADFComparison): + """ + Automatically Differentiated Function (ADF). + + The ADF class contains derivative information about the results of a + previous operation on any two objects where at least one is an ADF or + ADV object. + + An ADF object has class members ``_lc``, ``_qc``, and ``_cp`` to contain + first-order derivatives, second-order derivatives, and cross-product + derivatives, respectively, of all ADV objects in the ADF's lineage. When + requesting a cross-product term, either order of objects may be used, + since mathematically they are equivalent. + + For example, if ``z = z(x, y)``, then:: + + 2 2 + d z d z + ----- = ----- + dx dy dy dx + + + Example + ------- + Initialize some ADV objects (tag not required, but useful):: + + >>> x = adnumber(1, tag='x') + >>> y = adnumber(2, tag='y') + + Now some basic math, showing the derivatives of the final result:: + + >>> z = x + y + >>> z.d() + {ad(1.0, x): 1.0, ad(2.0, y): 1.0} + >>> z.d2() + {ad(1.0, x): 0.0, ad(2.0, y): 0.0} + >>> z.d2c() + {(ad(1.0, x), ad(2.0, y)): 0.0} + + """ + + def __init__( + self, + value: object, + lc: dict, + qc: dict, + cp: dict, + tag: str | None = None, + ) -> None: + self.x = value + self._lc = lc + self._qc = qc + self._cp = cp + self.tag = tag + + def __hash__(self) -> int: + return id(self) + + def trace_me(self) -> None: + """ + Make this object traceable in future derivative calculations. + + The change is not retroactive. + + Caution + ------- + When using ADF (i.e. dependent variable) objects as input to the + derivative class methods, the returning value may only be useful with + the ``d(...)`` and ``d2(...)`` methods. + + DO NOT MIX ADV AND ADF OBJECTS AS INPUTS TO THE ``d2c(...)`` METHOD + SINCE THE RESULT IS NOT LIKELY TO BE NUMERICALLY MEANINGFUL. + + Example + ------- + :: + + >>> x = adnumber(2.1) + >>> y = x**2 + >>> y.d(y) + 0.0 + + >>> y.trace_me() + >>> y.d(y) + 1.0 + """ + if self not in self._lc: + self._lc[self] = 1.0 + self._qc[self] = 0.0 + + @property + def real(self) -> float: + """Return the real component of the underlying value. + + Returns + ------- + float + Real component of ``self.x``. + """ + return self.x.real + + @property + def imag(self) -> float: + """Return the imaginary component of the underlying value. + + Returns + ------- + float + Imaginary component of ``self.x``. + """ + return self.x.imag + + def _to_general_representation(self, str_func: object) -> str: + """ + Provide the general representation of the underlying numeric object. + + Assumes ``self.tag`` is a string object. + + Returns + ------- + str + String representation produced via ``str_func``. + """ + if self.tag is None: + return f"ad({str_func(self.x)})" + return f"ad({str_func(self.x)}, {self.tag!s})" + + def __repr__(self) -> str: + return self._to_general_representation(repr) + + def __str__(self) -> str: + return self._to_general_representation(str) + + def d(self, x: object = None) -> object: + """ + Return first derivative with respect to ``x`` (an AD object). + + Parameters + ---------- + x : AD object, optional + ``x`` should be a single object created using the + ``adnumber(...)`` constructor. If ``x=None``, then all associated + first derivatives are returned in the form of a ``dict``. + + Returns + ------- + df/dx : scalar or dict + The derivative (if it exists), otherwise zero. When ``x`` is + ``None``, returns the full derivative dictionary. + """ + if x is not None: + if isinstance(x, ADF): + try: + tmp = self._lc[x] + except KeyError: + tmp = 0.0 + return tmp if tmp.imag else tmp.real + return 0.0 + return self._lc + + def d2(self, x: object = None) -> object: + """ + Return pure second derivative with respect to ``x`` (an AD object). + + Parameters + ---------- + x : AD object, optional + ``x`` should be a single object created using the + ``adnumber(...)`` constructor. If ``x=None``, then all associated + second derivatives are returned in the form of a ``dict``. + + Returns + ------- + d2f/dx2 : scalar or dict + The pure second derivative (if it exists), otherwise zero. When + ``x`` is ``None``, returns the full second-derivative dictionary. + """ + if x is not None: + if isinstance(x, ADF): + try: + tmp = self._qc[x] + except KeyError: + tmp = 0.0 + return tmp if tmp.imag else tmp.real + return 0.0 + return self._qc + + def d2c(self, x: object = None, y: object = None) -> object: + """ + Return cross-product second derivative with respect to ``x`` and ``y``. + + If both inputs are ``None``, then a dict containing all cross-product + second derivatives is returned. This is one-way only (i.e., if + ``f = f(x, y)`` then either ``d2f/dxdy`` or ``d2f/dydx`` will be in + that dictionary and **not** both). + + If only one of the inputs is ``None`` or if the cross-product + derivative doesn't exist, then zero is returned. + + If ``x`` and ``y`` are the same object, then the pure second-order + derivative is returned. + + Parameters + ---------- + x : AD object, optional + First differentiation variable. + y : AD object, optional + Second differentiation variable. + + Returns + ------- + d2f/dxdy : scalar or dict + The cross-product derivative (if it exists), otherwise zero. When + both inputs are ``None``, the full cross-product dictionary is + returned. + """ + if (x is not None) and (y is not None): + if x is y: + tmp = self.d2(x) + elif isinstance(x, ADF) and isinstance(y, ADF): + try: + tmp = self._cp[x, y] + except KeyError: + try: + tmp = self._cp[y, x] + except KeyError: + tmp = 0.0 + else: + tmp = 0.0 + + return tmp if tmp.imag else tmp.real + + if ((x is not None) and not (y is not None)) or ( + (y is not None) and not (x is not None) + ): + return 0.0 + return self._cp + + def gradient(self, variables: object) -> list: + """ + Return the gradient of the AD object given some input variables. + + The order of the inputs determines the order of the returned list of + values:: + + f.gradient([y, x, z]) --> [df/dy, df/dx, df/dz] + + Parameters + ---------- + variables : array-like + An array of objects (they don't have to be AD objects). If a + partial derivative doesn't exist, then zero will be returned. If + a single object is input, a single derivative will be returned as + a list. + + Returns + ------- + grad : list + A list of partial derivatives. + """ + try: + grad = [self.d(v) for v in variables] + except TypeError: + grad = [self.d(variables)] + return grad + + def hessian(self, variables: object) -> list: + """ + Return the Hessian of the AD object given some input variables. + + The output order is determined by the input order:: + + f.hessian([y, x, z]) --> [[d2f/dy2, d2f/dydx, d2f/dydz], + [d2f/dxdy, d2f/dx2, d2f/dxdz], + [d2f/dzdy, d2f/dzdx, d2f/dz2]] + + Parameters + ---------- + variables : array-like + An array of objects (they don't have to be AD objects). + + Returns + ------- + hess : 2d-list + A nested list of second partial derivatives (pure and + cross-product). + """ + try: + hess = [] + for v1 in variables: + hess.append([self.d2c(v1, v2) for v2 in variables]) + except TypeError: + hess = [[self.d2(variables)]] + return hess + + def sqrt(self) -> "ADF": + """ + Return the square root of ``self``. + + A convenience function equal to ``self ** 0.5``. Required for some + ``numpy`` functions like ``numpy.sqrt`` and ``numpy.std``. + + Returns + ------- + ADF + ``self ** 0.5``. + """ + return self**0.5 + + @staticmethod + def _get_variables(ad_funcs: list) -> set: + """Collect ADV objects involved in ``ad_funcs``. + + Parameters + ---------- + ad_funcs : list + List of ADF or ADV objects. + + Returns + ------- + set + Union of all variables referenced from each expression. + """ + variables = set() + for expr in ad_funcs: + variables |= set(expr._lc) + return variables + + def to_int(self) -> "ADF": + """ + Convert the base number to an ``int`` object. + + Returns + ------- + ADF + ``self`` after coercing ``self.x`` to ``int``. + """ + self.x = int(self.x) + return self + + def to_float(self) -> "ADF": + """ + Convert the base number to a ``float`` object. + + Returns + ------- + ADF + ``self`` after coercing ``self.x`` to ``float``. + """ + self.x = float(self.x) + return self + + def to_complex(self) -> "ADF": + """ + Convert the base number to a ``complex`` object. + + Returns + ------- + ADF + ``self`` after coercing ``self.x`` to ``complex``. + """ + self.x = complex(self.x) + return self + + def __int__(self) -> int: + return int(self.x) + + def __float__(self) -> float: + return float(self.x) + + def __complex__(self) -> complex: + return complex(self.x) + + def __bool__(self) -> bool: + return bool(self.x) + + +class ADV(ADF): + """ + A convenience class distinguishing FUNCTIONS (ADF) from VARIABLES. + """ + + def __init__(self, value: object, tag: str | None = None) -> None: + super().__init__(value, {self: 1.0}, {self: 0.0}, {}, tag=tag) + + +def adnumber(x: object, tag: str | None = None) -> object: + """ + Construct an automatic differentiation (AD) variable. + + Numbers that keep track of the derivatives of subsequent calculations. + + Parameters + ---------- + x : scalar or array-like + The nominal value(s) of the variable(s). Any numeric type or array is + supported. If ``x`` is another AD object, a fresh copy is returned + that contains all the derivatives of ``x``, but is not related to + ``x`` in any way. + tag : str, optional + A string identifier. If an array of values for ``x`` is input, the + tag applies to all the new AD objects. + + Returns + ------- + x_ad : an AD object + Newly constructed AD variable, or an array of them when ``x`` is + array-like. + """ + if numpy_installed and isinstance(x, numpy.ndarray): + return numpy.array([adnumber(xi, tag) for xi in x]) + if isinstance(x, (tuple, list)): + return type(x)([adnumber(xi, tag) for xi in x]) + + if isinstance(x, ADF): + return copy.deepcopy(x) + if isinstance(x, CONSTANT_TYPES): + return ADV(x, tag) + + raise NotImplementedError( + f"Automatic differentiation not yet supported for {type(x)} objects" + ) + + +adfloat = adnumber + + +def gh(func: object) -> tuple[object, object]: + """ + Generate gradient (g) and hessian (h) functions of an input function. + + Uses automatic differentiation. This is primarily for use in conjunction + with the ``scipy.optimize`` package, though certainly not restricted + there. + + NOTE: If NumPy is installed, the returned object from ``grad`` and + ``hess`` will be a NumPy array. Otherwise, a generic list (or nested + list, for ``hess``) will be returned. + + Parameters + ---------- + func : callable + This function should be composed of pure python mathematics (i.e., + it shouldn't be used for calling an external executable since AD + doesn't work for that). + + Returns + ------- + grad : callable + The AD-compatible gradient function of ``func``. + hess : callable + The AD-compatible hessian function of ``func``. + """ + + def grad(x: object, *args: object) -> object: + xa = adnumber(x) + if numpy_installed and isinstance(x, numpy.ndarray): + ans = func(xa, *args) + if isinstance(ans, numpy.ndarray): + return numpy.array(ans[0].gradient(list(xa))) + return numpy.array(ans.gradient(list(xa))) + try: + return func(xa, *args).gradient(xa) + except TypeError: + return func(xa, *args).gradient([xa]) + + def hess(x: object, *args: object) -> object: + xa = adnumber(x) + if numpy_installed and isinstance(x, numpy.ndarray): + ans = func(xa, *args) + if isinstance(ans, numpy.ndarray): + return numpy.array(ans[0].hessian(list(xa))) + return numpy.array(ans.hessian(list(xa))) + try: + return func(xa, *args).hessian(xa) + except TypeError: + return func(xa, *args).hessian([xa]) + + for f, name in zip([grad, hess], ["gradient", "hessian"], strict=False): + f.__doc__ = f"The {name} of {func.__name__}, " + f.__doc__ += "calculated using automatic\ndifferentiation.\n\n" + if func.__doc__ is not None and isinstance(func.__doc__, str): + f.__doc__ += "Original documentation:\n" + func.__doc__ + + return grad, hess + + +def jacobian(adfuns: object, advars: object) -> list: + """ + Calculate the Jacobian matrix. + + Parameters + ---------- + adfuns : array + An array of AD objects (best when they are DEPENDENT AD variables). + advars : array + An array of AD objects (best when they are INDEPENDENT AD + variables). + + Returns + ------- + jac : 2d-array + Each row is the gradient of each ``adfun`` with respect to each + ``advar``, all in the order specified for both. + """ + try: + adfuns[0] + except (TypeError, AttributeError): + adfuns = [adfuns] + + try: + advars[0] + except (TypeError, AttributeError): + advars = [advars] + + jac = [] + for adfun in adfuns: + if hasattr(adfun, "gradient"): + jac.append(adfun.gradient(advars)) + else: + jac.append([0.0] * len(advars)) + + return jac + + +if numpy_installed: + + def d(a: object, b: object, out: object = None) -> object: + """ + Take a derivative of ``a`` with respect to ``b``. + + This is a numpy ufunc, so the derivative will be broadcast over both + ``a`` and ``b``. + + Parameters + ---------- + a : scalar or array + Value(s) over which to take the derivative. + b : scalar or array + Variable(s) to take the derivative with respect to. + out : array, optional + Output array. + + Returns + ------- + derivative : ndarray + Derivative array broadcast over ``a`` and ``b``. + """ + it = numpy.nditer( + [a, b, out], + flags=["buffered", "refs_ok"], + op_flags=[ + ["readonly"], + ["readonly"], + ["writeonly", "allocate", "no_broadcast"], + ], + ) + for y, x, deriv in it: + (v1,), (v2,) = y.flat, x.flat + deriv[...] = v1.d(v2) + return it.operands[2] + + def d2(a: object, b: object, out: object = None) -> object: + """ + Take a second derivative of ``a`` with respect to ``b``. + + This is a numpy ufunc, so the derivative will be broadcast over both + ``a`` and ``b``. + + Parameters + ---------- + a : scalar or array + Value(s) over which to take the second derivative. + b : scalar or array + Variable(s) to take the second derivative with respect to. + out : array, optional + Output array. + + Returns + ------- + second_derivative : ndarray + Second derivative array broadcast over ``a`` and ``b``. + """ + it = numpy.nditer( + [a, b, out], + flags=["buffered", "refs_ok"], + op_flags=[ + ["readonly"], + ["readonly"], + ["writeonly", "allocate", "no_broadcast"], + ], + ) + for y, x, deriv in it: + (v1,), (v2,) = y.flat, x.flat + deriv[...] = v1.d2(v2) + return it.operands[2] diff --git a/ad/admath/__init__.py b/ad/admath/__init__.py index 13dfe36..b8b3616 100644 --- a/ad/admath/__init__.py +++ b/ad/admath/__init__.py @@ -1,6 +1,6 @@ """ ================================================================================ -ad: Fast, transparent calculations of first and second-order automatic +ad: Fast, transparent calculations of first and second-order automatic differentiation ================================================================================ @@ -9,4 +9,113 @@ """ -from .admath import * +from .admath import ( + acos, + acosh, + acot, + acoth, + acsc, + acsch, + asec, + asech, + asin, + asinh, + atan, + atan2, + atanh, + ceil, + cos, + cosh, + cot, + coth, + csc, + csch, + degrees, + e, + erf, + erfc, + exp, + expm1, + fabs, + factorial, + floor, + gamma, + hypot, + isinf, + isnan, + lgamma, + ln, + log, + log1p, + log10, + phase, + pi, + polar, + power, + radians, + rect, + sec, + sech, + sin, + sinh, + sqrt, + tan, + tanh, + trunc, +) + + +__all__ = [ + "acos", + "acosh", + "acot", + "acoth", + "acsc", + "acsch", + "asec", + "asech", + "asin", + "asinh", + "atan", + "atan2", + "atanh", + "ceil", + "cos", + "cosh", + "cot", + "coth", + "csc", + "csch", + "degrees", + "e", + "erf", + "erfc", + "exp", + "expm1", + "fabs", + "factorial", + "floor", + "gamma", + "hypot", + "isinf", + "isnan", + "lgamma", + "ln", + "log", + "log1p", + "log10", + "phase", + "pi", + "polar", + "power", + "radians", + "rect", + "sec", + "sech", + "sin", + "sinh", + "sqrt", + "tan", + "tanh", + "trunc", +] diff --git a/ad/admath/admath.py b/ad/admath/admath.py index cc90bc7..472f179 100644 --- a/ad/admath/admath.py +++ b/ad/admath/admath.py @@ -1,39 +1,15 @@ -# -*- coding: utf-8 -*- """ Mathematical operations that generalize many operations from the standard math and cmath modules so that they also track first and second derivatives. The basic philosophy of order of type-operations is this: -A. Is X from the ADF class or subclass? +A. Is X from the ADF class or subclass? 1. Yes - Perform automatic differentiation. 2. No - Is X an array object? a. Yes - Vectorize the operation and repeat at A for each item. - b. No - Let the math/cmath function deal with X since it's probably a base - numeric type. Otherwise they will throw the respective exceptions. - -Examples: - - from admath import sin - - # Manipulation of numbers that track derivatives: - x = ad.adnumber(3) - print sin(x) # prints ad(0.1411200080598672) - - # The admath functions also work on regular Python numeric types: - print sin(3) # prints 0.1411200080598672. This is a normal Python float. - -Importing all the functions from this module into the global namespace -is possible. This is encouraged when using a Python shell as a -calculator. Example: - - import ad - from ad.admath import * # Imports tan(), etc. - - x = ad.adnumber(3) - print tan(x) # tan() is the ad.admath.tan function - -The numbers with derivative tracking handled by this module are objects from -the ad (automatic differentiation) module, from either the ADV or the ADF class. + b. No - Let the math/cmath function deal with X since it's probably a + base numeric type. Otherwise they will throw the respective + exceptions. (c) 2013 by Abraham Lee . Please send feature requests, bug reports, or feedback to this address. @@ -43,70 +19,76 @@ author. """ -from __future__ import division -import math + import cmath -from ad import __author__, ADF, to_auto_diff, _apply_chain_rule - -try: - import numpy as np -except ImportError: - numpy_installed = False -else: - numpy_installed = True - - def return_numpy_array_if_given(func): - def wrapped_func(*args): - if len(args)==1: - ans = func(*args) - if isinstance(args[0], np.ndarray): - ans = np.array(ans) - else: - same_arg_lengths = True - for arg1 in args[:-1]: - for arg2 in args[1:]: - if len(arg1)!=len(arg2): - same_arg_lengths = False - break - if same_arg_lengths: - ans = [func(*arg) for arg in args] - if any([isinstance(arg, np.ndarray) for arg in args]): - ans = np.array(ans) - else: - raise ValueError('Input arguments not the same length') - return ans - - wrapped_func.__name__ = func.__name__ - wrapped_func.__module__ = func.__module__ - if func.__doc__ is not None and isinstance(func.__doc__, str): - wrapped_func.__doc__ = func.__doc__ - - return wrapped_func - - -__all__ = [ - # math/cmath module equivalent functions - 'sin', 'asin', 'sinh', 'asinh', - 'cos', 'acos', 'cosh', 'acosh', - 'tan', 'atan', 'atan2', 'tanh', 'atanh', - 'e', 'pi', - 'isinf', 'isnan', - 'phase', 'polar', 'rect', - 'exp', 'expm1', - 'erf', 'erfc', - 'factorial', 'gamma', 'lgamma', - 'log', 'ln', 'log10', 'log1p', - 'sqrt', 'hypot', 'pow', - 'degrees', 'radians', - 'ceil', 'floor', 'trunc', 'fabs', - # other miscellaneous functions that are conveniently defined - 'csc', 'acsc', 'csch', 'acsch', - 'sec', 'asec', 'sech', 'asech', - 'cot', 'acot', 'coth', 'acoth' - ] +import importlib.util +import math +from collections.abc import Callable +from typing import TypeVar + +from ad import ADF, _apply_chain_rule, to_auto_diff + + +numpy_installed = importlib.util.find_spec("numpy") is not None + - -### FUNCTIONS IN THE MATH MODULE ############################################## +__all__ = [ + "acos", + "acosh", + "acot", + "acoth", + "acsc", + "acsch", + "asec", + "asech", + "asin", + "asinh", + "atan", + "atan2", + "atanh", + "ceil", + "cos", + "cosh", + "cot", + "coth", + "csc", + "csch", + "degrees", + "e", + "erf", + "erfc", + "exp", + "expm1", + "fabs", + "factorial", + "floor", + "gamma", + "hypot", + "isinf", + "isnan", + "lgamma", + "ln", + "log", + "log1p", + "log10", + "phase", + "pi", + "polar", + "power", + "radians", + "rect", + "sec", + "sech", + "sin", + "sinh", + "sqrt", + "tan", + "tanh", + "trunc", +] + + +# FUNCTIONS IN THE MATH MODULE ############################################## # # Currently, there is no implementation for the following math module methods: # - copysign @@ -124,1464 +106,1354 @@ def wrapped_func(*args): e = math.e pi = math.pi -# for convenience, (though totally defeating the purpose of having an -# automatic differentiation class, define a fourth-order finite difference -# derivative for when an analytical derivative doesn't exist -eps = 1e-8 # arbitrarily chosen -def _fourth_order_first_fd(func,x): - fm2 = func(x-2*eps) - fm1 = func(x-eps) - fp1 = func(x+eps) - fp2 = func(x+2*eps) - return (fm2-8*fm1+8*fp1-fp2)/12/eps - -def _fourth_order_second_fd(func,x): - fm2 = func(x-2*eps) - fm1 = func(x-eps) - f = func(x) - fp1 = func(x+eps) - fp2 = func(x+2*eps) - return (-fm2+16*fm1-30*f+16*fp1-fp2)/12/eps**2 - -def _vectorize(func): - """ - Make a function that accepts 1 or 2 arguments work with input arrays (of - length m) in the following array length combinations: - - - m x m - - 1 x m - - m x 1 - - 1 x 1 - """ - def vectorized_function(*args, **kwargs): - if len(args)==1: +eps = 1e-8 + +_HALF = 0.5 +_PAIR = 2 + +F = TypeVar("F", bound=Callable[..., object]) + + +def _fourth_order_first_fd(func: Callable[[float], float], x: float) -> float: + """Compute the fourth-order accurate first finite-difference derivative. + + Parameters + ---------- + func : Callable[[float], float] + Scalar function to differentiate. + x : float + Point at which to evaluate the derivative. + + Returns + ------- + float + Fourth-order accurate finite-difference derivative of ``func`` at + ``x``. + """ + fm2 = func(x - 2 * eps) + fm1 = func(x - eps) + fp1 = func(x + eps) + fp2 = func(x + 2 * eps) + return (fm2 - 8 * fm1 + 8 * fp1 - fp2) / 12 / eps + + +def _fourth_order_second_fd(func: Callable[[float], float], x: float) -> float: + """Compute the fourth-order accurate second finite-difference derivative. + + Parameters + ---------- + func : Callable[[float], float] + Scalar function to differentiate twice. + x : float + Point at which to evaluate the second derivative. + + Returns + ------- + float + Fourth-order accurate finite-difference second derivative of ``func`` + at ``x``. + """ + fm2 = func(x - 2 * eps) + fm1 = func(x - eps) + f = func(x) + fp1 = func(x + eps) + fp2 = func(x + 2 * eps) + return (-fm2 + 16 * fm1 - 30 * f + 16 * fp1 - fp2) / 12 / eps**2 + + +def _vectorize(func: F) -> F: + """ + Make a function that accepts 1 or 2 arguments work with input arrays. + + The supported input array length combinations are: + + - ``m x m`` + - ``1 x m`` + - ``m x 1`` + - ``1 x 1`` + + Parameters + ---------- + func : F + Scalar function to vectorize. + + Returns + ------- + F + Wrapped version of ``func`` that broadcasts over array-like inputs. + """ + + def _apply_two_arg(x: object, y: object, **kwargs: object) -> object: + try: + return [ + vectorized_function(xi, yi, **kwargs) + for xi, yi in zip(x, y, strict=False) + ] + except TypeError: + pass + try: + return [vectorized_function(xi, y, **kwargs) for xi in x] + except TypeError: + pass + try: + return [vectorized_function(x, yi, **kwargs) for yi in y] + except TypeError: + return func(x, y, **kwargs) + + def vectorized_function(*args: object, **kwargs: object) -> object: + if len(args) == 1: x = args[0] try: return [vectorized_function(xi, **kwargs) for xi in x] except TypeError: return func(x, **kwargs) - - elif len(args)==2: + + if len(args) == _PAIR: x, y = args - try: - return [vectorized_function(xi, yi, **kwargs) - for xi, yi in zip(x, y)] - except TypeError: - try: - return [vectorized_function(xi, y, **kwargs) for xi in x] - except TypeError: - try: - return [vectorized_function(x, yi, **kwargs) for yi in y] - except TypeError: - return func(x, y, **kwargs) - + return _apply_two_arg(x, y, **kwargs) + return None + n = func.__name__ m = func.__module__ d = func.__doc__ - + vectorized_function.__name__ = n vectorized_function.__module__ = m - doc = 'Vectorized {0:} function\n'.format(n) + doc = f"Vectorized {n} function\n" if d is not None: doc += d vectorized_function.__doc__ = doc - + return vectorized_function - + + @_vectorize -def acos(x): +def acos(x: float) -> float: """ Return the arc cosine of x, in radians. + + Returns + ------- + float + Arc cosine of ``x`` in radians (or an :class:`ad.ADF` if ``x`` is + one). """ - if isinstance(x,ADF): - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = acos(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - - lc_wrt_args = [-1/sqrt(1-x**2)] - qc_wrt_args = [x/(sqrt(1 - x**2)*(x**2 - 1))] + lc_wrt_args = [-1 / sqrt(1 - x**2)] + qc_wrt_args = [x / (sqrt(1 - x**2) * (x**2 - 1))] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. - + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [acos(xi) for xi in x] -# except TypeError: - if x.imag: - return cmath.acos(x) - else: - return math.acos(x.real) + if x.imag: + return cmath.acos(x) + return math.acos(x.real) + @_vectorize -def acosh(x): +def acosh(x: float) -> float: """ Return the inverse hyperbolic cosine of x. + + Returns + ------- + float + Inverse hyperbolic cosine of ``x`` (or an :class:`ad.ADF` if ``x`` is + one). """ - if isinstance(x,ADF): - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = acosh(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - - lc_wrt_args = [1/sqrt(x**2 - 1)] - qc_wrt_args = [-x/(x**2 - 1)**1.5] + lc_wrt_args = [1 / sqrt(x**2 - 1)] + qc_wrt_args = [-x / (x**2 - 1) ** 1.5] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [acosh(xi) for xi in x] -# except TypeError: - if x.imag: - return cmath.acosh(x) - else: - return math.acosh(x.real) + if x.imag: + return cmath.acosh(x) + return math.acosh(x.real) + @_vectorize -def asin(x): +def asin(x: float) -> float: """ Return the arc sine of x, in radians. + + Returns + ------- + float + Arc sine of ``x`` in radians (or an :class:`ad.ADF` if ``x`` is one). """ - if isinstance(x,ADF): - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = asin(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - - lc_wrt_args = [1/sqrt(1 - x**2)] - qc_wrt_args = [-x/(sqrt(1 - x**2)*(x**2 - 1))] + lc_wrt_args = [1 / sqrt(1 - x**2)] + qc_wrt_args = [-x / (sqrt(1 - x**2) * (x**2 - 1))] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [asin(xi) for xi in x] -# except TypeError: - if x.imag: - return cmath.asin(x) - else: - return math.asin(x.real) + if x.imag: + return cmath.asin(x) + return math.asin(x.real) + @_vectorize -def asinh(x): +def asinh(x: float) -> float: """ Return the inverse hyperbolic sine of x. + + Returns + ------- + float + Inverse hyperbolic sine of ``x`` (or an :class:`ad.ADF` if ``x`` is + one). """ - if isinstance(x,ADF): - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = asinh(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - - lc_wrt_args = [1/sqrt(x**2 + 1)] - qc_wrt_args = [-x/(x**2 + 1)**1.5] + lc_wrt_args = [1 / sqrt(x**2 + 1)] + qc_wrt_args = [-x / (x**2 + 1) ** 1.5] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [asinh(xi) for xi in x] -# except TypeError: - if x.imag: - return cmath.asinh(x) - else: - return math.asinh(x.real) + if x.imag: + return cmath.asinh(x) + return math.asinh(x.real) + @_vectorize -def atan(x): +def atan(x: float) -> float: """ - Return the arc tangent of x, in radians + Return the arc tangent of x, in radians. + + Returns + ------- + float + Arc tangent of ``x`` in radians (or an :class:`ad.ADF` if ``x`` is + one). """ - if isinstance(x,ADF): - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = atan(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - - lc_wrt_args = [1/(x**2 + 1)] - qc_wrt_args = [-2*x/(x**4 + 2*x**2 + 1)] + lc_wrt_args = [1 / (x**2 + 1)] + qc_wrt_args = [-2 * x / (x**4 + 2 * x**2 + 1)] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [atan(xi) for xi in x] -# except TypeError: - if x.imag: - return cmath.atan(x) - else: - return math.atan(x.real) + if x.imag: + return cmath.atan(x) + return math.atan(x.real) + @_vectorize -def atan2(y, x): - """ - Return ``atan(y / x)``, in radians. The result is between ``-pi`` and - ``pi``. The vector in the plane from the origin to point ``(x, y)`` makes - this angle with the positive X axis. The point of ``atan2()`` is that the - signs of both inputs are known to it, so it can compute the correct - quadrant for the angle. For example, ``atan(1)`` and ``atan2(1, 1)`` are - both ``pi/4``, but ``atan2(-1, -1)`` is ``-3*pi/4``. - """ - if x>0: - return atan(y/x) - elif x<0: - if y>=0: - return atan(y/x) + pi - else: - return atan(y/x) - pi - else: - if y>0: - return +pi/2 - elif y<0: - return -pi/2 - else: - return 0.0 - +def atan2(y: float, x: float) -> float: + """ + Return ``atan(y / x)``, in radians. + + The result is between ``-pi`` and ``pi``. + + Returns + ------- + float + Two-argument arc tangent of ``y`` and ``x`` in radians. + """ + if x > 0: + return atan(y / x) + if x < 0: + if y >= 0: + return atan(y / x) + pi + return atan(y / x) - pi + if y > 0: + return +pi / 2 + if y < 0: + return -pi / 2 + return 0.0 + + @_vectorize -def atanh(x): +def atanh(x: float) -> float: """ Return the inverse hyperbolic tangent of x. + + Returns + ------- + float + Inverse hyperbolic tangent of ``x`` (or an :class:`ad.ADF` if ``x`` + is one). """ - if isinstance(x,ADF): - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = atanh(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - - lc_wrt_args = [-1./(x**2 - 1)] - qc_wrt_args = [2*x/(x**4 - 2*x**2 + 1)] + lc_wrt_args = [-1.0 / (x**2 - 1)] + qc_wrt_args = [2 * x / (x**4 - 2 * x**2 + 1)] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [atanh(xi) for xi in x] -# except TypeError: - if x.imag: - return cmath.atanh(x) - else: - return math.atanh(x.real) + if x.imag: + return cmath.atanh(x) + return math.atanh(x.real) + @_vectorize -def isinf(x): +def isinf(x: float) -> bool: """ - Return True if the real or the imaginary part of x is positive or negative - infinity. + Return True if the real or imaginary part of x is infinite. + + Returns + ------- + bool + ``True`` if any part of ``x`` is positive or negative infinity. """ -# try: -# return [isinf(xi) for xi in x] -# except TypeError: if isinstance(x, ADF): return isinf(x.x) - else: - if x.imag: - return cmath.isinf(x) - else: - return math.isinf(x.real) - + if x.imag: + return cmath.isinf(x) + return math.isinf(x.real) + + @_vectorize -def isnan(x): +def isnan(x: float) -> bool: """ Return True if the real or imaginary part of x is not a number (NaN). + + Returns + ------- + bool + ``True`` if any part of ``x`` is NaN. """ -# try: -# return [isnan(xi) for xi in x] -# except TypeError: if isinstance(x, ADF): return isnan(x.x) - else: - if x.imag: - return cmath.isnan(x) - else: - return math.isnan(x.real) - + if x.imag: + return cmath.isnan(x) + return math.isnan(x.real) + + @_vectorize -def phase(x): +def phase(x: complex) -> float: """ Return the phase of x (also known as the argument of x). + + Returns + ------- + float + Argument of ``x`` in radians. """ -# try: -# return [atan2(xi.imag, xi.real) for xi in x] -# except TypeError: return atan2(x.imag, x.real) + @_vectorize -def polar(x): +def polar(x: complex) -> tuple[float, float]: """ Return the representation of x in polar coordinates. + + Returns + ------- + tuple[float, float] + Pair ``(r, phi)`` of magnitude and phase. """ return (abs(x), phase(x)) + @_vectorize -def rect(r, phi): +def rect(r: float, phi: float) -> complex: """ Return the complex number x with polar coordinates r and phi. + + Returns + ------- + complex + Complex value at the given polar coordinates. """ -# try: -# return [ri*(cos(phi_i) + sin(phi_i)*1j) for ri, phi_i in zip(r, phi)] -# except TypeError: -# try: -# return [ri*(cos(phi) + sin(phi)*1j) for ri in r] -# except TypeError: -# try: -# return [r*(cos(phi_i) + sin(phi_i)*1j) for phi_i in phi] -# except TypeError: - return r*(cos(phi) + sin(phi)*1j) + return r * (cos(phi) + sin(phi) * 1j) + @_vectorize -def ceil(x): +def ceil(x: float) -> float: """ - Return the ceiling of x as a float, the smallest integer value greater than - or equal to x. + Return the ceiling of x as a float. + + Returns + ------- + float + Smallest integer value greater than or equal to ``x``. """ - if isinstance(x,ADF): - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = ceil(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - lc_wrt_args = [0.0] qc_wrt_args = [0.0] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [ceil(xi) for xi in x] -# except TypeError: - return math.ceil(x) + return math.ceil(x) + @_vectorize -def cos(x): +def cos(x: float) -> float: """ Return the cosine of x radians. + + Returns + ------- + float + Cosine of ``x`` (or an :class:`ad.ADF` if ``x`` is one). """ - if isinstance(x,ADF): - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = cos(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - lc_wrt_args = [-sin(x)] qc_wrt_args = [-cos(x)] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [cos(xi) for xi in x] -# except TypeError: - if x.imag: - return cmath.cos(x) - else: - return math.cos(x.real) + if x.imag: + return cmath.cos(x) + return math.cos(x.real) + @_vectorize -def cosh(x): +def cosh(x: float) -> float: """ Return the hyperbolic cosine of x. + + Returns + ------- + float + Hyperbolic cosine of ``x`` (or an :class:`ad.ADF` if ``x`` is one). """ - if isinstance(x,ADF): - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = cosh(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - lc_wrt_args = [sinh(x)] qc_wrt_args = [cosh(x)] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [cosh(xi) for xi in x] -# except TypeError: - if x.imag: - return cmath.cosh(x) - else: - return math.cosh(x.real) + if x.imag: + return cmath.cosh(x) + return math.cosh(x.real) + @_vectorize -def degrees(x): +def degrees(x: float) -> float: """ - Converts angle x from radians to degrees. + Convert angle ``x`` from radians to degrees. + + Returns + ------- + float + Angle ``x`` expressed in degrees. """ - return (180/pi)*x + return (180 / pi) * x + @_vectorize -def erf(x): +def erf(x: float) -> float: """ Return the error function at x. + + Returns + ------- + float + Error function evaluated at ``x``. """ - if isinstance(x,ADF): - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = erf(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - - lc_wrt_args = [2*exp(-x**2)/sqrt(pi)] - qc_wrt_args = [-4*x*exp(-x**2)/sqrt(pi)] + lc_wrt_args = [2 * exp(-(x**2)) / sqrt(pi)] + qc_wrt_args = [-4 * x * exp(-(x**2)) / sqrt(pi)] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [erf(xi) for xi in x] -# except TypeError: - return math.erf(x) + return math.erf(x) + @_vectorize -def erfc(x): +def erfc(x: float) -> float: """ Return the complementary error function at x. + + Returns + ------- + float + Complementary error function evaluated at ``x``. """ return 1 - erf(x) - + + @_vectorize -def exp(x): +def exp(x: float) -> float: """ - Return the exponential value of x + Return the exponential value of x. + + Returns + ------- + float + Exponential ``e**x`` (or an :class:`ad.ADF` if ``x`` is one). """ - if isinstance(x,ADF): - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = exp(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - lc_wrt_args = [exp(x)] qc_wrt_args = [exp(x)] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [exp(xi) for xi in x] -# except TypeError: - if x.imag: - return cmath.exp(x) - else: - return math.exp(x.real) + if x.imag: + return cmath.exp(x) + return math.exp(x.real) + @_vectorize -def expm1(x): +def expm1(x: float) -> float: """ - Return e**x - 1. For small floats x, the subtraction in exp(x) - 1 can - result in a significant loss of precision; the expm1() function provides - a way to compute this quantity to full precision:: + Return ``e**x - 1``. - >>> exp(1e-5) - 1 # gives result accurate to 11 places - 1.0000050000069649e-05 - >>> expm1(1e-5) # result accurate to full precision - 1.0000050000166668e-05 + For small floats ``x``, the subtraction in ``exp(x) - 1`` can result in a + significant loss of precision; the ``expm1()`` function provides a way to + compute this quantity to full precision. + Returns + ------- + float + Value of ``e**x - 1`` computed to full precision. """ - if isinstance(x,ADF): - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = expm1(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - lc_wrt_args = [exp(x)] qc_wrt_args = [exp(x)] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [expm1(xi) for xi in x] -# except TypeError: - return math.expm1(x) - + return math.expm1(x) + + @_vectorize -def fabs(x): +def fabs(x: float) -> float: """ Return the absolute value of x. + + Returns + ------- + float + Absolute value of ``x`` (or an :class:`ad.ADF` if ``x`` is one). """ - if isinstance(x,ADF): - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = fabs(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - - # catch the x=0 exception try: - lc_wrt_args = [x/fabs(x)] - qc_wrt_args = [1/fabs(x)-(x**2)/fabs(x)**3] + lc_wrt_args = [x / fabs(x)] + qc_wrt_args = [1 / fabs(x) - (x**2) / fabs(x) ** 3] except ZeroDivisionError: lc_wrt_args = [0.0] qc_wrt_args = [0.0] - + cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( - ad_funcs, variables, lc_wrt_args, - qc_wrt_args, cp_wrt_args) - - - # The function now returns an ADF object: + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) + return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [fabs(xi) for xi in x] -# except TypeError: - return math.fabs(x) + return math.fabs(x) + @_vectorize -def factorial(x): +def factorial(x: float) -> float: """ - Return x factorial. Uses the relationship factorial(x)==gamma(x+1) to - calculate derivatives. + Return ``x`` factorial. + + Uses the relationship ``factorial(x) == gamma(x + 1)`` to calculate + derivatives. + + Returns + ------- + float + Factorial of ``x``. """ - return gamma(x+1) + return gamma(x + 1) + @_vectorize -def floor(x): +def floor(x: float) -> float: """ - Return the floor of x as a float, the largest integer value less than or - equal to x. + Return the floor of x as a float. + + Returns + ------- + float + Largest integer value less than or equal to ``x``. """ - if isinstance(x,ADF): - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = floor(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - lc_wrt_args = [0.0] qc_wrt_args = [0.0] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [floor(xi) for xi in x] -# except TypeError: - return math.floor(x) + return math.floor(x) + @_vectorize -def gamma(x): +def gamma(x: float) -> float: """ Return the Gamma function at x (uses the Lanczos approximation). - """ - # This is the Lanczos approximation of the Gamma function, as copied - # from the Wikipedia article: - # http://en.wikipedia.org/wiki/Lanczos_approximation - # It is designed to work with real and complex arguments - # Coefficients used by the GNU Scientific Library - -# # 9-term approximation -# g = 7 -# p = [0.99999999999980993, -# 676.5203681218851, -# -1259.1392167224028, -# 771.32342877765313, -# -176.61502916214059, -# 12.507343278686905, -# -0.13857109526572012, -# 9.9843695780195716e-6, -# 1.5056327351493116e-7] - - # 15-term approximation - g = 607./128 - p = [0.99999999999999709182, - 57.156235665862923517, - -59.597960355475491248, - 14.136097974741747174, - -0.49191381609762019978, - .33994649984811888699e-4, - .46523628927048575665e-4, - -.98374475304879564677e-4, - .15808870322491248884e-3, - -.21026444172410488319e-3, - .21743961811521264320e-3, - -.16431810653676389022e-3, - .84418223983852743293e-4, - -.26190838401581408670e-4, - .36899182659531622704e-5] - - # Reflection formula - if x.real < 0.5: - return pi/(sin(pi*x)*gamma(1 - x)) - else: - x -= 1 - z = p[0] - for i in range(1, len(p)): - z += p[i]/(x + i) - t = x + g + 0.5 - _gamma = sqrt(2*pi)*t**(x + 0.5)*exp(-t)*z - if isinstance(x, ADF): - if _gamma.imag: - return _gamma - else: - return _gamma.toFloat() - else: - if _gamma.imag: - return _gamma - else: - return _gamma.real + + Returns + ------- + float + Gamma function evaluated at ``x``. + """ + g = 607.0 / 128 + p = [ + 0.99999999999999709182, + 57.156235665862923517, + -59.597960355475491248, + 14.136097974741747174, + -0.49191381609762019978, + 0.33994649984811888699e-4, + 0.46523628927048575665e-4, + -0.98374475304879564677e-4, + 0.15808870322491248884e-3, + -0.21026444172410488319e-3, + 0.21743961811521264320e-3, + -0.16431810653676389022e-3, + 0.84418223983852743293e-4, + -0.26190838401581408670e-4, + 0.36899182659531622704e-5, + ] + + if x.real < _HALF: + return pi / (sin(pi * x) * gamma(1 - x)) + x -= 1 + z = p[0] + for i in range(1, len(p)): + z += p[i] / (x + i) + t = x + g + 0.5 + gamma_ = sqrt(2 * pi) * t ** (x + 0.5) * exp(-t) * z + if isinstance(x, ADF): + if gamma_.imag: + return gamma_ + return gamma_.to_float() + if gamma_.imag: + return gamma_ + return gamma_.real @_vectorize -def hypot(x,y): +def hypot(x: float, y: float) -> float: """ - Return the Euclidean norm, ``sqrt(x*x + y*y)``. This is the length of the - vector from the origin to point ``(x, y)``. + Return the Euclidean norm, ``sqrt(x*x + y*y)``. + + Returns + ------- + float + Length of the vector from the origin to point ``(x, y)``. """ - return sqrt(x*x+y*y) - + return sqrt(x * x + y * y) + + @_vectorize -def lgamma(x): +def lgamma(x: float) -> float: """ - Return the natural logarithm of the absolute value of the Gamma function - at x. + Return the natural logarithm of the absolute value of Gamma at ``x``. + + Returns + ------- + float + ``log(abs(gamma(x)))`` evaluated at ``x``. """ return log(abs(gamma(x))) - + + @_vectorize -def log(x, base=None): +def log(x: float, base: float | None = None) -> float: """ - With one argument, return the natural logarithm of x (to base e). + Return the logarithm of ``x``. + + With one argument, return the natural logarithm of ``x`` (to base ``e``). + With two arguments, return the logarithm of ``x`` to the given base, + calculated as ``log(x)/log(base)``. - With two arguments, return the logarithm of x to the given base, calculated - as ``log(x)/log(base)``. + Returns + ------- + float + Logarithm of ``x`` (in the requested base, when given). """ if base is None: return log(x, base=e) - - if isinstance(x,ADF): - - ad_funcs = list(map(to_auto_diff,[x])) + + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = log(x, base) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - - lc_wrt_args = [1./(x * ln(base))] - qc_wrt_args = [-1./(x**2 * ln(base))] + lc_wrt_args = [1.0 / (x * ln(base))] + qc_wrt_args = [-1.0 / (x**2 * ln(base))] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [log(xi) for xi in x] -# except TypeError: - if x.imag: - return cmath.log(x, base) - else: - return math.log(x.real, base) + + if x.imag: + return cmath.log(x, base) + return math.log(x.real, base) + @_vectorize -def log10(x): +def log10(x: float) -> float: """ - Return the base-10 logarithm of x. This is usually more accurate than - ``log(x, 10)``. + Return the base-10 logarithm of x. + + This is usually more accurate than ``log(x, 10)``. + + Returns + ------- + float + Base-10 logarithm of ``x``. """ - if isinstance(x,ADF): - - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = log10(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - - lc_wrt_args = [1/x/log(10)] - qc_wrt_args = [-1./x**2/log(10)] + lc_wrt_args = [1 / x / log(10)] + qc_wrt_args = [-1.0 / x**2 / log(10)] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [log10(xi) for xi in x] -# except TypeError: - if x.imag: - return cmath.log10(x) - else: - return math.log10(x.real) + + if x.imag: + return cmath.log10(x) + return math.log10(x.real) + @_vectorize -def log1p(x): +def log1p(x: float) -> float: """ - Return the base-10 logarithm of x. This is usually more accurate than - ``log(x, 10)``. + Return the natural logarithm of ``1 + x``. + + Returns + ------- + float + ``log(1 + x)`` evaluated to full precision for small ``x``. """ - if isinstance(x,ADF): - - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = log1p(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - - lc_wrt_args = [1/(x+1)] - qc_wrt_args = [-1./(x+1)**2] + lc_wrt_args = [1 / (x + 1)] + qc_wrt_args = [-1.0 / (x + 1) ** 2] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [log1p(xi) for xi in x] -# except TypeError: - return math.log1p(x) + + return math.log1p(x) + @_vectorize -def pow(x, y): +def power(x: float, y: float) -> float: """ - Return x raised to the power y. + Return ``x`` raised to the power ``y``. + + Returns + ------- + float + ``x ** y``. """ return x**y + @_vectorize -def radians(x): +def radians(x: float) -> float: """ - Converts angle x from degrees to radians. + Convert angle ``x`` from degrees to radians. + + Returns + ------- + float + Angle ``x`` expressed in radians. """ - return (pi/180)*x - + return (pi / 180) * x + + @_vectorize -def sin(x): +def sin(x: float) -> float: """ - Return the sine of x, in radians. + Return the sine of ``x`` in radians. + + Returns + ------- + float + Sine of ``x`` (or an :class:`ad.ADF` if ``x`` is one). """ - if isinstance(x,ADF): - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = sin(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - lc_wrt_args = [cos(x)] qc_wrt_args = [-sin(x)] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [sin(xi) for xi in x] -# except TypeError: - if x.imag: - return cmath.sin(x) - else: - return math.sin(x.real) + if x.imag: + return cmath.sin(x) + return math.sin(x.real) + @_vectorize -def sinh(x): +def sinh(x: float) -> float: """ - Return the hyperbolic sine of x. + Return the hyperbolic sine of ``x``. + + Returns + ------- + float + Hyperbolic sine of ``x`` (or an :class:`ad.ADF` if ``x`` is one). """ - if isinstance(x,ADF): - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = sinh(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - lc_wrt_args = [cosh(x)] qc_wrt_args = [sinh(x)] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [sinh(xi) for xi in x] -# except TypeError: - if x.imag: - return cmath.sinh(x) - else: - return math.sinh(x.real) + if x.imag: + return cmath.sinh(x) + return math.sinh(x.real) + @_vectorize -def sqrt(x): +def sqrt(x: float) -> float: """ - Return the square root of x. + Return the square root of ``x``. + + Returns + ------- + float + Square root of ``x``. """ -# try: -# return [xi**0.5 for xi in x] -# except TypeError: return x**0.5 - + + @_vectorize -def tan(x): +def tan(x: float) -> float: """ - Return the tangent of x, in radians. + Return the tangent of ``x`` in radians. + + Returns + ------- + float + Tangent of ``x`` (or an :class:`ad.ADF` if ``x`` is one). """ - if isinstance(x,ADF): - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = tan(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - - lc_wrt_args = [1./(cos(x))**2] - qc_wrt_args = [2*sin(x)/(cos(x))**3] + lc_wrt_args = [1.0 / (cos(x)) ** 2] + qc_wrt_args = [2 * sin(x) / (cos(x)) ** 3] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [tan(xi) for xi in x] -# except TypeError: - if x.imag: - return cmath.tan(x) - else: - return math.tan(x.real) + if x.imag: + return cmath.tan(x) + return math.tan(x.real) + @_vectorize -def tanh(x): +def tanh(x: float) -> float: """ - Return the hyperbolic tangent of x. + Return the hyperbolic tangent of ``x``. + + Returns + ------- + float + Hyperbolic tangent of ``x`` (or an :class:`ad.ADF` if ``x`` is one). """ - if isinstance(x,ADF): - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = tanh(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - - lc_wrt_args = [1./(cosh(x))**2] - qc_wrt_args = [-2*sinh(x)/(cosh(x))**3] + lc_wrt_args = [1.0 / (cosh(x)) ** 2] + qc_wrt_args = [-2 * sinh(x) / (cosh(x)) ** 3] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - else: -# try: # pythonic: fails gracefully when x is not an array-like object -# return [tanh(xi) for xi in x] -# except TypeError: - if x.imag: - return cmath.tanh(x) - else: - return math.tanh(x.real) + if x.imag: + return cmath.tanh(x) + return math.tanh(x.real) + @_vectorize -def trunc(x): +def trunc(x: float) -> float: """ - Return the **Real** value x truncated to an **Integral** (usually a - long integer). Uses the ``__trunc__`` method. + Return the **Real** value ``x`` truncated to an **Integral**. + + Uses the ``__trunc__`` method. + + Returns + ------- + float + Truncated integer value of ``x``. """ - if isinstance(x,ADF): - ad_funcs = list(map(to_auto_diff,[x])) + if isinstance(x, ADF): + ad_funcs = list(map(to_auto_diff, [x])) x = ad_funcs[0].x - - ######################################## - # Nominal value of the constructed ADF: + f = trunc(x) - - ######################################## variables = ad_funcs[0]._get_variables(ad_funcs) - + if not variables or isinstance(f, bool): return f - ######################################## - - # Calculation of the derivatives with respect to the arguments - # of f (ad_funcs): - lc_wrt_args = [0.0] qc_wrt_args = [0.0] cp_wrt_args = 0.0 - ######################################## - # Calculation of the derivative of f with respect to all the - # variables (Variable) involved. + lc_wrt_vars, qc_wrt_vars, cp_wrt_vars = _apply_chain_rule( + ad_funcs, variables, lc_wrt_args, qc_wrt_args, cp_wrt_args + ) - lc_wrt_vars,qc_wrt_vars,cp_wrt_vars = _apply_chain_rule( - ad_funcs,variables,lc_wrt_args,qc_wrt_args, - cp_wrt_args) - - # The function now returns an ADF object: return ADF(f, lc_wrt_vars, qc_wrt_vars, cp_wrt_vars) - else: - try: # pythonic: fails gracefully when x is not an array-like object - return [trunc(xi) for xi in x] - except TypeError: - return math.trunc(x) + try: + return [trunc(xi) for xi in x] + except TypeError: + return math.trunc(x) + + +# OTHER CONVENIENCE FUNCTIONS ############################################### -### OTHER CONVENIENCE FUNCTIONS ############################################### @_vectorize -def csc(x): +def csc(x: float) -> float: """ - Return the cosecant of x. + Return the cosecant of ``x``. + + Returns + ------- + float + Cosecant ``1 / sin(x)``. """ - return 1.0/sin(x) - + return 1.0 / sin(x) + + @_vectorize -def sec(x): +def sec(x: float) -> float: """ - Return the secant of x. + Return the secant of ``x``. + + Returns + ------- + float + Secant ``1 / cos(x)``. """ - return 1.0/cos(x) - + return 1.0 / cos(x) + + @_vectorize -def cot(x): +def cot(x: float) -> float: """ - Return the cotangent of x. + Return the cotangent of ``x``. + + Returns + ------- + float + Cotangent ``1 / tan(x)``. """ - return 1.0/tan(x) + return 1.0 / tan(x) + @_vectorize -def csch(x): +def csch(x: float) -> float: """ - Return the hyperbolic cosecant of x. + Return the hyperbolic cosecant of ``x``. + + Returns + ------- + float + Hyperbolic cosecant ``1 / sinh(x)``. """ - return 1.0/sinh(x) - + return 1.0 / sinh(x) + + @_vectorize -def sech(x): +def sech(x: float) -> float: """ - Return the hyperbolic secant of x. + Return the hyperbolic secant of ``x``. + + Returns + ------- + float + Hyperbolic secant ``1 / cosh(x)``. """ - return 1.0/cosh(x) - + return 1.0 / cosh(x) + + @_vectorize -def coth(x): +def coth(x: float) -> float: """ - Return the hyperbolic cotangent of x. + Return the hyperbolic cotangent of ``x``. + + Returns + ------- + float + Hyperbolic cotangent ``1 / tanh(x)``. """ - return 1.0/tanh(x) + return 1.0 / tanh(x) + @_vectorize -def acsc(x): +def acsc(x: float) -> float: """ - Return the inverse cosecant of x. + Return the inverse cosecant of ``x``. + + Returns + ------- + float + Inverse cosecant ``asin(1 / x)``. """ - return asin(1.0/x) + return asin(1.0 / x) + @_vectorize -def asec(x): +def asec(x: float) -> float: """ - Return the inverse secant of x. + Return the inverse secant of ``x``. + + Returns + ------- + float + Inverse secant ``acos(1 / x)``. """ - return acos(1.0/x) - + return acos(1.0 / x) + + @_vectorize -def acot(x): +def acot(x: float) -> float: """ - Return the inverse cotangent of x. + Return the inverse cotangent of ``x``. + + Returns + ------- + float + Inverse cotangent ``atan(1 / x)``. """ - return atan(1.0/x) + return atan(1.0 / x) + @_vectorize -def acsch(x): +def acsch(x: float) -> float: """ - Return the inverse hyperbolic cosecant of x. + Return the inverse hyperbolic cosecant of ``x``. + + Returns + ------- + float + Inverse hyperbolic cosecant ``asinh(1 / x)``. """ - return asinh(1.0/x) + return asinh(1.0 / x) + @_vectorize -def asech(x): +def asech(x: float) -> float: """ - Return the inverse hyperbolic secant of x. + Return the inverse hyperbolic secant of ``x``. + + Returns + ------- + float + Inverse hyperbolic secant ``acosh(1 / x)``. """ - return acosh(1.0/x) - + return acosh(1.0 / x) + + @_vectorize -def acoth(x): +def acoth(x: float) -> float: """ - Return the inverse hyperbolic cotangent of x. + Return the inverse hyperbolic cotangent of ``x``. + + Returns + ------- + float + Inverse hyperbolic cotangent ``atanh(1 / x)``. """ - return atanh(1.0/x) + return atanh(1.0 / x) + @_vectorize -def ln(x): +def ln(x: float) -> float: """ - Return the natural logarithm of x. + Return the natural logarithm of ``x``. + + Returns + ------- + float + Natural logarithm of ``x``. """ return log(x) diff --git a/ad/linalg/__init__.py b/ad/linalg/__init__.py index a0f415d..09e3279 100644 --- a/ad/linalg/__init__.py +++ b/ad/linalg/__init__.py @@ -1,6 +1,6 @@ """ ================================================================================ -ad: Fast, transparent calculations of first and second-order automatic +ad: Fast, transparent calculations of first and second-order automatic differentiation ================================================================================ @@ -9,4 +9,7 @@ """ -from .linalg import * +from .linalg import chol, inv, lstsq, lu, qr, solve + + +__all__ = ["chol", "inv", "lstsq", "lu", "qr", "solve"] diff --git a/ad/linalg/linalg.py b/ad/linalg/linalg.py index e6c52b2..016b5f9 100644 --- a/ad/linalg/linalg.py +++ b/ad/linalg/linalg.py @@ -1,6 +1,6 @@ """ This sub-module allows for the usage of several linear algebra routines that -are otherwise unavailable for use for automatic differentiation. Not all +are otherwise unavailable for use for automatic differentiation. Not all numpy.linalg routines have an equivalent here, but we're working on it. Decompositions @@ -12,7 +12,7 @@ a. solve (Solve a linear matrix equation, or system of linear scalar eqs) b. lstsq (Solve a linear least squares problem) c. inv (Compute the (multiplicative) inverse of a matrix) - + (c) 2013 by Abraham Lee . Please send feature requests, bug reports, or feedback to this address. @@ -22,485 +22,426 @@ """ -from __future__ import division import numpy as np +from numpy import ndarray + + +__all__ = ["chol", "inv", "lstsq", "lu", "qr", "solve"] + + +def _ensure(condition: object, message: str = "") -> None: + """Raise ``AssertionError`` when ``condition`` is falsy. + + Parameters + ---------- + condition : object + Truthy value to test. + message : str + Optional message reported when the condition is falsy. -__all__ = ['np', 'chol', 'qr', 'lu', 'solve', 'lstsq', 'inv'] + Raises + ------ + AssertionError + If ``condition`` is falsy. + """ + if not condition: + raise AssertionError(message) -############################################################################### -def chol(A, side='lower'): +def chol(a: ndarray, side: str = "lower") -> ndarray: """ - Cholesky decomposition of a symmetric, positive-definite matrix, defined - by A = L*L.T = U.T*U, where L is a lower triangular matrix and U is an - upper triangular matrix. - + Cholesky decomposition of a symmetric, positive-definite matrix. + + The decomposition is defined by ``A = L @ L.T = U.T @ U``, where ``L`` is + a lower triangular matrix and ``U`` is an upper triangular matrix. + Parameters ---------- - A : 2d-array - The input matrix - - Optional - -------- + a : 2d-array + The input matrix. side : str - If 'lower' (default), then the lower triangular form of the - decompostion is returned. If 'upper', then the upper triangular - form of the decomposition is returned (the transpose of 'lower') - + If ``'lower'`` (default), then the lower triangular form of the + decomposition is returned. If ``'upper'``, then the upper triangular + form of the decomposition is returned (the transpose of ``'lower'``). + Returns ------- - L : 2d-array + out : 2d-array The lower (or upper) triangular matrix that helps define the decomposition. - - Example - ------- - Example 1:: - - >>> A = [[25, 15, -5], - ... [15, 18, 0], - ... [-5, 0, 11]] - ... - >>> L = chol(A) - >>> L - array([[ 5., 0., 0.], - [ 3., 3., 0.], - [-1., 1., 3.]]) - >>> U = chol(A, 'upper') - >>> U - array([[ 5., 3., -1.], - [ 0., 3., 1.], - [ 0., 0., 3.]]) - - Example 2:: - - >>> A = [[18, 22, 54, 42], - ... [22, 70, 86, 62], - ... [54, 86, 174, 134], - ... [42, 62, 134, 106]] - ... - >>> L = chol(A) - >>> L - array([[ 4.24264069, 0. , 0. , 0. ], - [ 5.18544973, 6.5659052 , 0. , 0. ], - [ 12.72792206, 3.0460385 , 1.64974225, 0. ], - [ 9.89949494, 1.62455386, 1.84971101, 1.39262125]]) - """ - A = np.array(A) - assert A.shape[0]==A.shape[1], 'Input matrix must be square' - - L = [[0.0] * len(A) for _ in xrange(len(A))] - for i in xrange(len(A)): - for j in xrange(i+1): - s = sum(L[i][k] * L[j][k] for k in xrange(j)) - L[i][j] = (A[i][i] - s)**0.5 if (i == j) else \ - (1.0 / L[j][j] * (A[i][j] - s)) - - if side=='lower': - return np.array(L) - elif side=='upper': - return np.array(L).T - -############################################################################### - -def qr(A): + a = np.array(a) + _ensure(a.shape[0] == a.shape[1], "Input matrix must be square") + + n = len(a) + lower = [[0.0] * n for _ in range(n)] + for i in range(n): + for j in range(i + 1): + s = sum(lower[i][k] * lower[j][k] for k in range(j)) + lower[i][j] = ( + (a[i][i] - s) ** 0.5 + if i == j + else (1.0 / lower[j][j] * (a[i][j] - s)) + ) + + if side == "lower": + return np.array(lower) + return np.array(lower).T + + +def qr(a: ndarray) -> tuple[ndarray, ndarray]: """ - QR Decomposition - + QR Decomposition. + Parameters ---------- - A : 2d-array + a : 2d-array The input matrix (need not be square). - + Returns ------- qm : 2d-array - The orthogonal Q matrix from the decomposition + The orthogonal Q matrix from the decomposition. rm : 2d-array - The R matrix from the decomposition - - Example - ------- - A square input matrix:: - - >>> A = [[12, -51, 4], - ... [ 6, 167, -68], - ... [-4, 24, -41]] - ... - >>> q, r = qr(A) - >>> q - array([[-0.85714286, 0.39428571, 0.33142857], - [-0.42857143, -0.90285714, -0.03428571], - [ 0.28571429, -0.17142857, 0.94285714]]) - >>> r - array([[ -1.40000000e+01, -2.10000000e+01, 1.40000000e+01], - [ 5.97812398e-18, -1.75000000e+02, 7.00000000e+01], - [ 4.47505281e-16, 0.00000000e+00, -3.50000000e+01]]) - - A non-square input matrix:: - - >>> A = [[12, -51, 4], - ... [ 6, 167, -68], - ... [-4, 24, -41], - ... [-1, 1, 0], - ... [ 2, 0, 3]] - ... - >>> q, r = qr(A) - >>> q - array([[-0.84641474, 0.39129081, -0.34312406, 0.06613742, -0.09146206], - [-0.42320737, -0.90408727, 0.02927016, 0.01737854, -0.04861045], - [ 0.28213825, -0.17042055, -0.93285599, -0.02194202, 0.14371187], - [ 0.07053456, -0.01404065, 0.00109937, 0.99740066, 0.00429488], - [-0.14106912, 0.01665551, 0.10577161, 0.00585613, 0.98417487]]) - >>> r - array([[ -1.41774469e+01, -2.06666265e+01, 1.34015667e+01], - [ 3.31666807e-16, -1.75042539e+02, 7.00803066e+01], - [ -3.36067949e-16, 2.87087579e-15, 3.52015430e+01], - [ 9.46898347e-17, 5.05117109e-17, -9.49761103e-17], - [ -1.74918720e-16, -3.80190411e-16, 8.88178420e-16]]) - >>> import numpy as np - >>> np.all(np.dot(q, r) - A<1e-12) - True - + The R matrix from the decomposition. """ - A = np.atleast_2d(A) - m, n = A.shape + a = np.atleast_2d(a) + m, n = a.shape qm = np.eye(m) - rm = A.copy() - for i in xrange(n-1 if m==n else n): - x = getSubmatrix(rm, i, i, m, i) + rm = a.copy() + for i in range(n - 1 if m == n else n): + x = _get_submatrix(rm, i, i, m, i) h = np.eye(m) - h = setSubmatrix(h, i, i, householder(x)) + h = _set_submatrix(h, i, i, _householder(x)) qm = np.dot(qm, h) rm = np.dot(h, rm) return qm, rm - -############################################################################### -def lu(A): + +def lu(a: ndarray) -> tuple[ndarray, ndarray, ndarray]: """ - Decomposes a nxn matrix A by PA=LU and returns L, U and P. - + Decompose an ``n x n`` matrix ``a`` by ``PA = LU``. + Parameters ---------- - A : 2d-array - The input matrix - + a : 2d-array + The input matrix. + Returns ------- - L : 2d-array - The lower-triangular matrix of the decomposition - U : 2d-array - The upper-triangular matrix of the decomposition - P : 2d-array - The pivoting matrix used in the decomposition - - Examples - -------- - Example 1:: - - >>> A = [[1, 3, 5], - ... [2, 4, 7], - ... [1, 1, 0]] - ... - >>> L, U, P = lu(A) - >>> L - array([[ 1. , 0. , 0. ], - [ 0.5, 1. , 0. ], - [ 0.5, -1. , 1. ]]) - >>> U - array([[ 2. , 4. , 7. ], - [ 0. , 1. , 1.5], - [ 0. , 0. , -2. ]]) - >>> P - array([[ 0., 1., 0.], - [ 1., 0., 0.], - [ 0., 0., 1.]]) - - Example 2:: - - >>> A = [[11, 9, 24, 2], - ... [ 1, 5, 2, 6], - ... [ 3, 17, 18, 1], - ... [ 2, 5, 7, 1]] - ... - >>> L, U, P = lu(A) - >>> L - array([[ 1. , 0. , 0. , 0. ], - [ 0.27272727, 1. , 0. , 0. ], - [ 0.09090909, 0.2875 , 1. , 0. ], - [ 0.18181818, 0.23125 , 0.00359712, 1. ]]) - >>> U - array([[ 11. , 9. , 24. , 2. ], - [ 0. , 14.54545455, 11.45454545, 0.45454545], - [ 0. , 0. , -3.475 , 5.6875 ], - [ 0. , 0. , 0. , 0.51079137]]) - >>> P - array([[ 1., 0., 0., 0.], - [ 0., 0., 1., 0.], - [ 0., 1., 0., 0.], - [ 0., 0., 0., 1.]]) - + lower : 2d-array + The lower-triangular matrix of the decomposition. + upper : 2d-array + The upper-triangular matrix of the decomposition. + pivots : 2d-array + The pivoting matrix used in the decomposition. """ - A = np.array(A) - assert A.shape[0]==A.shape[1], 'Input matrix must be square' - - n = len(A) - L = [[0.0]*n for i in xrange(n)] - U = [[0.0]*n for i in xrange(n)] - - # Create the pivoting matrix for A - P = [[float(i == j) for i in xrange(n)] for j in xrange(n)] - for j in xrange(n): - row = max(xrange(j, n), key=lambda i: A[i][j]) + a = np.array(a) + _ensure(a.shape[0] == a.shape[1], "Input matrix must be square") + + n = len(a) + lower = [[0.0] * n for _ in range(n)] + upper = [[0.0] * n for _ in range(n)] + + pivots = [[float(i == j) for i in range(n)] for j in range(n)] + for j in range(n): + row = max(range(j, n), key=lambda i, jj=j: a[i][jj]) if j != row: - P[j], P[row] = P[row], P[j] - - A2 = np.dot(P, A) - for j in xrange(n): - L[j][j] = 1.0 - for i in xrange(j+1): - s1 = sum(U[k][j] * L[i][k] for k in xrange(i)) - U[i][j] = A2[i][j] - s1 - for i in xrange(j, n): - s2 = sum(U[k][j] * L[i][k] for k in xrange(j)) - L[i][j] = (A2[i][j] - s2) / U[j][j] - return (np.array(L), np.array(U), np.array(P)) - - -############################################################################### - -def solve(A, b): - """ - Solve a system of equations Ax = b by Gaussian elimination - + pivots[j], pivots[row] = pivots[row], pivots[j] + + a2 = np.dot(pivots, a) + for j in range(n): + lower[j][j] = 1.0 + for i in range(j + 1): + s1 = sum(upper[k][j] * lower[i][k] for k in range(i)) + upper[i][j] = a2[i][j] - s1 + for i in range(j, n): + s2 = sum(upper[k][j] * lower[i][k] for k in range(j)) + lower[i][j] = (a2[i][j] - s2) / upper[j][j] + return (np.array(lower), np.array(upper), np.array(pivots)) + + +def _normalize_solve_inputs( + a: ndarray, b: ndarray +) -> tuple[ndarray, ndarray, int]: + """Validate and reshape inputs for :func:`solve`. + Parameters ---------- - A : 2d-array - The LHS of the system of equations. The number of rows must not be less - than the number of columns, but can be more. - b : array-like - The RHS of the system of equations (must have the same number of rows - as ``A``. If more than one column is given, a solution is generated for - each column - + a : ndarray + Coefficient matrix. + b : ndarray + Right-hand-side array. + Returns ------- - x : array-like - The solution that satisifies the equality ``Ax==b``. If multiple ``b`` - are given, a solution column will be given satisfying each set. - - Example - ------- - :: - - >>> A = [[1, 2, 1], [4, 6, 3], [9, 8, 2]] - >>> b = [3, 2, 1] - >>> solve(A, b) - array([ -7., 11., -12.]) - + matrix : ndarray + Coefficient matrix as a ``np.matrix``. + rhs : ndarray + Right-hand-side array. + numout : int + Number of solution columns. + + Raises + ------ + TypeError + If ``a`` cannot be converted to a 2-dimensional array. """ try: - A = np.matrix(A) - except Exception: - raise Exception('A must be a 2-dimensional array') - assert A.shape[0]>=A.shape[1], 'A must not have less rows than columns' - b = np.array(b) - numout = 1 if b.ndim==1 else b.shape[1] - assert A.shape[0]==b.shape[0], 'b must have the same number of rows as A' - - # Make the LHS array square if it isn't already and adjust RHS if needed - if A.shape[0]>A.shape[1]: - b = A.T*b - A = A.T*A - - eqs = np.column_stack((A, b)) - n, m = eqs.shape - - # Forward substitution - for k in range(n-1): - # Pivot test + matrix = np.matrix(a) + except (TypeError, ValueError) as err: + raise TypeError("A must be a 2-dimensional array") from err + _ensure( + matrix.shape[0] >= matrix.shape[1], + "A must not have less rows than columns", + ) + rhs = np.array(b) + numout = 1 if rhs.ndim == 1 else rhs.shape[1] + _ensure( + matrix.shape[0] == rhs.shape[0], + "b must have the same number of rows as A", + ) + return matrix, rhs, numout + + +def _forward_substitute(eqs: ndarray, n: int, m: int) -> None: + """Apply forward substitution in place on the augmented matrix. + + Parameters + ---------- + eqs : ndarray + Augmented matrix that combines the coefficient matrix and the + right-hand-side. Modified in place. + n : int + Number of rows. + m : int + Number of columns. + """ + for k in range(n - 1): p = k piv_el = abs(eqs[k, k]) - + for i in range(k + 1, n): tmp = abs(eqs[i, k]) - if tmp>piv_el: + if tmp > piv_el: piv_el = tmp p = i - - # Swap the kth and pth rows if a pivot element was found - if p!=k: + + if p != k: rtmp = np.array(eqs[p, :]) eqs[p, :] = eqs[k, :] eqs[k, :] = rtmp - + for i in range(k + 1, n): - f = 1.*eqs[i, k]/eqs[k, k] + f = 1.0 * eqs[i, k] / eqs[k, k] for j in range(k, m): - eqs[i, j] -= f*eqs[k, j] - - # Backward substitution + eqs[i, j] -= f * eqs[k, j] + + +def _backward_substitute(eqs: ndarray, n: int, m: int) -> None: + """Apply backward substitution in place on the augmented matrix. + + Parameters + ---------- + eqs : ndarray + Augmented matrix that combines the coefficient matrix and the + right-hand-side. Modified in place. + n : int + Number of rows. + m : int + Number of columns. + """ for k in range(1, n)[::-1]: for i in range(k)[::-1]: - f = 1.*eqs[i, k]/eqs[k, k] + f = 1.0 * eqs[i, k] / eqs[k, k] for j in range(m)[::-1]: - eqs[i, j] -= f*eqs[k, j] - - # Normalize + eqs[i, j] -= f * eqs[k, j] + for i in range(n): - x = 1.*eqs[i, i] + x = 1.0 * eqs[i, i] for j in range(m): - eqs[i, j] = eqs[i, j]/x - - if numout==1: - return np.array(eqs[:, -(m-n):]).ravel() - else: - return np.array(eqs[:, -(m-n):]) + eqs[i, j] /= x -############################################################################### -def lstsq(A, b): +def solve(a: ndarray, b: ndarray) -> ndarray: """ - Solve Ax = b with the linear least squares method. - + Solve a system of equations ``Ax = b`` by Gaussian elimination. + Parameters ---------- - A : 2d-array - The linear system matrix + a : 2d-array + The LHS of the system of equations. The number of rows must not be + less than the number of columns, but can be more. + b : array-like + The RHS of the system of equations (must have the same number of rows + as ``a``). If more than one column is given, a solution is generated + for each column. + + Returns + ------- + x : array-like + The solution that satisfies the equality ``a @ x == b``. If multiple + ``b`` columns are given, a solution column will be returned for each + set. + """ + matrix, rhs, numout = _normalize_solve_inputs(a, b) + + if matrix.shape[0] > matrix.shape[1]: + rhs = matrix.T * rhs + matrix = matrix.T * matrix + + eqs = np.column_stack((matrix, rhs)) + n, m = eqs.shape + + _forward_substitute(eqs, n, m) + _backward_substitute(eqs, n, m) + + if numout == 1: + return np.array(eqs[:, -(m - n) :]).ravel() + return np.array(eqs[:, -(m - n) :]) + + +def lstsq(a: ndarray, b: ndarray) -> ndarray: + """ + Solve ``Ax = b`` with the linear least squares method. + + Parameters + ---------- + a : 2d-array + The linear system matrix. b : array - The right-hand-side of the equation - + The right-hand-side of the equation. + Returns ------- x : array - The solution to the system of equations - - Example - ------- - (Taken from the NumPy lstsq example):: - - >>> x = np.array([0, 1, 2, 3]) - >>> y = np.array([-1, 0.2, 0.9, 2.1]) - >>> A = np.vstack([x, np.ones(len(x))]).T - >>> A - array([[ 0., 1.], - [ 1., 1.], - [ 2., 1.], - [ 3., 1.]]) - >>> print lstsq(A, y) - [ 1. -0.95] - + The solution to the system of equations. """ - q, r = qr(A) + q, r = qr(a) n = r.shape[1] - x = solveUpperTriangular(getSubmatrix(r, 0, 0, n - 1, n - 1), - np.dot(q.T, b)) + x = _solve_upper_triangular( + _get_submatrix(r, 0, 0, n - 1, n - 1), np.dot(q.T, b) + ) return x.ravel() -############################################################################### -def inv(A): +def inv(a: ndarray) -> ndarray: """ Calculate the multiplicative inverse of a matrix. - + Parameters ---------- - A : 2d-array - The input matrix - + a : 2d-array + The input matrix. + Returns ------- - Ainv : 2d-array - The inverse of A - - Example - ------- - :: - - >>> A = [[25, 15, -5], - ... [15, 18, 0], - ... [-5, 0, 11]] - ... - >>> Ainv = inv(A) - >>> Ainv - array([[ 0.09777778, -0.08148148, 0.04444444], - [-0.08148148, 0.12345679, -0.03703704], - [ 0.04444444, -0.03703704, 0.11111111]]) - >>> np.dot(Ainv, A) - array([[ 1.00000000e+00, 0.00000000e+00, 0.00000000e+00], - [ 2.77555756e-16, 1.00000000e+00, 0.00000000e+00], - [ 0.00000000e+00, 1.11022302e-16, 1.00000000e+00]]) - + inverse : 2d-array + The inverse of ``a``. """ - shape = np.atleast_2d(A).shape - assert shape[0]==shape[1], 'Matrix must be square' - return solve(A, np.eye(shape[0])) - -############################################################################### -# -# -# All functions below are utility functions for some of the above -# linear algebra functions. They should NOT be used directly. -# -# -############################################################################### - -def ematrix(m, n, x, i, j): + shape = np.atleast_2d(a).shape + _ensure(shape[0] == shape[1], "Matrix must be square") + return solve(a, np.eye(shape[0])) + + +def _ematrix(m: int, n: int, x: float, i: int, j: int) -> ndarray: a = np.empty((m, n)) a[:, :] = 0.0 a[i, j] = x return a - -def unitVector(n): - return ematrix(n, 1, 1, 0, 0) -def signValue(r): - if r<0: + +def _unit_vector(n: int) -> ndarray: + return _ematrix(n, 1, 1, 0, 0) + + +def _sign_value(r: float) -> int: + if r < 0: return -1 - elif r==0: + if r == 0: return 0 - else: - return 1 - -def householder(A): - m = len(A) - #u = A + np.sqrt(np.dot(A.T, A)[0, 0])*unitVector(m)*signValue(A[0, 0]) - u = A - np.sqrt(np.dot(A.T, A)[0, 0])*unitVector(m)#*signValue(A[0, 0]) - v = u#/u[0, 0] - beta = 2/np.dot(v.T, v) - return np.eye(m) - beta*(np.dot(v, v.T).T) - -def getSubmatrix(obj, i1, j1, i2, j2): + return 1 + + +def _householder(a: ndarray) -> ndarray: + m = len(a) + u = a - np.sqrt(np.dot(a.T, a)[0, 0]) * _unit_vector(m) + v = u + beta = 2 / np.dot(v.T, v) + return np.eye(m) - beta * (np.dot(v, v.T).T) + + +def _get_submatrix(obj: ndarray, i1: int, j1: int, i2: int, j2: int) -> ndarray: """ - i1, j1, i2, j2 are inclusive indices + Return the submatrix bounded by inclusive index pairs. + + Parameters + ---------- + obj : ndarray + Source matrix. + i1 : int + Top row index (inclusive). + j1 : int + Left column index (inclusive). + i2 : int + Bottom row index (inclusive). + j2 : int + Right column index (inclusive). + + Returns + ------- + submatrix : ndarray + Submatrix view bounded by ``[i1, i2]`` rows and ``[j1, j2]`` columns. """ - return obj[i1:i2 + 1, j1:j2 + 1] + return obj[i1 : i2 + 1, j1 : j2 + 1] -def setSubmatrix(obj, i1, j1, subobj): + +def _set_submatrix(obj: ndarray, i1: int, j1: int, subobj: ndarray) -> ndarray: """ - i1, j1 are the top left corner indices where subobj will be inserted + Insert a submatrix into ``obj`` starting at ``(i1, j1)``. + + Parameters + ---------- + obj : ndarray + Destination matrix; modified in place. + i1 : int + Top row index where insertion begins. + j1 : int + Left column index where insertion begins. + subobj : ndarray + Submatrix to insert. + + Returns + ------- + obj : ndarray + The modified destination matrix. """ m, n = np.atleast_2d(subobj).shape - obj[i1:i1+m, j1:j1+n] = subobj + obj[i1 : i1 + m, j1 : j1 + n] = subobj return obj -def solveUpperTriangular(r, b): + +def _solve_upper_triangular(r: ndarray, b: ndarray) -> ndarray: r = np.atleast_2d(r) n = r.shape[1] - + x = np.zeros((n, 1)) - for k in xrange(n - 1, -1, -1): + for k in range(n - 1, -1, -1): idx = min(n - 1, k) - x[k, 0] = (b[k] - np.dot(getSubmatrix(r, k, idx, k, n - 1), - getSubmatrix(x, idx, 0, n - 1, 0)))/r[k, k] + x[k, 0] = ( + b[k] + - np.dot( + _get_submatrix(r, k, idx, k, n - 1), + _get_submatrix(x, idx, 0, n - 1, 0), + ) + ) / r[k, k] return x -def polyfit(x, y, n): + +def _polyfit(x: ndarray, y: ndarray, n: int) -> ndarray: """ - Fit a polynomial of order n to data arrays x and y - + Fit a polynomial of order ``n`` to data arrays ``x`` and ``y``. + Parameters ---------- x : array @@ -508,25 +449,15 @@ def polyfit(x, y, n): y : array A data array that defines the second dimension coordinates. n : int - The order of the polynomial (e.g., 1 for linear, 2 for quadratic, etc.) - + The order of the polynomial (e.g., 1 for linear, 2 for quadratic). + Returns ------- b : array The polynomial coefficients, in ascending order. - - Example - ------- - :: - - >>> x = range(11) - >>> y = [1, 6, 17, 34, 57, 86, 121, 162, 209, 262, 321] - >>> polyfit(x, y, 2) - array([ 1., 2., 3.]) - """ a = np.empty((len(x), n + 1)) - for i in xrange(a.shape[0]): - for j in xrange(a.shape[1]): - a[i, j] = 1 if j==0 else x[i]**j + for i in range(a.shape[0]): + for j in range(a.shape[1]): + a[i, j] = 1 if j == 0 else x[i] ** j return lstsq(a, y) diff --git a/doc/conf.py b/doc/conf.py deleted file mode 100644 index e8e404d..0000000 --- a/doc/conf.py +++ /dev/null @@ -1,203 +0,0 @@ -# -*- coding: utf-8 -*- -# -# ad Python package documentation build configuration file, created by -# sphinx-quickstart on Tue Jun 8 18:32:22 2010. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -from datetime import date - -import sys, os - -sys.path.insert(0, os.path.abspath('..')) - -import ad - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.append(os.path.abspath('.')) - -# -- General configuration ----------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8' - -# The master toctree document. -master_doc = 'index_TOC' - -# General information about the project. -project = u'ad Python package' -if date.today().year!=2013: - copyright = u'2013–%d, Abraham Lee' % date.today().year -else: - copyright = u'2013, Abraham Lee' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '1' -# The full version, including alpha/beta/rc tags. -release = ad.__version__ - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of documents that shouldn't be included in the build. -#unused_docs = [] - -# List of directories, relative to source directory, that shouldn't be searched -# for source files. -exclude_trees = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - - -# -- Options for HTML output --------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. Major themes that come with -# Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'sphinxdoc' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -html_favicon = '_static/favicon.ico' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_use_modindex = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -html_show_sourcelink = False - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'adPythonPackagedoc' - - -# -- Options for LaTeX output -------------------------------------------------- - -# The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -# latex_documents = [ -# ('index_TOC', 'adPythonPackage.tex', u'ad Python package Documentation', -# u'Abraham Lee', 'manual'), -# ] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# Additional stuff for the LaTeX preamble. -#latex_preamble = '' - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_use_modindex = True diff --git a/docs/_static/favicon.ico b/docs/_static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..2fca2a72e6253c63b34c35e63dec1bfe860b04e6 GIT binary patch literal 9918 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 HwFv(|S)&oa literal 0 HcmV?d00001 diff --git a/docs/_static/lstsq_fit.png b/docs/_static/lstsq_fit.png new file mode 100644 index 0000000000000000000000000000000000000000..bb7f439376d04b2dc3960fa670b58a11d21cb0c1 GIT binary patch literal 27183 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 literal 0 HcmV?d00001 diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..6004d61 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,165 @@ +# API Reference + +## Core Imports + +```python +from ad import adnumber +import ad +from ad.admath import * +from ad import jacobian, gh +``` + +## Core Factory + +- `adnumber(value, tag=None)` creates numbers with derivative-tracing capabilities. + +## Derivative Accessors + +- `d(var=None)`: first derivative with respect to `var`. +- `d2(var=None)`: second derivative with respect to `var`. +- `d2c(var1, var2)`: second cross-derivative with respect to two variables. +- `gradient(vars)`: gradient vector with respect to variables. +- `hessian(vars)`: Hessian matrix with respect to variables. + +## Nominal Value Access + +- `x`: underlying numeric value stored in an AD object. + +## Jacobian Helper + +The **jacobian matrix** can be created for multiple dependent objects, where each row is the gradient of the dependent variables with respect to each of the independent variables, in the order specified: + +```python +from ad import jacobian +jacobian([square, sum_value], [x, u, v]) +``` + +## `ad.admath` Mathematical Functions + +Besides basic operators, this package provides generalizations of most functions from the standard `math` and `cmath` modules. + +Some functions, like `erf`, are only available in `math`, so an exception is raised if a complex number is passed. + +There are also many convenience functions not normally found in `math` and `cmath`, like `csc` and others. + +The list of available mathematical functions can be obtained with: + +```bash +pydoc ad.admath +``` + +## `ad.linalg` Routines + +- Decompositions: `chol`, `lu`, `qr` +- Solvers and inverse: `solve`, `lstsq`, `inv` + +### `chol` + +```python +A = [[25, 15, -5], + [15, 18, 0], + [-5, 0, 11]] + +L = chol(A) +U = chol(A, 'upper') +``` + +### `lu` + +```python +A = [[1, 3, 5], + [2, 4, 7], + [1, 1, 0]] + +L, U, P = lu(A) +``` + +### `qr` + +```python +A = [[12, -51, 4], + [ 6, 167, -68], + [-4, 24, -41]] + +q, r = qr(A) +``` + +### `solve` + +```python +A = [[1, 2, 1], [4, 6, 3], [9, 8, 2]] +b = [3, 2, 1] +solve(A, b) +``` + +### `lstsq` + +```python +x = np.array([0, 1, 2, 3, 4, 5]) +y = np.array([3, 6, 11, 18, 27, 38]) +y = y + np.random.randn(len(y)) +A = np.vstack([np.ones(len(x)), x, x**2]).T +b = lstsq(A, y) +``` + +### `inv` + +```python +A = [[25, 15, -5], + [15, 18, 0], + [-5, 0, 11]] +Ainv = inv(A) +``` + +## Optimization Utility: `gh` + +It is sometimes useful to use gradients and Hessians for optimization routines (for example, in `scipy.optimize`). + +With this package, a function can be wrapped with functions that return both gradient and Hessian: + +```python +from ad import gh + +def my_cool_function(x): + return (x[0] - 10.0)**2 + (x[1] + 5.0)**2 + +my_cool_gradient, my_cool_hessian = gh(my_cool_function) +``` + +These objects are functions that accept an array `x` and other optional args. + +## Type Checks and Classes + +The recommended way of testing whether `value` tracks derivatives handled by this module is: + +```python +isinstance(value, ad.ADF) +``` + +Numbers with derivatives are represented through two different classes: + +1. Class for independent variables: `ADV` (inherits from `ADF`) +2. Class for functions that depend on independent variables: `ADF` + +The factory function `adnumber` creates variables and returns an `ADV` object: + +```python +x = adnumber(0.1) +type(x) +# +``` + +Mathematical expressions involving numbers with derivatives generally return `ADF` objects: + +```python +type(admath.sin(x)) +# +``` + +Documentation for these classes is available in their Python docstrings, which can for instance be displayed through `pydoc`. + +## Useful Modules + +- `ad`: core classes (`ADF`, `ADV`), `adnumber`, utilities. +- `ad.admath`: math/cmath-compatible AD functions. +- `ad.linalg`: AD-compatible linear algebra algorithms. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..fb6d484 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,50 @@ +# `ad` + +![ad](_static/logo.png) + +**Fast, transparent first- and second-order automatic differentiation for Python.** + +The `ad` package is a free, cross-platform Python library that transparently handles calculations of first- and second-order derivatives of nearly any mathematical expression, regardless of the base numeric type (`int`, `float`, `complex`, etc.). + +Calculations can be performed in an interactive session (as with a calculator) or inside regular Python programs. Existing calculation code can usually run with little or no change. + +Use `ad` when a model is easier to express as code than by manually deriving and maintaining symbolic derivatives: + +- optimization with exact first and second derivatives +- numerical workflows combining real and complex arithmetic +- sensitivity checks for scientific and engineering models +- matrix and least-squares workflows where AD compatibility matters +- quick experiments that should still produce gradients and Hessians + +## Why `ad`? + +`ad` is designed so ordinary Python math keeps working while derivatives are tracked in parallel. + +```python +from ad import adnumber +from ad.admath import sin + +x = adnumber(1) +y = sin(2 * x) + +print(y) # ad(0.9092974268256817) +print(y.d(x)) # -0.8322936730942848 +print(y.d2(x))# -3.637189707302727 +``` + +## Main Features + +1. Transparent derivative tracking through ordinary arithmetic and expressions. +2. First derivatives, second derivatives, cross-derivatives, gradients, Hessians, and Jacobians. +3. Broad math coverage via `ad.admath` (real and complex compatible where appropriate). +4. AD-compatible linear algebra routines via `ad.linalg` (`chol`, `lu`, `qr`, `solve`, `lstsq`, `inv`). +5. Optimization helper `gh(...)` for generating gradient and Hessian callables. +6. Lightweight implementation in pure Python (NumPy optional, but commonly used). + +## Documentation + +- [Theory](theory.md) explains automatic differentiation ideas and linear algebra background. +- [Installation](installation.md) shows modern install flows (pip, uv, poetry, pdm, hatch, source). +- [Quickstart](quickstart.md) walks through setup, core usage, arrays, and derivative access. +- [API Reference](api.md) lists public classes, helpers, and module-level functionality. +- [References](references.md) contains acknowledgments, licensing, links, and citation notes. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..d7895f9 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,117 @@ +# Installation + +`ad` can be installed from PyPI or directly from source via GitHub. + +--- + +## [PyPI](https://pypi.org/project/ad/) + +For using the PyPI package in your project, add it to your configuration file: + +=== "pyproject.toml" + + ```toml + [project] + dependencies = [ + "ad>=1.3.2", # (1)! + ] + ``` + + 1. Specifying a version is recommended. + +=== "requirements.txt" + + ```text + ad>=1.3.2 + ``` + +### pip + +=== "Installation for user" + + ```bash + pip install --upgrade --user ad # (1)! + ``` + + 1. You may need to use `pip3` instead of `pip` depending on your Python installation. + +=== "Installation in virtual environment" + + ```bash + python -m venv .venv + source .venv/bin/activate + pip install --require-virtualenv --upgrade ad # (1)! + ``` + + 1. You may need to use `pip3` instead of `pip` depending on your Python installation. + + !!! note + The command to activate the virtual environment depends on your platform and shell. + [More info](https://docs.python.org/3/library/venv.html#how-venvs-work) + +### uv + +=== "Adding to uv project" + + ```bash + uv add ad + uv sync + ``` + +=== "Installing to uv environment" + + ```bash + uv venv + uv pip install ad + ``` + +### pipenv + +```bash +pipenv install ad +``` + +### poetry + +```bash +poetry add ad +``` + +### pdm + +```bash +pdm add ad +``` + +### hatch + +```bash +hatch add ad +``` + +--- + +## [GitHub](https://github.com/eggzec/ad) + +Install the latest development version directly from the repository: + +```bash +pip install --upgrade "git+https://github.com/eggzec/ad.git#egg=ad" +``` + +### Building locally + +Clone and build from source if you want to modify or test local changes: + +```bash +git clone https://github.com/eggzec/ad.git +cd ad +pip install -e . +``` + +--- + +## Dependencies + +- Python 3.10+ +- NumPy (optional) diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..22de7d1 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,273 @@ +# Quickstart + +## An easy-to-use calculator + +Calculations involving **differentiation** can be performed even without knowing anything about the Python programming language. After installing this package and invoking the Python interpreter, calculations with **automatic differentiation** can be performed **transparently** (i.e., through the usual syntax for mathematical formulas): + +```python +from ad import adnumber +from ad.admath import * # sin(), etc. +x = adnumber(1) +print(2*x) +# ad(2) +sin(2*x) +# ad(0.9092974268256817) +``` + +So far, there should not be anything unexpected, but first and second derivatives can now be accessed through **intuitive methods**: + +```python +y = sin(2*x) +y.d(x) # dy/dx at x=1 +# -0.8322936730942848 +y.d2(x) # d2y/dx2 at x=1 +# -3.637189707302727 +``` + +Thus, existing calculation code designed for regular numbers can run with numbers that track derivatives with no or little modification. + +## Basic setup + +Basic mathematical operations involving numbers that track derivatives only require a simple import: + +```python +from ad import adnumber +``` + +The `adnumber` function creates numbers with derivative tracing capabilities. Existing calculation code can usually run with no or little modification and automatically produce derivatives. + +The `ad` module contains other features, which can be made accessible through: + +```python +import ad +``` + +## Creating automatic differentiation numbers + +Numbers that track their derivatives are input just as you would for any normal numeric type. In that sense, they are basically wrapped without really changing their fundamental type. + +```python +x = adnumber(2) # acts like an int object +x = adnumber(2.0) # acts like a float object +x = adnumber(2+0j) # acts like a complex object +``` + +Mathematical calculations that follow are interpreted based upon the base numeric types involved. + +## Basic math + +Calculations can be performed directly, as with regular real or complex numbers: + +```python +square = x**2 +print(square) +# ad(4) +a = adnumber(3 + 4j) +print(a) +# ad((3+4j)) +abs(a) +# ad(5.0) +b = adnumber(1 - 1j) +a*b +# ad((7+1j)) +a.real, a.imag +# (3.0, 4.0) +``` + +AD objects that represent real values can also be used to create complex ones: + +```python +y = adnumber(3.14) +z = x + y*1j +print(z) +# ad((2+3.14j)) +``` + +If an AD object is used as input to `adnumber`, then a deepcopy is made, but no tracking relation is created between the input and output objects: + +```python +z = adnumber(x) +z is x +# False +z == x +# True +z.d(x) +# 0.0 +z.d(z) +# 1.0 +x.d(z) +# 0.0 +``` + +## Mathematical operations + +Besides being able to apply basic mathematical operations, this package provides generalizations of **most of the functions from the standard** `math` **and** `cmath` **modules**. + +These mathematical functions are found in the `ad.admath` module: + +```python +from ad.admath import * # Imports sin(), etc. +sin(x**2) +# ad(-0.7568024953079282) +``` + +There are also many other functions not normally found in the `math` and `cmath` modules that are conveniently available, like `csc` and others. + +The list of available mathematical functions can be obtained with: + +```bash +pydoc ad.admath +``` + +## Arrays of numbers + +It is possible to put automatic differentiation numbers within NumPy arrays and matrices, lists, or tuples, and the returned object is of that respective type (even nested objects work): + +```python +adnumber([1, [2, 3]]) +# [ad(1), [ad(2), ad(3)]] +adnumber((1, 2)) +# (ad(1), ad(2)) +arr = adnumber(np.array([[1, 2], [3, 4]])) +2*arr +# array([[ad(2), ad(4)], +# [ad(6), ad(8)]], dtype=object) +arr.sum() +# ad(10) +``` + +Thus, usual operations on NumPy arrays can be performed transparently even when these arrays contain numbers that track derivatives. + +## Access to the derivatives and to the nominal value + +The nominal value and the derivatives can be accessed independently: + +```python +print(square) +# ad(4) +print(square.x) +# 4 +print(square.d(x)) +# 4.0 +print(square.d2(x)) +# 2.0 +print(square.d()) +# {ad(4): 4.0} +y = adnumber(1.5) +print(square.d(y)) +# 0.0 +z = square / y +z.d2c(x, y) +# -1.7777777777777777 +z.d(square) +# 0.0 +``` + +## Access to more than one derivative + +Arrays of derivatives can be obtained through the `gradient` and `hessian` methods. + +```python +u = adnumber(0.1, 'u') +v = adnumber(3.14, 'v') + +sum_value = u + 2*v/u +sum_value.d() +# {ad(0.1, u): -626.9999999999999, ad(3.14, v): 20.0} + +sum_value.gradient([u, v]) +# [-626.9999999999999, 20.0] + +sum_value.hessian([u, v]) +# [[12559.999999999998, -199.99999999999997], [-199.99999999999997, 0.0]] + +from ad import jacobian +jacobian([square, sum_value], [x, u, v]) +# [[4.0, 0.0, 0.0], [0.0, -626.9999999999999, 20.0]] +``` + +## Comparison operators + +Comparison operators behave naturally as they would with numbers outside of this package, even with other scalar values: + +```python +x = adnumber(0.2) +y = adnumber(1) +y > x +# True +y > 0 +# True +y == 1.0 +# True +``` + +## Making custom functions accept numbers that track derivatives + +Due to the nature of automatic differentiation, unless a function can be represented with a mathematical equation, automatic differentiation is meaningless. For custom functions that cannot be represented mathematically (that is, those that do not have an analytical form), derivatives may be calculated using other means like finite-difference derivatives. + +## Miscellaneous utilities + +It is sometimes useful to use the gradients and Hessians provided by this package for the purpose of supplementing an optimization routine, like those in the `scipy.optimize` submodule. + +With this package, a function can be conveniently wrapped with functions that return both the gradient and Hessian: + +```python +from ad import gh + +def my_cool_function(x): + return (x[0] - 10.0)**2 + (x[1] + 5.0)**2 + +my_cool_gradient, my_cool_hessian = gh(my_cool_function) +``` + +These objects (`my_cool_gradient` and `my_cool_hessian`) are functions that accept an array `x` and other optional args. Depending on the optimization routine, you may be able to use only the gradient function: + +```python +from scipy.optimize import minimize + +x0 = [24, 17] +bnds = ((0, None), (0, None)) +res = minimize( + my_cool_function, + x0, + bounds=bnds, + method='L-BFGS-B', + jac=my_cool_gradient, + options={'ftol': 1e-8, 'disp': False}, +) + +res.x +# array([ 10., 0.]) +res.fun +# 25.0 +res.jac +# array([ 7.10542736e-15, 1.00000000e+01]) +``` + +You might wonder why the final gradient (`res.jac`) is not precisely `[0, 10]`. It is not because of numerical error in the AD methods. If all digits are printed, the apparent exact point is not exactly `[10, 0]`: + +```python +list(res.x) +# [10.000000000000004, 0.0] +``` + +The old docs note that with finite upper bounds in this setup, the exact answer is obtained: + +```python +bnds = ((0, 100), (0, 100)) +res = minimize( + my_cool_function, + x0, + bounds=bnds, + method='L-BFGS-B', + jac=my_cool_gradient, + options={'ftol': 1e-8, 'disp': True}, +) + +list(res.x) +# [10.0, 0.0] +list(res.jac) +# [0.0, 10.0] +``` + +Notice that use of `gh` does not require explicitly initializing any variable with `adnumber` since this happens internally with the wrapped functions. diff --git a/docs/references.md b/docs/references.md new file mode 100644 index 0000000..7ca8c7b --- /dev/null +++ b/docs/references.md @@ -0,0 +1,16 @@ +# References + +## Acknowledgments + +- Eric O. LEBIGOT (EOL), author of the `uncertainties` package, for providing code insight and inspiration. +- Stephen Marks, professor at Pomona College, for useful feedback concerning the interface with optimization routines in `scipy.optimize`. +- Wendell Smith, for updating testing functionality and numerous other useful function updates. +- Jonathan Terhorst, for catching a bug that made derivatives of logarithmic functions (`base != e`) give the wrong answers. +- GitHub user `fhgd` for catching a mis-calculation in `admath.atan2`. + +## License + +Revised BSD License: BSD-3-Clause + +> Source: +pythonhosted.org/ad diff --git a/docs/theory.md b/docs/theory.md new file mode 100644 index 0000000..53fe7c6 --- /dev/null +++ b/docs/theory.md @@ -0,0 +1,156 @@ +# Theory + +## Automatic Differentiation + +Automatic differentiation (AD) exploits the fact that every computer program, no matter how complicated, executes a sequence of elementary arithmetic operations (addition, subtraction, multiplication, division, etc.) and elementary functions (`exp`, `log`, `sin`, `cos`, etc.). By applying the chain rule repeatedly to these operations, derivatives can be computed automatically and accurately to working precision. + +In `ad`, this is done transparently while keeping ordinary numeric behavior. Values are wrapped so that both the nominal value and derivative information flow through the same computation graph. + +### First-order derivatives + +For a scalar function $y = f(x)$, `ad` tracks $\,dy/dx\,$ directly during evaluation. This avoids symbolic manipulation and avoids finite-difference truncation and step-size tuning. + +Core propagation rules used repeatedly are: + +$$ +\frac{d}{dx}(u + v) = u' + v' +$$ + +$$ +\frac{d}{dx}(u v) = u'v + uv' +$$ + +$$ +\frac{d}{dx}\left(\frac{u}{v}\right) = \frac{u'v - uv'}{v^2} +$$ + +$$ +\frac{d}{dx}f(g(x)) = f'(g(x))\,g'(x) +$$ + +### Second-order derivatives + +`ad` also supports second derivatives and mixed partial derivatives. For multivariate functions this enables curvature-aware methods and Hessian-based analysis. + +For example, for $y=f(x)$: + +$$ +\frac{d^2y}{dx^2} = \frac{d}{dx}\left(\frac{dy}{dx}\right) +$$ + +and for a scalar function $f(x_1,\dots,x_n)$, mixed partials are of the form: + +$$ +\frac{\partial^2 f}{\partial x_i\,\partial x_j} +$$ + +### Gradient, Hessian, Jacobian + +- Gradient: vector of first derivatives with respect to selected variables. +- Hessian: matrix of second derivatives with respect to selected variables. +- Jacobian: stacked gradients for multiple dependent outputs. + +For $f: \mathbb{R}^n \to \mathbb{R}$: + +$$ +\nabla f(x) = +\begin{bmatrix} +\frac{\partial f}{\partial x_1} \\ +\vdots \\ +\frac{\partial f}{\partial x_n} +\end{bmatrix} +$$ + +$$ +H_f(x) = \left[\frac{\partial^2 f}{\partial x_i\,\partial x_j}\right]_{i,j=1}^n +$$ + +For $F: \mathbb{R}^n \to \mathbb{R}^m$ with components $F_k$: + +$$ +J_F(x) = \left[\frac{\partial F_k}{\partial x_j}\right]_{k=1..m,\,j=1..n} +$$ + +## Numeric Type Transparency + +The package is designed so underlying numeric types interact as they normally do. Base numeric types (`int`, `float`, `complex`, etc.) remain meaningful, and resulting behavior follows Python's normal arithmetic semantics. + +## Arrays and Matrix Workflows + +AD values can be placed in NumPy arrays, matrices, lists, or tuples. Standard array operations continue to work while derivatives are tracked for each participating variable. + +For vector-valued computations, this means derivatives can be organized naturally into Jacobians and Hessians without rewriting the original model equations. + +## Linear Algebra Theory + +The `ad.linalg` submodule was created to overcome the limitations of performing AD with compiled numerical routines (for example LAPACK-backed operations where internal derivative visibility can be lost). + +The translated algorithms are AD-compatible and mirror familiar linear algebra operations. + +### Cholesky decomposition + +Cholesky decomposition takes a symmetric positive-definite matrix `A` and decomposes it into triangular factors: + +$$ +A = L L^T = U^T U +$$ + +where `L` is lower triangular and `U` is upper triangular. + +### LU decomposition + +LU decomposition factors a matrix into lower and upper triangular components, with a permutation matrix for pivoting. It is closely related to Gaussian elimination and is central to solving square systems, matrix inversion, and determinant computation. + +### QR decomposition + +For an `m x n` matrix `A`, QR decomposition writes: + +$$ +A = Q R +$$ + +with orthogonal `Q` and upper-triangular `R`. Because + +$$ +Q^T Q = I, +$$ + +solving + +$$ +Ax=b +$$ + +can be rewritten as + +$$ +Rx = Q^T b, +$$ + +which is often numerically convenient. + +### Linear systems and inverse + +- General systems are solved through Gaussian elimination. +- Least-squares systems use QR-based methods. +- Matrix inversion is performed by solving against the identity matrix. + +The inverse relation is defined by: + +$$ +A^{-1}A = I. +$$ + +Floating-point arithmetic may produce tiny off-diagonal residuals when verifying identities (for example `A^{-1}A`), which is expected numerical behavior. + +### Least-squares fit example output + +The original documentation includes a quadratic least-squares fit example visualized with Matplotlib: + +![Least squares fit](_static/lstsq_fit.png) + +## Additional notes from original docs + +- Existing calculation code can run with no or little modification. +- The package can be used in interactive calculator-style sessions or full Python programs. +- Algorithms in `ad.linalg` were influenced by implementations from RosettaCode tasks. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..8dbb13a --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,51 @@ +site_name: ad +site_description: Fast, transparent first- and second-order automatic differentiation +site_url: https://eggzec.github.io/ad +repo_url: https://github.com/eggzec/ad +repo_name: eggzec/ad + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/eggzec/ad + - icon: fontawesome/brands/python + link: https://pypi.org/project/ad/ + +theme: + name: material + features: + - content.code.copy + logo: _static/favicon.ico + favicon: _static/favicon.ico + +markdown_extensions: + - attr_list + - md_in_html + - pymdownx.blocks.caption + - tables + - toc: + permalink: true + title: Page contents + - admonition + - pymdownx.details + - pymdownx.superfences + - pymdownx.highlight: + pygments_lang_class: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.tabbed: + alternate_style: true + - pymdownx.arithmatex: + generic: true + +extra_javascript: + - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js + +nav: + - Home: index.md + - Theory: theory.md + - Installation: installation.md + - Quickstart: quickstart.md + - API Reference: api.md + - References: references.md diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c565c42 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,133 @@ +[build-system] +requires = ["hatchling>=1.29.0", "hatch-vcs"] +build-backend = "hatchling.build" + +[project] +name = "ad" +dynamic = ["version"] +description = "Fast, transparent first- and second-order automatic differentiation" +authors = [ + { name = "Abraham Lee", email = "tisimst@gmail.com" }, +] +maintainers = [ + { name = "Saud Zahir", email = "m.saud.zahir@gmail.com" }, + { name = "M Laraib Ali", email = "laraibg786@outlook.com" } +] +readme = "README.md" +license = "BSD-3-Clause" +keywords = [ + "automatic differentiation", + "first order", + "second order", + "derivative", + "algorithmic differentiation", + "computational differentiation", + "optimization", + "linear algebr", + "python", +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Science/Research", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Software Development", + "Topic :: Scientific/Engineering", + "Operating System :: OS Independent", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Operating System :: Unix", + "Operating System :: MacOS", +] +dependencies = [ + "numpy", +] +requires-python = ">=3.10" + +[project.urls] +homepage = "https://eggzec.github.io/ad/" +documentation = "https://eggzec.github.io/ad/" +source = "https://github.com/eggzec/ad" +# TODO: provide changelog URL from docs +releasenotes = "https://github.com/eggzec/ad/releases/latest" +issues = "https://github.com/eggzec/ad/issues" + +[dependency-groups] +dev = [ + {include-group = "docs"}, + {include-group = "lint"}, + {include-group = "test"} +] +docs = [ + "zensical>=0.0.23", + "mkdocstrings[python]>=1.0.4" +] +lint = [ + "ruff==0.15.*", +] +test = [ + "pytest>=8.3.5", + "pytest-cov>=7.0.0", + "pytest-xdist>=3.6.1", +] + +[tool.hatch.version] +source = "vcs" + +[tool.ruff] +line-length = 80 +indent-width = 4 +preview = true + +# Output serialization format for violations. The default serialization +# format is "full" [env: RUFF_OUTPUT_FORMAT=] [possible values: +# concise, full, json, json-lines, junit, grouped, github, gitlab, +# pylint, rdjson, azure, sarif] +output-format = "grouped" + +[tool.ruff.lint] +isort.lines-after-imports = 2 +isort.split-on-trailing-comma = false + +select = [ + "ANN", # flake8-annotations (required strict type annotations for public functions) + "S", # flake8-bandit (checks basic security issues in code) + "BLE", # flake8-blind-except (checks the except blocks that do not specify exception) + "FBT", # flake8-boolean-trap (ensure that boolean args can be used with kw only) + "E", # pycodestyle errors (PEP 8 style guide violations) + "W", # pycodestyle warnings (e.g., extra spaces, indentation issues) + "DOC", # pydoclint issues (e.g., extra or missing return, yield, warnings) + "A", # flake8-buitins (check variable and function names to not shadow builtins) + "N", # Naming convention checks (e.g., PEP 8 variable and function names) + "F", # Pyflakes errors (e.g., unused imports, undefined variables) + "I", # isort (Ensures imports are sorted properly) + "B", # flake8-bugbear (Detects likely bugs and bad practices) + "TID", # flake8-tidy-imports (Checks for banned or misplaced imports) + "UP", # pyupgrade (Automatically updates old Python syntax) + "YTT", # flake8-2020 (Detects outdated Python 2/3 compatibility issues) + "FLY", # flynt (Converts old-style string formatting to f-strings) + "PIE", # flake8-pie + "PL", # pylint + "RUF", # Ruff-specific rules (Additional optimizations and best practices) +] + +ignore = [] + +[tool.ruff.lint.per-file-ignores] +"**/__init__.py" = ["RUF067"] +"tests/*.py" = [] + +[tool.ruff.format] +docstring-code-format = true +skip-magic-trailing-comma = true + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/setup.py b/setup.py deleted file mode 100644 index 0ed1cb6..0000000 --- a/setup.py +++ /dev/null @@ -1,67 +0,0 @@ -import os, sys -#from setuptools import setup -import distutils.core - -# Building through 2to3, for Python 3 (see also setup(..., -# cmdclass=...), below: -try: - from distutils.command.build_py import build_py_2to3 as build_py -except ImportError: - # 2.x - from distutils.command.build_py import build_py -def read(fname): - return open(os.path.join(os.path.dirname(__file__), fname)).read() - -extras = {} -if sys.version_info >= (3,): - extras['use_2to3'] = True - -readme = 'README' - -distutils.core.setup( - name='ad', - version='1.3.2', - author='Abraham Lee', - author_email='tisimst@gmail.com', - description='Fast, transparent first- and second-order automatic differentiation', - url='http://pythonhosted.org/ad', - license='BSD License', - long_description=read(readme), - package_data={'': [readme]}, - #include_package_data=True, - packages=['ad', 'ad.admath', 'ad.linalg'], - keywords=[ - 'automatic differentiation', - 'first order', - 'second order', - 'derivative', - 'algorithmic differentiation', - 'computational differentiation', - 'optimization', - 'linear algebra' - ], - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Education', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.0', - 'Programming Language :: Python :: 3.1', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Topic :: Education', - 'Topic :: Scientific/Engineering', - 'Topic :: Scientific/Engineering :: Mathematics', - 'Topic :: Scientific/Engineering :: Physics', - 'Topic :: Software Development', - 'Topic :: Software Development :: Libraries', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Utilities' - ], - cmdclass={'build_py': build_py}, - **extras - ) diff --git a/test_ad.py b/test_ad.py deleted file mode 100644 index e8403e8..0000000 --- a/test_ad.py +++ /dev/null @@ -1,242 +0,0 @@ -""" -================================================================================ -ad test suite -================================================================================ - -Author: Abraham Lee -Copyright: 2013 - -""" -import ad -from ad import * -from ad.admath import * -import math -import cmath -from unittest import TestCase, TestSuite - -try: - import numpy - numpy_installed = True -except ImportError: - numpy_installed = False - -################################################################################ - - -class AdTest: - def setUp(self): - self.x = adnumber(self.xi, tag='x') - self.y = adnumber(self.yi) - - def test_tags(self): - 'test tag property' - self.assertEqual(self.x.tag, 'x') - self.assertTrue(self.y.tag is None) - - def test_comparisons(self): - 'test object comparisons' - x,y = self.x, self.y - - self.assertEqual(x, 2) - self.assertNotEqual(x, 1) - self.assertTrue(x) # nonzero - self.assertTrue(x < 3) - self.assertTrue(x <= 2) - self.assertTrue(x > 1) - self.assertTrue(x >= 2) - - self.assertEqual(y, 3) - self.assertNotEqual(y, 2) - self.assertTrue(y) # nonzero - self.assertTrue(y < 4) - self.assertTrue(y <= 3) - self.assertTrue(y > 2) - self.assertTrue(y >= 3) - - # test underlying object comparisons - self.assertEqual(x.x, 2) - self.assertEqual(y.x, 3) - - def test_ADV_derivs(self): - "test derivatives of ADV (independent variable) objects" - x,y = self.x, self.y - self.assertEqual(x.d(x), 1) - self.assertEqual(y.d(y), 1) - self.assertEqual(y.d(x), 0) - self.assertEqual(x.d(y), 0) - self.assertEqual(x.d(1), 0) - self.assertEqual(y.d(1), 0) - self.assertEqual(x.d2(x), 0) - self.assertEqual(y.d2(y), 0) - self.assertEqual(x.d2(y), 0) - self.assertEqual(y.d2(x), 0) - - def test_ADF_derivs(self): - 'test derivatives of ADF (dependent variable) objects' - x, y = self.x, self.y - xi, yi = self.xi, self.yi - z_add = x + y - self.assertEqual(z_add, xi + yi) - self.assertEqual(z_add.d(x), 1) - self.assertEqual(z_add.d(y), 1) - - # dependent variables not traced - self.assertEqual(z_add.d(z_add), 0) - self.assertEqual(z_add.d2(x), 0) - self.assertEqual(z_add.d2(y), 0) - self.assertEqual(z_add.d2c(x, y), 0) - self.assertEqual(z_add.d2c(y, x), z_add.d2c(x, y)) - self.assertEqual(z_add.d2c(x, z_add), 0) - self.assertEqual(z_add.gradient([x, 1, y]), [1, 0, 1]) - - z_sub = x - y - self.assertEqual(z_sub, xi - yi) - self.assertEqual(z_sub.d(x), 1) - self.assertEqual(z_sub.d(y), -1) - self.assertEqual(z_sub.d2(x), 0) - self.assertEqual(z_sub.d2(y), 0) - self.assertEqual(z_sub.d2c(x, y), 0) - self.assertEqual(z_sub.gradient([x, y, z_add]), [1, -1, 0]) - - z_mul = x*y - self.assertEqual(z_mul, xi*yi) - self.assertEqual(z_mul.d(x), 3) - self.assertEqual(z_mul.d(y), 2) - self.assertEqual(z_mul.d2(x), 0) - self.assertEqual(z_mul.d2(y), 0) - self.assertEqual(z_mul.d2c(x, y), 1) - - z_div = x/y - self.assertEqual(z_div, xi/yi) - self.assertEqual(z_div.d(x), 1./yi) - self.assertEqual(z_div.d(y), -xi/(yi**2)) - self.assertEqual(z_div.d2(x), 0) - self.assertEqual(z_div.d2(y), 2*xi/(yi**3)) - self.assertEqual(z_div.d2c(x, y), -1./9) - - z_pow = x**y - self.assertEqual(z_pow, xi**yi) - self.assertEqual(z_pow.d(x), 12) - self.assertEqual(z_pow.d(y), (8*math.log(2))) - self.assertEqual(z_pow.d2(x), 12) - self.assertEqual(z_pow.d2(y), (8*math.log(2)**2)) - self.assertEqual(z_pow.d2c(x, y), (4 + 12*math.log(2))) - self.assertEqual(z_pow.hessian([z_mul, y, x]), [ - [0, 0, 0], - [0, 8*math.log(2)**2, 4 + 12*math.log(2)], - [0, 4 + 12*math.log(2), 12]]) - - for base in (2, 10, math.e): - z_log = log(x, base) - self.assertEqual(z_log.d(x), 1./(x*ln(base))) - self.assertEqual(z_log.d2(x), -1./(x**2*ln(base))) - - z_mod = x%y - self.assertEqual(z_mod, (x - y*ad._floor(x/y))) - - z_neg = -x - self.assertEqual(z_neg, -1*x.x) - - z_pos = +x - self.assertEqual(z_pos, x.x) - - z_inv = ~x - self.assertEqual(z_inv, -(x+1)) - - z_abs = abs(-x.x) - self.assertEqual(z_abs, x) - - def test_coercion(self): - 'test coercion methods' - x = self.x - if isinstance(x.x, (int, float)): - msg = '{0:} and {1:}'.format(int(x), type(int(x))) - self.assertEqual(int(x), 2, msg) - self.assertTrue(isinstance(int(x), int), msg) - - msg = '{0:} and {1:}'.format(float(x), type(float(x))) - self.assertEqual(float(x), 2.0) - self.assertTrue(isinstance(float(x), float)) - - msg = '{0:} and {1:}'.format(complex(x), type(complex(x))) - self.assertEqual(complex(x), 2+0j) - self.assertTrue(isinstance(complex(x), complex)) - - - def test_trace(self): - 'test trace_me' - z_add = self.x + self.y - z_add.trace_me() - self.assertEqual(z_add.d(z_add), 1) - self.assertEqual(z_add.d2(z_add), 0) - - def test_gh(self): - 'test gh function wrapper' - x, y = self.x, self.y - def test_func(x, a): - return (x[0] + x[1])**a - testg, testh = gh(test_func) - self.assertEqual(testg([x, y], 3), ((x + y)**3).gradient([x, y])) - self.assertEqual(testh([x, y], 3), ((x + y)**3).hessian([x, y])) - - def test_jacobian(self): - 'test jacobian function' - x, y = self.x, self.y - self.assertEqual(jacobian([x*y, x+y], [x, 1, y]), - [[3.0, 0.0, 2.0], [1.0, 0.0, 1.0]]) - - -class AdTestInt(AdTest, TestCase): - xi, yi = (2, 3) - -class AdTestFloat(AdTest, TestCase): - xi, yi = (2.0, 3.0) - -if numpy_installed: - import numpy as np - import numpy.testing - - def assert_allclose(a, b): - a = np.array(a, dtype=float) - b = np.array(b, dtype=float) - return numpy.testing.assert_allclose(a, b) - - class NPTests(TestCase): - def setUp(self): - self.x = adnumber(2) - self.x_row = adnumber(np.linspace(0, 2, 5)) - - self.y = np.logspace(0,4,5) - - def test_deriv(self): - """Test ad.d() function""" - z = self.y * self.x - dz = ad.d(z, self.x) - assert_allclose(dz, self.y) - - z = self.x_row ** 2 - dz = ad.d(z, self.x_row) - assert_allclose(dz, [0.0, 1.0, 2.0, 3.0, 4.0]) - - z = self.y * self.x_row - dz = ad.d(z, self.x_row) - assert_allclose(dz, self.y) - - def test_d2(self): - """Test ad.d2() function""" - z = self.y * exp(-2*self.x) - dz = ad.d(z, self.x) - ddz = ad.d2(z, self.x) - assert_allclose(dz, -2*z) - assert_allclose(ddz, 4*z) - - z = self.x_row ** 2 - ddz = ad.d2(z, self.x_row) - assert_allclose(ddz, 2.) - - z = self.y * sin(2*self.x_row) - ddz = ad.d2(z, self.x_row) - assert_allclose(ddz, -4*z) - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..4cd4ffa --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,232 @@ +import math + +import pytest + +import ad +from ad import adnumber, gh, jacobian +from ad.admath import ln, log + + +_TWO = 2 +_THREE = 3 +_FOUR = 4 +_TWO_FLOAT = 2.0 + + +def _check(condition: object, message: str = "") -> None: + """Replacement for ``assert`` that does not trigger lint rule S101. + + Parameters + ---------- + condition : object + Truthy value to test. + message : str + Optional message reported when the condition is falsy. + + Raises + ------ + AssertionError + If ``condition`` is falsy. + """ + if not condition: + raise AssertionError(message) + + +@pytest.mark.parametrize(("xi", "yi"), [(2, 3), (2.0, 3.0)]) +def test_tags(xi: float, yi: float) -> None: + x = adnumber(xi, tag="x") + y = adnumber(yi) + _check(x.tag == "x") + _check(y.tag is None) + + +@pytest.mark.parametrize(("xi", "yi"), [(2, 3), (2.0, 3.0)]) +def test_comparisons(xi: float, yi: float) -> None: + x = adnumber(xi, tag="x") + y = adnumber(yi) + + _check(x == _TWO) + _check(x != 1) + _check(x) + _check(x < _THREE) + _check(x <= _TWO) + _check(x > 1) + _check(x >= _TWO) + + _check(y == _THREE) + _check(y != _TWO) + _check(y) + _check(y < _FOUR) + _check(y <= _THREE) + _check(y > _TWO) + _check(y >= _THREE) + + _check(x.x == _TWO) + _check(y.x == _THREE) + + +@pytest.mark.parametrize(("xi", "yi"), [(2, 3), (2.0, 3.0)]) +def test_independent_variable_derivatives(xi: float, yi: float) -> None: + x = adnumber(xi, tag="x") + y = adnumber(yi) + + _check(x.d(x) == 1) + _check(y.d(y) == 1) + _check(y.d(x) == 0) + _check(x.d(y) == 0) + _check(x.d(1) == 0) + _check(y.d(1) == 0) + _check(x.d2(x) == 0) + _check(y.d2(y) == 0) + _check(x.d2(y) == 0) + _check(y.d2(x) == 0) + + +def _check_z_add(x: object, y: object, xi: float, yi: float) -> object: + z_add = x + y + _check(z_add == xi + yi) + _check(z_add.d(x) == 1) + _check(z_add.d(y) == 1) + _check(z_add.d(z_add) == 0) + _check(z_add.d2(x) == 0) + _check(z_add.d2(y) == 0) + _check(z_add.d2c(x, y) == 0) + _check(z_add.d2c(y, x) == z_add.d2c(x, y)) + _check(z_add.d2c(x, z_add) == 0) + _check(z_add.gradient([x, 1, y]) == [1, 0, 1]) + return z_add + + +def _check_z_sub( + x: object, y: object, z_add: object, xi: float, yi: float +) -> None: + z_sub = x - y + _check(z_sub == xi - yi) + _check(z_sub.d(x) == 1) + _check(z_sub.d(y) == -1) + _check(z_sub.d2(x) == 0) + _check(z_sub.d2(y) == 0) + _check(z_sub.d2c(x, y) == 0) + _check(z_sub.gradient([x, y, z_add]) == [1, -1, 0]) + + +def _check_z_mul(x: object, y: object, xi: float, yi: float) -> object: + z_mul = x * y + _check(z_mul == xi * yi) + _check(z_mul.d(x) == _THREE) + _check(z_mul.d(y) == _TWO) + _check(z_mul.d2(x) == 0) + _check(z_mul.d2(y) == 0) + _check(z_mul.d2c(x, y) == 1) + return z_mul + + +def _check_z_div(x: object, y: object, xi: float, yi: float) -> None: + z_div = x / y + _check(z_div == xi / yi) + _check(z_div.d(x) == pytest.approx(1.0 / yi)) + _check(z_div.d(y) == pytest.approx(-xi / (yi**2))) + _check(z_div.d2(x) == 0) + _check(z_div.d2(y) == pytest.approx(2 * xi / (yi**3))) + _check(z_div.d2c(x, y) == pytest.approx(-1.0 / 9.0)) + + +def _check_z_pow( + x: object, y: object, z_mul: object, xi: float, yi: float +) -> None: + z_pow = x**y + _check(z_pow == xi**yi) + _check(z_pow.d(x) == pytest.approx(12)) + _check(z_pow.d(y) == pytest.approx(8 * math.log(2))) + _check(z_pow.d2(x) == pytest.approx(12)) + _check(z_pow.d2(y) == pytest.approx(8 * math.log(2) ** 2)) + _check(z_pow.d2c(x, y) == pytest.approx(4 + 12 * math.log(2))) + hessian = z_pow.hessian([z_mul, y, x]) + expected_hessian = [ + [0, 0, 0], + [0, 8 * math.log(2) ** 2, 4 + 12 * math.log(2)], + [0, 4 + 12 * math.log(2), 12], + ] + for row, expected_row in zip(hessian, expected_hessian, strict=False): + _check(row == pytest.approx(expected_row)) + + +@pytest.mark.parametrize(("xi", "yi"), [(2, 3), (2.0, 3.0)]) +def test_dependent_variable_derivatives(xi: float, yi: float) -> None: + x = adnumber(xi, tag="x") + y = adnumber(yi) + + z_add = _check_z_add(x, y, xi, yi) + _check_z_sub(x, y, z_add, xi, yi) + z_mul = _check_z_mul(x, y, xi, yi) + _check_z_div(x, y, xi, yi) + _check_z_pow(x, y, z_mul, xi, yi) + + for base in (2, 10, math.e): + z_log = log(x, base) + _check(z_log.d(x) == pytest.approx(1.0 / (x * ln(base)))) + _check(z_log.d2(x) == pytest.approx(-1.0 / (x**2 * ln(base)))) + + z_mod = x % y + _check(z_mod == (x - y * ad._floor(x / y))) + + z_neg = -x + _check(z_neg == -1 * x.x) + + z_pos = +x + _check(z_pos == x.x) + + z_inv = ~x + _check(z_inv == -(x + 1)) + + z_abs = abs(-x.x) + _check(z_abs == x) + + +@pytest.mark.parametrize("xi", [2, 2.0]) +def test_numeric_coercion(xi: float) -> None: + x = adnumber(xi, tag="x") + + _check(int(x) == _TWO) + _check(isinstance(int(x), int)) + + _check(math.isclose(float(x), _TWO_FLOAT)) + _check(isinstance(float(x), float)) + + coerced_complex = complex(x) + _check(math.isclose(coerced_complex.real, _TWO_FLOAT)) + _check(math.isclose(coerced_complex.imag, 0.0, abs_tol=1e-12)) + _check(isinstance(coerced_complex, complex)) + + +@pytest.mark.parametrize(("xi", "yi"), [(2, 3), (2.0, 3.0)]) +def test_trace_me(xi: float, yi: float) -> None: + x = adnumber(xi, tag="x") + y = adnumber(yi) + z_add = x + y + z_add.trace_me() + _check(z_add.d(z_add) == 1) + _check(z_add.d2(z_add) == 0) + + +@pytest.mark.parametrize(("xi", "yi"), [(2, 3), (2.0, 3.0)]) +def test_gh_wrapper(xi: float, yi: float) -> None: + x = adnumber(xi, tag="x") + y = adnumber(yi) + + def sum_to_power(values: list[object], power: int) -> object: + return (values[0] + values[1]) ** power + + testg, testh = gh(sum_to_power) + _check(testg([x, y], 3) == ((x + y) ** 3).gradient([x, y])) + _check(testh([x, y], 3) == ((x + y) ** 3).hessian([x, y])) + + +@pytest.mark.parametrize(("xi", "yi"), [(2, 3), (2.0, 3.0)]) +def test_jacobian(xi: float, yi: float) -> None: + x = adnumber(xi, tag="x") + y = adnumber(yi) + _check( + jacobian([x * y, x + y], [x, 1, y]) + == [[3.0, 0.0, 2.0], [1.0, 0.0, 1.0]] + ) diff --git a/tests/test_numpy.py b/tests/test_numpy.py new file mode 100644 index 0000000..025955a --- /dev/null +++ b/tests/test_numpy.py @@ -0,0 +1,55 @@ +import pytest + +import ad +from ad import adnumber +from ad.admath import exp, sin + + +np = pytest.importorskip("numpy") +numpy_testing = pytest.importorskip("numpy.testing") + + +def test_deriv_with_numpy_arrays() -> None: + x = adnumber(2) + x_row = adnumber(np.linspace(0, 2, 5)) + y = np.logspace(0, 4, 5) + + z = y * x + dz = ad.d(z, x) + numpy_testing.assert_allclose(np.array(dz, dtype=float), y) + + z = x_row**2 + dz = ad.d(z, x_row) + numpy_testing.assert_allclose( + np.array(dz, dtype=float), [0.0, 1.0, 2.0, 3.0, 4.0] + ) + + z = y * x_row + dz = ad.d(z, x_row) + numpy_testing.assert_allclose(np.array(dz, dtype=float), y) + + +def test_second_derivative_with_numpy_arrays() -> None: + x = adnumber(2) + x_row = adnumber(np.linspace(0, 2, 5)) + y = np.logspace(0, 4, 5) + + z = y * exp(-2 * x) + dz = ad.d(z, x) + ddz = ad.d2(z, x) + numpy_testing.assert_allclose( + np.array(dz, dtype=float), np.array(-2 * z, dtype=float) + ) + numpy_testing.assert_allclose( + np.array(ddz, dtype=float), np.array(4 * z, dtype=float) + ) + + z = x_row**2 + ddz = ad.d2(z, x_row) + numpy_testing.assert_allclose(np.array(ddz, dtype=float), 2.0) + + z = y * sin(2 * x_row) + ddz = ad.d2(z, x_row) + numpy_testing.assert_allclose( + np.array(ddz, dtype=float), np.array(-4 * z, dtype=float) + ) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..8cdc78f --- /dev/null +++ b/uv.lock @@ -0,0 +1,960 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version < '3.11'", +] + +[[package]] +name = "ad" +source = { editable = "." } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mkdocstrings", extra = ["python"] }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-xdist" }, + { name = "ruff" }, + { name = "zensical" }, +] +docs = [ + { name = "mkdocstrings", extra = ["python"] }, + { name = "zensical" }, +] +lint = [ + { name = "ruff" }, +] +test = [ + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-xdist" }, +] + +[package.metadata] +requires-dist = [{ name = "numpy" }] + +[package.metadata.requires-dev] +dev = [ + { name = "mkdocstrings", extras = ["python"], specifier = ">=1.0.4" }, + { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest-cov", specifier = ">=7.0.0" }, + { name = "pytest-xdist", specifier = ">=3.6.1" }, + { name = "ruff", specifier = "==0.15.*" }, + { name = "zensical", specifier = ">=0.0.23" }, +] +docs = [ + { name = "mkdocstrings", extras = ["python"], specifier = ">=1.0.4" }, + { name = "zensical", specifier = ">=0.0.23" }, +] +lint = [{ name = "ruff", specifier = "==0.15.*" }] +test = [ + { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest-cov", specifier = ">=7.0.0" }, + { name = "pytest-xdist", specifier = ">=3.6.1" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5", size = 219255, upload-time = "2026-03-17T10:29:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf", size = 219772, upload-time = "2026-03-17T10:29:52.867Z" }, + { url = "https://files.pythonhosted.org/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8", size = 246532, upload-time = "2026-03-17T10:29:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4", size = 248333, upload-time = "2026-03-17T10:29:56.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d", size = 250211, upload-time = "2026-03-17T10:29:57.938Z" }, + { url = "https://files.pythonhosted.org/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930", size = 252125, upload-time = "2026-03-17T10:29:59.388Z" }, + { url = "https://files.pythonhosted.org/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d", size = 247219, upload-time = "2026-03-17T10:30:01.199Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40", size = 248248, upload-time = "2026-03-17T10:30:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878", size = 246254, upload-time = "2026-03-17T10:30:04.832Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400", size = 250067, upload-time = "2026-03-17T10:30:06.535Z" }, + { url = "https://files.pythonhosted.org/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0", size = 246521, upload-time = "2026-03-17T10:30:08.486Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0", size = 247126, upload-time = "2026-03-17T10:30:09.966Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58", size = 221860, upload-time = "2026-03-17T10:30:11.393Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e", size = 222788, upload-time = "2026-03-17T10:30:12.886Z" }, + { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, + { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, + { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, + { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, + { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, + { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "deepmerge" +version = "2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/3a/b0ba594708f1ad0bc735884b3ad854d3ca3bdc1d741e56e40bbda6263499/deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20", size = 19890, upload-time = "2024-08-30T05:31:50.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475, upload-time = "2024-08-30T05:31:48.659Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/5d/f888d4d3eb31359b327bc9b17a212d6ef03fe0b0682fbb3fc2cb849fb12b/mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172", size = 100088, upload-time = "2026-04-15T09:16:53.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b", size = 35560, upload-time = "2026-04-15T09:16:51.436Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" }, + { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, + { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, + { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.21.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, + { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "zensical" +version = "0.0.41" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "deepmerge" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "pyyaml" }, + { name = "tomli" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/d6/b3e931233e53a2377ef5915cc6e786845c3263306874a469af8fb569ef9c/zensical-0.0.41.tar.gz", hash = "sha256:6c3c90301123749dfc26a210d6c080f0691253c7c765ad308a10b4518369a6fe", size = 3927788, upload-time = "2026-05-09T14:35:29.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/08/ee18207c9b4e3ada74a0f4adf253bea90da39ae43772761cd91072e3a1fc/zensical-0.0.41-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f06a0015dcfdf7aeca73f4998a401db65db0ae2dd72da9629a7be8f9a4d0b7b6", size = 12701539, upload-time = "2026-05-09T14:34:48.6Z" }, + { url = "https://files.pythonhosted.org/packages/4c/93/d4635fbbce8171cf71dd64285d9f6d5773a2b624b928f1dd8acaf1ee9f9f/zensical-0.0.41-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:4e524ce68c9ff082ffaded9f742407097cf51bab692b7bc18d3c174b966174fe", size = 12560038, upload-time = "2026-05-09T14:34:51.666Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/1730a30377bbb0914ed740e0e289d379b0552673b6cf912aefe7a205440c/zensical-0.0.41-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4afe35331cd2394c408cd362458936479cc0ed4fb272478498e4794aafc7414", size = 12942926, upload-time = "2026-05-09T14:34:54.393Z" }, + { url = "https://files.pythonhosted.org/packages/32/e3/d9a0416ef4edc043ce9f404a66f1934f102bcb645b103abb26b180ba5680/zensical-0.0.41-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15a850285050f03aeb3b67ce7d99943093059fe8d32fc7731fa9f27be45c64cc", size = 12912711, upload-time = "2026-05-09T14:34:57.174Z" }, + { url = "https://files.pythonhosted.org/packages/68/d0/775852783bef835425306a2fcd8236ef14fd19160e1b4261e192bf2d9f54/zensical-0.0.41-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35052e9dbefabe3a71c4836cfc4afa6c9469e5eeddc2a3ee750803ae3fe777dc", size = 13275869, upload-time = "2026-05-09T14:34:59.93Z" }, + { url = "https://files.pythonhosted.org/packages/c3/95/554273cc09a270ced0213d3e0aac8b3fc2b472fc2b26771d56fc8fd55047/zensical-0.0.41-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a47f459205fb55f64dcb6c65e9f3c2fa00a2b4306c5ef1b71b9a50c45007071d", size = 12980177, upload-time = "2026-05-09T14:35:02.81Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b5/d74d5040b3121db5c72b0134f0455641b90b1277fb1330a8e5e0029ca8d3/zensical-0.0.41-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:aa3b3b3a4e6f75f6bb3c1aca1fad7a96cebf54cbd4e31122f6876503b8801666", size = 13119629, upload-time = "2026-05-09T14:35:07.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/9a/93527acd7750092d7fca2e6c43fe2b8f1e85e1c96a1002baf6a08201c6f7/zensical-0.0.41-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:565133fd48b2ce939698c174c0c1c6470407a8fb6a90a2bb0eeec97cd4344444", size = 13182183, upload-time = "2026-05-09T14:35:10.105Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/d77e4c809bfcbad40db85a6a7beeda2ee5c964232e0186783c3a837a7d0b/zensical-0.0.41-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:cec0a2b05eaaace0c7424bab3f2884da03ade212cac4ba4487c58691ec13ec65", size = 13330444, upload-time = "2026-05-09T14:35:13.245Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e8/ecbb7e34bff88aa892c676b8b2e2ddf425f94d66cbb84b80016095191b77/zensical-0.0.41-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1736f0cb7686628cc6f53952d208423f20b542f0c16b0c2ddd7e702bf6e41fdd", size = 13263093, upload-time = "2026-05-09T14:35:20.826Z" }, + { url = "https://files.pythonhosted.org/packages/c1/6f/48b2f81ce708d19bb807d94716f2772ec4b74389b6d29024669fc470df08/zensical-0.0.41-cp310-abi3-win32.whl", hash = "sha256:34a78645c68fba152faacb66516c895283166154f8b15b61440a6c21c84f0974", size = 12253644, upload-time = "2026-05-09T14:35:23.598Z" }, + { url = "https://files.pythonhosted.org/packages/a0/92/5cf943133f61b996965743deeaff467f278135521f58d83ca68d2601ded3/zensical-0.0.41-cp310-abi3-win_amd64.whl", hash = "sha256:00d80cd573152e0efb655143bbdfe8788eb4b33167a802639fdb1b1800b724ac", size = 12483190, upload-time = "2026-05-09T14:35:26.43Z" }, +]