diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b47ad1889..8717de9c9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,5 +10,5 @@ updates: schedule: interval: "daily" open-pull-requests-limit: 99 - target-branch: "update-v1" + target-branch: "master" versioning-strategy: lockfile-only diff --git a/.github/workflows/elasticapp.yml b/.github/workflows/elasticapp.yml new file mode 100644 index 000000000..8fbc24d55 --- /dev/null +++ b/.github/workflows/elasticapp.yml @@ -0,0 +1,61 @@ +name: elasticapp (Elastica++ based backend) tests + +# trigger run only on changes to the backend folder. +on: + push: + paths: + - backend/** + pull_request: + paths: + - backend/** + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: ["3.11"] #, "3.12"] + os: [ubuntu-latest] # , macos-latest] + include: + - os: ubuntu-latest + path: ~/.cache/pip + defaults: + run: + shell: bash + + steps: + - uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + uv-version: latest + - name: Compile OpenMP + env: + OMP_NUM_THREADS: 2 + run: | + sudo apt-get update; sudo apt-get install -y libomp5 libomp-dev + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Set up cache + uses: actions/cache@v5 + with: + path: ${{ matrix.path }} + key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }}-${{ hashFiles('uv.lock') }} + restore-keys: | + ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }}-${{ hashFiles('uv.lock') }} + ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }} + + - name: Install PyElastica and dependencies + run: | + make install-dev-deps PYTHON_VERSION=${{ matrix.python-version }} + uv cache prune --ci + + - name: Run elasticapp tests + run: | + source .venv/bin/activate + cd backend + make clean-build + make test + # pytest backend/tests/py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 989f99642..8acf2e14a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,14 +3,6 @@ name: CI # Controls when the action will run. on: [push, pull_request] -# Older settings: -# Triggers the workflow on push request events for the master branch, -# and pull request events for all branches. -#on: -# push: -# branches: [ master ] -# pull_request: -# branches: [ '**' ] # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -52,7 +44,7 @@ jobs: run: python -c "import sys; print(sys.version)" # Set up cache - name: Set up cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ matrix.path }} key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }}-${{ hashFiles('uv.lock') }} @@ -62,7 +54,7 @@ jobs: # Install dependencies - name: Install dependencies run: | - make install-dev-deps + make install-dev-deps PYTHON_VERSION=${{ matrix.python-version }} uv cache prune --ci source .venv/bin/activate # Runs a single command using the runners shell @@ -102,7 +94,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Set up cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ matrix.path }} key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }}-${{ hashFiles('uv.lock') }} @@ -111,7 +103,7 @@ jobs: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }} - name: Install dependencies run: | - make install-dev-deps + make install-dev-deps PYTHON_VERSION=${{ matrix.python-version }} uv cache prune --ci source .venv/bin/activate - name: Test PyElastica using pytest @@ -119,6 +111,6 @@ jobs: run: | make test_coverage_xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml deleted file mode 100644 index 86f70c90c..000000000 --- a/.github/workflows/publish-to-pypi.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: publish - -on: - release: - types: [created] - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - uv-version: latest - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - name: Build - run: | - make build - - name: Publish distribution 📦 to PyPI - if: startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.gitignore b/.gitignore index da20c6d3e..cba0be5fe 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,10 @@ __pycache__/ *.so *.swp +.vscode +docs/_gallery +docs/gen_modules +docs/sg_execution_times.rst # Distribution / packaging .Python @@ -215,6 +219,7 @@ sample_prog.py # txt files *.txt +!CMakelists.txt # movie or video file formats *.mp4 @@ -237,3 +242,6 @@ outcmaes/* # csv files *.csv + +# ./backend dependencies +deps diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 43ebcfe21..deeafcc14 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,20 @@ default_language_version: python: python3 -default_stages: [commit, push] +default_stages: [pre-commit, pre-push] repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.5.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: check-json - id: check-yaml + - id: check-toml - id: end-of-file-fixer exclude: LICENSE + - id: check-added-large-files + - id: mixed-line-ending - repo: local hooks: diff --git a/.readthedocs.yaml b/.readthedocs.yaml index e0a0cc28c..cb878e965 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -4,6 +4,8 @@ version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-24.04 + apt_packages: + - ffmpeg tools: { python: "3.10" } jobs: create_environment: @@ -14,6 +16,7 @@ build: install: - "true" + # Build documentation in the docs/ directory with Sphinx sphinx: builder: html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e4108c526..9358a501b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -121,7 +121,7 @@ Please don't hesitate improving [documentation](https://github.com/GazzolaLab/Py We also have many related projects in separate repositories that utilize the PyElastica as a core library. Since the package is often used for research purpose, many on-going projects are typically not published. -If you are interested in hearing more, please contact one of our the maintainer. +If you are interested in hearing more, please contact the maintainer. ### Pull requests diff --git a/Makefile b/Makefile index 523ab386a..7be633efc 100644 --- a/Makefile +++ b/Makefile @@ -2,15 +2,24 @@ PYTHON := python3 PYTHONPATH := `pwd` AUTOFLAKE_ARGS := -r +PYTHON_VERSION := #* Installation .PHONY: install install: +ifdef PYTHON_VERSION + uv sync --python $(PYTHON_VERSION) +else uv sync +endif .PHONY: install-dev-deps install-dev-deps: +ifdef PYTHON_VERSION + uv sync --all-groups --all-extras --python $(PYTHON_VERSION) +else uv sync --all-groups --all-extras +endif .PHONY: build @@ -25,7 +34,7 @@ pre-commit-install: .PHONY: black black: uv run black --version - uv run black --config pyproject.toml elastica tests examples + uv run black --config pyproject.toml elastica tests examples backend .PHONY: black-check black-check: @@ -35,7 +44,7 @@ black-check: .PHONY: flake8 flake8: uv run flake8 --version - uv run flake8 elastica tests + uv run flake8 elastica .PHONY: autoflake-check autoflake-check: @@ -45,7 +54,7 @@ autoflake-check: .PHONY: autoflake-format autoflake-format: uv run autoflake --version - uv run autoflake --in-place $(AUTOFLAKE_ARGS) elastica tests examples + uv run autoflake --in-place $(AUTOFLAKE_ARGS) elastica tests examples backend .PHONY: format-codestyle format-codestyle: black autoflake-format @@ -62,15 +71,15 @@ mypy: .PHONY: test test: - uv run pytest -c pyproject.toml + uv run pytest -c pyproject.toml tests .PHONY: test_coverage test_coverage: - NUMBA_DISABLE_JIT=1 uv run pytest --cov=elastica -c pyproject.toml + NUMBA_DISABLE_JIT=1 uv run pytest --cov=elastica -c pyproject.toml tests .PHONY: test_coverage_xml test_coverage_xml: - NUMBA_DISABLE_JIT=1 uv run pytest --cov=elastica --cov-report=xml -c pyproject.toml + NUMBA_DISABLE_JIT=1 uv run pytest --cov=elastica --cov-report=xml -c pyproject.toml tests .PHONY: check-codestyle check-codestyle: black-check flake8 autoflake-check @@ -103,8 +112,12 @@ pytestcache-remove: build-remove: rm -rf build/ dist/ +.PHONY: doc-remove +doc-remove: + rm -rf docs/_build docs/gen_modules/ docs/sg_execution_times.rst docs/_gallery/ + .PHONY: cleanup -cleanup: pycache-remove dsstore-remove ipynbcheckpoints-remove pytestcache-remove mypycache-remove build-remove +cleanup: pycache-remove dsstore-remove ipynbcheckpoints-remove pytestcache-remove mypycache-remove build-remove doc-remove all: format-codestyle cleanup test diff --git a/README.md b/README.md index 8112480b6..837e6d6e1 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@

PyElastica

-[![CI][badge-CI]][link-CI] [![Documentation Status][badge-docs-status]][link-docs-status] [![codecov][badge-codecov]][link-codecov] [![Downloads][badge-pepy-download-count]][link-pepy-download-count] [![DOI][badge-doi]][link-doi] [![Binder][badge-binder]][link-binder] [![Gitter][badge-gitter]][link-gitter] +[![CI][badge-CI]][link-CI] [![Documentation Status][badge-docs-status]][link-docs-status] [![codecov][badge-codecov]][link-codecov] [![Downloads][badge-pepy-download-count]][link-pepy-download-count] [![DOI][badge-doi]][link-doi] [![Gitter][badge-gitter]][link-gitter]
PyElastica is the python implementation of **Elastica**: an *open-source* project for simulating assemblies of slender, one-dimensional structures using Cosserat Rod theory. -[![gallery][link-readme-gallary]][link-project-website] +[![gallery][link-readme-gallery]][link-project-website] Visit [www.cosseratrods.org][link-project-website] for more information and learn about Elastica and Cosserat rod theory. @@ -84,13 +84,6 @@ We ask that any publications which use Elastica cite as following: - [Controlling a CyberOctopus soft arm with muscle-like actuation](https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=9683318) (UIUC, 2020) (IEEE CDC 2021) - [Energy shaping control of a CyberOctopus soft arm](https://ieeexplore.ieee.org/document/9304408) (UIUC, 2020) (IEEE CDC 2020) -## Tutorials -[![Binder][badge-binder-tutorial]][link-binder] - -We have created several Jupyter notebooks and Python scripts to help users get started with PyElastica. The Jupyter notebooks are available on Binder, allowing you to try out some of the tutorials without having to install PyElastica. - -We have also included an example script for visualizing PyElastica simulations using POVray. This script is located in the examples folder ([`examples/Visualization`](examples/Visualization)). - ## Contribution If you would like to participate, please read our [contribution guideline](CONTRIBUTING.md). Private development branches are moved to [elastica-python](https://github.com/GazzolaLab/elastica-python) repository; access is limited to the core developers, collaborators, and maintainers. @@ -113,7 +106,7 @@ _Names arranged alphabetically_ [//]: # (Collection of URLs.) -[link-readme-gallary]: https://github.com/skim0119/PyElastica/blob/assets_logo/assets/alpha_gallery.gif +[link-readme-gallery]: https://github.com/skim0119/PyElastica/blob/assets_logo/assets/alpha_gallery.gif [link-project-website]: https://cosseratrods.org [link-lab-website]: http://mattia-lab.com/ @@ -122,7 +115,6 @@ _Names arranged alphabetically_ [badge-pypi]: https://badge.fury.io/py/pyelastica.svg [badge-CI]: https://github.com/GazzolaLab/PyElastica/workflows/CI/badge.svg [badge-docs-status]: https://readthedocs.org/projects/pyelastica/badge/?version=latest -[badge-binder]: https://mybinder.org/badge_logo.svg [badge-pepy-download-count]: https://pepy.tech/badge/pyelastica [badge-codecov]: https://codecov.io/gh/GazzolaLab/PyElastica/branch/master/graph/badge.svg [badge-gitter]: https://badges.gitter.im/PyElastica/community.svg @@ -133,7 +125,5 @@ _Names arranged alphabetically_ [link-pepy-download-count]: https://pepy.tech/project/pyelastica [link-codecov]: https://codecov.io/gh/GazzolaLab/PyElastica -[badge-binder-tutorial]: https://img.shields.io/badge/Launch-PyElastica%20Tutorials-579ACA.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFkAAABZCAMAAABi1XidAAAB8lBMVEX///9XmsrmZYH1olJXmsr1olJXmsrmZYH1olJXmsr1olJXmsrmZYH1olL1olJXmsr1olJXmsrmZYH1olL1olJXmsrmZYH1olJXmsr1olL1olJXmsrmZYH1olL1olJXmsrmZYH1olL1olL0nFf1olJXmsrmZYH1olJXmsq8dZb1olJXmsrmZYH1olJXmspXmspXmsr1olL1olJXmsrmZYH1olJXmsr1olL1olJXmsrmZYH1olL1olLeaIVXmsrmZYH1olL1olL1olJXmsrmZYH1olLna31Xmsr1olJXmsr1olJXmsrmZYH1olLqoVr1olJXmsr1olJXmsrmZYH1olL1olKkfaPobXvviGabgadXmsqThKuofKHmZ4Dobnr1olJXmsr1olJXmspXmsr1olJXmsrfZ4TuhWn1olL1olJXmsqBi7X1olJXmspZmslbmMhbmsdemsVfl8ZgmsNim8Jpk8F0m7R4m7F5nLB6jbh7jbiDirOEibOGnKaMhq+PnaCVg6qWg6qegKaff6WhnpKofKGtnomxeZy3noG6dZi+n3vCcpPDcpPGn3bLb4/Mb47UbIrVa4rYoGjdaIbeaIXhoWHmZYHobXvpcHjqdHXreHLroVrsfG/uhGnuh2bwj2Hxk17yl1vzmljzm1j0nlX1olL3AJXWAAAAbXRSTlMAEBAQHx8gICAuLjAwMDw9PUBAQEpQUFBXV1hgYGBkcHBwcXl8gICAgoiIkJCQlJicnJ2goKCmqK+wsLC4usDAwMjP0NDQ1NbW3Nzg4ODi5+3v8PDw8/T09PX29vb39/f5+fr7+/z8/Pz9/v7+zczCxgAABC5JREFUeAHN1ul3k0UUBvCb1CTVpmpaitAGSLSpSuKCLWpbTKNJFGlcSMAFF63iUmRccNG6gLbuxkXU66JAUef/9LSpmXnyLr3T5AO/rzl5zj137p136BISy44fKJXuGN/d19PUfYeO67Znqtf2KH33Id1psXoFdW30sPZ1sMvs2D060AHqws4FHeJojLZqnw53cmfvg+XR8mC0OEjuxrXEkX5ydeVJLVIlV0e10PXk5k7dYeHu7Cj1j+49uKg7uLU61tGLw1lq27ugQYlclHC4bgv7VQ+TAyj5Zc/UjsPvs1sd5cWryWObtvWT2EPa4rtnWW3JkpjggEpbOsPr7F7EyNewtpBIslA7p43HCsnwooXTEc3UmPmCNn5lrqTJxy6nRmcavGZVt/3Da2pD5NHvsOHJCrdc1G2r3DITpU7yic7w/7Rxnjc0kt5GC4djiv2Sz3Fb2iEZg41/ddsFDoyuYrIkmFehz0HR2thPgQqMyQYb2OtB0WxsZ3BeG3+wpRb1vzl2UYBog8FfGhttFKjtAclnZYrRo9ryG9uG/FZQU4AEg8ZE9LjGMzTmqKXPLnlWVnIlQQTvxJf8ip7VgjZjyVPrjw1te5otM7RmP7xm+sK2Gv9I8Gi++BRbEkR9EBw8zRUcKxwp73xkaLiqQb+kGduJTNHG72zcW9LoJgqQxpP3/Tj//c3yB0tqzaml05/+orHLksVO+95kX7/7qgJvnjlrfr2Ggsyx0eoy9uPzN5SPd86aXggOsEKW2Prz7du3VID3/tzs/sSRs2w7ovVHKtjrX2pd7ZMlTxAYfBAL9jiDwfLkq55Tm7ifhMlTGPyCAs7RFRhn47JnlcB9RM5T97ASuZXIcVNuUDIndpDbdsfrqsOppeXl5Y+XVKdjFCTh+zGaVuj0d9zy05PPK3QzBamxdwtTCrzyg/2Rvf2EstUjordGwa/kx9mSJLr8mLLtCW8HHGJc2R5hS219IiF6PnTusOqcMl57gm0Z8kanKMAQg0qSyuZfn7zItsbGyO9QlnxY0eCuD1XL2ys/MsrQhltE7Ug0uFOzufJFE2PxBo/YAx8XPPdDwWN0MrDRYIZF0mSMKCNHgaIVFoBbNoLJ7tEQDKxGF0kcLQimojCZopv0OkNOyWCCg9XMVAi7ARJzQdM2QUh0gmBozjc3Skg6dSBRqDGYSUOu66Zg+I2fNZs/M3/f/Grl/XnyF1Gw3VKCez0PN5IUfFLqvgUN4C0qNqYs5YhPL+aVZYDE4IpUk57oSFnJm4FyCqqOE0jhY2SMyLFoo56zyo6becOS5UVDdj7Vih0zp+tcMhwRpBeLyqtIjlJKAIZSbI8SGSF3k0pA3mR5tHuwPFoa7N7reoq2bqCsAk1HqCu5uvI1n6JuRXI+S1Mco54YmYTwcn6Aeic+kssXi8XpXC4V3t7/ADuTNKaQJdScAAAAAElFTkSuQmCC -[link-binder]: https://mybinder.org/v2/gh/GazzolaLab/PyElastica/master?filepath=examples%2FBinder%2F0_PyElastica_Tutorials_Overview.ipynb [link-gitter]: https://gitter.im/PyElastica/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge [link-doi]: https://zenodo.org/badge/latestdoi/254172891 diff --git a/backend/.clang-format b/backend/.clang-format new file mode 100644 index 000000000..48d4ec71d --- /dev/null +++ b/backend/.clang-format @@ -0,0 +1,17 @@ +--- +# We'll use defaults from the Google style. +# See http://clang.llvm.org/docs/ClangFormat.html for help. +Language: Cpp +BasedOnStyle: Google +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +PointerAlignment: Left +DerivePointerAlignment: false +FixNamespaceComments: true +IncludeCategories: + - Regex: "^<.*" + Priority: 1 + - Regex: ".*" + Priority: 2 +NamespaceIndentation: All +SortIncludes: false diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 000000000..4689dcf2c --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,12 @@ +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps + diff --git a/backend/CMakeLists.txt b/backend/CMakeLists.txt new file mode 100644 index 000000000..be937721d --- /dev/null +++ b/backend/CMakeLists.txt @@ -0,0 +1,290 @@ +cmake_minimum_required(VERSION 3.20) +project(elasticapp VERSION 0.0.4 LANGUAGES CXX) + +# Set C++ standard +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# Compiler flags +add_compile_options(-Wno-unused -fdiagnostics-color=always -Wall -Wextra -pedantic -flto) + +# Find Python and pybind11 (temp) +# pybind11 will find Python and NumPy automatically +# Set Python interpreter to ../.venv +set(Python3_ROOT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.venv") +set(PYTHON_EXECUTABLE "${Python3_ROOT_DIR}/bin/python") + +# Find pybind11 and ensure it uses the Python interpreter from our venv +set(PYBIND11_FINDPYTHON "ON") +set(PYBIND11_PYTHON_EXECUTABLE "${PYTHON_EXECUTABLE}" CACHE FILEPATH "" FORCE) +find_package(pybind11 REQUIRED) + +# ------------------------------------------------------------ # +# External dependency management # +# Include FetchContent for Catch2 and Eigen3 +include(FetchContent) + +# Save current BUILD_TESTING state and disable it for dependencies +set(BUILD_TESTING_SAVED ${BUILD_TESTING}) +set(BUILD_TESTING OFF) + +# Fetch Eigen3 for numerical computations +FetchContent_Declare( + Eigen3 + GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git + GIT_TAG 3.4.0 +) +# Disable Eigen3 tests and documentation +set(EIGEN_BUILD_DOC OFF) +# Set Eigen's default storage order to row-major +# This ensures Eigen's default behavior matches our explicit MatrixType template parameter +set(EIGEN_DEFAULT_TO_ROW_MAJOR ON CACHE BOOL "Use row-major as default matrix storage order" FORCE) +FetchContent_MakeAvailable(Eigen3) + +# Restore BUILD_TESTING state for our tests +set(BUILD_TESTING ${BUILD_TESTING_SAVED}) + +# ------------------------------------------------------------ # + +# Define directories # +set(TEST_DIR "${CMAKE_CURRENT_SOURCE_DIR}/tests") +set(PYTHON_PACKAGE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/py") +set(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src") + +option(ELASTICAPP_VECTORIZATION_REPORTS "Enable compiler vectorization reports" OFF) +option(ELASTICAPP_BUILD_TESTS "Build C++ unit tests" OFF) +option(ELASTICAPP_GIL_RELEASE "Release Python GIL around compute-heavy bindings" ON ) + +# Global CMake option to enable/disable for unittests +if (DEFINED ENV{ELASTICAPP_BUILD_TESTS}) + string(TOUPPER "$ENV{ELASTICAPP_BUILD_TESTS}" ELASTICAPP_BUILD_TESTS_ENV_VALUE) + if ("${ELASTICAPP_BUILD_TESTS_ENV_VALUE}" MATCHES "^(ON|TRUE|1)$") + set(ELASTICAPP_BUILD_TESTS ON) + message(STATUS "ELASTICAPP_BUILD_TESTS set to TRUE from environment variable.") + elseif ("${ELASTICAPP_BUILD_TESTS_ENV_VALUE}" MATCHES "^(OFF|FALSE|0)$") + set(ELASTICAPP_BUILD_TESTS OFF) + message(STATUS "ELASTICAPP_BUILD_TESTS set to OFF from environment variable.") + else() + message(WARNING "Environment variable ELASTICAPP_BUILD_TESTS has an unrecognized value. It will not be used to set the CMake option.") + endif() +endif() + +# Option to set number of threads per component in update_dynamics parallel sections +# This controls how many threads are used for each spatial component (x, y, z) in the parallel sections +# Default is 3 (one thread per component), but can be adjusted for different hardware configurations +set(ELASTICAPP_COMPONENT_THREADS "2" CACHE STRING "Number of threads per component in update_dynamics parallel sections (default: 3)") +mark_as_advanced(ELASTICAPP_COMPONENT_THREADS) + +# Configure threading support (OpenMP) - check early but apply per-target +# On macOS with Homebrew, help CMake find libomp +# This is needed for both Makefile builds and scikit-build-core builds +if(APPLE) + # Try to find Homebrew-installed libomp + execute_process( + COMMAND brew --prefix libomp + OUTPUT_VARIABLE HOMEBREW_LIBOMP_PREFIX + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) + if(HOMEBREW_LIBOMP_PREFIX) + message(STATUS "Found Homebrew libomp at: ${HOMEBREW_LIBOMP_PREFIX}") + # Set OpenMP variables for clang on macOS + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + set(OpenMP_CXX_FLAGS "-Xpreprocessor -fopenmp -I${HOMEBREW_LIBOMP_PREFIX}/include") + set(OpenMP_CXX_LIB_NAMES "omp") + set(OpenMP_omp_LIBRARY "${HOMEBREW_LIBOMP_PREFIX}/lib/libomp.dylib") + set(OpenMP_CXX_LIBRARIES "${HOMEBREW_LIBOMP_PREFIX}/lib/libomp.dylib") + message(STATUS "Configured OpenMP for clang with Homebrew libomp") + endif() + endif() +endif() + +find_package(OpenMP COMPONENTS CXX REQUIRED) + +# Helper function to apply optimization flags to a target +# This function must be defined before targets are created +function(apply_optimization_flags target_name) + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(${target_name} PRIVATE -O3 -march=native) + target_compile_options(${target_name} PRIVATE -ffunction-sections -fdata-sections) + elseif(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") + target_compile_options(${target_name} PRIVATE /O2) + endif() + # Add NDEBUG flag to disable assertions in release builds + target_compile_definitions(${target_name} PRIVATE NDEBUG) + + # Enable Eigen vectorization + target_compile_definitions(${target_name} PRIVATE EIGEN_VECTORIZE) + + # Add architecture-specific optimization flags + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + # AVX2 and FMA are only available on x86_64, not ARM + if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") + target_compile_options(${target_name} PRIVATE -mavx2 -mfma) + endif() + + # Add vectorization report flags if requested + if(ELASTICAPP_VECTORIZATION_REPORTS) + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + # Clang vectorization reports + target_compile_options(${target_name} PRIVATE + -Rpass=loop-vectorize + -Rpass-missed=loop-vectorize + -Rpass-analysis=loop-vectorize + ) + message(STATUS "Clang vectorization reports enabled for ${target_name}") + elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU") + # GCC vectorization reports + target_compile_options(${target_name} PRIVATE + -fopt-info-vec + -fopt-info-vec-missed + -fopt-info-vec-all + ) + message(STATUS "GCC vectorization reports enabled for ${target_name}") + endif() + endif() + elseif(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") + # MSVC flags for AVX2 + target_compile_options(${target_name} PRIVATE /arch:AVX2) + endif() + + if(OpenMP_CXX_FOUND) + target_link_libraries(${target_name} PRIVATE OpenMP::OpenMP_CXX) + message(STATUS "Threading support enabled for ${target_name}") + endif() +endfunction() + +# Extension module: _memory_block (Python module name) +pybind11_add_module(_memory_block + "${SRC_DIR}/_api.cpp" +) +target_compile_definitions(_memory_block PRIVATE VERSION_INFO=${PROJECT_VERSION}) +target_include_directories(_memory_block PRIVATE ${SRC_DIR}) +target_link_libraries(_memory_block PRIVATE Eigen3::Eigen) + +if(APPLE) + target_link_options(_memory_block PRIVATE -Wl,-dead_strip) +elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU") + target_link_options(_memory_block PRIVATE -Wl,--gc-sections) +elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + target_link_options(_memory_block PRIVATE -Wl,--gc-sections) +endif() + + +# Apply component threads option to _memory_block (only if explicitly set and non-empty) +# If not set, the code will use OpenMP's default thread count +if(ELASTICAPP_COMPONENT_THREADS AND NOT ELASTICAPP_COMPONENT_THREADS STREQUAL "" AND NOT ELASTICAPP_COMPONENT_THREADS STREQUAL "0") + target_compile_definitions(_memory_block PRIVATE ELASTICAPP_COMPONENT_THREADS=${ELASTICAPP_COMPONENT_THREADS}) +endif() + +if(ELASTICAPP_GIL_RELEASE) + target_compile_definitions(_memory_block PRIVATE ELASTICAPP_GIL_RELEASE) +endif() + +# Apply optimization flags to _memory_block +apply_optimization_flags(_memory_block) + +# Install _memory_block module +install(TARGETS _memory_block + LIBRARY DESTINATION "elasticapp" + COMPONENT python) + +# Extension module: version (Python module name) +pybind11_add_module(version + "${SRC_DIR}/_version.cpp" +) +target_compile_definitions(version PRIVATE VERSION_INFO=${PROJECT_VERSION}) +target_include_directories(version PRIVATE ${SRC_DIR}) + +# Install version module +install(TARGETS version + LIBRARY DESTINATION "elasticapp" + COMPONENT python) + +# Extension module: _collision (Python module name) +pybind11_add_module(_collision + "${SRC_DIR}/environment/_api.cpp" + "${SRC_DIR}/environment/collision/course_detection/hash_grid.cpp" + "${SRC_DIR}/environment/collision/fine_detection/max_contacts.cpp" + "${SRC_DIR}/environment/collision/batching/union_find.cpp" + "${SRC_DIR}/environment/collision/physics/linear_spring_dashpot.cpp" + "${SRC_DIR}/environment/collision/physics/no_interaction.cpp" + "${SRC_DIR}/math/node_element_mapping.cpp" +) +target_compile_definitions(_collision PRIVATE VERSION_INFO=${PROJECT_VERSION}) +target_include_directories(_collision PRIVATE ${SRC_DIR}) +target_link_libraries(_collision PRIVATE Eigen3::Eigen) + +if(APPLE) + target_link_options(_collision PRIVATE -Wl,-dead_strip) +elseif(CMAKE_CXX_COMPILER_ID MATCHES "GNU") + target_link_options(_collision PRIVATE -Wl,--gc-sections) +elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + target_link_options(_collision PRIVATE -Wl,--gc-sections) +endif() + +if(ELASTICAPP_GIL_RELEASE) + target_compile_definitions(_collision PRIVATE ELASTICAPP_GIL_RELEASE) +endif() + +# Apply optimization flags to _collision +apply_optimization_flags(_collision) + +# Install _collision module +install(TARGETS _collision + LIBRARY DESTINATION "elasticapp" + COMPONENT python) + +# Install Python package files +install(FILES + "${PYTHON_PACKAGE_DIR}/__init__.py" + "${PYTHON_PACKAGE_DIR}/typing_alias.py" + "${PYTHON_PACKAGE_DIR}/memory_block_rod.py" + "${PYTHON_PACKAGE_DIR}/collision_physics.py" + "${PYTHON_PACKAGE_DIR}/module_collision.py" + DESTINATION "elasticapp" + COMPONENT python) + +# C++ unit tests with Catch2 +if(ELASTICAPP_BUILD_TESTS) + enable_testing() + include(CTest) + + # Fetch Catch2 for C++ unit testing + FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.5.4 + ) + set(CATCH_BUILD_TESTING OFF CACHE BOOL "" FORCE) + set(CATCH_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + FetchContent_MakeAvailable(Catch2) + + # Test executable + add_executable(cpp_tests + "${TEST_DIR}/cpp/test_version.cpp" + "${TEST_DIR}/cpp/test_block.cpp" + "${TEST_DIR}/cpp/test_traits.cpp" + "${TEST_DIR}/cpp/test_system.cpp" + "${TEST_DIR}/cpp/test_system_variable_validation.cpp" + "${TEST_DIR}/cpp/test_block_view.cpp" + "${TEST_DIR}/cpp/test_block_operations.cpp" + "${TEST_DIR}/cpp/collision/test_collision_system.cpp" + ) + + target_compile_definitions(cpp_tests PRIVATE VERSION_INFO=${PROJECT_VERSION}) + target_include_directories(cpp_tests PRIVATE ${SRC_DIR} ${TEST_DIR}/cpp/mock) + target_link_libraries(cpp_tests PRIVATE Catch2::Catch2WithMain Eigen3::Eigen) + + # Apply optimization flags to cpp_tests + apply_optimization_flags(cpp_tests) + + # Use Catch2's test discovery to automatically register tests with CTest + # Add Catch2 extras directory to module path (FetchContent uses uppercase) + list(APPEND CMAKE_MODULE_PATH ${Catch2_SOURCE_DIR}/extras) + include(Catch) + catch_discover_tests(cpp_tests) +else() + message(STATUS "C++ tests disabled (ELASTICAPP_BUILD_TESTS=OFF)") +endif() diff --git a/backend/Makefile b/backend/Makefile new file mode 100644 index 000000000..9e94afa7d --- /dev/null +++ b/backend/Makefile @@ -0,0 +1,62 @@ +#* Variables +PYTHON := python3 +PYTHONPATH := `pwd`/.. +PYTHON_VERSION := + +#* Installation +.PHONY: install +install: + uv pip install "." -v + +#* C++ Build (direct CMake, no pip) +.PHONY: build-cpp +build-cpp: + @echo "Building C++ code with CMake..." && \ + VENV_PYTHON=$$(which python) && \ + PYBIND11_DIR=$$(python -c "import pybind11; import os; print(os.path.join(os.path.dirname(pybind11.__file__), 'share', 'cmake', 'pybind11'))") && \ + mkdir -p build && \ + cd build && \ + LIBOMP_PREFIX=$$(brew --prefix libomp 2>/dev/null || echo "") && \ + LLVM_PREFIX=$$(brew --prefix llvm 2>/dev/null || echo "") && \ + cmake .. \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER=$$LLVM_PREFIX/bin/clang \ + -DCMAKE_CXX_COMPILER=$$LLVM_PREFIX/bin/clang++ \ + -DPython3_ROOT_DIR=$$(cd .. && pwd)/../.venv \ + -DPython3_EXECUTABLE="$$VENV_PYTHON" \ + -DPYTHON_EXECUTABLE="$$VENV_PYTHON" \ + -DPython_EXECUTABLE="$$VENV_PYTHON" \ + -Dpybind11_DIR="$$PYBIND11_DIR" \ + -DOpenMP_CXX_FLAGS="-Xpreprocessor -fopenmp -I$$LIBOMP_PREFIX/include" \ + -DOpenMP_CXX_LIB_NAMES="omp" \ + -DOpenMP_omp_LIBRARY="$$LIBOMP_PREFIX/lib/libomp.dylib" \ + -DOpenMP_CXX_LIBRARIES="$$LIBOMP_PREFIX/lib/libomp.dylib" \ + -DELASTICAPP_VECTORIZATION_REPORTS=ON \ + -DELASTICAPP_SAVE_ASSEMBLY=ON \ + -DELASTICAPP_BUILD_TESTS=OFF; \ + cmake --build . --parallel 2>&1 | tee ../vectorization_report.txt + +#* C++ Tests (build and run, no pip) +.PHONY: test-cpp-direct +test-cpp-direct: build-cpp + @echo "Running C++ tests..." + @cd build && ./cpp_tests + +#* Testing +.PHONY: test +test: + @echo "Running C++ and Python tests..." + ELASTICAPP_BUILD_TESTS=ON make install && cd build && ./cpp_tests + pytest tests/py -v + +#* Cleaning +.PHONY: clean +clean: clean-build + rm -rf dist/ *.egg-info + find . -type d -name __pycache__ -exec rm -r {} + 2>/dev/null || true + find . -type f -name "*.pyc" -delete + find . -type f -name "*.pyo" -delete + +.PHONY: clean-build +clean-build: + rm -rf build/ diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 000000000..a8828bc6e --- /dev/null +++ b/backend/README.md @@ -0,0 +1,61 @@ +# C++ for PyElastica + +This directory includes basic C++ backend for experimental purposes. +The full functionality is not yet moved to this directory. +The purpose of this update is to provide a way to implement functionality in C++, such as multithreading and more controlled vectorization. +For large-number of rods and complex environment, computation with contact and self-interactions requires higher control over parallelization. + +> Note: manual tuning of vectorization and threading is required for optimal performance, which may vary depending on the hardware. + +> Note: expected performance improvement depends on the vector size. It is expected to be faster for large rod systems, mostly due to the overhead created by the python-binding and interpreter. In most of the case with less than 1e5 elements, the performance of `numba` could be better. + +## Usage + +```python +import elastica as ea +import elasticapp as epp + +class SystemSimulator( + ea.BaseSystemCollection, + ... +): + pass + +simulator = SystemSimulator() +# Replace C++ block module for CosseratRod +simulator.enable_block_supports(ea.CosseratRod, epp.MemoryBlockCosseratRod) +``` + +## Installation + +From the `backend` directory, run: + +```bash +cd backend +make install # or run pip install "." +``` +> Make sure you install the package from _PyElastica source tree_. + +## Testing + +Test includes both `cpp` and `python` testing. + +```bash +make test +``` + +## File structure + +- All cpp files are in `src` directory. `src/py` directory includes python bindings for C++ classes. +- All test files are in `tests` directory. `cpp` tests are in `tests/cpp` directory, and `python` tests are in `tests/py` directory. +- All benchmark files are in `benchmarking` directory. + + +## Contributed By + +- Tejaswin Parthasarathy (Teja) +- [Seung Hyun Kim](https://github.com/skim0119) +- Ankith Pai +- [Yashraj Bhosale](https://github.com/bhosale2) +- Arman Tekinalp +- Songyuan Cui diff --git a/backend/benchmarking/PDE_benchmark.ipynb b/backend/benchmarking/PDE_benchmark.ipynb new file mode 100644 index 000000000..f76de55b6 --- /dev/null +++ b/backend/benchmarking/PDE_benchmark.ipynb @@ -0,0 +1,231 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "ea3e0b99", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Any\n", + "import timeit\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import time\n", + "from tqdm import tqdm\n", + "import os\n", + "# os.environ[\"NUMBA_DISABLE_JIT\"] = \"1\"\n", + "\n", + "import elastica as ea\n", + "from elasticapp import MemoryBlockCosseratRod\n", + "\n", + "# Try to import set_num_threads if threading is enabled\n", + "try:\n", + " from elasticapp._memory_block import set_num_threads, get_max_threads\n", + " THREADING_AVAILABLE = True\n", + "except ImportError:\n", + " THREADING_AVAILABLE = False\n", + " raise RuntimeError(\"Threading not available (ELASTICAPP_USE_THREADING not enabled)\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "bc221bc9", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 50000/50000 [00:24<00:00, 2016.30it/s]\n" + ] + } + ], + "source": [ + "# Create 50000 rods with 6 elements each, iterate 100 operations\n", + "N = 100\n", + "n_rods = 50000\n", + "n_elems_per_rod = 6\n", + "\n", + "# Create rods\n", + "rods = [\n", + " ea.CosseratRod.straight_rod(\n", + " n_elements=n_elems_per_rod,\n", + " start=np.zeros(3),\n", + " direction=np.array([0.0, 0.0, 1.0]),\n", + " normal=np.array([1.0, 0.0, 0.0]),\n", + " base_length=1.0,\n", + " base_radius=0.01,\n", + " density=3000,\n", + " youngs_modulus=1e6,\n", + " )\n", + " for _ in tqdm(range(n_rods))\n", + "]\n", + "\n", + "def jitter_rod(rod):\n", + " rng = np.random.default_rng(42)\n", + " rod.external_forces[:] = rng.uniform(-1e-6, 1e-6, rod.external_forces.shape)\n", + " rod.external_torques[:] = rng.uniform(-1e-6, 1e-6, rod.external_torques.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "35e3d633", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time per call: 42.549 ms (compute_internal_forces_and_torques - inverse rotation, equations)\n", + "Time per call: 5.615 ms (update_accelerations - block addition)\n", + "Time per call: 15.882 ms (update_kinematics - rodrigues)\n", + "Time per call: 0.974 ms (update_dynamics)\n" + ] + } + ], + "source": [ + "block_rod = ea.MemoryBlockCosseratRod(rods, range(n_rods))\n", + "jitter_rod(block_rod)\n", + "\n", + "py_result = {}\n", + "\n", + "for _ in range(10): # Pre-run\n", + " block_rod.compute_internal_forces_and_torques(0.0)\n", + "T = timeit.timeit(lambda: block_rod.compute_internal_forces_and_torques(0.0), number=N)\n", + "py_result['compute_internal_forces_and_torques'] = T/N*1000\n", + "print(f\"Time per call: {T/N*1000:.3f} ms (compute_internal_forces_and_torques - inverse rotation, equations)\")\n", + "for _ in range(10): # Pre-run\n", + " block_rod.update_accelerations(0.0)\n", + "T = timeit.timeit(lambda: block_rod.update_accelerations(0.0), number=N)\n", + "py_result['update_accelerations'] = T/N*1000\n", + "print(f\"Time per call: {T/N*1000:.3f} ms (update_accelerations - block addition)\")\n", + "for _ in range(10): # Pre-run\n", + " block_rod.update_kinematics(0.0, 1e-4)\n", + "T = timeit.timeit(lambda: block_rod.update_kinematics(0.0, 1e-4), number=N)\n", + "py_result['update_kinematics'] = T/N*1000\n", + "print(f\"Time per call: {T/N*1000:.3f} ms (update_kinematics - rodrigues)\")\n", + "for _ in range(10): # Pre-run\n", + " block_rod.update_dynamics(0.0, 1e-4)\n", + "T = timeit.timeit(lambda: block_rod.update_dynamics(0.0, 1e-4), number=N)\n", + "py_result['update_dynamics'] = T/N*1000\n", + "print(f\"Time per call: {T/N*1000:.3f} ms (update_dynamics)\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "31e6d7a1", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 50000/50000 [00:24<00:00, 2057.62it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time per call: 21.076 ms (compute_internal_forces_and_torques - inverse rotation, equations)\n", + "Time per call: 3.041 ms (update_accelerations - block addition)\n", + "Time per call: 1.601 ms (update_kinematics - rodrigues)\n", + "Time per call: 0.756 ms (update_dynamics)\n", + "------------------------------------------------------------\n" + ] + } + ], + "source": [ + "assert THREADING_AVAILABLE\n", + "\n", + "\n", + "# Benchmark parameters\n", + "# thread_counts = [1, 2, 4, 8, 16]\n", + "\n", + "# Storage for results\n", + "cpp_results_for_threads = []\n", + "\n", + "# Run benchmark for each thread count\n", + "# for num_threads in thread_counts:\n", + " # set_num_threads(num_threads)\n", + " # print(f\"Threading available: {num_threads}\")\n", + "\n", + "rods = [\n", + " ea.CosseratRod.straight_rod(\n", + " n_elements=n_elems_per_rod,\n", + " start=np.zeros(3),\n", + " direction=np.array([0.0, 0.0, 1.0]),\n", + " normal=np.array([1.0, 0.0, 0.0]),\n", + " base_length=1.0,\n", + " base_radius=0.01,\n", + " density=3000,\n", + " youngs_modulus=1e6,\n", + " )\n", + " for _ in tqdm(range(n_rods))\n", + "]\n", + "\n", + "block_rod = MemoryBlockCosseratRod(rods, range(n_rods))\n", + "jitter_rod(block_rod)\n", + "\n", + "cpp_result = {}\n", + "cpp_results_for_threads.append(cpp_result)\n", + "\n", + "for _ in range(10): # Pre-run\n", + " block_rod.compute_internal_forces_and_torques(0.0)\n", + "T = timeit.timeit(lambda: block_rod.compute_internal_forces_and_torques(0.0), number=N)\n", + "cpp_result['compute_internal_forces_and_torques'] = T/N*1000\n", + "print(f\"Time per call: {T/N*1000:.3f} ms (compute_internal_forces_and_torques - inverse rotation, equations)\")\n", + "for _ in range(10): # Pre-run\n", + " block_rod.update_accelerations(0.0)\n", + "T = timeit.timeit(lambda: block_rod.update_accelerations(0.0), number=N)\n", + "cpp_result['update_accelerations'] = T/N*1000\n", + "print(f\"Time per call: {T/N*1000:.3f} ms (update_accelerations - block addition)\")\n", + "for _ in range(10): # Pre-run\n", + " block_rod.update_kinematics(0.0, 1e-4)\n", + "T = timeit.timeit(lambda: block_rod.update_kinematics(0.0, 1e-4), number=N)\n", + "cpp_result['update_kinematics'] = T/N*1000\n", + "print(f\"Time per call: {T/N*1000:.3f} ms (update_kinematics - rodrigues)\")\n", + "for _ in range(10): # Pre-run\n", + " block_rod.update_dynamics(0.0, 1e-4)\n", + "T = timeit.timeit(lambda: block_rod.update_dynamics(0.0, 1e-4), number=N)\n", + "cpp_result['update_dynamics'] = T/N*1000\n", + "print(f\"Time per call: {T/N*1000:.3f} ms (update_dynamics)\")\n", + "\n", + "print(\"-\"*60)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb8950dc", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/backend/benchmarking/memory_block_integrity_check.py b/backend/benchmarking/memory_block_integrity_check.py new file mode 100644 index 000000000..98e4e6964 --- /dev/null +++ b/backend/benchmarking/memory_block_integrity_check.py @@ -0,0 +1,111 @@ +""" +Example script to benchmark C++ operations with timing done entirely in C++. + +This script demonstrates how to use the benchmark_cpp function to measure +performance without Python loop or pybind11 call overhead. +""" + +import numpy as np +import matplotlib.pyplot as plt +import elastica as epy +import elasticapp as epp + +# %% +# Create test rods +n_rods = 50 +n_elems_per_rod = 200 + +print(f"Creating {n_rods} rods with {n_elems_per_rod} elements each...") +keys = list(epp.memory_block_rod.PY2CPP_VARNAMES.keys()) +rods_cpp = [ + epy.CosseratRod.straight_rod( + n_elements=n_elems_per_rod, + start=np.zeros(3), + direction=np.array([0.0, 0.0, 1.0]), + normal=np.array([1.0, 0.0, 0.0]), + base_length=1.0, + base_radius=0.01, + density=3000, + youngs_modulus=1e6, + ) + for _ in range(n_rods) +] +rods_py = [ + epy.CosseratRod.straight_rod( + n_elements=n_elems_per_rod, + start=np.zeros(3), + direction=np.array([0.0, 0.0, 1.0]), + normal=np.array([1.0, 0.0, 0.0]), + base_length=1.0, + base_radius=0.01, + density=3000, + youngs_modulus=1e6, + ) + for _ in range(n_rods) +] +rng = np.random.default_rng(43) +for i in range(n_rods): + for key in ["rest_kappa", "rest_sigma"]: + shape = getattr(rods_py[i], key).shape + values = rng.random(size=shape) + getattr(rods_py[i], key)[...] = values.copy() + getattr(rods_cpp[i], key)[...] = values.copy() + +# Create block rod system +block_cpp = epp.MemoryBlockCosseratRod(rods_cpp, range(n_rods)) +block_py = epy.MemoryBlockCosseratRod(rods_py, range(n_rods)) + +# %% +# Cross-check ghost indices +# ------------------------- +np.testing.assert_array_equal(block_cpp.ghost_nodes_idx, block_py.ghost_nodes_idx) +np.testing.assert_array_equal(block_cpp.ghost_elems_idx[:-1], block_py.ghost_elems_idx) +np.testing.assert_array_equal( + block_cpp.ghost_voronoi_idx[:-2], block_py.ghost_voronoi_idx +) + + +# %% +# Cross-check block memory +# ------------------------ +def cross_check_block_memory(): + for key in keys: + cpp_value = getattr(block_cpp, key) + py_value = getattr(block_py, key) + assert cpp_value.shape == py_value.shape + assert np.allclose(cpp_value, py_value), f"{key} is not equal" + print(f"{key} | values and shapes checked") + + +cross_check_block_memory() + +# %% +# Cross-check computing internal forces and torques +# ------------------------------------------------- +block_cpp.compute_internal_forces_and_torques(0.0) +block_py.compute_internal_forces_and_torques(0.0) + +cross_check_block_memory() + +# %% +# Cross-check updating accelerations +# ---------------------------------- +block_cpp.update_accelerations(0.0) +block_py.update_accelerations(0.0) + +cross_check_block_memory() +# %% +# Cross-check updating kinematics +# ----------------------------- +block_cpp.update_kinematics(0.0, 1.4) +block_py.update_kinematics(0.0, 1.4) + +cross_check_block_memory() +# %% +# Cross-check updating dynamics +# ----------------------------- +block_cpp.update_dynamics(0.0, 1.6) +block_py.update_dynamics(0.0, 1.6) + +cross_check_block_memory() +# %% diff --git a/backend/examples/run_timoshenko.py b/backend/examples/run_timoshenko.py new file mode 100644 index 000000000..c4603bcc9 --- /dev/null +++ b/backend/examples/run_timoshenko.py @@ -0,0 +1,176 @@ +""" +Test case for many-rod simulation with CPP memory block in the back. +""" + +import numpy as np + +pass +from tqdm import tqdm + +import elastica as ea +import elasticapp as epp + +# %% +# Now that we have imported all the necessary classes, we want to create our beam system. We do this by combining all the modules we need to represent the physics that we to include in the simulation. In this case, that is the ``BaseSystemCollection``, ``Constraint``, ``Forcings`` and ``Damping`` because the simulation will consider a rod that is fixed in place on one end, and subject to an applied force on the other end. + + +class TimoshenkoBeamSimulator( + ea.BaseSystemCollection, ea.Constraints, ea.Forcing, ea.CallBacks, ea.Damping +): + pass + + +timoshenko_sim = TimoshenkoBeamSimulator() +timoshenko_sim.enable_block_supports(ea.CosseratRod, epp.MemoryBlockCosseratRod) + +# %% +# Creating Rods +# ------------- +# With our simulator set up, we can now define the numerical, material, and geometric properties. +# +# First we define the number of elements in the rod. Next, the material properties are defined for every rod. These are the Young's modulus, the Poisson ratio, the density and the viscous damping coefficient. Finally, the geometry of the rod also needs to be defined by specifying the location of the rod and its orientation, length and radius. +# +# All of the values defined here are done in SI units, though this is not strictly necessary. You can rescale properties however you want, as long as you use consistent units throughout the simulation. See `here `_ for an example of consistent units. +# +# In order to make the difference between a shearable and unshearable rod more clear, we are using a Poisson ratio of 99. This is an unphysical value, as Poisson ratios can not exceed 0.5, however, it is used here for demonstration purposes. + +# setting up test params +simulation_time = 5 + +n_elem = 100 +start = np.zeros((3,)) +direction = np.array([0.0, 0.0, 1.0]) +normal = np.array([0.0, 1.0, 0.0]) +base_length = 3.0 +base_radius = 0.25 +base_area = np.pi * base_radius**2 +density = 5000 +nu = 0.1 / 7 / density / base_area +E = 1e6 +# For shear modulus of 1e4, nu is 99! +poisson_ratio = 99 +shear_modulus = E / (poisson_ratio + 1.0) + +# %% +# With all of the rod's parameters set, we can now create a rod with the specificed properties and add the rod to the simulator system. **Important:** Make sure that any rods you create get added to the simulator system (``timoshenko_sim``), otherwise they will not be included in your simulation. + +n_rods = 10_000_000 +for _ in range(n_rods): + shearable_rod = ea.CosseratRod.straight_rod( + n_elem, + start, + direction, + normal, + base_length, + base_radius, + density, + youngs_modulus=E, + shear_modulus=shear_modulus, + ) + timoshenko_sim.append(shearable_rod) + +# %% +# Adding Damping +# -------------- +# With the rod added to the simulator, we can add damping to the rod. We do this using the ``.dampen()`` option and the ``AnalyticalLinearDamper``. We are modifying ``timoshenko_sim`` simulator to ``dampen`` the ``shearable_rod`` object using ``AnalyticalLinearDamper`` type of dissipation (damping) model. +# +# We also need to define ``damping_constant`` and simulation ``time_step`` and pass in ``.using()`` method. + +dl = base_length / n_elem +dt = 0.07 * dl +timoshenko_sim.dampen(shearable_rod).using( + ea.AnalyticalLinearDamper, + damping_constant=nu, + time_step=dt, +) + +# %% +# Adding Boundary Conditions +# -------------------------- +# With the rod added to the system, we need to apply boundary conditions. The first condition we will apply is fixing the location of one end of the rod. We do this using the ``.constrain()`` option and the ``OneEndFixedRod`` boundary condition. We are modifying the ``timoshenko_sim`` simulator to ``constrain`` the ``shearable_rod`` object using the ``OneEndFixedRod`` type of constraint. +# +# We also need to define which node of the rod is being constrained. We do this by passing the index of the nodes that we want to constrain to ``constrained_position_idx``. Here we are fixing the first node in the rod. In order to keep the rod from rotating around the fixed node, we also need to constrain an element between two nodes. This fixes the orientation of the rod. We do this by passing the index of the element that we want to fix to ``constrained_director_idx``. Like with the position, we are fixing the first element of the rod. Together, this constrains the position and orientation of the rod at the origin. + +# One end of the rod is now fixed in place +timoshenko_sim.constrain(shearable_rod).using( + ea.OneEndFixedBC, constrained_position_idx=(0,), constrained_director_idx=(0,) +) + +# %% +# The next boundary condition that we want to apply is the endpoint force. Similarly to how we constrained one of the points, we want the ``timoshenko_sim`` simulator to ``add_forcing_to`` the ``shearable_rod`` object using the ``EndpointForces`` type of forcing. This ``EndpointForces`` applies forces to both ends of the rod. We want to apply a negative force in the :math:`d_1` direction, but only at the end of the rod. We do this by specifying the force vector to be applied at each end as ``origin_force`` and ``end_force``. We also want to ramp up the force over time, so we make the force take some ``ramp_up_time`` to reach its steady-state value. This helps avoid numerical errors due to discontinuities in the applied force. + +# Forces added to the rod +end_force = np.array([-15.0, 0.0, 0.0]) +timoshenko_sim.add_forcing_to(shearable_rod).using( + ea.EndpointForces, 0.0 * end_force, end_force, ramp_up_time=simulation_time / 2.0 +) + +# %% +# Add Unshearable Rod +# ------------------- +# +# Along with the shearable rod, we also want to add an unshearable rod to be able to compare the difference between the two. We do this the same way we did for the first rod, however, because this rod is unsherable, we need to change the Poisson ratio to make the rod unsherable. For a truely unsheraable rod, you would need a Poisson ratio of -1.0, however, this causes the system to be numerically unstable, so instead we make the system nearly unshearable by using a Poisson ratio of -0.85. + +# Start into the plane +unshearable_start = np.array([0.0, -1.0, 0.0]) +shear_modulus = E / (-0.7 + 1.0) +unshearable_rod = ea.CosseratRod.straight_rod( + n_elem, + unshearable_start, + direction, + normal, + base_length, + base_radius, + density, + youngs_modulus=E, + # Unshearable rod needs G -> inf, which is achievable with -ve poisson ratio + shear_modulus=shear_modulus, +) + +timoshenko_sim.append(unshearable_rod) + +# add damping +timoshenko_sim.dampen(unshearable_rod).using( + ea.AnalyticalLinearDamper, + damping_constant=nu, + time_step=dt, +) +# add boundary conditions +timoshenko_sim.constrain(unshearable_rod).using( + ea.OneEndFixedBC, constrained_position_idx=(0,), constrained_director_idx=(0,) +) +timoshenko_sim.add_forcing_to(unshearable_rod).using( + ea.EndpointForces, 0.0 * end_force, end_force, ramp_up_time=simulation_time / 2.0 +) + +# %% +# System Finalization +# ------------------- +# We have now added all the necessary rods and boundary conditions to our system. The last thing we need to do is finalize the system. This goes through the system, rearranges things, and precomputes useful quantities to prepare the system for simulation. +# +# As a note, if you make any changes to the rod after calling finalize, you will need to re-setup the system. This requires rerunning all cells above this point. + +timoshenko_sim.finalize() + +# %% +# Define Simulation Time +# ---------------------- +# The last thing we need to do decide how long we want the simulation to run for and what timestepping method to use. Currently, the PositionVerlet algorithim is suggested default method. +# +# In this example, we are trying to match a steady-state solution by temporally evolving our system to reach equilibrium. As such, there is a tradeoff between letting the simulation run long enough to reach the equilibrium and waiting around for the simulation to be done. Here we are running the simulation for 10 seconds, this produces reasonable agreement with the analytical solution without taking to long to finish. If you run the simulation for longer, you will get better agreement with the analytical solution. + +timestepper = ea.PositionVerlet() +# timestepper = PEFRL() + +total_steps = int(simulation_time / dt) +print("Total steps", total_steps) + +# %% +# Run Simulation +# -------------- +# +# We are now ready to perform the simulation. To run the simulation, we ``integrate`` the ``timoshenko_sim`` system using the ``timestepper`` method until ``final_time`` by taking ``total_steps``. As currently setup, the beam simulation takes about 1 minute to run. + +time = 0.0 +for i in tqdm(range(total_steps)): + time = timestepper.step(timoshenko_sim, time, dt) diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 000000000..5ebedb6e0 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,78 @@ +[project] +name = "elasticapp" +version = "0.0.4" +description = "A CPP accelerated backend for PyElastica kernels" +requires-python = ">=3.10" +license = {text = "MIT License"} +dependencies = [ + "pybind11", + "pytest", + "pytest-rng", + "pytest-mock", +] + +# this is a list that can be expanded +authors = [{name = "Seung Hyun Kim", email = "skim0119@gmail.com"}] + +# classifiers can be added here, later +classifiers = [ +] + +[project.urls] +"Homepage" = "https://github.com/GazzolaLab/PyElastica/" +"Bug Reports" = "https://github.com/GazzolaLab/PyElastica/issues" +"Source" = "https://github.com/GazzolaLab/PyElastica/" +"Documentation" = "https://docs.cosseratrods.org/" + +[build-system] +requires = ["scikit-build-core", "pybind11"] +build-backend = "scikit_build_core.build" + +[tool.scikit-build] +build-dir = "build" + +[tool.scikit-build.cmake] +version = ">=3.20" +build-type = "Release" +args = [] + +# [tool.mypy] +# files = "setup.py" +# python_version = "3.7" +# strict = true +# show_error_codes = true +# enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] +# warn_unreachable = true +# +# [[tool.mypy.overrides]] +# module = ["ninja"] +# ignore_missing_imports = true + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"] +xfail_strict = true +filterwarnings = [ + "error", + "ignore:(ast.Str|Attribute s|ast.NameConstant|ast.Num) is deprecated:DeprecationWarning:_pytest", +] +testpaths = ["tests/py"] + +[tool.cibuildwheel] +test-command = "pytest {project}/tests" +test-extras = ["test"] +test-skip = ["*universal2:arm64"] +# Setuptools bug causes collision between pypy and cpython artifacts +before-build = "rm -rf {project}/build" + +[tool.ruff] +target-version = "py37" + +[tool.ruff.lint] +extend-select = [ + "B", # flake8-bugbear + "I", # isort + "PGH", # pygrep-hooks + "RUF", # Ruff-specific + "UP", # pyupgrade +] diff --git a/backend/src/_api.cpp b/backend/src/_api.cpp new file mode 100644 index 000000000..2e3ba0b58 --- /dev/null +++ b/backend/src/_api.cpp @@ -0,0 +1,603 @@ +#include +#include +#include +#include "block.h" +#include "block_view.h" +#include "traits.h" +#include "api.h" +#include "operations.h" +#include "cosserat_rod_system.h" +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace py = pybind11; + +namespace elasticapp { + +#ifdef ELASTICAPP_GIL_RELEASE +#define ELASTICAPP_GIL_RELEASE_SCOPE() py::gil_scoped_release gil_release; +#else +#define ELASTICAPP_GIL_RELEASE_SCOPE() +#endif + +// Helper to find a variable type by name at runtime +// Iterates through SystemType::Variables tuple and finds matching name +template +auto get_variable_by_name_impl(BlockView& view, const std::string& var_name) { + using CurrentVar = std::tuple_element_t; + + // Check if current variable's name matches + if (var_name == std::string(CurrentVar::name)) { + return view.template get(); + } + + // Recurse to next variable if not last + if constexpr (Index + 1 < std::tuple_size_v) { + return get_variable_by_name_impl(view, var_name); + } + + // If we've exhausted all variables, throw error + throw std::runtime_error("Unknown variable name: " + var_name); +} + +// Helper function to map string variable names to types and call get() +// This allows Python to use block.at(index).get("position") syntax +template +auto get_variable_by_name(BlockView& view, const std::string& var_name) { + using VariablesTuple = typename SystemType::Variables; + + if constexpr (std::tuple_size_v > 0) { + return get_variable_by_name_impl(view, var_name); + } else { + throw std::runtime_error("System has no variables"); + } +} + +// Helper to find a variable type by name at runtime for Block +// Iterates through SystemType::Variables tuple and finds matching name +template +auto get_block_variable_by_name_impl(BlockType& block, const std::string& var_name) { + using CurrentVar = std::tuple_element_t; + + // Check if current variable's name matches + if (var_name == std::string(CurrentVar::name)) { + return block.template get(); + } + + // Recurse to next variable if not last + if constexpr (Index + 1 < std::tuple_size_v) { + return get_block_variable_by_name_impl(block, var_name); + } + + // If we've exhausted all variables, throw error + throw std::runtime_error("Unknown variable name: " + var_name); +} + +// Helper function to map string variable names to types and call get() for Block +// This allows Python to use block.get("position") syntax +template +auto get_block_variable_by_name(BlockType& block, const std::string& var_name) { + using VariablesTuple = typename BlockType::Variables; + + if constexpr (std::tuple_size_v > 0) { + return get_block_variable_by_name_impl(block, var_name); + } else { + throw std::runtime_error("System has no variables"); + } +} + +// Helper to reset ghost for a variable by name +template +void reset_ghost_for_variable_by_name_impl(BlockType& block, const std::string& var_name) { + using CurrentVar = std::tuple_element_t; + + // Check if current variable's name matches + if (var_name == std::string(CurrentVar::name)) { + block.template reset_ghost_for_variable(); + return; + } + + // Recurse to next variable if not last + if constexpr (Index + 1 < std::tuple_size_v) { + reset_ghost_for_variable_by_name_impl(block, var_name); + } else { + throw std::runtime_error("Unknown variable name: " + var_name); + } +} + +// Helper function to reset ghost for a variable by name +template +void reset_ghost_for_variable_by_name(BlockType& block, const std::string& var_name) { + using VariablesTuple = typename BlockType::Variables; + + if constexpr (std::tuple_size_v > 0) { + reset_ghost_for_variable_by_name_impl(block, var_name); + } else { + throw std::runtime_error("System has no variables"); + } +} + +// Helper to convert Eigen Block view to numpy array +// For Scalar variables (rows == 1), returns a 1D array instead of 2D +// For Matrix variables (rows == 9), returns a 3D array (3, 3, N) directly +template +py::object block_to_numpy(BlockExpr&& block_expr, py::object parent) { + // Evaluate the expression to get dimensions + auto rows = static_cast(block_expr.rows()); + auto cols = static_cast(block_expr.cols()); + + // Get actual strides from the Eigen Block expression + // For Eigen Blocks, innerStride() is the stride between elements in the same row/column + // and outerStride() is the stride between rows/columns depending on storage order + // For column-major: innerStride() = 1 (between rows), outerStride() = underlying_rows (between columns) + // For row-major: innerStride() = 1 (between columns), outerStride() = underlying_cols (between rows) + auto inner_stride = static_cast(block_expr.innerStride() * sizeof(double)); + auto outer_stride = static_cast(block_expr.outerStride() * sizeof(double)); + + // For Scalar variables (rows == 1), return as 1D array + if (rows == 1) { + // For 1D array, stride is the column stride + py::ssize_t stride; + if constexpr (IsColMajor) { + // Column-major: stride between columns is outer_stride + stride = outer_stride; + } else { + // Row-major: stride between columns is inner_stride + stride = inner_stride; + } + + return py::array_t( + {cols}, + {stride}, + const_cast(block_expr.data()), + parent // Keep parent object alive + ); + } + + // For Matrix variables (rows == 9), return as 3D array (3, 3, N) + // Matrix variables represent 3x3 matrices stored as flattened 9-element vectors + // Storage order: [m00, m10, m20, m01, m11, m21, m02, m12, m22] (column-major) + if (rows == 9) { + // For (3, 3, N) view from (9, N) column-major: + // Mapping: arr_3d[a, b, c] -> arr_2d[a*3 + b, c] + // Strides: + // - Page stride (a dimension): 3 * row_stride (to skip 3 rows) + // - Row stride (b dimension): row_stride (to skip 1 row) + // - Col stride (c dimension): col_stride (to skip 1 column) + py::ssize_t page_stride, row_stride_3d, col_stride_3d; + if constexpr (IsColMajor) { + // Column-major: row_stride = inner_stride, col_stride = outer_stride + page_stride = 3 * inner_stride; // Stride for first 3x3 dimension (skip 3 rows) + row_stride_3d = inner_stride; // Stride between rows in 3x3 (skip 1 row) + col_stride_3d = outer_stride; // Stride between columns (N dimension) + } else { + // Row-major: row_stride = outer_stride, col_stride = inner_stride + page_stride = 3 * outer_stride; + row_stride_3d = outer_stride; + col_stride_3d = inner_stride; + } + + // Use py::buffer_info for 3D arrays with custom strides + std::vector shape = {3, 3, cols}; + std::vector strides = {page_stride, row_stride_3d, col_stride_3d}; + py::buffer_info buf_info( + const_cast(block_expr.data()), + sizeof(double), + py::format_descriptor::format(), + 3, + shape, + strides + ); + return py::array_t(buf_info, parent); + } + + // For Vector variables (rows == 3), return as 2D array + // For numpy, strides are in bytes and represent the step size for each dimension + // For column-major (Eigen default): row_stride = inner_stride, col_stride = outer_stride + // For row-major: row_stride = outer_stride, col_stride = inner_stride + py::ssize_t row_stride, col_stride; + if constexpr (IsColMajor) { + // Column-major: stride between rows is inner_stride, between columns is outer_stride + row_stride = inner_stride; + col_stride = outer_stride; + } else { + // Row-major: stride between rows is outer_stride, between columns is inner_stride + row_stride = outer_stride; + col_stride = inner_stride; + } + + // Create numpy array view (non-owning) with correct strides + return py::array_t( + {rows, cols}, + {row_stride, col_stride}, + const_cast(block_expr.data()), + parent // Keep parent object alive + ); +} + +PYBIND11_MODULE(_memory_block, m) { + m.doc() = R"pbdoc( + Elasticapp BlockCosseratRod module + ---------------------------------------- + + Provides BlockCosseratRod class for 2D array management with Eigen backend. + )pbdoc"; + + // Forward declare BlockRodSystemView so it can be used as a return type + using BlockRodSystemViewType = BlockRodSystem::View; + + // Thread management functions (only available when threading is enabled) + // Disable Eigen's internal threading to prevent oversubscription with OpenMP + // We use OpenMP for explicit parallelization, so Eigen should use single-threaded operations + Eigen::setNbThreads(1); + + m.def("set_num_threads", [](int num_threads) { + if (num_threads > 0) { + omp_set_num_threads(num_threads); + // Ensure Eigen uses single thread to avoid oversubscription + Eigen::setNbThreads(1); + } + }, + R"pbdoc( + Set the number of OpenMP threads to use for parallel operations. + + Args: + num_threads: Number of threads to use (must be > 0). + If 0 or negative, OpenMP will use its default + (typically all available CPU cores). + + Note: + This affects all subsequent parallel regions in the code. + The environment variable OMP_NUM_THREADS can also be used + to control thread count. + )pbdoc", + py::arg("num_threads")); + + m.def("get_num_threads", []() { + return omp_get_num_threads(); + }, + R"pbdoc( + Get the current number of threads in the current parallel region. + + Returns: + int: Number of threads (returns 1 if called outside a parallel region). + )pbdoc"); + + m.def("get_max_threads", []() { + return omp_get_max_threads(); + }, + R"pbdoc( + Get the maximum number of threads that can be used. + + Returns: + int: Maximum number of threads available. + )pbdoc"); + + m.def("get_thread_num", []() { + return omp_get_thread_num(); + }, + R"pbdoc( + Get the current thread number (0 to num_threads-1). + + Returns: + int: Current thread number (returns 0 if called outside a parallel region). + )pbdoc"); + + // BlockRodSystem class + py::class_(m, "BlockRodSystem") + .def(py::init([](py::object n_elems_per_rod_obj) { + std::vector n_elems_per_rod; + + // Handle list, tuple, or numpy array + if (py::isinstance(n_elems_per_rod_obj)) { + py::list lst = n_elems_per_rod_obj.cast(); + for (auto item : lst) { + n_elems_per_rod.push_back(item.cast()); + } + } else if (py::isinstance(n_elems_per_rod_obj)) { + py::tuple tup = n_elems_per_rod_obj.cast(); + for (auto item : tup) { + n_elems_per_rod.push_back(item.cast()); + } + } else if (py::isinstance(n_elems_per_rod_obj)) { + py::array arr = n_elems_per_rod_obj.cast(); + auto buf = arr.request(); + if (buf.ndim != 1) { + throw std::runtime_error("numpy array must be 1-dimensional"); + } + // Properly convert numpy array elements to std::size_t + // Handle different integer types safely + n_elems_per_rod.reserve(buf.size); + if (buf.itemsize == sizeof(std::int32_t)) { + auto* ptr = static_cast(buf.ptr); + for (py::ssize_t i = 0; i < buf.size; ++i) { + n_elems_per_rod.push_back(static_cast(ptr[i])); + } + } else if (buf.itemsize == sizeof(std::int64_t)) { + auto* ptr = static_cast(buf.ptr); + for (py::ssize_t i = 0; i < buf.size; ++i) { + n_elems_per_rod.push_back(static_cast(ptr[i])); + } + } else if (buf.itemsize == sizeof(std::size_t)) { + auto* ptr = static_cast(buf.ptr); + n_elems_per_rod.assign(ptr, ptr + buf.size); + } else { + // Fallback: iterate and cast each element + for (py::ssize_t i = 0; i < buf.size; ++i) { + py::object item = arr.attr("__getitem__")(i); + n_elems_per_rod.push_back(item.cast()); + } + } + } else { + throw std::runtime_error("n_elems_per_rod must be a list, tuple, or numpy array"); + } + + return new BlockRodSystem(n_elems_per_rod); + }), + R"pbdoc( + Create a BlockRodSystem from list of element counts per rod. + + Args: + n_elems_per_rod: List, tuple, or numpy array of integers representing + number of elements in each rod + )pbdoc", + py::arg("n_elems_per_rod")) + .def_property_readonly("n_systems", [](const BlockRodSystem& block) { + return block.n_systems(); + }, + R"pbdoc( + Number of systems (rods) in the block. + )pbdoc") + .def_property_readonly("shape", [](const BlockRodSystem& block) { + auto shape = block.shape(); + return py::make_tuple(shape.first, shape.second); + }, + R"pbdoc( + Get the shape of the block as (depth, width). + + Returns: + tuple: (depth, width) tuple representing the block dimensions + )pbdoc") + .def("as_ref", [](const BlockRodSystem& block) { + return block_to_numpy(block.data(), py::cast(block)); + }, + R"pbdoc( + Get a numpy array view of the entire block data. + + Returns: + numpy.ndarray: A writable numpy array view into the block's data. + The array does not own the data. + )pbdoc", + py::keep_alive<0, 1>()) + .def("system_start_index", [](const BlockRodSystem& block, std::size_t index) { + return block.system_start_index(index); + }, + R"pbdoc( + Get the starting column index for a specific rod. + + Args: + index: Index of the rod + + Returns: + int: Starting column index for the rod in the block + )pbdoc", + py::arg("index")) + .def("at", [](BlockRodSystem& block, std::size_t index) -> BlockRodSystemViewType { + return block.at(index); + }, + R"pbdoc( + Get a view for a specific rod. + + Args: + index: Index of the rod + + Returns: + BlockRodSystemView: View object for accessing variables of this rod + )pbdoc", + py::arg("index"), py::return_value_policy::reference_internal) + .def("get", [](BlockRodSystem& block, const std::string& var_name) { + auto block_expr = get_block_variable_by_name(block, var_name); + return block_to_numpy(block_expr, py::cast(block)); + }, + R"pbdoc( + Get a variable by name as a numpy array view across all rods. + + Args: + var_name: Name of the variable (e.g., "position", "velocity", "director") + + Returns: + numpy.ndarray: A writable numpy array view into the variable's data + across all rods. The array does not own the data. + )pbdoc", + py::arg("var_name"), py::keep_alive<0, 1>()) + .def("compute_internal_forces_and_torques", [](BlockRodSystem& block, double time) { + ELASTICAPP_GIL_RELEASE_SCOPE(); + block.compute_internal_forces_and_torques(time); + }, + R"pbdoc( + Compute internal forces and torques for all rods in the block. + + This operation computes the internal forces and torques based on the + current state of the rods (positions, velocities, etc.). + + Args: + time: Current simulation time. + )pbdoc", + py::arg("time")) + .def("compute_strains", [](BlockRodSystem& block, double time) { + ELASTICAPP_GIL_RELEASE_SCOPE(); + block.compute_strains(time); + }, + R"pbdoc( + Compute strains for all rods in the block. + + This operation computes the shear/stretch strains and bending/twist strains + based on the current state of the rods. + + Args: + time: Current simulation time (included for API compatibility, not used in implementation). + )pbdoc", + py::arg("time")) + .def("update_accelerations", [](BlockRodSystem& block, double time) { + ELASTICAPP_GIL_RELEASE_SCOPE(); + block.update_accelerations(time); + }, + R"pbdoc( + Update accelerations based on forces and torques. + + This operation updates the acceleration variables based on the + computed forces and torques. + + Args: + time: Current simulation time (included for API compatibility, not used in implementation). + )pbdoc", + py::arg("time")) + .def("zeroed_out_external_forces_and_torques", [](BlockRodSystem& block, double time) { + ELASTICAPP_GIL_RELEASE_SCOPE(); + block.zeroed_out_external_forces_and_torques(time); + }, + R"pbdoc( + Zero out external forces and torques for all rods. + + This operation sets all external forces and torques to zero, + typically called at the beginning of each time step. + + Args: + time: Current simulation time (included for API compatibility, not used in implementation). + )pbdoc", + py::arg("time")) + .def("update_kinematics", [](BlockRodSystem& block, double time, double prefac) { + ELASTICAPP_GIL_RELEASE_SCOPE(); + block.update_kinematics(prefac); + }, + R"pbdoc( + Update kinematics (position, director) for all rods. + + This operation updates the kinematic variables based on velocity and omega. + Updates: position += prefac * velocity, director = R(prefac * omega) @ director + + Args: + time: Current time (for compatibility with Python interface, not used in C++) + prefac: Integration prefactor (e.g., time step dt) + )pbdoc", + py::arg("time"), py::arg("prefac")) + .def("update_dynamics", [](BlockRodSystem& block, double time, double prefac) { + ELASTICAPP_GIL_RELEASE_SCOPE(); + block.update_dynamics(prefac); + }, + R"pbdoc( + Update dynamics (velocity, omega) for all rods. + + This operation updates the dynamic variables based on acceleration and alpha. + Updates: velocity += prefac * acceleration, omega += prefac * alpha + + Args: + time: Current time (for compatibility with Python interface, not used in C++) + prefac: Integration prefactor (e.g., time step dt) + )pbdoc", + py::arg("time"), py::arg("prefac")) + .def_property_readonly("ghost_nodes_idx", [](const BlockRodSystem& block) { + auto indices = block.ghost_nodes_idx(); + // Convert to numpy array (pybind11 will handle the conversion automatically) + return py::cast(indices); + }, + R"pbdoc( + Get indices of ghost nodes between rods. + + Returns: + numpy.ndarray: An array of ghost node indices (length: n_rods - 1). + The array does not own the data. + )pbdoc") + .def_property_readonly("ghost_elems_idx", [](const BlockRodSystem& block) { + auto indices = block.ghost_elems_idx(); + // Convert to numpy array (pybind11 will handle the conversion automatically) + return py::cast(indices); + }, + R"pbdoc( + Get indices of ghost elements between rods. + + Returns: + numpy.ndarray: An array of ghost element indices (length: 2 * (n_rods - 1)). + The array does not own the data. + )pbdoc") + .def_property_readonly("ghost_voronoi_idx", [](const BlockRodSystem& block) { + auto indices = block.ghost_voronoi_idx(); + // Convert to numpy array (pybind11 will handle the conversion automatically) + return py::cast(indices); + }, + R"pbdoc( + Get indices of ghost voronoi nodes between rods. + + Returns: + numpy.ndarray: An array of ghost voronoi indices (length: 3 * (n_rods - 1)). + The array does not own the data. + )pbdoc") + .def("reset_ghost_for_variable", [](BlockRodSystem& block, const std::string& var_name) { + // Helper to reset ghost for a variable by name + reset_ghost_for_variable_by_name(block, var_name); + }, + R"pbdoc( + Reset ghost values for a specific variable by name. + + Args: + var_name: Name of the variable (e.g., "position", "velocity", "director") + )pbdoc", + py::arg("var_name")) + .def("reset_ghost", [](BlockRodSystem& block) { + block.reset_ghost(); + }, + R"pbdoc( + Reset ghost values for all variables. + + This operation sets all ghost node/element/voronoi values to their + default ghost_value as defined in each variable type. + )pbdoc"); + + // BlockRodSystemView class + py::class_(m, "BlockRodSystemView") + .def_property_readonly("shape", [](const BlockRodSystemViewType& view) { + auto shape = view.shape(); + return py::make_tuple(shape.first, shape.second); + }, + R"pbdoc( + Get the shape of the view as (depth, width). + + Returns: + tuple: (depth, width) tuple representing the view dimensions + )pbdoc") + .def("as_ref", [](const BlockRodSystemViewType& view) { + return block_to_numpy(view.data(), py::cast(view)); + }, + R"pbdoc( + Get a numpy array view of the entire view data. + + Returns: + numpy.ndarray: A writable numpy array view into the view's data. + The array does not own the data. + )pbdoc", + py::keep_alive<0, 1>()) + .def("get", [](BlockRodSystemViewType& view, const std::string& var_name) { + auto block_expr = get_variable_by_name(view, var_name); + return block_to_numpy(block_expr, py::cast(view)); + }, + R"pbdoc( + Get a variable by name as a numpy array view. + + Args: + var_name: Name of the variable (e.g., "position", "velocity", "director") + + Returns: + numpy.ndarray: A writable numpy array view into the variable's data. + The array does not own the data. + )pbdoc", + py::arg("var_name"), py::keep_alive<0, 1>()); + +} +} // namespace elasticapp diff --git a/backend/src/_version.cpp b/backend/src/_version.cpp new file mode 100644 index 000000000..e4e99d909 --- /dev/null +++ b/backend/src/_version.cpp @@ -0,0 +1,27 @@ +#include +#include "version.h" + +namespace py = pybind11; + +PYBIND11_MODULE(version, m) { + m.doc() = R"pbdoc( + Elasticapp version module + ------------------------- + + Provides version information for the elasticapp package. + )pbdoc"; + + // Version function + m.def("version", &elasticapp::version, R"pbdoc( + Return the current version of elasticapp. + + Returns: + str: The version string + )pbdoc"); + +#ifdef VERSION_INFO + m.attr("__version__") = elasticapp::version(); +#else + m.attr("__version__") = "dev"; +#endif +} diff --git a/backend/src/api.h b/backend/src/api.h new file mode 100644 index 000000000..e17670b42 --- /dev/null +++ b/backend/src/api.h @@ -0,0 +1,21 @@ +#pragma once + +#include "block.h" +#include "cosserat_rod_system.h" +#include "operations.h" + +namespace elasticapp { + +// BlockRodSystem is a CRTP mix of CosseratRodSystem, Block, and CosseratRodOperations +// It combines System functionality with Block data storage and rod-specific operations +// This allows Block to have access to all CosseratRodSystem variables and operations +// Block inherits from: +// - CosseratRodSystem (system variables and methods) +// - CosseratRodOperations> (rod-specific operations) +// So it has: +// - All CosseratRodSystem methods +// - All Block methods +// - All CosseratRodOperations methods (compute_internal_forces_and_torques, etc.) +// - Access to CosseratRodSystem::Variables for template metaprogramming +using BlockRodSystem = Block; +} // namespace elasticapp diff --git a/backend/src/block.h b/backend/src/block.h new file mode 100644 index 000000000..0f9cb4514 --- /dev/null +++ b/backend/src/block.h @@ -0,0 +1,280 @@ +#pragma once + +#include +#include +#include +#include +#include "traits.h" +#include "system.h" +#include "block_get_impl.h" +#include "block_view.h" +#include "operations.h" + +namespace elasticapp { + +// Block class using CRTP with SystemModel and Operations +// Block inherits from SystemModel, giving it access to all system variables and methods +// Block also inherits from OperationsType using CRTP, allowing for extensible operations +// SystemModel must be a System type +// OperationsType must be a template class that takes the Block type as a template parameter +template class OperationsType = DefaultOperations> +class Block : public SystemType, public OperationsType> { +public: + + using System = SystemType; + using Variables = typename SystemType::Variables; + using View = BlockView; + + constexpr static std::size_t GHOST_NODE_WIDTH = 1; // Size of ghost for node variables. + + // Constructor: takes list of element counts per rod + explicit Block(const std::vector& n_elems_per_rod) { + // Validate input: reject empty list or less than 6 total elements + if (n_elems_per_rod.empty()) { + throw std::invalid_argument("n_elems_per_rod cannot be empty"); + } + + std::size_t total_elements = 0; + for (std::size_t n_elems : n_elems_per_rod) { + total_elements += n_elems; + } + if (total_elements < 6) { + throw std::invalid_argument("Total number of elements must be at least 6, got " + + std::to_string(total_elements)); + } + + compute_width_and_indices(n_elems_per_rod); + depth_ = SystemType::get_depth(); + data_ = MatrixType(static_cast(depth_), static_cast(width_)); + data_.setZero(); + initialize_ghost_indices(); + reset_ghost(); // Initialize all ghost values + } + + std::pair shape() const { + return std::make_pair( + static_cast(data_.rows()), + static_cast(data_.cols()) + ); + } + + // Accessors + std::size_t width() const { return width_; } + std::size_t depth() const { return depth_; } + std::size_t n_systems() const { return rod_start_indices_.size(); } + + std::size_t system_start_index(std::size_t rod_index) const { + if (rod_index >= rod_start_indices_.size()) { + throw std::out_of_range("System index out of range"); + } + return rod_start_indices_[rod_index]; + } + + MatrixType& data() { return data_; } + const MatrixType& data() const { return data_; } + + // Get number of elements for a specific rod + std::size_t rod_n_elems(std::size_t rod_index) const { + if (rod_index >= rod_n_elems_.size()) { + throw std::out_of_range("System index out of range"); + } + return rod_n_elems_[rod_index]; + } + + std::size_t rod_n_nodes(std::size_t rod_index) const { + return rod_n_elems(rod_index) + 1; + } + + std::size_t rod_n_voronoi(std::size_t rod_index) const { + return rod_n_elems(rod_index) - 1; + } + + // Get the n_elems_per_rod vector (for BlockView construction) + const std::vector& get_n_elems_per_rod() const { return rod_n_elems_; } + + // Get ghost indices (return const references for efficiency) + inline const std::vector& ghost_nodes_idx() const { + return ghost_nodes_idx_; + } + inline const std::vector& ghost_elems_idx() const { + return ghost_elems_idx_; + } + inline const std::vector& ghost_voronoi_idx() const { + return ghost_voronoi_idx_; + } + + // Reset ghost values for a specific variable + // Uses VariableTag::ghost_value and appropriate ghost indices based on placement + template + void reset_ghost_for_variable() { + static_assert(tuple_contains_v>, + "VariableTag is not a valid member of tuple SystemType::Variables"); + + // Compute row offset for this variable + constexpr std::size_t row_offset = compute_variable_offset(); + constexpr std::size_t var_dimension = get_dimension_v; + + // Get appropriate ghost indices based on placement + std::vector ghost_indices; + if constexpr (std::is_base_of_v) { + ghost_indices = ghost_nodes_idx(); + } else if constexpr (std::is_base_of_v) { + ghost_indices = ghost_elems_idx(); + } else if constexpr (std::is_base_of_v) { + ghost_indices = ghost_voronoi_idx(); + } + + // Set ghost values at each ghost index + // Note: ghost indices are in the full width coordinate system + // For OnElement and OnVoronoi variables, we need to ensure ghost indices + // are within the adjusted width (since get() returns adjusted width views) + const auto& ghost_val = VariableTag::ghost_value; + + // Optimized: Add simd hint for inner loop (var_dimension is compile-time constant) + for (std::size_t ghost_col : ghost_indices) { + // Only reset ghost values that are within the adjusted width + // (ghost indices beyond adjusted width are not accessible via get()) + IndexType data_col = static_cast(ghost_col); + #pragma omp simd + for (std::size_t row = 0; row < var_dimension; ++row) { + IndexType data_row = static_cast(row_offset + row); + data_(data_row, data_col) = ghost_val(static_cast(row), 0); + } + } + } + + // Reset ghost values for all variables + // Iterates over all variables and calls reset_ghost_for_variable for each + void reset_ghost() { + reset_ghost_impl, 0>(); + } + + // Get a view for a specific variable across all rods + // Returns a view into the variable's data (rows) and adjusted columns based on placement + // - OnNode: full width + // - OnElement: width - 1 + // - OnVoronoi: width - 2 + // No data is copied - returns a reference to the same matrix + template + auto get() { + // Assert VariableTag is a valid member of tuple SystemType::Variables + static_assert(tuple_contains_v>, + "VariableTag is not a valid member of tuple SystemType::Variables"); + // Compute row offset for this variable + constexpr std::size_t row_offset = compute_variable_offset(); + constexpr std::size_t var_dimension = get_dimension_v; + + // Adjust width based on placement type + std::size_t adjusted_width = width_; + if constexpr (std::is_base_of_v) { + adjusted_width = width_ > 0 ? width_ - 1 : 0; + } else if constexpr (std::is_base_of_v) { + adjusted_width = width_ > 1 ? width_ - 2 : 0; + } + // OnNode: no adjustment needed (full width) + + // Return a view into the specific rows and adjusted columns + return get_block_slice(data_, row_offset, var_dimension, 0, adjusted_width); + } + + template + auto get() const { + // Assert VariableTag is a valid member of tuple SystemType::Variables + static_assert(tuple_contains_v>, + "VariableTag is not a valid member of tuple SystemType::Variables"); + // Compute row offset for this variable + constexpr std::size_t row_offset = compute_variable_offset(); + constexpr std::size_t var_dimension = get_dimension_v; + + // Adjust width based on placement type + std::size_t adjusted_width = width_; + if constexpr (std::is_base_of_v) { + adjusted_width = width_ > 0 ? width_ - 1 : 0; + } else if constexpr (std::is_base_of_v) { + adjusted_width = width_ > 1 ? width_ - 2 : 0; + } + // OnNode: no adjustment needed (full width) + + // Return a view into the specific rows and adjusted columns + return get_block_slice(data_, row_offset, var_dimension, 0, adjusted_width); + } + + // Create a view for a specific rod + View at(std::size_t rod_index); + +private: + MatrixType data_; + std::size_t width_; + std::size_t depth_; + std::vector rod_start_indices_; + std::vector rod_n_elems_; // Store n_elems for each rod + + std::vector ghost_nodes_idx_; // Indices of ghost nodes between rods. + std::vector ghost_elems_idx_; // Indices of ghost elements between rods. + std::vector ghost_voronoi_idx_; // Indices of ghost voronoi nodes between rods. + + void compute_width_and_indices(const std::vector& n_elems_per_rod) { + width_ = 0; + rod_start_indices_.clear(); + rod_n_elems_.clear(); + rod_start_indices_.reserve(n_elems_per_rod.size()); + rod_n_elems_.reserve(n_elems_per_rod.size()); + + for (std::size_t n_elems : n_elems_per_rod) { + rod_start_indices_.push_back(width_); + rod_n_elems_.push_back(n_elems); + // Each rod has n_elems + 1 nodes + width_ += n_elems + 1 + GHOST_NODE_WIDTH; + } + width_ -= GHOST_NODE_WIDTH; // Remove the last ghost node width. + } + + void initialize_ghost_indices() { + ghost_nodes_idx_.reserve(rod_n_elems_.size() * GHOST_NODE_WIDTH); + ghost_elems_idx_.reserve(rod_n_elems_.size() * (1 + GHOST_NODE_WIDTH)); + ghost_voronoi_idx_.reserve(rod_n_elems_.size() * (2 + GHOST_NODE_WIDTH)); + + std::size_t cumulative_nodes = 0; + for (std::size_t i = 0; i < rod_n_elems_.size(); ++i) { + cumulative_nodes += rod_n_elems_[i] + 1; // n_elems + 1 = n_nodes + ghost_elems_idx_.push_back(cumulative_nodes - 1); + ghost_voronoi_idx_.push_back(cumulative_nodes - 2); + ghost_voronoi_idx_.push_back(cumulative_nodes - 1); + for (std::size_t j = 0; j < GHOST_NODE_WIDTH; ++j) { + ghost_nodes_idx_.push_back(cumulative_nodes + j); + ghost_elems_idx_.push_back(cumulative_nodes + j); + ghost_voronoi_idx_.push_back(cumulative_nodes + j); + } + cumulative_nodes += GHOST_NODE_WIDTH; + } + + ghost_nodes_idx_.pop_back(); + ghost_elems_idx_.pop_back(); + ghost_voronoi_idx_.pop_back(); + } + + // Helper to iterate over all variables and reset ghost values + template + void reset_ghost_impl() { + using CurrentVar = std::tuple_element_t; + reset_ghost_for_variable(); + + // Recurse to next variable if not last + if constexpr (Index + 1 < std::tuple_size_v) { + reset_ghost_impl(); + } + } + +}; + + +// Implement at() after BlockView is fully defined +template class OperationsType> +typename Block::View Block::at(std::size_t rod_index) { + std::size_t rod_start_col = system_start_index(rod_index); + std::size_t rod_n_elems = rod_n_elems_[rod_index]; + return BlockView(data_, rod_index, rod_start_col, rod_n_elems); +} + + +} // namespace elasticapp diff --git a/backend/src/block_get_impl.h b/backend/src/block_get_impl.h new file mode 100644 index 000000000..3b70341d4 --- /dev/null +++ b/backend/src/block_get_impl.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include +#include "system.h" +#include "variable_offsets.h" +#include "traits.h" + +namespace elasticapp { + +// Uses C++17 fold expressions for a concise implementation +// Helper to check if a type is in a tuple +template +struct tuple_contains_impl; + +template +struct tuple_contains_impl> { + static constexpr bool value = (std::is_same_v || ...); +}; + +template +constexpr bool tuple_contains_v = tuple_contains_impl::value; + + +// Helper function to get number of columns for a variable based on placement +template +inline constexpr std::size_t get_variable_num_cols(std::size_t rod_n_nodes, + std::size_t rod_n_elems, + std::size_t rod_n_voronoi) { + if constexpr (std::is_base_of_v) { + return rod_n_nodes; + } else if constexpr (std::is_base_of_v) { + return rod_n_elems; + } else if constexpr (std::is_base_of_v) { + return rod_n_voronoi; + } else { + static_assert(std::is_base_of_v || + std::is_base_of_v || + std::is_base_of_v, + "VariableTag must have a Placement tag"); + return 0; // Should never reach here + } +} + +// Generic implementation helper for Block::get() and BlockRodSystemView::get() +// Extracted to reduce code duplication between const and non-const versions +// This can be used by both Block and BlockRodSystemView classes +template +inline auto get_impl(MatrixRef&& matrix, + std::size_t rod_n_nodes, + std::size_t rod_n_elems, + std::size_t rod_n_voronoi, + std::size_t rod_start_col) { + // Assert VariableTag is a valid member of tuple SystemType::Variables + static_assert(tuple_contains_v>, + "VariableTag is not a valid member of tuple SystemType::Variables"); + // Compute row offset for this variable + constexpr std::size_t row_offset = compute_variable_offset(); + constexpr std::size_t var_dimension = get_dimension_v; + + // Determine number of columns based on placement + std::size_t num_cols = get_variable_num_cols( + rod_n_nodes, rod_n_elems, rod_n_voronoi); + + // Return a view into the specific rows and columns + return get_block_slice(matrix, row_offset, var_dimension, rod_start_col, num_cols); +} + +} // namespace elasticapp diff --git a/backend/src/block_view.h b/backend/src/block_view.h new file mode 100644 index 000000000..2e98627fd --- /dev/null +++ b/backend/src/block_view.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include "system.h" +#include "block_get_impl.h" +#include "traits.h" + +namespace elasticapp { + +// BlockView provides access to variables for a specific rod +// This is templated on SystemType and uses template metaprogramming to expose variables +template +class BlockView { +public: + // using Variables = typename SystemType::Variables; // tuple of variables + constexpr static std::size_t depth = SystemType::get_depth(); + + BlockView(MatrixType& data, std::size_t rod_index, + std::size_t rod_start_col, std::size_t rod_n_elems); + + std::pair shape() const { + return std::make_pair(depth, rod_n_nodes_); + } + + // Return a view into the submatrix (columns for this rod) + // Uses traits helper function to encapsulate Eigen-specific operations + auto data() { + return get_column_slice(data_, rod_start_col_, rod_n_nodes_); + } + + auto data() const { + return get_column_slice(data_, rod_start_col_, rod_n_nodes_); + } + + // Get a view for a specific variable + // Returns a view into the variable's data (rows) and this rod's columns + // No data is copied - returns a reference to the same matrix + template + auto get() { + return get_impl( + data_, rod_n_nodes_, rod_n_elems_, rod_n_voronoi_, rod_start_col_); + } + + template + auto get() const { + return get_impl( + data_, rod_n_nodes_, rod_n_elems_, rod_n_voronoi_, rod_start_col_); + } + +protected: + MatrixType& data_; + std::size_t rod_index_; + std::size_t rod_n_elems_; + std::size_t rod_n_nodes_; + std::size_t rod_n_voronoi_; + std::size_t rod_start_col_; +}; + +// Note: block.h is included after BlockView definition to break circular dependency +// The BlockView constructor implementation is in block.h after Block is fully defined +template +BlockView::BlockView(MatrixType& data, std::size_t rod_index, + std::size_t rod_start_col, std::size_t rod_n_elems) + : data_(data), rod_index_(rod_index), + rod_n_elems_(rod_n_elems), + rod_n_nodes_(rod_n_elems + 1), + rod_n_voronoi_((rod_n_elems > 0) ? rod_n_elems - 1 : 0), + rod_start_col_(rod_start_col) +{ +} + +} // namespace elasticapp diff --git a/backend/src/cosserat_equations.h b/backend/src/cosserat_equations.h new file mode 100644 index 000000000..75a62f149 --- /dev/null +++ b/backend/src/cosserat_equations.h @@ -0,0 +1,607 @@ +#pragma once + +#include +#include +#include "cosserat_rod_system.h" +#include "traits.h" +#include "math/eigen_detail/eigen_linear_algebra.hpp" +#include "math/eigen_detail/eigen_calculus.hpp" +#include + +namespace elasticapp { + +// Forward declaration +template class OperationsType> +class Block; + +// Compute geometry from state (lengths, tangents, radius) +// Updates: lengths, tangents, radius +template +inline void compute_geometry_from_state(BlockType& block) { + // Get variable views + auto&& position = block.template get(); + auto&& volume = block.template get(); + auto&& lengths = block.template get(); + auto&& tangents = block.template get(); + auto&& radius = block.template get(); + + // Compute position differences using difference_kernel + // position_diff = position[:, 1:] - position[:, :-1] + // This is equivalent to: difference_kernel(position) + auto position_diff = difference_kernel(position); + + // Compute lengths = norm(position_diff) + 1e-14 + // FIXME: 1e-14 is added to fix ghost lengths, which is 0, and causes division by zero error! + auto lengths_vec = batch_norm(position_diff); + const IndexType n_elems = lengths.cols(); + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_elems; ++k) { + lengths(0, k) = lengths_vec(k) + 1e-14; + } + + // Compute tangents = position_diff / lengths + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_elems; ++k) { + const double len = lengths(0, k); + tangents(0, k) = position_diff(0, k) / len; + tangents(1, k) = position_diff(1, k) / len; + tangents(2, k) = position_diff(2, k) / len; + } + + // Compute radius from volume conservation: radius = sqrt(volume / (lengths * pi)) + const double pi = 3.14159265358979323846; + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_elems; ++k) { + radius(0, k) = std::sqrt(volume(0, k) / (lengths(0, k) * pi)); + } +} + +// Compute all dilatations (element and voronoi) +// Updates: lengths, tangents, radius, dilatation, voronoi_dilatation +template +inline void compute_all_dilatations(BlockType& block) { + // Get variable views + auto&& lengths = block.template get(); + auto&& rest_lengths = block.template get(); + auto&& dilatation = block.template get(); + auto&& rest_voronoi_lengths = block.template get(); + auto&& voronoi_dilatation = block.template get(); + + // Compute dilatation = lengths / rest_lengths + const IndexType n_elems = lengths.cols(); + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_elems; ++k) { + dilatation(0, k) = lengths(0, k) / rest_lengths(0, k); + } + + // Compute voronoi_lengths from average of lengths + // voronoi_lengths = 0.5 * (lengths[k+1] + lengths[k]) for k in [0, n_voronoi-1] + auto voronoi_lengths = average_kernel(lengths); + + // Compute voronoi_dilatation = voronoi_lengths / rest_voronoi_lengths + const IndexType n_voronoi = voronoi_dilatation.cols(); + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_voronoi; ++k) { + voronoi_dilatation(0, k) = voronoi_lengths(0, k) / rest_voronoi_lengths(0, k); + } +} + +// Compute shear/stretch strains (sigma) +// Updates: lengths, tangents, radius, dilatation, voronoi_dilatation, sigma +template +inline void compute_shear_stretch_strains(BlockType& block) { + // Get variable views + auto&& dilatation = block.template get(); + auto&& director = block.template get(); + auto&& tangents = block.template get(); + auto&& sigma = block.template get(); + + // Compute sigma = dilatation * batch_matvec(director_collection, tangents) - z_vector + // director is stored as (9, n_elems) - flattened 3x3 matrices + // Storage order: [d00, d10, d20, d01, d11, d21, d02, d12, d22] (column-major) + // tangents is (3, n_elems) + // sigma is (3, n_elems) + const IndexType n_elems = sigma.cols(); + + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_elems; ++k) { + // Extract 3x3 director matrix for element k + // director[:, k] is (row-wise flattened 3x3 matrix) + // [d00, d01, d02] + // [d10, d11, d12] + // [d20, d21, d22] + double d00 = director(0, k); + double d01 = director(1, k); + double d02 = director(2, k); + double d10 = director(3, k); + double d11 = director(4, k); + double d12 = director(5, k); + double d20 = director(6, k); + double d21 = director(7, k); + double d22 = director(8, k); + + // Compute director @ tangents for element k + // result = director_matrix * tangents[:, k] + double result0 = d00 * tangents(0, k) + d01 * tangents(1, k) + d02 * tangents(2, k); + double result1 = d10 * tangents(0, k) + d11 * tangents(1, k) + d12 * tangents(2, k); + double result2 = d20 * tangents(0, k) + d21 * tangents(1, k) + d22 * tangents(2, k); + + // Compute sigma = dilatation * result - z_vector + const double dil = dilatation(0, k); + sigma(0, k) = dil * result0; + sigma(1, k) = dil * result1; + sigma(2, k) = dil * result2 - 1.0; + } +} + +// Compute internal shear/stretch stresses from model +// Updates: lengths, tangents, radius, dilatation, voronoi_dilatation, sigma, internal_stress +template +inline void compute_internal_shear_stretch_stresses_from_model(BlockType& block) { + // Get variable views + auto&& shear_matrix = block.template get(); + auto&& sigma = block.template get(); + auto&& rest_sigma = block.template get(); + auto&& internal_stress = block.template get(); + + // Compute sigma_diff = sigma - rest_sigma + // sigma is (3, n_elems), rest_sigma is (3, n_elems) + const IndexType n_elems = sigma.cols(); + + // Compute internal_stress = batch_matvec(shear_matrix, sigma - rest_sigma) + // shear_matrix is stored as (9, n_elems) - flattened 3x3 matrices + // Storage order: [m00, m10, m20, m01, m11, m21, m02, m12, m22] (column-major) + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_elems; ++k) { + // Compute sigma_diff = sigma - rest_sigma for element k + double sigma_diff0 = sigma(0, k) - rest_sigma(0, k); + double sigma_diff1 = sigma(1, k) - rest_sigma(1, k); + double sigma_diff2 = sigma(2, k) - rest_sigma(2, k); + + // Extract 3x3 shear_matrix for element k + // shear_matrix[:, k] is [m00, m10, m20, m01, m11, m21, m02, m12, m22] + double m00 = shear_matrix(0, k); + double m10 = shear_matrix(1, k); + double m20 = shear_matrix(2, k); + double m01 = shear_matrix(3, k); + double m11 = shear_matrix(4, k); + double m21 = shear_matrix(5, k); + double m02 = shear_matrix(6, k); + double m12 = shear_matrix(7, k); + double m22 = shear_matrix(8, k); + + // Compute shear_matrix @ sigma_diff for element k + // result = shear_matrix * sigma_diff + internal_stress(0, k) = m00 * sigma_diff0 + m01 * sigma_diff1 + m02 * sigma_diff2; + internal_stress(1, k) = m10 * sigma_diff0 + m11 * sigma_diff1 + m12 * sigma_diff2; + internal_stress(2, k) = m20 * sigma_diff0 + m21 * sigma_diff1 + m22 * sigma_diff2; + } +} + +// Compute internal forces for Cosserat rod +// This is a template function that will be implemented later +template +inline void compute_internal_forces(BlockType& block) { + // Get variable views + auto&& director = block.template get(); + auto&& internal_stress = block.template get(); + auto&& dilatation = block.template get(); + auto&& internal_forces = block.template get(); + auto&& cosserat_internal_stress = block.template get(); + + // Compute cosserat_internal_stress = director^T @ internal_stress / dilatation + // director is stored as (9, n_elems) - flattened 3x3 matrices + // Storage order: [d00, d10, d20, d01, d11, d21, d02, d12, d22] (column-major) + // internal_stress is (3, n_elems) where n_elems includes ghost elements + // cosserat_internal_stress is (3, n_elems) + // Note: internal_stress has adjusted width (width - 1 for OnElement), but we need full width + // Get the full width from director which also has adjusted width + const IndexType n_elems = director.cols(); + + // Temporary matrix for cosserat_internal_stress + // MatrixType cosserat_internal_stress(3, n_elems); + + // Compute cosserat_internal_stress = director^T @ internal_stress + // Python: cosserat_internal_stress[i, k] = sum_j(director_collection[j, i, k] * internal_stress[j, k]) + // In C++: director_collection[j, i, k] = director(j*3 + i, k) + for (IndexType i = 0; i < 3; ++i) { + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_elems; ++k) { + double sum = 0.0; + for (IndexType j = 0; j < 3; ++j) { + // director_collection[j, i, k] = director(j*3 + i, k) + IndexType director_idx = j * 3 + i; + sum += director(director_idx, k) * internal_stress(j, k); + } + cosserat_internal_stress(i, k) = sum / dilatation(0, k); + } + } + + // Reset ghost values for cosserat_internal_stress (OnElement) + block.template reset_ghost_for_variable(); + + // Compute internal_forces = two_point_difference_kernel(cosserat_internal_stress) + // internal_forces is OnNode (3, n_nodes) where n_nodes = n_elems + 1 + two_point_difference_kernel(internal_forces, cosserat_internal_stress); +} + +// Compute bending/twist strains (kappa) +// Updates: kappa +template +inline void compute_bending_twist_strains(BlockType& block) { + // Get variable views + auto&& director = block.template get(); + auto&& rest_voronoi_lengths = block.template get(); + auto&& kappa = block.template get(); + + // director is stored as (9, n_elems) - flattened 3x3 matrices + // Storage order: [d00, d10, d20, d01, d11, d21, d02, d12, d22] (column-major) + // rest_voronoi_lengths is (1, n_voronoi) where n_voronoi = n_elems - 1 + // kappa is (3, n_voronoi) + const IndexType n_voronoi = kappa.cols(); + + // Compute temp = inv_rotate(director_collection) + // inv_rotate computes relative rotation between consecutive director frames + // Python: temp = _inv_rotate(director_collection) + // temp has shape (3, n_voronoi) where n_voronoi = n_elems - 1 + double temp_x, temp_y, temp_z; + + // Compute inv_rotate manually: relative rotation between consecutive directors + // Python implementation computes cross products between consecutive director rows + #pragma omp parallel for schedule(static) + for (IndexType k = 0; k < n_voronoi; ++k) { + // Extract director matrices for k and k+1 + // director[:, k] is (row-wise flattened 3x3 matrix) + // [d00, d01, d02] + // [d10, d11, d12] + // [d20, d21, d22] + + // For director[k]: + double d00_k = director(0, k); + double d01_k = director(1, k); + double d02_k = director(2, k); + double d10_k = director(3, k); + double d11_k = director(4, k); + double d12_k = director(5, k); + double d20_k = director(6, k); + double d21_k = director(7, k); + double d22_k = director(8, k); + + // For director[k+1]: + double d00_k1 = director(0, k + 1); + double d01_k1 = director(1, k + 1); + double d02_k1 = director(2, k + 1); + double d10_k1 = director(3, k + 1); + double d11_k1 = director(4, k + 1); + double d12_k1 = director(5, k + 1); + double d20_k1 = director(6, k + 1); + double d21_k1 = director(7, k + 1); + double d22_k1 = director(8, k + 1); + + // Compute inv_rotate: cross product between consecutive director frames + temp_x = (d20_k1 * d10_k + d21_k1 * d11_k + d22_k1 * d12_k) - + (d10_k1 * d20_k + d11_k1 * d21_k + d12_k1 * d22_k); + temp_y = (d00_k1 * d20_k + d01_k1 * d21_k + d02_k1 * d22_k) - + (d20_k1 * d00_k + d21_k1 * d01_k + d22_k1 * d02_k); + temp_z = (d10_k1 * d00_k + d11_k1 * d01_k + d12_k1 * d02_k) - + (d00_k1 * d10_k + d01_k1 * d11_k + d02_k1 * d12_k); + + double trace = (d00_k1 * d00_k + d01_k1 * d01_k + d02_k1 * d02_k) + + (d10_k1 * d10_k + d11_k1 * d11_k + d12_k1 * d12_k) + + (d20_k1 * d20_k + d21_k1 * d21_k + d22_k1 * d22_k); + + // Clip the trace to between -1 and 3. + // Any deviation beyond this is numerical error + trace = std::clamp(trace, -1.0, 3.0); + double cos = 0.5 * trace - 0.5; + double theta = std::acos(cos) + 1e-14; + double magnitude = -0.5 * theta / std::sin(theta) / rest_voronoi_lengths(0, k); + kappa(0, k) = temp_x * magnitude; + kappa(1, k) = temp_y * magnitude; + kappa(2, k) = temp_z * magnitude; + } +} + +// Compute internal bending/twist stresses from model +// Updates: kappa, internal_couple +template +inline void compute_internal_bending_twist_stresses_from_model(BlockType& block) { + // Get variable views + auto&& kappa = block.template get(); + auto&& rest_kappa = block.template get(); + auto&& bend_matrix = block.template get(); + auto&& internal_couple = block.template get(); + + // kappa is (3, n_voronoi), rest_kappa is (3, n_voronoi) + // bend_matrix is stored as (9, n_voronoi) - flattened 3x3 matrices + // internal_couple is (3, n_voronoi) + const IndexType n_voronoi = kappa.cols(); + + // Compute diff_kappa = kappa - rest_kappa + // MatrixType diff_kappa(3, n_voronoi); + auto&& diff_kappa = block.template get(); + + for (IndexType i = 0; i < 3; ++i) { + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_voronoi; ++k) { + diff_kappa(i, k) = kappa(i, k) - rest_kappa(i, k); + } + } + + // Compute internal_couple = batch_matvec(bend_matrix, diff_kappa) + // bend_matrix is stored as (9, n_voronoi) - flattened 3x3 matrices + // Storage order: [m00, m10, m20, m01, m11, m21, m02, m12, m22] (column-major) + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_voronoi; ++k) { + // Extract 3x3 bend_matrix for voronoi k + // bend_matrix[:, k] is [m00, m10, m20, m01, m11, m21, m02, m12, m22] + double m00 = bend_matrix(0, k); + double m10 = bend_matrix(1, k); + double m20 = bend_matrix(2, k); + double m01 = bend_matrix(3, k); + double m11 = bend_matrix(4, k); + double m21 = bend_matrix(5, k); + double m02 = bend_matrix(6, k); + double m12 = bend_matrix(7, k); + double m22 = bend_matrix(8, k); + + // Compute bend_matrix @ diff_kappa for voronoi k + // result = bend_matrix * diff_kappa[:, k] + internal_couple(0, k) = m00 * diff_kappa(0, k) + m01 * diff_kappa(1, k) + m02 * diff_kappa(2, k); + internal_couple(1, k) = m10 * diff_kappa(0, k) + m11 * diff_kappa(1, k) + m12 * diff_kappa(2, k); + internal_couple(2, k) = m20 * diff_kappa(0, k) + m21 * diff_kappa(1, k) + m22 * diff_kappa(2, k); + } +} + +// Compute dilatation rate +// Updates: dilatation_rate +template +inline void compute_dilatation_rate(BlockType& block) { + // Get variable views + auto&& position = block.template get(); + auto&& velocity = block.template get(); + auto&& lengths = block.template get(); + auto&& rest_lengths = block.template get(); + auto&& dilatation_rate = block.template get(); + + // position is (3, n_nodes), velocity is (3, n_nodes) + // lengths is (1, n_elems), rest_lengths is (1, n_elems) + // dilatation_rate is (1, n_elems) + const IndexType n_nodes = position.cols(); + const IndexType n_elems = lengths.cols(); + + // Compute r_dot_v = batch_dot(position, velocity) + // This is the dot product of position and velocity at each node + // MatrixType r_dot_v(1, n_nodes); + auto&& r_dot_v = block.template get(); + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_nodes; ++k) { + r_dot_v(0, k) = position(0, k) * velocity(0, k) + + position(1, k) * velocity(1, k) + + position(2, k) * velocity(2, k); + } + + // Compute r_plus_one_dot_v = batch_dot(position[..., 1:], velocity[..., :-1]) + // Dot product of position[1:] and velocity[:-1] (both have n_elems elements) + // MatrixType r_plus_one_dot_v(1, n_elems); + auto&& r_plus_one_dot_v = block.template get(); + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_elems; ++k) { + r_plus_one_dot_v(0, k) = position(0, k + 1) * velocity(0, k) + + position(1, k + 1) * velocity(1, k) + + position(2, k + 1) * velocity(2, k); + } + + // Compute r_dot_v_plus_one = batch_dot(position[..., :-1], velocity[..., 1:]) + // Dot product of position[:-1] and velocity[1:] (both have n_elems elements) + // MatrixType r_dot_v_plus_one(1, n_elems); + auto&& r_dot_v_plus_one = block.template get(); + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_elems; ++k) { + r_dot_v_plus_one(0, k) = position(0, k) * velocity(0, k + 1) + + position(1, k) * velocity(1, k + 1) + + position(2, k) * velocity(2, k + 1); + } + + // Compute dilatation_rate for each element + // dilatation_rate[k] = (r_dot_v[k] + r_dot_v[k + 1] - r_dot_v_plus_one[k] - r_plus_one_dot_v[k]) + // / lengths[k] / rest_lengths[k] + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_elems; ++k) { + dilatation_rate(0, k) = (r_dot_v(0, k) + r_dot_v(0, k + 1) - + r_dot_v_plus_one(0, k) - r_plus_one_dot_v(0, k)) / + lengths(0, k) / rest_lengths(0, k); + } +} + +// Compute internal torques for Cosserat rod +// This is a template function that will be implemented later +template +inline void compute_internal_torques(BlockType& block) { + // Get variable views + auto&& voronoi_dilatation = block.template get(); + auto&& internal_couple = block.template get(); + auto&& kappa = block.template get(); + auto&& rest_voronoi_lengths = block.template get(); + auto&& director = block.template get(); + auto&& tangents = block.template get(); + auto&& internal_stress = block.template get(); + auto&& rest_lengths = block.template get(); + auto&& mass_second_moment_of_inertia = block.template get(); + auto&& omega = block.template get(); + auto&& dilatation = block.template get(); + auto&& dilatation_rate = block.template get(); + auto&& internal_torques = block.template get(); + + // voronoi_dilatation is (1, n_voronoi), internal_couple is (3, n_voronoi) + // kappa is (3, n_voronoi), rest_voronoi_lengths is (1, n_voronoi) + // director is (9, n_elems), tangents is (3, n_elems) + // internal_stress is (3, n_elems), rest_lengths is (1, n_elems) + // mass_second_moment_of_inertia is (9, n_elems), omega is (3, n_elems) + // dilatation is (1, n_elems), dilatation_rate is (1, n_elems) + // internal_torques is (3, n_elems) + const IndexType n_voronoi = voronoi_dilatation.cols(); + const IndexType n_elems = internal_torques.cols(); + const IndexType n_nodes = n_elems + 1; + + // Scratch buffers + auto&& voronoi_dilatation_inv_cube_cached = block.template get(); + auto&& bend_twist_couple_2D = block.template get(); + auto&& J_omega_upon_e = block.template get(); + auto&& scratch_vec_b_voronoi = block.template get(); + auto&& bend_twist_couple_3D = block.template get(); + auto&& shear_stretch_couple = block.template get(); + auto&& lagrangian_transport = block.template get(); + auto&& unsteady_dilatation = block.template get(); + + // Compute voronoi_dilatation_inv_cube_cached = 1.0 / voronoi_dilatation^3 + // MatrixType voronoi_dilatation_inv_cube_cached(1, n_voronoi); + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_voronoi; ++k) { + double voronoi_dil = voronoi_dilatation(0, k); + voronoi_dilatation_inv_cube_cached(0, k) = 1.0 / (voronoi_dil * voronoi_dil * voronoi_dil); + } + + // Compute bend_twist_couple_2D = difference_kernel(internal_couple * voronoi_dilatation_inv_cube_cached, ghost_voronoi_idx) + // First compute the product + // MatrixType internal_couple_scaled(3, n_voronoi); + auto internal_couple_scaled = scratch_vec_b_voronoi; + for (IndexType i = 0; i < 3; ++i) { + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_voronoi; ++k) { + internal_couple_scaled(i, k) = internal_couple(i, k) * voronoi_dilatation_inv_cube_cached(0, k); + } + } + + // Reset ghost values for internal_couple_scaled (OnVoronoi) + // reset_ghost :: internal_couple_scaled + block.template reset_ghost_for_variable(); + + // Apply difference_kernel (two_point_difference_kernel) + // MatrixType bend_twist_couple_2D(3, n_nodes); + two_point_difference_kernel(bend_twist_couple_2D, internal_couple_scaled); + + // Compute bend_twist_couple_3D = quadrature_kernel((kappa x internal_couple) * rest_voronoi_lengths * voronoi_dilatation_inv_cube_cached, ghost_voronoi_idx) + // First compute kappa x internal_couple + // MatrixType kappa_cross_internal_couple(3, n_voronoi); + auto kappa_cross_internal_couple = scratch_vec_b_voronoi; + batch_cross(kappa_cross_internal_couple, kappa, internal_couple); + + // Multiply by rest_voronoi_lengths and voronoi_dilatation_inv_cube_cached + // MatrixType bend_twist_couple_3D_input(3, n_voronoi); + auto bend_twist_couple_3D_input = scratch_vec_b_voronoi; + for (IndexType i = 0; i < 3; ++i) { + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_voronoi; ++k) { + bend_twist_couple_3D_input(i, k) = kappa_cross_internal_couple(i, k) * + rest_voronoi_lengths(0, k) * + voronoi_dilatation_inv_cube_cached(0, k); + } + } + + // Reset ghost values + // reset_ghost:: bend_twist_couple_3D_input; + block.template reset_ghost_for_variable(); + + // Apply quadrature_kernel (trapezoidal) + // MatrixType bend_twist_couple_3D(3, n_nodes); + quadrature_kernel(bend_twist_couple_3D, bend_twist_couple_3D_input); + + // Compute shear_stretch_couple = (Q^T * tangents) x internal_stress * rest_lengths + // First compute Q^T * tangents (same as director^T @ tangents) + // { // DEPRECATED BLOCK: REPLACED WITH BELOW OPS + // MatrixType director_tangents(3, n_elems); + // for (IndexType i = 0; i < 3; ++i) { + // #pragma omp parallel for simd schedule(static) + // for (IndexType k = 0; k < n_elems; ++k) { + // double sum = 0.0; + // for (IndexType j = 0; j < 3; ++j) { + // IndexType director_idx = j * 3 + i; + // sum += director(director_idx, k) * tangents(j, k); + // } + // director_tangents(i, k) = sum; + // } + // } + + // // Compute cross product: (Q^T * tangents) x internal_stress + // // MatrixType shear_stretch_couple(3, n_elems); + // batch_cross(shear_stretch_couple, director_tangents, internal_stress); + + // // Multiply by rest_lengths + // for (IndexType i = 0; i < 3; ++i) { + // #pragma omp parallel for simd schedule(static) + // for (IndexType k = 0; k < n_elems; ++k) { + // shear_stretch_couple(i, k) *= rest_lengths(0, k); + // } + // } + // } + + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_elems; ++k) { + // Compute Q^T * tangents + + double a0 = director(0, k) * tangents(0, k) + director(3, k) * tangents(1, k) + director(6, k) * tangents(2, k); + double a1 = director(1, k) * tangents(0, k) + director(4, k) * tangents(1, k) + director(7, k) * tangents(2, k); + double a2 = director(2, k) * tangents(0, k) + director(5, k) * tangents(1, k) + director(8, k) * tangents(2, k); + + // internal_stress alias + double i0 = internal_stress(0, k); + double i1 = internal_stress(1, k); + double i2 = internal_stress(2, k); + + // cross-product (Q*t x n) * l_hat + shear_stretch_couple(0, k) += (a1 * i2 - a2 * i1) * rest_lengths(0, k); + shear_stretch_couple(1, k) += (a2 * i0 - a0 * i2) * rest_lengths(0, k); + shear_stretch_couple(2, k) += (a0 * i1 - a1 * i0) * rest_lengths(0, k); + } + + // Compute J_omega_upon_e = batch_matvec(mass_second_moment_of_inertia, omega) / dilatation + // MatrixType J_omega_upon_e(3, n_elems); + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_elems; ++k) { + // Extract 3x3 mass_second_moment_of_inertia for element k + double m00 = mass_second_moment_of_inertia(0, k); + double m10 = mass_second_moment_of_inertia(1, k); + double m20 = mass_second_moment_of_inertia(2, k); + double m01 = mass_second_moment_of_inertia(3, k); + double m11 = mass_second_moment_of_inertia(4, k); + double m21 = mass_second_moment_of_inertia(5, k); + double m02 = mass_second_moment_of_inertia(6, k); + double m12 = mass_second_moment_of_inertia(7, k); + double m22 = mass_second_moment_of_inertia(8, k); + + // Compute mass_second_moment_of_inertia @ omega + J_omega_upon_e(0, k) = (m00 * omega(0, k) + m01 * omega(1, k) + m02 * omega(2, k)) / dilatation(0, k); + J_omega_upon_e(1, k) = (m10 * omega(0, k) + m11 * omega(1, k) + m12 * omega(2, k)) / dilatation(0, k); + J_omega_upon_e(2, k) = (m20 * omega(0, k) + m21 * omega(1, k) + m22 * omega(2, k)) / dilatation(0, k); + } + + // Compute lagrangian_transport = (J * omega / dilatation) x omega + // MatrixType lagrangian_transport(3, n_elems); + batch_cross(lagrangian_transport, J_omega_upon_e, omega); + + // Compute unsteady_dilatation = J_omega_upon_e * dilatation_rate / dilatation + // MatrixType unsteady_dilatation(3, n_elems); + for (IndexType i = 0; i < 3; ++i) { + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_elems; ++k) { + unsteady_dilatation(i, k) = J_omega_upon_e(i, k) * dilatation_rate(0, k) / dilatation(0, k); + } + } + + // Compute internal_torques = sum of all components + // Note: bend_twist_couple_2D and bend_twist_couple_3D are (3, n_nodes), but we need (3, n_elems) + // So we need to take the element values (columns 0 to n_elems-1) + for (IndexType i = 0; i < 3; ++i) { + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_elems; ++k) { + internal_torques(i, k) += // bend_twist_couple_2D(i, k) + // Note: internal_torques share bend_twist_couple_2D + bend_twist_couple_3D(i, k) + + shear_stretch_couple(i, k) + + lagrangian_transport(i, k) + + unsteady_dilatation(i, k); + } + } +} + +} // namespace elasticapp diff --git a/backend/src/cosserat_rod_system.h b/backend/src/cosserat_rod_system.h new file mode 100644 index 000000000..11d6875ed --- /dev/null +++ b/backend/src/cosserat_rod_system.h @@ -0,0 +1,181 @@ +#pragma once + +#include "system.h" + +namespace elasticapp { + +// CosseratRod-specific variable tags +// These variable types are now made internal to this translation unit +namespace system::cosserat_rod { + // Node variables + struct Position : Placement::OnNode, DataType::Vector { + static constexpr std::string_view name = "position"; + }; + struct Velocity : Placement::OnNode, DataType::Vector { + static constexpr std::string_view name = "velocity"; + }; + struct Acceleration : Placement::OnNode, DataType::Vector { + static constexpr std::string_view name = "acceleration"; + }; + struct Mass : Placement::OnNode, DataType::Scalar { + static constexpr std::string_view name = "mass"; + inline static MatrixType ghost_value = MatrixType::Constant(1, 1, 1.0); + }; + struct InternalForces : Placement::OnNode, DataType::Vector { + static constexpr std::string_view name = "internal_forces"; + }; + struct ExternalForces : Placement::OnNode, DataType::Vector { + static constexpr std::string_view name = "external_forces"; + }; + + // Element variables + struct Director : Placement::OnElement, DataType::Matrix { + static constexpr std::string_view name = "director"; + }; + struct Omega : Placement::OnElement, DataType::Vector { + static constexpr std::string_view name = "omega"; + }; + struct Alpha : Placement::OnElement, DataType::Vector { + static constexpr std::string_view name = "alpha"; + }; + struct RestLengths : Placement::OnElement, DataType::Scalar { + static constexpr std::string_view name = "rest_lengths"; + // Note: Not constexpr because Eigen dynamic matrices don't have constexpr constructors + // For dynamic matrices, Constant requires dimensions: Constant(rows, cols, value) + inline static MatrixType ghost_value = MatrixType::Constant(1, 1, 1.0); + }; + struct Density : Placement::OnElement, DataType::Scalar { + static constexpr std::string_view name = "density"; + }; + struct Volume : Placement::OnElement, DataType::Scalar { + static constexpr std::string_view name = "volume"; + }; + struct MassSecondMomentOfInertia : Placement::OnElement, DataType::Matrix { + static constexpr std::string_view name = "mass_second_moment_of_inertia"; + }; + struct InvMassSecondMomentOfInertia : Placement::OnElement, DataType::Matrix { + static constexpr std::string_view name = "inv_mass_second_moment_of_inertia"; + }; + struct InternalTorques : Placement::OnElement, DataType::Vector { + static constexpr std::string_view name = "internal_torques"; + }; + struct ExternalTorques : Placement::OnElement, DataType::Vector { + static constexpr std::string_view name = "external_torques"; + }; + struct Lengths : Placement::OnElement, DataType::Scalar { + static constexpr std::string_view name = "lengths"; + }; + struct Tangents : Placement::OnElement, DataType::Vector { + static constexpr std::string_view name = "tangents"; + }; + struct Radius : Placement::OnElement, DataType::Scalar { + static constexpr std::string_view name = "radius"; + }; + struct Dilatation : Placement::OnElement, DataType::Scalar { + static constexpr std::string_view name = "dilatation"; + }; + struct DilatationRate : Placement::OnElement, DataType::Scalar { + static constexpr std::string_view name = "dilatation_rate"; + }; + struct Sigma : Placement::OnElement, DataType::Vector { + static constexpr std::string_view name = "sigma"; + }; + struct RestSigma : Placement::OnElement, DataType::Vector { + static constexpr std::string_view name = "rest_sigma"; + }; + struct InternalStress : Placement::OnElement, DataType::Vector { + static constexpr std::string_view name = "internal_stress"; + }; + struct ShearMatrix : Placement::OnElement, DataType::Matrix { + static constexpr std::string_view name = "shear_matrix"; + }; + + // Voronoi variables + struct RestVoronoiLengths : Placement::OnVoronoi, DataType::Scalar { + static constexpr std::string_view name = "rest_voronoi_lengths"; + // Note: Not constexpr because Eigen dynamic matrices don't have constexpr constructors + // For dynamic matrices, Constant requires dimensions: Constant(rows, cols, value) + inline static MatrixType ghost_value = MatrixType::Constant(1, 1, 1.0); + }; + struct VoronoiDilatation : Placement::OnVoronoi, DataType::Scalar { + static constexpr std::string_view name = "voronoi_dilatation"; + }; + struct Kappa : Placement::OnVoronoi, DataType::Vector { + static constexpr std::string_view name = "kappa"; + }; + struct RestKappa : Placement::OnVoronoi, DataType::Vector { + static constexpr std::string_view name = "rest_kappa"; + }; + struct InternalCouple : Placement::OnVoronoi, DataType::Vector { + static constexpr std::string_view name = "internal_couple"; + }; + struct BendMatrix : Placement::OnVoronoi, DataType::Matrix { + static constexpr std::string_view name = "bend_matrix"; + }; + + // Dummy variables for computation + struct ScratchVectorA : Placement::OnElement, DataType::Vector { static constexpr std::string_view name = "scratch_vector_a"; }; + struct ScratchVectorB : Placement::OnVoronoi, DataType::Vector { static constexpr std::string_view name = "scratch_vector_b"; }; + struct ScratchVectorC : Placement::OnNode, DataType::Vector { static constexpr std::string_view name = "scratch_vector_c"; }; + struct ScratchVectorD : Placement::OnNode, DataType::Vector { static constexpr std::string_view name = "scratch_vector_d"; }; + struct ScratchVectorE : Placement::OnNode, DataType::Vector { static constexpr std::string_view name = "scratch_vector_e"; }; + struct ScratchVectorF : Placement::OnNode, DataType::Vector { static constexpr std::string_view name = "scratch_vector_f"; }; + + struct ScratchScalarA : Placement::OnNode, DataType::Scalar { static constexpr std::string_view name = "scratch_scalar_a"; }; + struct ScratchScalarB : Placement::OnNode, DataType::Scalar { static constexpr std::string_view name = "scratch_scalar_b"; }; + struct ScratchScalarC : Placement::OnNode, DataType::Scalar { static constexpr std::string_view name = "scratch_scalar_c"; }; +} + +// CosseratRodSystem is a System with all variables from CosseratRod +// Variables are organized by placement (Node, Element, Voronoi) +using CosseratRodSystem = System< + // Node variables + system::cosserat_rod::Position, + system::cosserat_rod::Velocity, + system::cosserat_rod::Acceleration, + system::cosserat_rod::Mass, + system::cosserat_rod::InternalForces, + system::cosserat_rod::ExternalForces, + + // Element variables + system::cosserat_rod::Omega, + system::cosserat_rod::Alpha, + system::cosserat_rod::Director, + system::cosserat_rod::RestLengths, + system::cosserat_rod::Density, + system::cosserat_rod::Volume, + system::cosserat_rod::MassSecondMomentOfInertia, + system::cosserat_rod::InvMassSecondMomentOfInertia, + system::cosserat_rod::InternalTorques, + system::cosserat_rod::ExternalTorques, + system::cosserat_rod::Lengths, + system::cosserat_rod::Tangents, + system::cosserat_rod::Radius, + system::cosserat_rod::Dilatation, + system::cosserat_rod::DilatationRate, + system::cosserat_rod::Sigma, + system::cosserat_rod::RestSigma, + system::cosserat_rod::InternalStress, + system::cosserat_rod::ShearMatrix, + + // Voronoi variables + system::cosserat_rod::RestVoronoiLengths, + system::cosserat_rod::VoronoiDilatation, + system::cosserat_rod::Kappa, + system::cosserat_rod::RestKappa, + system::cosserat_rod::InternalCouple, + system::cosserat_rod::BendMatrix, + + // Dummy variables + system::cosserat_rod::ScratchVectorA, + system::cosserat_rod::ScratchVectorB, + system::cosserat_rod::ScratchVectorC, + system::cosserat_rod::ScratchVectorD, + system::cosserat_rod::ScratchVectorE, + system::cosserat_rod::ScratchVectorF, + system::cosserat_rod::ScratchScalarA, + system::cosserat_rod::ScratchScalarB, + system::cosserat_rod::ScratchScalarC +>; + +} // namespace elasticapp diff --git a/backend/src/environment/_api.cpp b/backend/src/environment/_api.cpp new file mode 100644 index 000000000..419df9813 --- /dev/null +++ b/backend/src/environment/_api.cpp @@ -0,0 +1,142 @@ +#include +#include +#include +#include "../api.h" +#include "api.h" +#include "collision/collision_system.h" +#include "collision/physics/no_interaction.h" +#include "collision/physics/linear_spring_dashpot.h" + +namespace py = pybind11; + +PYBIND11_MODULE(_collision, m) { + m.doc() = R"pbdoc( + Elasticapp Collision module + --------------------------- + + Provides collision detection and resolution for Cosserat rods using + the Discrete Element Method (DEM). + )pbdoc"; + + using namespace elasticapp; + using namespace elasticapp::environment::collision; + using namespace elasticapp::environment::collision::physics; + + // NoInteraction model class (for testing) + py::class_(m, "NoInteraction") + .def(py::init<>(), + R"pbdoc( + Initialize a NoInteraction collision physics model. + + This model returns zero force for all contacts, useful for testing + collision detection without applying forces. + + Args: + None (no parameters required) + )pbdoc"); + + // LinearSpringDashpot model class + py::class_(m, "LinearSpringDashpot") + .def(py::init(), + R"pbdoc( + Initialize a LinearSpringDashpot collision physics model. + + Args: + k_normal: Normal spring constant for repulsion force + eta_normal: Normal damping coefficient (also used for tangential damping) + friction: Static friction coefficient + )pbdoc", + py::arg("k_normal") = 1.0, + py::arg("eta_normal") = 0.1, + py::arg("friction") = 0.5) + .def(py::init(), + R"pbdoc( + Initialize a LinearSpringDashpot collision physics model with explicit tangential damping. + + Args: + k_normal: Normal spring constant for repulsion force + eta_normal: Normal damping coefficient + eta_tangential: Tangential damping coefficient + friction: Static friction coefficient + )pbdoc", + py::arg("k_normal"), + py::arg("eta_normal"), + py::arg("eta_tangential"), + py::arg("friction")) + .def(py::init(), + R"pbdoc( + Initialize a LinearSpringDashpot collision physics model with explicit tangential spring and damping. + + Args: + k_normal: Normal spring constant for repulsion force + eta_normal: Normal damping coefficient + k_tangential: Tangential spring constant + eta_tangential: Tangential damping coefficient + friction: Static friction coefficient + )pbdoc", + py::arg("k_normal"), + py::arg("eta_normal"), + py::arg("k_tangential"), + py::arg("eta_tangential"), + py::arg("friction")); + + // CollisionSystem class (using DefaultCollisionSystem) + // Support both LinearSpringDashpot and NoInteraction models + py::class_(m, "CollisionSystem") + .def(py::init(), + R"pbdoc( + Initialize a CollisionSystem with a LinearSpringDashpot physics model. + + Uses default policies: HashGrid (coarse), MaxContacts (fine), UnionFind (batching). + + Args: + model: The LinearSpringDashpot collision physics model + detect_every: Perform coarse detection every N steps (default: 1, meaning every step) + )pbdoc", + py::arg("model"), + py::arg("detect_every") = 1) + .def(py::init(), + R"pbdoc( + Initialize a CollisionSystem with a NoInteraction physics model. + + Uses default policies: HashGrid (coarse), MaxContacts (fine), UnionFind (batching). + Useful for testing collision detection without applying forces. + + Args: + model: The NoInteraction collision physics model + detect_every: Perform coarse detection every N steps (default: 1, meaning every step) + )pbdoc", + py::arg("model"), + py::arg("detect_every") = 1) + .def("detect_every", &DefaultCollisionSystem::detect_every, + R"pbdoc( + Get the detect_every parameter. + + Returns: + int: Number of steps between coarse detection calls + )pbdoc") + .def("set_detect_every", &DefaultCollisionSystem::set_detect_every, + R"pbdoc( + Set the detect_every parameter. + + Args: + detect_every: Number of steps between coarse detection calls + )pbdoc", + py::arg("detect_every")) + .def("resolve", &DefaultCollisionSystem::resolve, + R"pbdoc( + Resolve collisions for a BlockRodSystem. + + This method performs the full collision detection and resolution pipeline: + 1. Data extraction from Block + 2. Coarse collision detection (HashGrid) + 3. Fine collision detection (MaxContacts) + 4. Contact batching (UnionFind) + 5. Contact resolution (LinearSpringDashpot) + 6. Force application to ExternalForces + + Args: + system: The BlockRodSystem to resolve collisions for + )pbdoc", + py::arg("system")); +} diff --git a/backend/src/environment/api.h b/backend/src/environment/api.h new file mode 100644 index 000000000..24f356cf7 --- /dev/null +++ b/backend/src/environment/api.h @@ -0,0 +1,16 @@ +#pragma once + +#include "../api.h" // For BlockRodSystem +#include "collision/collision_system.h" +#include "collision/course_detection/hash_grid.h" +#include "collision/fine_detection/max_contacts.h" +#include "collision/batching/union_find.h" + +namespace elasticapp::environment { + +// Re-export CollisionSystem from collision namespace for convenience +namespace collision { + using DefaultCollisionSystem = CollisionSystem; +} + +} // namespace elasticapp::environment diff --git a/backend/src/environment/collision/batching/union_find.cpp b/backend/src/environment/collision/batching/union_find.cpp new file mode 100644 index 000000000..702cec507 --- /dev/null +++ b/backend/src/environment/collision/batching/union_find.cpp @@ -0,0 +1,105 @@ +#include "union_find.h" +#include +#include + +namespace elasticapp::environment { +namespace collision { + +//============================================================================== +// UnionFind::UnionFindDS Implementation +//============================================================================== + +UnionFind::UnionFindDS::UnionFindDS(std::size_t max_node) + : parent_(max_node + 1), rank_(max_node + 1, 0) { + // Initialize: each node is its own parent (root) + for (std::size_t i = 0; i <= max_node; ++i) { + parent_[i] = i; + } +} + +std::size_t UnionFind::UnionFindDS::find_root(std::size_t x) { + // Path compression: make parent point directly to root + if (parent_[x] != x) { + parent_[x] = find_root(parent_[x]); + } + return parent_[x]; +} + +void UnionFind::UnionFindDS::union_nodes(std::size_t x, std::size_t y) { + std::size_t root_x = find_root(x); + std::size_t root_y = find_root(y); + + if (root_x == root_y) { + return; // Already in same component + } + + // Union by rank: attach smaller tree under larger tree + if (rank_[root_x] < rank_[root_y]) { + parent_[root_x] = root_y; + } else if (rank_[root_x] > rank_[root_y]) { + parent_[root_y] = root_x; + } else { + // Same rank: attach one to the other and increment rank + parent_[root_y] = root_x; + rank_[root_x]++; + } +} + +//============================================================================== +// UnionFind Implementation +//============================================================================== + +std::vector> UnionFind::batch( + std::vector& contacts +) const { + const std::size_t n_contacts = contacts.size(); + + if (n_contacts == 0) { + return {}; + } + + // Step 1: Find maximum node index to size the Union-Find structure + std::size_t max_node = 0; + for (const auto& contact : contacts) { + max_node = std::max(max_node, std::max(contact.node1_idx, contact.node2_idx)); + } + + // Step 2: Initialize Union-Find data structure + UnionFindDS uf(max_node); + + // Step 3: Union all nodes connected by contacts + // This builds the connected components: nodes connected by contacts are in the same component + for (const auto& contact : contacts) { + uf.union_nodes(contact.node1_idx, contact.node2_idx); + } + + // Step 4: Group contacts by their root component and set their indices + // Contacts whose nodes share the same root are in the same batch + std::unordered_map> batches_map; + batches_map.reserve(n_contacts); // Reserve space (worst case: one batch per contact) + + for (std::size_t i = 0; i < n_contacts; ++i) { + auto& contact = contacts[i]; + // Use root of node1 (both nodes in a contact have the same root after union) + std::size_t root = uf.find_root(contact.node1_idx); + auto& batch = batches_map[root]; + + // Set contact index to its position within the batch (matches reference implementation) + contact.set_index(batch.size()); + + // Add contact index to batch + batch.push_back(i); + } + + // Step 5: Convert map to vector of batches + std::vector> batches; + batches.reserve(batches_map.size()); + for (auto& [root, batch] : batches_map) { + batches.push_back(std::move(batch)); + } + + return batches; +} + +} // namespace collision +} // namespace elasticapp::environment diff --git a/backend/src/environment/collision/batching/union_find.h b/backend/src/environment/collision/batching/union_find.h new file mode 100644 index 000000000..5a4412be6 --- /dev/null +++ b/backend/src/environment/collision/batching/union_find.h @@ -0,0 +1,86 @@ +#pragma once + +#include "../types.h" +#include +#include +#include + +namespace elasticapp::environment { +namespace collision { + +/** + * UnionFind batching policy. + * + * Groups contacts into batches using a Union-Find data structure. + * Based on the Elastica UnionFind batching implementation. + * + * Algorithm: + * 1. Build Union-Find structure on nodes: For each contact, union the two nodes it connects + * 2. Identify root nodes: Each connected component has a unique root + * 3. Group contacts by root: Contacts whose nodes share the same root are in the same batch + * + * This creates independent batches of contacts that can be processed in parallel, + * as contacts in different batches don't share any nodes and thus don't interfere. + * + * This is a CRTP policy class that will be mixed into CollisionSystem. + */ +class UnionFind { +public: + /** + * Default constructor. + */ + UnionFind() = default; + + /** + * Destructor. + */ + ~UnionFind() = default; + + /** + * Group contacts into batches. + * + * Uses Union-Find to identify connected components of contacts. + * Contacts are connected if they share a common node (directly or transitively). + * + * Algorithm: + * 1. Initialize Union-Find: Each node is its own root initially + * 2. Union nodes: For each contact, union node1 and node2 + * 3. Group contacts: Contacts whose nodes have the same root are grouped together + * 4. Set contact indices: Each contact's index is set to its position within its batch + * + * @param contacts Vector of all contacts (non-const to allow setting index) + * @return Vector of batches, where each batch is a vector of contact indices + */ + std::vector> batch( + std::vector& contacts + ) const; + +private: + /** + * Union-Find data structure for finding connected components. + */ + class UnionFindDS { + public: + /** + * Constructor with maximum node index. + */ + explicit UnionFindDS(std::size_t max_node); + + /** + * Find root of a node with path compression. + */ + std::size_t find_root(std::size_t x); + + /** + * Union two nodes. + */ + void union_nodes(std::size_t x, std::size_t y); + + private: + std::vector parent_; // parent[i] = parent of node i + std::vector rank_; // rank[i] = approximate tree height (for union by rank) + }; +}; + +} // namespace collision +} // namespace elasticapp::environment diff --git a/backend/src/environment/collision/collision_system.h b/backend/src/environment/collision/collision_system.h new file mode 100644 index 000000000..3d46be631 --- /dev/null +++ b/backend/src/environment/collision/collision_system.h @@ -0,0 +1,275 @@ +#pragma once + +#include "../../api.h" // For BlockRodSystem +#include "../../cosserat_rod_system.h" // For system::cosserat_rod namespace +#include "../../utility/hash.h" +#include "concepts.hpp" +#include "physics/linear_spring_dashpot.h" +#include "physics/no_interaction.h" +#include "types.h" +#include +#include +#include +#include // For std::pair +#include // For std::hash + +namespace elasticapp::environment { +namespace collision { + +/** + * CollisionSystem class for collision detection and resolution. + * + * This class orchestrates the collision detection and resolution pipeline: + * 1. Data extraction from Block + * 2. Coarse detection (HashGrid) + * 3. Fine detection (MaxContacts) + * 4. Contact batching (UnionFind) + * 5. Contact resolution (PhysicsModel - runtime selectable) + * 6. Force application to ExternalForces + * + * Uses CRTP (Curiously Recurring Template Pattern) to mix in policy classes + * for coarse detection, fine detection, and batching strategies. + * + * Physics models are stored in a std::variant for runtime selection without + * virtual function overhead. + * + * @tparam CoarseDetectionPolicy Policy for coarse collision detection + * @tparam FineDetectionPolicy Policy for fine collision detection + * @tparam BatchingPolicy Policy for contact batching + */ +template +class CollisionSystem : public CoarseDetection, public FineDetection, public Batching { +public: + /** + * Variant type for physics models. + * Currently supports LinearSpringDashpot, but can be extended with additional models. + * This syntax is used to avoid using virtual functions and CRTP hierarchies. + * Different structures can be considered once a more compositional approach to the physics model is needed. + * + * To add a new physics model: + * 1. Implement the model class with a compute_force(Contact&, double&) method + * 2. Add it to this variant: using PhysicsModel = std::variant<...>; + */ + + using PhysicsModel = std::variant; + + /** + * Constructor takes a physics model and detection frequency. + * + * @param model The collision physics model (e.g., LinearSpringDashpot instance) + * @param detect_every Perform coarse detection every N steps (default: 1, meaning every step) + */ + CollisionSystem(const PhysicsModel& model, std::size_t detect_every = 1) : model_(model), detect_every_(detect_every), step_counter_(0) {} + + /** + * Resolve collisions for a BlockRodSystem. + * + * This method performs the full collision detection and resolution pipeline. + */ + void resolve(BlockRodSystem& block); + + /** + * Get the contact cache (non-const). + * + * @return Reference to the contact cache vector + */ + std::vector>& contact_cache() { + return contact_cache_; + } + + /** + * Get the contact cache (const). + * + * @return Const reference to the contact cache vector + */ + const std::vector>& contact_cache() const { + return contact_cache_; + } + + /** + * Get the detect_every parameter. + * + * @return The number of steps between coarse detection calls + */ + std::size_t detect_every() const { + return detect_every_; + } + + /** + * Set the detect_every parameter. + * + * @param detect_every Number of steps between coarse detection calls + */ + void set_detect_every(std::size_t detect_every) { + detect_every_ = detect_every; + } + +private: + PhysicsModel model_; + + /** + * Number of steps between coarse detection calls. + * + * Coarse detection (HashGrid) is only performed every detect_every_ steps. + * Fine detection, batching, and resolution still occur every step using + * the cached contact pairs from the last coarse detection. + * + * Default: 1 (detect every step) + */ + std::size_t detect_every_; + + /** + * Current step counter for tracking when to perform coarse detection. + */ + mutable std::size_t step_counter_; + + /** + * Contact cache for storing candidate pairs from coarse detection. + * + * This cache stores the candidate pairs identified by coarse detection, + * allowing fine detection to reuse them without re-running coarse detection. + * The cache is cleared when coarse detection is performed. + */ + mutable std::vector> contact_cache_; + + /** + * Previous contacts for tracking tangential displacement over time. + * + * Stores contacts from the previous resolve() call, keyed by normalized node pair. + * This allows accumulation of tangential displacement between calls. + * Key: Normalized node pair (min(node1, node2), max(node1, node2)) + * Value: Contact from previous call with accumulated displacement + */ + std::unordered_map, Contact, utility::PairHash> previous_contacts_; + + /** + * Normalize a node pair to ensure consistent ordering. + * + * Returns (min(n1, n2), max(n1, n2)) to ensure the same pair + * always maps to the same key regardless of order. + * + * @param n1 First node index + * @param n2 Second node index + * @return Normalized pair (min, max) + */ + static std::pair normalize_pair( + std::size_t n1, + std::size_t n2 + ) { + return n1 < n2 ? std::make_pair(n1, n2) : std::make_pair(n2, n1); + } +}; + + +template +void CollisionSystem::resolve( + BlockRodSystem& block) { + + // Get data from BlockRodSystem + namespace variable_tags = ::elasticapp::system::cosserat_rod; + auto&& positions = block.template get(); + auto&& velocities = block.template get(); + auto&& radii_elem = block.template get(); + auto&& external_forces = block.template get(); + + const std::size_t n_nodes = positions.cols(); + const std::size_t n_elems = radii_elem.cols(); + + /* + + // Convert element radii to node radii + // Treat entire block as a single rod - ghost nodes handle edge cases + // Mapping strategy for a rod with n_elems elements and n_nodes nodes: + // - Node 0 -> Element 0 + // - Node i (1 to n_elems-1) -> Element i-1 + // - Node i >= n_elems -> Element n_elems-1 (ghost nodes use last element) + Eigen::MatrixXd radii(1, n_nodes); + for (std::size_t i = 0; i < n_nodes; ++i) { + std::size_t elem_idx; + if (i == 0) { + elem_idx = 0; + } else if (i < n_elems) { + elem_idx = i - 1; + } else { + // For ghost nodes (i >= n_elems), use the last element + elem_idx = n_elems - 1; + } + radii(0, i) = radii_elem(0, elem_idx); + } + + // Step 1: Coarse detection (only every detect_every_ steps) + bool should_detect = (step_counter_ % detect_every_ == 0); + + if (should_detect) { + // Clear contact cache and perform coarse detection + contact_cache_.clear(); + // Call coarse detection via CRTP (inherited from CoarseDetection) + contact_cache_ = static_cast(*this).detect(positions, radii); + } + // Otherwise, reuse cached contact pairs from previous detection + + // Step 2: Fine detection (always performed, uses cached pairs if detection was skipped) + std::vector contacts; + if (!contact_cache_.empty()) { + // Extract physics parameters from model and call fine detection via CRTP + std::visit([&](const auto& physics_model) { + contacts = static_cast(*this).detect(contact_cache_, positions, velocities, radii, physics_model); + }, model_); + } + + // Step 3: Contact batching (via CRTP) + std::vector> batches = static_cast(*this).batch(contacts); + + // Step 4 & 5: Contact resolution and force application + for (const auto& batch : batches) { + for (std::size_t contact_idx : batch) { + Contact& contact = contacts[contact_idx]; + + // Update tangential displacement from previous contact if exists + auto pair_key = normalize_pair(contact.node1_idx, contact.node2_idx); + auto prev_it = previous_contacts_.find(pair_key); + if (prev_it != previous_contacts_.end()) { + // Accumulate tangential displacement + const Eigen::Vector3d& prev_pos = prev_it->second.position; + const Eigen::Vector3d& curr_pos = contact.position; + const Eigen::Vector3d& prev_normal = prev_it->second.normal; + + // Project displacement onto tangent plane + Eigen::Vector3d displacement = curr_pos - prev_pos; + Eigen::Vector3d normal_displacement = displacement.dot(prev_normal) * prev_normal; + contact.tangential_displacement = prev_it->second.tangential_displacement + + (displacement - normal_displacement); + } + + // Compute force using physics model + double penetration_depth; + Eigen::Vector3d force; + std::visit([&](const auto& physics_model) { + force = physics_model.compute_force(contact, penetration_depth); + }, model_); + + // Apply forces to external_forces + if (penetration_depth > 0.0) { + // Force on node1 + external_forces(0, contact.node1_idx) += force(0); + external_forces(1, contact.node1_idx) += force(1); + external_forces(2, contact.node1_idx) += force(2); + + // Force on node2 (opposite direction) + external_forces(0, contact.node2_idx) -= force(0); + external_forces(1, contact.node2_idx) -= force(1); + external_forces(2, contact.node2_idx) -= force(2); + } + + // Store contact for next iteration + previous_contacts_[pair_key] = contact; + } + } + + // Increment step counter + step_counter_++; + */ +} + +} // namespace collision +} // namespace elasticapp::environment diff --git a/backend/src/environment/collision/concepts.hpp b/backend/src/environment/collision/concepts.hpp new file mode 100644 index 000000000..de464a648 --- /dev/null +++ b/backend/src/environment/collision/concepts.hpp @@ -0,0 +1,90 @@ +#pragma once + +#include +#include +#include +#include +#include +#include "types.h" +#include "physics/linear_spring_dashpot.h" + +namespace elasticapp::environment { +namespace collision { + +/** + * Concept for CoarseDetectionPolicy. + * + * Coarse detection policies perform broad-phase collision detection to identify + * candidate contact pairs. They should be fast but may produce false positives + * that will be filtered by fine detection. + * + * Requirements: + * - Must have a detect() method that takes positions and radii matrices + * - Must return a vector of candidate node pairs (indices) + * - Positions and radii are Eigen::MatrixXd + */ +template +concept CoarseDetectionPolicy = requires( + T policy, + const Eigen::MatrixXd& positions, + const Eigen::MatrixXd& radii +) { + // Must have a detect method that takes positions and radii + { policy.detect(positions, radii) } -> std::convertible_to>>; +}; + +/** + * Concept for FineDetectionPolicy. + * + * Fine detection policies perform narrow-phase collision detection on candidate + * pairs from coarse detection. They determine actual contact geometry and properties. + * + * Requirements: + * - Must have a detect() method that takes candidate pairs, positions, velocities, + * radii, and a physics model (any type) + * - Must return a vector of Contact objects with full contact information + * - Positions, velocities, and radii are Eigen::MatrixXd + */ +template +concept FineDetectionPolicyFor = requires( + T policy, + const std::vector>& candidate_pairs, + const Eigen::MatrixXd& positions, + const Eigen::MatrixXd& velocities, + const Eigen::MatrixXd& radii, + const PhysicsModel& physics_model +) { + { policy.detect(candidate_pairs, positions, velocities, radii, physics_model) } + -> std::convertible_to>; +}; + +/** + * Concept for FineDetectionPolicy (unconstrained version). + * + * A policy satisfies FineDetectionPolicy if it can work with any physics model type. + * This is checked by requiring it to work with at least one model type (LinearSpringDashpot). + */ +template +concept FineDetectionPolicy = FineDetectionPolicyFor; + +/** + * Concept for BatchingPolicy. + * + * Batching policies group contacts into batches for efficient parallel processing. + * Contacts in the same batch can be processed independently. + * + * Requirements: + * - Must have a batch() method that takes a vector of contacts + * - Must return a vector of batches, where each batch is a vector of contact indices + */ +template +concept BatchingPolicy = requires( + T policy, + std::vector& contacts +) { + // Must have a batch method that takes contacts and returns batches + { policy.batch(contacts) } -> std::convertible_to>>; +}; + +} // namespace collision +} // namespace elasticapp::environment diff --git a/backend/src/environment/collision/course_detection/hash_grid.cpp b/backend/src/environment/collision/course_detection/hash_grid.cpp new file mode 100644 index 000000000..96e6c5703 --- /dev/null +++ b/backend/src/environment/collision/course_detection/hash_grid.cpp @@ -0,0 +1,439 @@ +#include "hash_grid.h" +#include +#include +#include +#include + +namespace elasticapp::environment { +namespace collision { + +//============================================================================== +// HashGrid::SingleHashGrid Implementation +//============================================================================== + +HashGrid::SingleHashGrid::SingleHashGrid(double cell_span, const Parameters& params) + : params_(params), + cell_span_(cell_span), + inverse_cell_span_(1.0 / cell_span), + x_cell_count_(params.initial_x_cell_count), + y_cell_count_(params.initial_y_cell_count), + z_cell_count_(params.initial_z_cell_count) { + + // Ensure grid dimensions are powers of 2 for bitwise operations + x_cell_count_ = round_up_to_power_of_2(x_cell_count_); + y_cell_count_ = round_up_to_power_of_2(y_cell_count_); + z_cell_count_ = round_up_to_power_of_2(z_cell_count_); + + // Compute hash masks (for bitwise modulo: x & mask instead of x % size) + x_hash_mask_ = x_cell_count_ - 1; + y_hash_mask_ = y_cell_count_ - 1; + z_hash_mask_ = z_cell_count_ - 1; + + xy_cell_count_ = x_cell_count_ * y_cell_count_; + xyz_cell_count_ = xy_cell_count_ * z_cell_count_; + + // Allocate cells + cells_.resize(xyz_cell_count_); + + // Initialize neighbor offsets + initialize_neighbor_offsets(); +} + +HashGrid::SingleHashGrid::~SingleHashGrid() { + clear_neighbor_offsets(); +} + +void HashGrid::SingleHashGrid::clear_neighbor_offsets() { + for (auto& cell : cells_) { + if (cell.neighbor_offsets != nullptr && cell.neighbor_offsets != std_neighbor_offsets_) { + delete[] cell.neighbor_offsets; + cell.neighbor_offsets = nullptr; + } + } +} + +void HashGrid::SingleHashGrid::initialize_neighbor_offsets() { + // Initialize standard neighbor offsets (for inner cells) + // 27 neighbors in 3D: (-1,-1,-1) to (1,1,1) + long xc = static_cast(x_cell_count_); + long yc = static_cast(y_cell_count_); + long zc = static_cast(z_cell_count_); + long xyc = static_cast(xy_cell_count_); + + std::size_t i = 0; + for (long zz = -xyc; zz <= xyc; zz += xyc) { + for (long yy = -xc; yy <= xc; yy += xc) { + for (long xx = -1; xx <= 1; ++xx, ++i) { + std_neighbor_offsets_[i] = xx + yy + zz; + } + } + } + + // Set up neighbor offsets for each cell + for (std::size_t z = 0; z < z_cell_count_; ++z) { + for (std::size_t y = 0; y < y_cell_count_; ++y) { + for (std::size_t x = 0; x < x_cell_count_; ++x) { + std::size_t cell_idx = x + y * x_cell_count_ + z * xy_cell_count_; + Cell& cell = cells_[cell_idx]; + + // Check if border cell + bool is_border = (x == 0 || x == x_cell_count_ - 1 || + y == 0 || y == y_cell_count_ - 1 || + z == 0 || z == z_cell_count_ - 1); + cell.is_border_cell = is_border; + + if (is_border) { + // Border cells need custom offsets (wrapping or clamping) + cell.neighbor_offsets = new long[27]; + + i = 0; + for (long zz = -xyc; zz <= xyc; zz += xyc) { + long zo = zz; + // Handle z wrapping + if (z == 0 && zz == -xyc) { + zo = static_cast(xyz_cell_count_) - xyc; + } else if (z == z_cell_count_ - 1 && zz == xyc) { + zo = xyc - static_cast(xyz_cell_count_); + } + + for (long yy = -xc; yy <= xc; yy += xc) { + long yo = yy; + // Handle y wrapping + if (y == 0 && yy == -xc) { + yo = xyc - xc; + } else if (y == y_cell_count_ - 1 && yy == xc) { + yo = xc - xyc; + } + + for (long xx = -1; xx <= 1; ++xx, ++i) { + long xo = xx; + // Handle x wrapping + if (x == 0 && xx == -1) { + xo = xc - 1; + } else if (x == x_cell_count_ - 1 && xx == 1) { + xo = 1 - xc; + } + + cell.neighbor_offsets[i] = xo + yo + zo; + } + } + } + } else { + // Inner cells use standard offsets + cell.neighbor_offsets = std_neighbor_offsets_; + } + } + } + } +} + +std::size_t HashGrid::SingleHashGrid::hash(const Eigen::Vector3d& position) const { + const double x = position.x(); + const double y = position.y(); + const double z = position.z(); + + std::size_t x_hash, y_hash, z_hash; + + // Use inverse multiplication and bitwise AND for efficiency + if (x < 0) { + double i = (-x) * inverse_cell_span_; + x_hash = x_cell_count_ - 1 - (static_cast(i) & x_hash_mask_); + } else { + double i = x * inverse_cell_span_; + x_hash = static_cast(i) & x_hash_mask_; + } + + if (y < 0) { + double i = (-y) * inverse_cell_span_; + y_hash = y_cell_count_ - 1 - (static_cast(i) & y_hash_mask_); + } else { + double i = y * inverse_cell_span_; + y_hash = static_cast(i) & y_hash_mask_; + } + + if (z < 0) { + double i = (-z) * inverse_cell_span_; + z_hash = z_cell_count_ - 1 - (static_cast(i) & z_hash_mask_); + } else { + double i = z * inverse_cell_span_; + z_hash = static_cast(i) & z_hash_mask_; + } + + return x_hash + y_hash * x_cell_count_ + z_hash * xy_cell_count_; +} + +void HashGrid::SingleHashGrid::add_node(const NodeInfo& node) { + nodes_.push_back(node); + std::size_t node_idx = nodes_.size() - 1; + + // Compute cell index + std::size_t cell_idx = hash(node.position); + + // Add to cell + Cell& cell = cells_[cell_idx]; + if (cell.node_indices.empty()) { + cell.node_indices.reserve(params_.initial_cell_vector_size); + } + cell.node_indices.push_back(node_idx); + + // Store mapping + node_to_cell_[node.index] = cell_idx; +} + +void HashGrid::SingleHashGrid::detect_collisions( + std::vector>& candidate_pairs, + const std::vector& finer_grid_nodes +) const { + // Use set to avoid duplicates + std::unordered_set processed_pairs; + + auto make_pair_key = [](std::size_t i, std::size_t j) -> std::size_t { + if (i > j) std::swap(i, j); + return i * 4294967291ULL + j; // Large prime for hashing + }; + + // Process collisions within this grid + for (std::size_t cell_idx = 0; cell_idx < cells_.size(); ++cell_idx) { + const Cell& cell = cells_[cell_idx]; + if (cell.node_indices.empty()) continue; + + const auto& node_indices = cell.node_indices; + + // Check pairs within the same cell + for (std::size_t i = 0; i < node_indices.size(); ++i) { + for (std::size_t j = i + 1; j < node_indices.size(); ++j) { + std::size_t idx1 = nodes_[node_indices[i]].index; + std::size_t idx2 = nodes_[node_indices[j]].index; + + std::size_t pair_key = make_pair_key(idx1, idx2); + if (processed_pairs.find(pair_key) == processed_pairs.end()) { + candidate_pairs.push_back({idx1, idx2}); + processed_pairs.insert(pair_key); + } + } + } + + // Check pairs with neighboring cells + constexpr std::size_t hnn = get_half_number_of_neighbors(); + for (std::size_t i = 0; i < hnn; ++i) { + long offset = cell.neighbor_offsets[i]; + std::size_t neighbor_idx = cell_idx + offset; + + // Bounds check + if (neighbor_idx >= cells_.size()) { + continue; // Out of bounds + } + + const Cell& neighbor_cell = cells_[neighbor_idx]; + if (neighbor_cell.node_indices.empty()) continue; + + // Check all pairs between this cell and neighbor + for (std::size_t idx1 : node_indices) { + for (std::size_t idx2 : neighbor_cell.node_indices) { + std::size_t n1 = nodes_[idx1].index; + std::size_t n2 = nodes_[idx2].index; + + if (n1 < n2) { // Only check once per pair + std::size_t pair_key = make_pair_key(n1, n2); + if (processed_pairs.find(pair_key) == processed_pairs.end()) { + candidate_pairs.push_back({n1, n2}); + processed_pairs.insert(pair_key); + } + } + } + } + } + } + + // Check collisions with nodes from finer grids + if (!finer_grid_nodes.empty()) { + for (const auto& fine_node : finer_grid_nodes) { + std::size_t cell_idx = hash(fine_node.position); + const Cell& cell = cells_[cell_idx]; + + // Check all neighbors + for (std::size_t i = 0; i < get_number_of_neighbors(); ++i) { + long offset = cell.neighbor_offsets[i]; + std::size_t neighbor_idx = cell_idx + offset; + + // Bounds check + if (neighbor_idx >= cells_.size()) { + continue; + } + + const Cell& neighbor_cell = cells_[neighbor_idx]; + for (std::size_t idx : neighbor_cell.node_indices) { + std::size_t n1 = fine_node.index; + std::size_t n2 = nodes_[idx].index; + + if (n1 < n2) { + std::size_t pair_key = make_pair_key(n1, n2); + if (processed_pairs.find(pair_key) == processed_pairs.end()) { + candidate_pairs.push_back({n1, n2}); + processed_pairs.insert(pair_key); + } + } + } + } + } + } +} + +void HashGrid::SingleHashGrid::clear() { + nodes_.clear(); + node_to_cell_.clear(); + for (auto& cell : cells_) { + cell.node_indices.clear(); + } +} + +std::vector HashGrid::SingleHashGrid::get_all_nodes() const { + return nodes_; +} + +//============================================================================== +// HashGrid Implementation +//============================================================================== + +HashGrid::HashGrid() : HashGrid(Parameters()) {} + +HashGrid::HashGrid(const Parameters& params) : params_(params) {} + +HashGrid::~HashGrid() { + clear(); +} + +std::size_t HashGrid::round_up_to_power_of_2(std::size_t n) { + if (n == 0) return 1; + n--; + n |= n >> 1; + n |= n >> 2; + n |= n >> 4; + n |= n >> 8; + n |= n >> 16; + if (sizeof(std::size_t) > 4) { + n |= n >> 32; + } + return n + 1; +} + +double HashGrid::compute_cell_span_for_node(double node_diameter, const Parameters& params) { + // Cell span should be slightly larger than node diameter + // Use hierarchy factor to determine appropriate level + double base_span = node_diameter * std::sqrt(params.hierarchy_factor); + + // Round to reasonable value + return base_span; +} + +void HashGrid::clear() { + grids_.clear(); +} + +std::vector> HashGrid::detect( + const Eigen::MatrixXd& positions, + const Eigen::MatrixXd& radii +) { + const Eigen::Index n_nodes = positions.cols(); + + if (n_nodes == 0) { + return std::vector>(); + } + + // Clear previous state + clear(); + + // Build node info + std::vector nodes; + nodes.reserve(n_nodes); + double max_diameter = 0.0; + + for (Eigen::Index i = 0; i < n_nodes; ++i) { + Eigen::Vector3d pos = positions.col(i); + double radius = std::abs(radii(i)); + double diameter = 2.0 * radius; + max_diameter = std::max(max_diameter, diameter); + + nodes.emplace_back(static_cast(i), pos, radius); + } + + // If too few nodes, use simple approach + if (n_nodes < params_.hashgrid_activation_threshold) { + // Simple all-pairs (for very small systems) + std::vector> pairs; + for (std::size_t i = 0; i < nodes.size(); ++i) { + for (std::size_t j = i + 1; j < nodes.size(); ++j) { + pairs.push_back({nodes[i].index, nodes[j].index}); + } + } + return pairs; + } + + // Create initial grid with appropriate cell span + double initial_cell_span = compute_cell_span_for_node(max_diameter, params_); + auto initial_grid = std::make_unique(initial_cell_span, params_); + grids_.push_back(std::move(initial_grid)); + + // Assign nodes to appropriate grids + for (const auto& node : nodes) { + double required_cell_span = compute_cell_span_for_node(node.diameter, params_); + + // Find or create appropriate grid + SingleHashGrid* target_grid = nullptr; + auto it = grids_.begin(); + + while (it != grids_.end()) { + double grid_span = (*it)->get_cell_span(); + + if (node.diameter < grid_span) { + // Check if next grid is too small + auto next_it = std::next(it); + if (next_it == grids_.end() || (*next_it)->get_cell_span() > required_cell_span) { + // This grid is appropriate + target_grid = it->get(); + break; + } + } + ++it; + } + + // If no appropriate grid found, create a new one + if (target_grid == nullptr) { + // Create grid with larger cell span + double new_span = required_cell_span; + if (!grids_.empty()) { + double largest_span = grids_.back()->get_cell_span(); + while (new_span <= largest_span) { + new_span *= params_.hierarchy_factor; + } + } + + auto new_grid = std::make_unique(new_span, params_); + target_grid = new_grid.get(); + grids_.push_back(std::move(new_grid)); + } + + target_grid->add_node(node); + } + + // Detect collisions hierarchically + std::vector> candidate_pairs; + + // Process grids from finest to coarsest + for (auto it = grids_.begin(); it != grids_.end(); ++it) { + // Collect nodes from finer grids + std::vector finer_nodes; + for (auto prev_it = grids_.begin(); prev_it != it; ++prev_it) { + auto grid_nodes = (*prev_it)->get_all_nodes(); + finer_nodes.insert(finer_nodes.end(), grid_nodes.begin(), grid_nodes.end()); + } + + // Detect collisions in this grid + (*it)->detect_collisions(candidate_pairs, finer_nodes); + } + + return candidate_pairs; +} + +} // namespace collision +} // namespace elasticapp::environment diff --git a/backend/src/environment/collision/course_detection/hash_grid.h b/backend/src/environment/collision/course_detection/hash_grid.h new file mode 100644 index 000000000..d88d0f26c --- /dev/null +++ b/backend/src/environment/collision/course_detection/hash_grid.h @@ -0,0 +1,262 @@ +#pragma once + +#include "../types.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace elasticapp::environment { +namespace collision { + +/** + * HashGrid coarse collision detection policy. + * + * Implements hierarchical spatial hashing for O(N) broad-phase collision detection. + * Based on the Elastica HashGrids implementation, adapted for node-based systems. + * + * Key features: + * - Hierarchical grids: Multiple grids with different cell sizes for optimal performance + * - Precomputed neighbor offsets: Fast access to adjacent cells + * - Bitwise hash operations: Efficient cell coordinate computation (requires power-of-2 dimensions) + * - Automatic grid sizing: Adapts to node sizes at runtime + * + * Algorithm: + * 1. Creates a hierarchy of grids with different cell sizes + * 2. Assigns each node to the grid with smallest cells larger than the node's diameter + * 3. Uses spatial hashing to partition nodes into grid cells + * 4. For collision detection, checks nodes in same cell and 26 neighboring cells (3D) + * + * This is a CRTP policy class that will be mixed into CollisionSystem. + */ +class HashGrid { +public: + /** + * Configuration parameters for the hash grid. + */ + struct Parameters { + std::size_t initial_x_cell_count = 64; // Initial grid size in x (must be power of 2) + std::size_t initial_y_cell_count = 64; // Initial grid size in y (must be power of 2) + std::size_t initial_z_cell_count = 64; // Initial grid size in z (must be power of 2) + std::size_t initial_cell_vector_size = 16; // Initial capacity for node vectors in cells + std::size_t minimal_grid_density = 4; // Minimum cells per node + std::size_t hashgrid_activation_threshold = 10; // Min nodes to use hash grid + double hierarchy_factor = 2.0; // Factor between grid levels + + Parameters() = default; + }; + + /** + * Default constructor. + */ + HashGrid(); + + /** + * Constructor with parameters. + */ + explicit HashGrid(const Parameters& params); + + /** + * Destructor. + */ + ~HashGrid(); + + /** + * Perform coarse collision detection. + * + * Identifies pairs of nodes that are potentially colliding using + * hierarchical spatial hashing. Returns candidate pairs for fine detection. + * + * @param positions Node positions (3 x n_nodes) + * @param radii Node radii (1 x n_nodes) + * @return Vector of pairs (node1_idx, node2_idx) that are potentially colliding + */ + std::vector> detect( + const Eigen::MatrixXd& positions, + const Eigen::MatrixXd& radii + ); + + /** + * Clear all grids (for reuse). + */ + void clear(); + +private: + // Forward declaration + class SingleHashGrid; + + /** + * Grid cell coordinates (3D integer coordinates). + */ + struct CellCoord { + int x, y, z; + + bool operator==(const CellCoord& other) const { + return x == other.x && y == other.y && z == other.z; + } + }; + + /** + * Hash function for CellCoord to use in unordered_map. + */ + struct CellCoordHash { + std::size_t operator()(const CellCoord& coord) const { + const std::size_t h1 = std::hash{}(coord.x); + const std::size_t h2 = std::hash{}(coord.y); + const std::size_t h3 = std::hash{}(coord.z); + return h1 ^ (h2 << 1) ^ (h3 << 2); + } + }; + + /** + * Node information for grid assignment. + */ + struct NodeInfo { + std::size_t index; + Eigen::Vector3d position; + double radius; + double diameter; // 2 * radius + + NodeInfo(std::size_t idx, const Eigen::Vector3d& pos, double r) + : index(idx), position(pos), radius(r), diameter(2.0 * r) {} + }; + + /** + * Single hash grid in the hierarchy. + * Implements one level of the hierarchical grid system. + */ + class SingleHashGrid { + public: + /** + * Constructor. + * + * @param cell_span Size of each grid cell (edge length) + * @param params Configuration parameters + */ + SingleHashGrid(double cell_span, const Parameters& params); + + /** + * Destructor. + */ + ~SingleHashGrid(); + + /** + * Get cell span (cell size). + */ + double get_cell_span() const noexcept { return cell_span_; } + + /** + * Add a node to this grid. + */ + void add_node(const NodeInfo& node); + + /** + * Detect collisions within this grid and with nodes from finer grids. + * + * @param candidate_pairs Output vector for candidate pairs + * @param finer_grid_nodes Nodes from finer grids (smaller cell sizes) to check against + */ + void detect_collisions( + std::vector>& candidate_pairs, + const std::vector& finer_grid_nodes = {} + ) const; + + /** + * Clear all nodes from this grid. + */ + void clear(); + + /** + * Get all nodes in this grid. + */ + std::vector get_all_nodes() const; + + private: + /** + * Cell structure for storing nodes. + */ + struct Cell { + std::vector node_indices; // Indices into nodes_ vector + long* neighbor_offsets; // Offsets to neighboring cells (27 in 3D) + bool is_border_cell; // Whether this is a border cell + + Cell() : neighbor_offsets(nullptr), is_border_cell(false) {} + }; + + /** + * Compute hash (cell index) from position. + * Uses bitwise operations for efficiency (requires power-of-2 grid dimensions). + */ + std::size_t hash(const Eigen::Vector3d& position) const; + + /** + * Initialize neighbor offsets for all cells. + */ + void initialize_neighbor_offsets(); + + /** + * Clear neighbor offsets. + */ + void clear_neighbor_offsets(); + + /** + * Get number of neighbors (27 in 3D). + */ + static constexpr std::size_t get_number_of_neighbors() { return 27; } + + /** + * Get half number of neighbors (13, excluding center). + */ + static constexpr std::size_t get_half_number_of_neighbors() { return 13; } + + // Member variables + Parameters params_; + double cell_span_; + double inverse_cell_span_; // Precomputed 1.0 / cell_span_ for efficiency + + // Grid dimensions (must be powers of 2 for bitwise operations) + std::size_t x_cell_count_; + std::size_t y_cell_count_; + std::size_t z_cell_count_; + std::size_t xy_cell_count_; // x_cell_count_ * y_cell_count_ + std::size_t xyz_cell_count_; // Total number of cells + + // Hash masks for bitwise modulo operations + std::size_t x_hash_mask_; + std::size_t y_hash_mask_; + std::size_t z_hash_mask_; + + // Grid cells (linear array) + std::vector cells_; + + // Standard neighbor offsets (for inner cells) + long std_neighbor_offsets_[27]; + + // Node storage + std::vector nodes_; + std::unordered_map node_to_cell_; // node index -> cell index + }; + + /** + * Compute appropriate cell span for a node based on its diameter. + */ + static double compute_cell_span_for_node(double node_diameter, const Parameters& params); + + /** + * Round up to next power of 2. + */ + static std::size_t round_up_to_power_of_2(std::size_t n); + + // Member variables + Parameters params_; + mutable std::list> grids_; // Grids sorted by cell span (ascending) +}; + +} // namespace collision +} // namespace elasticapp::environment diff --git a/backend/src/environment/collision/fine_detection/max_contacts.cpp b/backend/src/environment/collision/fine_detection/max_contacts.cpp new file mode 100644 index 000000000..bb37fb50a --- /dev/null +++ b/backend/src/environment/collision/fine_detection/max_contacts.cpp @@ -0,0 +1,78 @@ +#include "max_contacts.h" +#include +#include + +namespace elasticapp::environment { +namespace collision { + +std::vector MaxContacts::detect_sphere_sphere( + std::size_t node1_idx, + std::size_t node2_idx, + const Eigen::Vector3d& pos1, + const Eigen::Vector3d& pos2, + const Eigen::Vector3d& vel1, + const Eigen::Vector3d& vel2, + double r1, + double r2, + double stiffness, + double normal_damping, + double tangential_damping, + double friction +) const { + std::vector contacts; + + // Ordering: Normal from node2 to node1 always (matching reference implementation) + // This ensures consistent normal direction regardless of pair order + Eigen::Vector3d normal = pos1 - pos2; + const double normal_length = normal.norm(); + + // Avoid division by zero for coincident nodes + if (normal_length < 1e-12) { + return contacts; // Nodes are coincident, skip + } + + // Compute separation distance: dist = length(normal) - r1 - r2 + // - dist < 0: penetrating (overlapping) + // - dist > 0: separated + // - dist = 0: touching + const double dist = normal_length - r1 - r2; + + // Check if in contact (dist < contact_threshold) + // This allows contacts even when slightly separated for stability + if (dist < contact_threshold) { + // Normalize the normal vector + normal /= normal_length; + + // Compute contact position + // Contact point lies on the line connecting centers, at the point of maximum overlap + // Formula from reference: gPos = pos2 + normal * (r2 + 0.5 * dist) + // When dist < 0 (penetrating), this places the contact point between the surfaces + const double k = r2 + 0.5 * dist; + const Eigen::Vector3d contact_pos = pos2 + normal * k; + + // Create contact + // Note: distance is stored as dist (negative when penetrating, positive when separated) + // The Contact struct expects distance to be negative when penetrating + Contact contact( + node1_idx, node2_idx, + contact_pos, + normal, + dist, // dist is already negative when penetrating + vel1, + vel2, + stiffness, + normal_damping, + tangential_damping, + friction + ); + + contacts.push_back(contact); + } + + return contacts; +} + +// Template implementation moved to header file (max_contacts.h) due to template nature + +} // namespace collision +} // namespace elasticapp::environment diff --git a/backend/src/environment/collision/fine_detection/max_contacts.h b/backend/src/environment/collision/fine_detection/max_contacts.h new file mode 100644 index 000000000..a4e78c663 --- /dev/null +++ b/backend/src/environment/collision/fine_detection/max_contacts.h @@ -0,0 +1,186 @@ +#pragma once + +#include "../types.h" +#include +#include +#include + +#include + +namespace elasticapp::environment { +namespace collision { + +/** + * MaxContacts fine collision detection policy. + * + * Finds points of maximum overlap between potentially colliding pairs. + * This is the default fine detection algorithm in the reference implementation. + * + * Based on the Elastica MaxContacts implementation, which creates the maximal + * number of contact points necessary to handle collisions between rigid bodies + * physically accurately. + * + * For sphere-sphere collisions (nodes), a single contact point is generated + * at the point of maximum overlap along the line connecting the two centers. + * + * This is a CRTP policy class that will be mixed into CollisionSystem. + */ +class MaxContacts { +public: + /** + * Contact threshold for determining if two objects are in contact. + * + * Objects are considered in contact if their separation distance is less + * than this threshold. This allows contacts even when objects are slightly + * separated, which is important for stability in discrete time simulations. + * + * Value based on typical Elastica implementations (typically 1e-6 to 1e-9). + */ + static constexpr double contact_threshold = 1e-8; + + /** + * Default constructor. + */ + MaxContacts() = default; + + /** + * Destructor. + */ + ~MaxContacts() = default; + + /** + * Perform fine collision detection on candidate pairs. + * + * Performs precise intersection tests and finds points of maximum overlap. + * Returns detailed contact information for confirmed collisions. + * + * Algorithm (for sphere-sphere): + * 1. Compute normal vector from node2 to node1: normal = pos1 - pos2 + * 2. Compute separation distance: dist = length(normal) - r1 - r2 + * - dist < 0: penetrating (overlapping) + * - dist > 0: separated + * 3. If dist < contact_threshold, create contact: + * - Normalize normal vector + * - Contact position: pos2 + normal * (r2 + 0.5 * dist) + * - Contact normal points from node2 to node1 + * - Distance stored as dist (negative when penetrating) + * + * Material properties (stiffness, damping, friction) are taken from + * the physics model and stored in each Contact for later force computation. + * + * @param candidate_pairs Pairs of node indices from coarse detection + * @param positions Node positions (3 x n_nodes) + * @param velocities Node velocities (3 x n_nodes) + * @param radii Node radii (1 x n_nodes) + * @param physics_model Physics model containing material properties + * @return Vector of Contact objects for confirmed collisions + */ + template + std::vector detect( + const std::vector>& candidate_pairs, + const Eigen::MatrixXd& positions, + const Eigen::MatrixXd& velocities, + const Eigen::MatrixXd& radii, + const PhysicsModel& physics_model + ) const { + std::vector contacts; + contacts.reserve(candidate_pairs.size()); // Reserve space for potential contacts + + // Extract material properties from physics model + const double stiffness = physics_model.k_normal; + const double normal_damping = physics_model.eta_normal; + const double tangential_damping = physics_model.eta_tangential; + const double friction = physics_model.friction; + + // Process each candidate pair (parallelized with OpenMP) + const std::size_t n_pairs = candidate_pairs.size(); + + // Thread-local storage for contacts (each thread collects its own contacts) + std::vector> thread_contacts(omp_get_max_threads()); + + #pragma omp parallel for + for (std::size_t idx = 0; idx < n_pairs; ++idx) { + const auto& pair = candidate_pairs[idx]; + const std::size_t i1 = pair.first; + const std::size_t i2 = pair.second; + + // Get node positions + const Eigen::Vector3d pos1 = positions.col(i1); + const Eigen::Vector3d pos2 = positions.col(i2); + + // Get node velocities + const Eigen::Vector3d vel1 = velocities.col(i1); + const Eigen::Vector3d vel2 = velocities.col(i2); + + // Get node radii + const double r1 = std::abs(radii(i1)); + const double r2 = std::abs(radii(i2)); + + // Detect sphere-sphere collision + auto pair_contacts = detect_sphere_sphere( + i1, i2, + pos1, pos2, + vel1, vel2, + r1, r2, + stiffness, + normal_damping, + tangential_damping, + friction + ); + + // Add contacts to thread-local storage + const int thread_id = omp_get_thread_num(); + thread_contacts[thread_id].insert( + thread_contacts[thread_id].end(), + pair_contacts.begin(), + pair_contacts.end() + ); + } + + // Combine thread-local contacts into final result + for (const auto& thread_contact_vec : thread_contacts) { + contacts.insert(contacts.end(), thread_contact_vec.begin(), thread_contact_vec.end()); + } + + return contacts; + } + +private: + /** + * Detect collision between two spheres (nodes). + * + * Implements the sphere-sphere collision detection algorithm from the + * reference implementation. + * + * @param node1_idx Index of first node + * @param node2_idx Index of second node + * @param pos1 Position of first node + * @param pos2 Position of second node + * @param vel1 Velocity of first node + * @param vel2 Velocity of second node + * @param r1 Radius of first node + * @param r2 Radius of second node + * @param stiffness Contact stiffness + * @param normal_damping Normal damping coefficient + * @param tangential_damping Tangential damping coefficient + * @param friction Friction coefficient + * @return Contact object if collision detected, or empty optional + */ + std::vector detect_sphere_sphere( + std::size_t node1_idx, + std::size_t node2_idx, + const Eigen::Vector3d& pos1, + const Eigen::Vector3d& pos2, + const Eigen::Vector3d& vel1, + const Eigen::Vector3d& vel2, + double r1, + double r2, + double stiffness, + double normal_damping, + double tangential_damping, + double friction + ) const; +}; + +} // namespace collision +} // namespace elasticapp::environment diff --git a/backend/src/environment/collision/physics/linear_spring_dashpot.cpp b/backend/src/environment/collision/physics/linear_spring_dashpot.cpp new file mode 100644 index 000000000..0dd93b3fc --- /dev/null +++ b/backend/src/environment/collision/physics/linear_spring_dashpot.cpp @@ -0,0 +1,91 @@ +#include "linear_spring_dashpot.h" +#include "../types.h" +#include +#include + +namespace elasticapp::environment::collision { +namespace physics { + +inline Eigen::Vector3d LinearSpringDashpot::compute_force( + const collision::Contact& contact, + double& penetration_depth +) const { + // Check if contact is penetrating + if (!is_penetrating(contact)) { + penetration_depth = 0.0; + return Eigen::Vector3d::Zero(); + } + + // Penetration depth: positive when penetrating + // contact.distance is negative when penetrating (overlap) + // Matching CONTEXT: real_t delta(-contact.get_distance()); + penetration_depth = -contact.distance; + + // Get contact normal (unit vector pointing from body2 to body1) + const Eigen::Vector3d& normal = contact.normal; + + // Calculate relative velocity matching CONTEXT/COLLISION.md documentation: + // tangential_velocity = (other_elem_velocity - this_elem_velocity) - normal_velocity * normal_direction + // where other_elem is node2 and this_elem is node1 + // So: relative_velocity = velocity2 - velocity1 + const Eigen::Vector3d rel_velocity = contact.velocity2 - contact.velocity1; + + // Normal component of relative velocity + // Matching CONTEXT/COLLISION.md: normal_velocity = (other_elem_velocity - this_elem_velocity).dot(normal_direction) + const double rel_vel_normal = rel_velocity.dot(normal); + + // Tangential component of relative velocity + // Matching CONTEXT/COLLISION.md: tangential_velocity = (other_elem_velocity - this_elem_velocity) - normal_velocity * normal_direction + const Eigen::Vector3d rel_vel_tangential = rel_velocity - rel_vel_normal * normal; + + // Normal force magnitude (ReLU operation - no negative forces) + // Matching CONTEXT/COLLISION.md: normal_force = k_normal * penetration + eta_normal * normal_velocity + // Use physics parameters from LinearSpringDashpot instance, not from contact + // (Contact may have material properties, but we use the model's parameters) + const double f_normal_mag = std::max(0.0, + k_normal * penetration_depth + + eta_normal * rel_vel_normal + ); + + // Normal force vector + // Matching CONTEXT: Vec3 fN(fNabs * contact.get_normal()); + const Eigen::Vector3d f_normal = f_normal_mag * normal; + + // Tangential force calculation (full spring-dashpot model) + // Matching CONTEXT/COLLISION.md exactly: + // auto tangential_force = -k_tangential * tangential_displacement_vec; + // tangential_force += -eta_tangential * tangential_velocity; + // This enables positive k_tangential for full spring-dashpot friction model + Eigen::Vector3d f_tangential = -k_tangential * contact.tangential_displacement + - eta_tangential * rel_vel_tangential; + + // Apply Coulomb friction limit + // Matching reference: regularized_tangential_force_mag = min(tangential_force_mag, mu_static * normal_force) + const double f_tangential_mag = f_tangential.norm(); + const double regularized_f_tangential_mag = std::min( + f_tangential_mag, + friction * f_normal_mag + ); + + // Normalize tangential force if magnitude is significant + // Matching reference: if (tangential_force_mag > 1e-12) { + // tangential_force *= regularized_tangential_force_mag / tangential_force_mag; + // } + if (f_tangential_mag > 1e-12) { + f_tangential *= regularized_f_tangential_mag / f_tangential_mag; + } else { + f_tangential = Eigen::Vector3d::Zero(); + } + + // Net force (to be applied to body1, body2 gets -net_force) + // Matching CONTEXT: Vec3 net_force = (fN + fT); + return f_normal + f_tangential; +} + +inline bool LinearSpringDashpot::is_penetrating(const collision::Contact& contact) { + // Contact is penetrating if distance < 0 (overlap) + return contact.distance < 0.0; +} + +} // namespace physics +} // namespace elasticapp::environment::collision diff --git a/backend/src/environment/collision/physics/linear_spring_dashpot.h b/backend/src/environment/collision/physics/linear_spring_dashpot.h new file mode 100644 index 000000000..c23d0e66c --- /dev/null +++ b/backend/src/environment/collision/physics/linear_spring_dashpot.h @@ -0,0 +1,126 @@ +#pragma once + +#include +#include +#include "../types.h" + +namespace elasticapp::environment::collision { +namespace physics { + +/** + * Linear spring-dashpot collision physics model. + * + * Implements the Haff and Werner model for DEM collision response. + * Computes normal (repulsive) and tangential (friction) forces based on: + * - Normal force: F_n = max(0, k_n * delta + eta_n * v_n) + * - Tangential force: F_t = -k_t * d_t - eta_t * v_t (regularized by Coulomb limit) + * + * where: + * - k_n: normal spring constant (stiffness) + * - k_t: tangential spring constant (default: 0.0 for velocity-only friction) + * - eta_n: normal damping coefficient + * - eta_t: tangential damping coefficient + * - mu: friction coefficient + * - delta: penetration depth + * - d_t: tangential displacement (accumulated over time) + * - v_n: normal relative velocity + * - v_t: tangential relative velocity + */ +struct LinearSpringDashpot { + // Physics parameters + double k_normal; // Normal spring constant + double k_tangential; // Tangential spring constant + double eta_normal; // Normal damping coefficient + double eta_tangential; // Tangential damping coefficient + double friction; // Static friction coefficient + + /** + * Constructor with physics parameters (3 parameters - uses eta_normal for tangential). + * + * @param k_normal Normal spring constant for repulsion + * @param eta_normal Normal damping coefficient (also used for tangential) + * @param friction Static friction coefficient + */ + LinearSpringDashpot( + double k_normal, + double eta_normal, + double friction + ) : k_normal(k_normal), + k_tangential(0.0), // Default: no tangential spring (velocity-only friction) + eta_normal(eta_normal), + eta_tangential(eta_normal), // Use same value for tangential + friction(friction) {} + + /** + * Constructor with physics parameters (4 parameters - explicit tangential damping). + * + * @param k_normal Normal spring constant for repulsion + * @param eta_normal Normal damping coefficient + * @param eta_tangential Tangential damping coefficient + * @param friction Static friction coefficient + */ + LinearSpringDashpot( + double k_normal, + double eta_normal, + double eta_tangential, + double friction + ) : k_normal(k_normal), + k_tangential(0.0), // Default: no tangential spring (velocity-only friction) + eta_normal(eta_normal), + eta_tangential(eta_tangential), + friction(friction) {} + + /** + * Constructor with physics parameters (5 parameters - explicit tangential spring and damping). + * + * @param k_normal Normal spring constant for repulsion + * @param eta_normal Normal damping coefficient + * @param k_tangential Tangential spring constant + * @param eta_tangential Tangential damping coefficient + * @param friction Static friction coefficient + */ + LinearSpringDashpot( + double k_normal, + double eta_normal, + double k_tangential, + double eta_tangential, + double friction + ) : k_normal(k_normal), + k_tangential(k_tangential), + eta_normal(eta_normal), + eta_tangential(eta_tangential), + friction(friction) {} + + /** + * Compute collision forces for a contact. + * + * This method implements the LinearSpringDashpot force model: + * 1. Calculates penetration depth + * 2. Computes normal force (spring + damping) + * 3. Computes tangential force (spring + damping, regularized by Coulomb limit) + * 4. Returns the net force vector + * + * Tangential force formula: + * F_t = -k_tangential * tangential_displacement - eta_tangential * tangential_velocity + * Then regularized: F_t = min(|F_t|, mu * F_n) * normalize(F_t) + * + * @param contact The contact information (position, normal, distance, velocities, tangential_displacement, etc.) + * @param penetration_depth Output: penetration depth (positive if penetrating) + * @return Net force vector to apply to the first body (force on second body is -net_force) + */ + inline Eigen::Vector3d compute_force( + const collision::Contact& contact, + double& penetration_depth + ) const; + + /** + * Check if contact is penetrating. + * + * @param contact The contact information + * @return True if bodies are penetrating (distance < 0) + */ + inline static bool is_penetrating(const collision::Contact& contact); +}; + +} // namespace physics +} // namespace elasticapp::environment::collision diff --git a/backend/src/environment/collision/physics/no_interaction.cpp b/backend/src/environment/collision/physics/no_interaction.cpp new file mode 100644 index 000000000..977b03a59 --- /dev/null +++ b/backend/src/environment/collision/physics/no_interaction.cpp @@ -0,0 +1,17 @@ +#include "no_interaction.h" +#include "../types.h" + +namespace elasticapp::environment::collision { +namespace physics { + +inline Eigen::Vector3d NoInteraction::compute_force( + const collision::Contact& contact, + double& penetration_depth +) const { + // No interaction - always return zero force + penetration_depth = 0.0; + return Eigen::Vector3d::Zero(); +} + +} // namespace physics +} // namespace elasticapp::environment::collision diff --git a/backend/src/environment/collision/physics/no_interaction.h b/backend/src/environment/collision/physics/no_interaction.h new file mode 100644 index 000000000..fff09ae1e --- /dev/null +++ b/backend/src/environment/collision/physics/no_interaction.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include "../types.h" + +namespace elasticapp::environment::collision { +namespace physics { + +/** + * NoInteraction physics model for testing purposes. + * + * This model returns zero force for all contacts, allowing collision detection + * to be tested without applying any forces. Useful for: + * - Testing collision detection algorithms + * - Validating contact geometry + * - Debugging collision pipeline without force effects + */ +struct NoInteraction { + /** + * Default constructor (no parameters needed). + */ + NoInteraction() = default; + + /** + * Compute collision forces for a contact. + * + * Always returns zero force regardless of contact state. + * + * @param contact The contact information (unused, but required for interface compatibility) + * @param penetration_depth Output: penetration depth (set to 0.0) + * @return Zero force vector + */ + inline Eigen::Vector3d compute_force( + const collision::Contact& contact, + double& penetration_depth + ) const; +}; + +} // namespace physics +} // namespace elasticapp::environment::collision diff --git a/backend/src/environment/collision/types.h b/backend/src/environment/collision/types.h new file mode 100644 index 000000000..c68625e81 --- /dev/null +++ b/backend/src/environment/collision/types.h @@ -0,0 +1,87 @@ +#pragma once + +#include +#include + +namespace elasticapp::environment { +namespace collision { + +/** + * Contact information between two nodes. + * + * Represents a collision contact point between two nodes (spheres) + * in the collision detection system. + */ +struct Contact { + // Node indices in the block + std::size_t node1_idx; // Index of first node + std::size_t node2_idx; // Index of second node + + // Contact geometry + Eigen::Vector3d position; // Contact point position (world coordinates) + Eigen::Vector3d normal; // Contact normal (unit vector, pointing from node2 to node1) + double distance; // Signed distance: negative when penetrating (overlap) + + // Node velocities at contact point + Eigen::Vector3d velocity1; // Velocity of node1 + Eigen::Vector3d velocity2; // Velocity of node2 + + // Physics parameters (from material properties) + double stiffness; // Contact stiffness (k_normal) + double normal_damping; // Normal damping coefficient (eta_normal) + double tangential_damping; // Tangential damping coefficient (eta_tangential) + double friction; // Static friction coefficient (mu) + + // Tangential displacement tracking (for spring-dashpot model) + Eigen::Vector3d tangential_displacement; // Accumulated tangential displacement + + // Contact index within batch + std::size_t index; // Index within batch (default: 0) + + /** + * Default constructor. + */ + Contact() : node1_idx(0), node2_idx(0), + position(Eigen::Vector3d::Zero()), + normal(Eigen::Vector3d::Zero()), + distance(0.0), + velocity1(Eigen::Vector3d::Zero()), + velocity2(Eigen::Vector3d::Zero()), + stiffness(1.0), + normal_damping(0.1), + tangential_damping(0.1), + friction(0.5), + tangential_displacement(Eigen::Vector3d::Zero()), + index(0) {} + + /** + * Constructor with all parameters. + */ + Contact( + std::size_t n1, std::size_t n2, + const Eigen::Vector3d& pos, + const Eigen::Vector3d& n, + double dist, + const Eigen::Vector3d& v1, + const Eigen::Vector3d& v2, + double k, double eta_n, double eta_t, double mu + ) : node1_idx(n1), node2_idx(n2), + position(pos), normal(n), distance(dist), + velocity1(v1), velocity2(v2), + stiffness(k), normal_damping(eta_n), + tangential_damping(eta_t), friction(mu), + tangential_displacement(Eigen::Vector3d::Zero()), + index(0) {} + + /** + * Set the contact index within its batch. + * + * @param idx The index to set + */ + void set_index(std::size_t idx) { + index = idx; + } +}; + +} // namespace collision +} // namespace elasticapp::environment diff --git a/backend/src/math/eigen_detail/eigen_calculus.hpp b/backend/src/math/eigen_detail/eigen_calculus.hpp new file mode 100644 index 000000000..e87c6e9ec --- /dev/null +++ b/backend/src/math/eigen_detail/eigen_calculus.hpp @@ -0,0 +1,104 @@ +#pragma once + +#include +#include "traits.h" +#include +#include + +namespace elasticapp { + +//************************************************************************** +/*!\brief Simple 2-point difference rule with zero at end points. +// +// Discrete 2-point difference in elasticapp of a function f:[a,b]-> R, i.e +// D f[a,b] -> df[a,b] where f satisfies the conditions +// f(a) = f(b) = 0.0. Operates from rod's elemental space to nodal space. +// +// \param[out] out_matrix(3, n_nodes) difference values +// \param[in] in_matrix(3, n_elems) vector batch +// where n_nodes = n_elems + 1 +*/ +template +inline void two_point_difference_kernel(MT1& out_matrix, const MT2& in_matrix) { + constexpr std::size_t dimension(3UL); + assert(in_matrix.rows() == dimension); + assert(out_matrix.rows() == dimension); + const std::size_t n_elems = in_matrix.cols(); + const std::size_t n_nodes = n_elems + 1UL; + assert(out_matrix.cols() == n_nodes); + + // First column: f(a) = in_matrix[:, 0] + out_matrix.col(0) = in_matrix.col(0); + + // Last column: f(b) = -in_matrix[:, n_elems-1] + out_matrix.col(n_nodes - 1) = -in_matrix.col(n_elems - 1); + + // Middle columns: difference between consecutive elements + if (n_elems > 1) { + out_matrix.block(0, 1, dimension, n_elems - 1) = + in_matrix.block(0, 1, dimension, n_elems - 1) - + in_matrix.block(0, 0, dimension, n_elems - 1); + } +} + +//************************************************************************** +/*!\brief Simple trapezoidal quadrature rule with zero at end points. +// +// Discrete integral of a function in elasticapp +// f : [a,b] -> R, ∫[a,b] f -> R +// where f satisfies the conditions f(a) = f(b) = 0.0. +// Operates from rod's elemental space to nodal space. +// +// \param[out] out_matrix(3, n_nodes) quadrature values +// \param[in] in_matrix(3, n_elems) vector batch +// where n_nodes = n_elems + 1 +*/ +template +inline void quadrature_kernel(MT1& out_matrix, const MT2& in_matrix) { + constexpr std::size_t dimension(3UL); + const std::size_t n_elems = in_matrix.cols(); + assert(in_matrix.rows() == dimension); + assert(out_matrix.rows() == dimension); + const std::size_t n_nodes = n_elems + 1UL; + assert(out_matrix.cols() == n_nodes); + + using ValueType = typename MT1::Scalar; + + // First column: 0.5 * in_matrix[:, 0] + out_matrix.col(0) = ValueType(0.5) * in_matrix.col(0); + + // Last column: 0.5 * in_matrix[:, n_elems-1] + out_matrix.col(n_nodes - 1) = ValueType(0.5) * in_matrix.col(n_elems - 1); + + // Middle columns: 0.5 * (in_matrix[:, k] + in_matrix[:, k-1]) + out_matrix.block(0, 1, dimension, n_elems - 1) = + ValueType(0.5) * + (in_matrix.block(0, 1, dimension, n_elems - 1) + + in_matrix.block(0, 0, dimension, n_elems - 1)); +} + +//************************************************************************** +/*!\brief Average between consecutive elements. +// +// Computes average of consecutive elements: average[k] = 0.5 * (vector[k+1] + vector[k]) +// +// \param[in] in_vector(1, n_elems) or (n_elems,) scalar array +// +// \return output_vector(1, n_voronoi) where n_voronoi = n_elems - 1 +*/ +template +auto average_kernel(const MT& in_vector) { + const std::size_t n_elems = in_vector.cols(); + const std::size_t n_voronoi = n_elems > 0 ? n_elems - 1UL : 0UL; + + MatrixType result(1, n_voronoi); + + #pragma omp parallel for if(!omp_in_parallel()) + for (std::size_t k = 0; k < n_voronoi; ++k) { + result(0, k) = 0.5 * (in_vector(0, k + 1) + in_vector(0, k)); + } + + return result; +} + +} // namespace elasticapp diff --git a/backend/src/math/eigen_detail/eigen_linear_algebra.hpp b/backend/src/math/eigen_detail/eigen_linear_algebra.hpp new file mode 100644 index 000000000..3b8cce30d --- /dev/null +++ b/backend/src/math/eigen_detail/eigen_linear_algebra.hpp @@ -0,0 +1,197 @@ +#pragma once + +#include "traits.h" +#include +#include +#include +#include + +namespace elasticapp { + +//************************************************************************** +/*!\brief Vector Difference +// +// \param[in] in_vector(3, n_nodes) +// +// \return output_vector(3, n_elems) where n_elems = n_nodes - 1 +*/ +template +inline auto difference_kernel(const V& in_vector) { + constexpr std::size_t dimension(3UL); + assert(in_vector.rows() == dimension); + const std::size_t n_nodes = in_vector.cols(); + const std::size_t n_elems = n_nodes - 1UL; + + MatrixType result(dimension, n_elems); + result = in_vector.block(0, 1, dimension, n_elems) - + in_vector.block(0, 0, dimension, n_elems); + return result; +} + +//************************************************************************** +/*!\brief Batchwise matrix-vector product. +// +// Computes: matvec_batch{ik} = matrix_batch{ijk} * vector_batch{jk} +// +// \param[out] matvec_batch(3, n_elems) matrix-vector product +// \param[in] matrix_batch(3, 3, n_elems) +// \param[in] vector_batch(3, n_elems) +*/ +template +inline void batch_matvec(MT1& matvec_batch, + const TT& matrix_batch, + const MT2& vector_batch) { + constexpr std::size_t dimension(3UL); + const std::size_t n_elems = matrix_batch.cols(); + + assert(matvec_batch.rows() == dimension); + assert(matvec_batch.cols() == n_elems); + assert(matrix_batch.pages() == dimension); + assert(matrix_batch.rows() == dimension); + assert(vector_batch.rows() == dimension); + assert(vector_batch.cols() == n_elems); + + #pragma omp parallel for if(!omp_in_parallel()) + for (std::size_t i = 0; i < dimension; ++i) { + for (std::size_t k = 0; k < n_elems; ++k) { + double sum = 0.0; + for (std::size_t j = 0; j < dimension; ++j) { + sum += matrix_batch(i, j, k) * vector_batch(j, k); + } + matvec_batch(i, k) = sum; + } + } +} + +//************************************************************************** +/*!\brief Batchwise matrix-matrix product. +// +// Computes: matmul_batch{ilk} = first_matrix_batch{ijk} * second_matrix_batch{jlk} +// +// \param[out] matmul_batch(3, 3, n_elems) matrix-matrix product +// \param[in] first_matrix_batch(3, 3, n_elems) +// \param[in] second_matrix_batch(3, 3, n_elems) +*/ +template +inline void batch_matmul(TT1& matmul_batch, + const TT2& first_matrix_batch, + const TT3& second_matrix_batch) { + constexpr std::size_t dimension(3UL); + const std::size_t n_elems = first_matrix_batch.cols(); + + assert(matmul_batch.pages() == dimension); + assert(matmul_batch.rows() == dimension); + assert(matmul_batch.cols() == n_elems); + assert(first_matrix_batch.pages() == dimension); + assert(first_matrix_batch.rows() == dimension); + assert(second_matrix_batch.pages() == dimension); + assert(second_matrix_batch.rows() == dimension); + assert(second_matrix_batch.cols() == n_elems); + + for (std::size_t i = 0; i < dimension; ++i) { + for (std::size_t l = 0; l < dimension; ++l) { + for (std::size_t k = 0; k < n_elems; ++k) { + double sum = 0.0; + for (std::size_t j = 0; j < dimension; ++j) { + sum += first_matrix_batch(i, j, k) * second_matrix_batch(j, l, k); + } + matmul_batch(i, l, k) = sum; + } + } + } +} + +//************************************************************************** +/*!\brief Batchwise vector-vector cross product. +// +// Computes cross product for each column: cross_batch{ik} = first_vector_batch{ik} x second_vector_batch{ik} +// +// \param[out] cross_batch(3, n_elems) vector-vector cross product +// \param[in] first_vector_batch(3, n_elems) +// \param[in] second_vector_batch(3, n_elems) +*/ +template +inline void batch_cross(MT1& cross_batch, + const MT2& first_vector_batch, + const MT3& second_vector_batch) { + constexpr std::size_t dimension(3UL); + const std::size_t n_elems = first_vector_batch.cols(); + + assert(cross_batch.rows() == dimension); + assert(cross_batch.cols() == n_elems); + assert(first_vector_batch.rows() == dimension); + assert(second_vector_batch.rows() == dimension); + assert(second_vector_batch.cols() == n_elems); + + #pragma omp parallel for if(!omp_in_parallel()) + for (std::size_t k = 0; k < n_elems; ++k) { + cross_batch(0, k) = first_vector_batch(1, k) * second_vector_batch(2, k) - + first_vector_batch(2, k) * second_vector_batch(1, k); + cross_batch(1, k) = first_vector_batch(2, k) * second_vector_batch(0, k) - + first_vector_batch(0, k) * second_vector_batch(2, k); + cross_batch(2, k) = first_vector_batch(0, k) * second_vector_batch(1, k) - + first_vector_batch(1, k) * second_vector_batch(0, k); + } +} + +//************************************************************************** +/*!\brief Batchwise vector-vector dot product. +// +// Computes: dot_batch{j} = first_vector_batch{ij} * second_vector_batch{ij} +// +// \param[in] first_vector_batch(3, n_elems) +// \param[in] second_vector_batch(3, n_elems) +// +// \return dot_batch(n_elems) +*/ +template +inline auto batch_dot(const MT1& first_vector_batch, + const MT2& second_vector_batch) { + constexpr std::size_t dimension(3UL); + const std::size_t n_elems = first_vector_batch.cols(); + + assert(first_vector_batch.rows() == dimension); + assert(second_vector_batch.rows() == dimension); + assert(second_vector_batch.cols() == n_elems); + + VectorType result(n_elems); + #pragma omp parallel for if(!omp_in_parallel()) + for (std::size_t k = 0; k < n_elems; ++k) { + double sum = 0.0; + for (std::size_t i = 0; i < dimension; ++i) { + sum += first_vector_batch(i, k) * second_vector_batch(i, k); + } + result(k) = sum; + } + return result; +} + +//************************************************************************** +/*!\brief Batchwise vector L2 norm. +// +// Computes: norm_batch{j} = (vector_batch{ij} * vector_batch{ij})^0.5 +// +// \param[in] vector_batch(3, n_elems) +// +// \return norm_batch(n_elems) +*/ +template +inline auto batch_norm(const MT& vector_batch) { + constexpr std::size_t dimension(3UL); + assert(vector_batch.rows() == dimension); + + const std::size_t n_elems = vector_batch.cols(); + VectorType result(n_elems); + + #pragma omp parallel for if(!omp_in_parallel()) + for (std::size_t k = 0; k < n_elems; ++k) { + double sum = 0.0; + for (std::size_t i = 0; i < dimension; ++i) { + sum += vector_batch(i, k) * vector_batch(i, k); + } + result(k) = std::sqrt(sum); + } + return result; +} + +} // namespace elasticapp diff --git a/backend/src/math/eigen_detail/eigen_rotation.hpp b/backend/src/math/eigen_detail/eigen_rotation.hpp new file mode 100644 index 000000000..6fba88170 --- /dev/null +++ b/backend/src/math/eigen_detail/eigen_rotation.hpp @@ -0,0 +1,118 @@ +#pragma once + +#include "traits.h" +#include +#include +#include +#include + +namespace elasticapp { + +//************************************************************************** +/*!\brief Batchwise matrix logarithmic operator (inverse rotation). +// +// Batchwise for rotation matrix R computes the corresponding rotation +// axis vector {theta (u)} using the matrix log() operator. +// +// \param[out] rot_axis_vector_batch(3, n_elems) rotation axis vector batch +// \param[in] rot_matrix_batch(3, 3, n_elems) rotation matrix batch +*/ +template +void batch_inv_rotate(MT& rot_axis_vector_batch, const TT& rot_matrix_batch) { + constexpr std::size_t dimension(3UL); + const std::size_t n_elems = rot_matrix_batch.cols(); + using ValueType = double; + + assert(rot_matrix_batch.pages() == dimension); + assert(rot_matrix_batch.rows() == dimension); + assert(rot_axis_vector_batch.rows() == dimension); + assert(rot_axis_vector_batch.cols() == n_elems); + + #pragma omp parallel for if(!omp_in_parallel()) + for (std::size_t k = 0; k < n_elems; ++k) { + // Compute trace: tr(R) = R[0,0] + R[1,1] + R[2,2] + double trace = rot_matrix_batch(0, 0, k) + + rot_matrix_batch(1, 1, k) + + rot_matrix_batch(2, 2, k); + + // Clip trace to [-1, 3] for numerical stability + trace = std::max(-1.0, std::min(3.0, trace)); + + // theta = acos((tr(R) - 1) / 2) + double theta = std::acos(0.5 * (trace - 1.0) - 1e-12); + + // Compute R - R^T (skew-symmetric part) + // Extract vector from skew-symmetric matrix + rot_axis_vector_batch(0, k) = rot_matrix_batch(2, 1, k) - rot_matrix_batch(1, 2, k); + rot_axis_vector_batch(1, k) = rot_matrix_batch(0, 2, k) - rot_matrix_batch(2, 0, k); + rot_axis_vector_batch(2, k) = rot_matrix_batch(1, 0, k) - rot_matrix_batch(0, 1, k); + + // theta (u) = -theta * inv_skew([R - RT]) / (2 * sin(theta)) + double sin_theta = std::sin(theta) + 1e-14; + double magnitude = -0.5 * theta / sin_theta; + + rot_axis_vector_batch(0, k) *= magnitude; + rot_axis_vector_batch(1, k) *= magnitude; + rot_axis_vector_batch(2, k) *= magnitude; + } +} + +//************************************************************************** +/*!\brief Batchwise matrix exponential operator (Rodrigues formula). +// +// Batchwise for rotation axis vector {theta u} computes the corresponding +// rotation matrix R using the matrix exp() operator (Rodrigues formula): +// R = I + sin(theta) * U + (1 - cos(theta)) * U^2 +// +// \param[out] rot_matrix_batch(3, 3, n_elems) rotation matrix batch +// \param[in] rot_axis_vector_batch(3, n_elems) rotation axis vector batch +*/ +template +void exp_batch(TT& rot_matrix_batch, const MT& rot_axis_vector_batch) { + constexpr std::size_t dimension(3UL); + using ValueType = double; + + assert(rot_axis_vector_batch.rows() == dimension); + assert(rot_matrix_batch.pages() == dimension); + assert(rot_matrix_batch.rows() == dimension); + + const std::size_t n_elems = rot_axis_vector_batch.cols(); + + #pragma omp parallel for if(!omp_in_parallel()) + for (std::size_t k = 0; k < n_elems; ++k) { + // Compute theta = ||axis|| + double v0 = rot_axis_vector_batch(0, k); + double v1 = rot_axis_vector_batch(1, k); + double v2 = rot_axis_vector_batch(2, k); + double theta = std::sqrt(v0 * v0 + v1 * v1 + v2 * v2); + + // Normalize axis + double norm = theta + 1e-14; + v0 /= norm; + v1 /= norm; + v2 /= norm; + + // Precompute sin and cos + double sin_theta = std::sin(theta); + double cos_theta = std::cos(theta); + double one_minus_cos = 1.0 - cos_theta; + + // Build rotation matrix using Rodrigues formula + // Diagonal elements: R[ii] = cos(theta) + (1 - cos(theta)) * u[i]^2 + rot_matrix_batch(0, 0, k) = cos_theta + one_minus_cos * v0 * v0; + rot_matrix_batch(1, 1, k) = cos_theta + one_minus_cos * v1 * v1; + rot_matrix_batch(2, 2, k) = cos_theta + one_minus_cos * v2 * v2; + + // Off-diagonal elements + rot_matrix_batch(0, 1, k) = one_minus_cos * v0 * v1 + sin_theta * v2; + rot_matrix_batch(1, 0, k) = one_minus_cos * v0 * v1 - sin_theta * v2; + + rot_matrix_batch(0, 2, k) = one_minus_cos * v0 * v2 - sin_theta * v1; + rot_matrix_batch(2, 0, k) = one_minus_cos * v0 * v2 + sin_theta * v1; + + rot_matrix_batch(1, 2, k) = one_minus_cos * v1 * v2 + sin_theta * v0; + rot_matrix_batch(2, 1, k) = one_minus_cos * v1 * v2 - sin_theta * v0; + } +} + +} // namespace elasticapp diff --git a/backend/src/math/node_element_mapping.cpp b/backend/src/math/node_element_mapping.cpp new file mode 100644 index 000000000..7852a0c27 --- /dev/null +++ b/backend/src/math/node_element_mapping.cpp @@ -0,0 +1,120 @@ +#include "node_element_mapping.h" +#include +#include + +namespace elasticapp { +namespace math { + +std::size_t node_to_element_index( + std::size_t node_idx, + const std::vector& rod_start_indices, + const std::vector& rod_n_elems +) { + if (rod_start_indices.empty() || rod_n_elems.empty()) { + throw std::invalid_argument("rod_start_indices and rod_n_elems cannot be empty"); + } + + // Find which rod this node belongs to + std::size_t rod_index = 0; + for (std::size_t i = 0; i < rod_start_indices.size(); ++i) { + if (i + 1 < rod_start_indices.size()) { + // Not the last rod + if (node_idx >= rod_start_indices[i] && node_idx < rod_start_indices[i + 1]) { + rod_index = i; + break; + } + } else { + // Last rod + if (node_idx >= rod_start_indices[i]) { + rod_index = i; + break; + } + } + } + + // Check if node_idx is valid for the found rod + std::size_t rod_start = rod_start_indices[rod_index]; + std::size_t rod_n_nodes = rod_n_elems[rod_index] + 1; // n_elements + 1 nodes + std::size_t local_node_idx = node_idx - rod_start; + + if (local_node_idx >= rod_n_nodes) { + throw std::out_of_range("node_idx is beyond the last node of its rod"); + } + + // Map local node index to local element index + // Strategy: + // - Node 0 -> Element 0 + // - Node i (1 to n_elements-1) -> Element i-1 + // - Node n_elements (last) -> Element n_elements-1 + std::size_t local_elem_idx; + if (local_node_idx == 0) { + local_elem_idx = 0; + } else if (local_node_idx < rod_n_elems[rod_index]) { + local_elem_idx = local_node_idx - 1; + } else { + // Last node + local_elem_idx = rod_n_elems[rod_index] - 1; + } + + // Convert to global element index + // Need to compute element start indices + std::size_t elem_start = 0; + for (std::size_t i = 0; i < rod_index; ++i) { + elem_start += rod_n_elems[i]; + } + + return elem_start + local_elem_idx; +} + +std::pair node_to_rod_and_element( + std::size_t node_idx, + const std::vector& rod_start_indices, + const std::vector& rod_n_elems +) { + if (rod_start_indices.empty() || rod_n_elems.empty()) { + throw std::invalid_argument("rod_start_indices and rod_n_elems cannot be empty"); + } + + // Find which rod this node belongs to + std::size_t rod_index = 0; + for (std::size_t i = 0; i < rod_start_indices.size(); ++i) { + if (i + 1 < rod_start_indices.size()) { + // Not the last rod + if (node_idx >= rod_start_indices[i] && node_idx < rod_start_indices[i + 1]) { + rod_index = i; + break; + } + } else { + // Last rod + if (node_idx >= rod_start_indices[i]) { + rod_index = i; + break; + } + } + } + + // Check if node_idx is valid for the found rod + std::size_t rod_start = rod_start_indices[rod_index]; + std::size_t rod_n_nodes = rod_n_elems[rod_index] + 1; // n_elements + 1 nodes + std::size_t local_node_idx = node_idx - rod_start; + + if (local_node_idx >= rod_n_nodes) { + throw std::out_of_range("node_idx is beyond the last node of its rod"); + } + + // Map local node index to local element index + std::size_t local_elem_idx; + if (local_node_idx == 0) { + local_elem_idx = 0; + } else if (local_node_idx < rod_n_elems[rod_index]) { + local_elem_idx = local_node_idx - 1; + } else { + // Last node + local_elem_idx = rod_n_elems[rod_index] - 1; + } + + return std::make_pair(rod_index, local_elem_idx); +} + +} // namespace math +} // namespace elasticapp diff --git a/backend/src/math/node_element_mapping.h b/backend/src/math/node_element_mapping.h new file mode 100644 index 000000000..37ada32ef --- /dev/null +++ b/backend/src/math/node_element_mapping.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +namespace elasticapp { +namespace math { + +/** + * Map a global node index to the corresponding element index in a Block. + * + * For a rod with n_elements, there are n_elements+1 nodes. + * The mapping strategy: + * - For interior nodes (1 to n_elements-1): use the element before the node (element = node - 1) + * - For the first node (0): use element 0 + * - For the last node (n_elements): use element n_elements-1 + * + * This function handles multiple rods in a Block by using rod_start_indices + * to determine which rod a node belongs to. + * + * @param node_idx Global node index in the Block + * @param rod_start_indices Vector of starting node indices for each rod + * @param rod_n_elems Vector of number of elements for each rod + * @return Global element index corresponding to the node + * @throws std::out_of_range if node_idx is invalid + */ +std::size_t node_to_element_index( + std::size_t node_idx, + const std::vector& rod_start_indices, + const std::vector& rod_n_elems +); + +/** + * Map a global node index to the corresponding element index within its rod. + * + * This is a helper function that first finds which rod the node belongs to, + * then maps it to the element index within that rod. + * + * @param node_idx Global node index in the Block + * @param rod_start_indices Vector of starting node indices for each rod + * @param rod_n_elems Vector of number of elements for each rod + * @return Pair of (rod_index, local_element_index) + * @throws std::out_of_range if node_idx is invalid + */ +std::pair node_to_rod_and_element( + std::size_t node_idx, + const std::vector& rod_start_indices, + const std::vector& rod_n_elems +); + +} // namespace math +} // namespace elasticapp diff --git a/backend/src/operations.h b/backend/src/operations.h new file mode 100644 index 000000000..2b4d6fb8d --- /dev/null +++ b/backend/src/operations.h @@ -0,0 +1,379 @@ +#pragma once + +#include +#include +#include "cosserat_rod_system.h" +#include "cosserat_equations.h" +#include "traits.h" + +#include + +namespace elasticapp { + +// Thread management utilities +// Set the number of OpenMP threads to use +// This affects all subsequent parallel regions +// Args: +// num_threads: Number of threads to use (0 = use OpenMP default, typically all CPU cores) +inline void set_num_threads(int num_threads) { + if (num_threads > 0) { + omp_set_num_threads(num_threads); + } + // If num_threads is 0 or negative, OpenMP will use its default +} + +// Get the current number of threads in the current parallel region +// Returns 1 if called outside a parallel region +inline int get_num_threads() { + return omp_get_num_threads(); +} + +// Get the maximum number of threads that can be used +inline int get_max_threads() { + return omp_get_max_threads(); +} + +// Get the current thread number (0 to num_threads-1) +// Returns 0 if called outside a parallel region +inline int get_thread_num() { + return omp_get_thread_num(); +} + +// Default empty operations class using CRTP pattern +// This class can be extended with operations that work on the derived Block type +// +// Example usage with custom operations: +// template +// class MyOperations { +// public: +// void my_operation() { +// auto& block = static_cast(*this); +// // Access block members and perform operations +// } +// }; +// +// using MyBlock = Block; +template +class DefaultOperations { +public: + // Access to the derived class + Derived& derived() { return static_cast(*this); } + const Derived& derived() const { return static_cast(*this); } + +protected: + // Protected constructor to prevent direct instantiation + DefaultOperations() = default; + ~DefaultOperations() = default; + + // Prevent copying/moving (can be enabled in derived class if needed) + DefaultOperations(const DefaultOperations&) = default; + DefaultOperations(DefaultOperations&&) = default; + DefaultOperations& operator=(const DefaultOperations&) = default; + DefaultOperations& operator=(DefaultOperations&&) = default; +}; + +// Alias for backward compatibility +template +using Operations = DefaultOperations; + +// CosseratRodOperations class for Cosserat rod-specific operations +// This class provides operations for computing forces, torques, accelerations, etc. +template +class CosseratRodOperations { +public: + // Compute internal forces and torques + inline void compute_internal_forces_and_torques(double time) { + (void)time; // Suppress unused parameter warning + auto& block = static_cast(*this); + // Compute internal forces and torques separately + compute_geometry_from_state(block); + compute_all_dilatations(block); + compute_shear_stretch_strains(block); + compute_internal_shear_stretch_stresses_from_model(block); + compute_internal_forces(block); + + compute_bending_twist_strains(block); + compute_internal_bending_twist_stresses_from_model(block); + compute_dilatation_rate(block); + compute_internal_torques(block); + } + + // Compute strains + inline void compute_strains(double time) { + (void)time; // Suppress unused parameter warning + auto& block = static_cast(*this); + // Compute internal forces and torques separately + compute_geometry_from_state(block); + compute_all_dilatations(block); + compute_shear_stretch_strains(block); + compute_bending_twist_strains(block); + } + + // Update accelerations based on forces + inline void update_accelerations(double time) { + (void)time; // Suppress unused parameter warning + auto& block = static_cast(*this); + + // Get all required variables + auto&& acceleration = block.template get(); + auto&& internal_forces = block.template get(); + auto&& external_forces = block.template get(); + auto&& mass = block.template get(); + auto&& alpha = block.template get(); + auto&& inv_mass_second_moment_of_inertia = block.template get(); + auto&& internal_torques = block.template get(); + auto&& external_torques = block.template get(); + auto&& dilatation = block.template get(); + + // Update translational acceleration: a = (F_internal + F_external) / m + // acceleration is OnNode, Vector (3 rows, n_nodes cols) + const IndexType n_nodes = acceleration.cols(); + for (IndexType i = 0; i < 3; ++i) { + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_nodes; ++k) { + acceleration(i, k) = (internal_forces(i, k) + external_forces(i, k)) / mass(0, k); + } + } + + // Update angular acceleration: alpha = inv_J * (tau_internal + tau_external) * dilatation + // First zero out alpha + alpha.setZero(); + + // inv_mass_second_moment_of_inertia is OnElement, Matrix (9 rows, n_elems cols) + // Stored as 9xN where each column is a flattened 3x3 matrix: [I00, I01, I02, I10, I11, I12, I20, I21, I22] + // alpha is OnElement, Vector (3 rows, n_elems cols) + const IndexType n_elems = alpha.cols(); + for (IndexType i = 0; i < 3; ++i) { + for (IndexType j = 0; j < 3; ++j) { + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_elems; ++k) { + // Map 3x3 matrix index (i, j) to flattened 9x1 index: i*3 + j + IndexType inv_J_idx = i * 3 + j; + alpha(i, k) += inv_mass_second_moment_of_inertia(inv_J_idx, k) + * (internal_torques(j, k) + external_torques(j, k)) + * dilatation(0, k); + } + } + } + } + + // Zero out external forces and torques + // time parameter is included to match Python signature, but not used in implementation + inline void zeroed_out_external_forces_and_torques(double time) { + (void)time; // Suppress unused parameter warning + + auto& block = static_cast(*this); + auto&& external_forces = block.template get(); + auto&& external_torques = block.template get(); + + external_forces.setZero(); + external_torques.setZero(); + return; + + // Zero out all external forces (OnNode, Vector: 3 rows, n_nodes columns) + // Use explicit loop to ensure values are set (matching Python implementation) + const IndexType n_nodes = external_forces.cols(); + for (IndexType i = 0; i < 3; ++i) { + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_nodes; ++k) { + external_forces(i, k) = 0.0; + } + } + + // Zero out all external torques (OnElement, Vector: 3 rows, n_elems columns) + const IndexType n_elems = external_torques.cols(); + for (IndexType i = 0; i < 3; ++i) { + #pragma omp parallel for simd schedule(static) + for (IndexType k = 0; k < n_elems; ++k) { + external_torques(i, k) = 0.0; + } + } + } + + // Update kinematics (position, director) using velocity and omega + // Equivalent to Python: update_kinematics(time, prefac) + inline void update_kinematics(double prefac) { + auto& block = static_cast(*this); + + // Get variable views from block using accessible type names + auto&& position = block.template get(); + auto&& velocity = block.template get(); + auto&& director = block.template get(); + auto&& omega = block.template get(); + + // Update position: x += prefac * v + // position is (3, n_nodes), velocity is (3, n_nodes) + const IndexType n_nodes = position.cols(); + for (IndexType i = 0; i < 3; ++i) { + #pragma omp parallel for simd schedule(static) // num_threads(2) + for (IndexType k = 0; k < n_nodes; ++k) { + position(i, k) += prefac * velocity(i, k); + } + } + // position += prefac * velocity; + + // Update director using rotation matrix from omega + // director is stored as (9, n_elems) - flattened 3x3 matrices + // omega is (3, n_elems) + const std::size_t n_elems = director.cols(); + constexpr std::size_t dim = 3; + + // Parallelize loop if threading is enabled + #pragma omp parallel for simd schedule(static) + for (std::size_t k = 0; k < n_elems; ++k) { + // Match Python implementation: _get_rotation_matrix in _rotations.py + // Step 1: Get unscaled omega components + double v0 = omega(0, k); + double v1 = omega(1, k); + double v2 = omega(2, k); + + // Step 2: Compute theta = ||omega|| (magnitude before scaling) + double theta = std::sqrt(v0 * v0 + v1 * v1 + v2 * v2); + + // Step 3: Normalize axis (add epsilon to prevent division by zero, matching Python) + double norm = theta + 1e-14; + v0 /= norm; + v1 /= norm; + v2 /= norm; + + // Step 4: Scale theta by prefac (matching Python: theta *= scale) + theta *= prefac; + + // Step 5: Precompute sin and cos (matching Python: u_prefix = sin(theta), u_sq_prefix = 1.0 - cos(theta)) + double u_prefix = std::sin(theta); + double u_sq_prefix = 1.0 - std::cos(theta); + + // Step 6: Build rotation matrix using exact Python formulas + // Python: rot_mat[0, 0, k] = 1.0 - u_sq_prefix * (v1 * v1 + v2 * v2) + // This is equivalent to: cos(theta) + (1 - cos(theta)) * v0^2 + // but we use Python's formula for exact numerical match + double R00 = 1.0 - u_sq_prefix * (v1 * v1 + v2 * v2); + double R11 = 1.0 - u_sq_prefix * (v0 * v0 + v2 * v2); + double R22 = 1.0 - u_sq_prefix * (v0 * v0 + v1 * v1); + + // Off-diagonal elements (matching Python exactly) + double R01 = u_prefix * v2 + u_sq_prefix * v0 * v1; + double R10 = -u_prefix * v2 + u_sq_prefix * v0 * v1; + double R02 = -u_prefix * v1 + u_sq_prefix * v0 * v2; + double R20 = u_prefix * v1 + u_sq_prefix * v0 * v2; + double R12 = u_prefix * v0 + u_sq_prefix * v1 * v2; + double R21 = -u_prefix * v0 + u_sq_prefix * v1 * v2; + + // Extract current director (3x3 matrix stored as 9 elements) + // Storage order: [d00, d10, d20, d01, d11, d21, d02, d12, d22] + // (column-major order for 3x3 matrix) + double d00 = director(0, k); + double d10 = director(1, k); + double d20 = director(2, k); + double d01 = director(3, k); + double d11 = director(4, k); + double d21 = director(5, k); + double d02 = director(6, k); + double d12 = director(7, k); + double d22 = director(8, k); + + // Apply rotation: R @ director (matrix multiplication) + // New director = R * old_director + director(0, k) = R00 * d00 + R01 * d01 + R02 * d02; + director(1, k) = R10 * d00 + R11 * d01 + R12 * d02; + director(2, k) = R20 * d00 + R21 * d01 + R22 * d02; + director(3, k) = R00 * d10 + R01 * d11 + R02 * d12; + director(4, k) = R10 * d10 + R11 * d11 + R12 * d12; + director(5, k) = R20 * d10 + R21 * d11 + R22 * d12; + director(6, k) = R00 * d20 + R01 * d21 + R02 * d22; + director(7, k) = R10 * d20 + R11 * d21 + R12 * d22; + director(8, k) = R20 * d20 + R21 * d21 + R22 * d22; + } + } + + // Update dynamics (velocity, omega) using acceleration and alpha + // Equivalent to Python: update_dynamics(time, prefac) + // Optimized: Use direct loop instead of Eigen operations for better performance + // This matches the Python Numba implementation which uses simple nested loops + inline void update_dynamics(double prefac) { + auto& block = static_cast(*this); + + // Get variable views once (these are lightweight Eigen block expressions) + // The get<>() calls are template functions that should be fully inlined + // However, to minimize overhead, we compute dimensions once and reuse + auto&& velocity = block.template get(); + auto&& acceleration = block.template get(); + auto&& omega = block.template get(); + auto&& alpha = block.template get(); + + // Cache dimensions (avoid repeated .cols() calls) + const IndexType n_nodes = velocity.cols(); + const IndexType n_elems = omega.cols(); + + #pragma omp parallel sections + { + #pragma omp section + { + #ifdef ELASTICAPP_COMPONENT_THREADS + #pragma omp parallel for simd schedule(static) num_threads(ELASTICAPP_COMPONENT_THREADS) + #endif + for (IndexType k = 0; k < n_nodes; ++k) { + velocity(0, k) += prefac * acceleration(0, k); + } + } + #pragma omp section + { + #ifdef ELASTICAPP_COMPONENT_THREADS + #pragma omp parallel for simd schedule(static) num_threads(ELASTICAPP_COMPONENT_THREADS) + #endif + for (IndexType k = 0; k < n_nodes; ++k) { + velocity(1, k) += prefac * acceleration(1, k); + } + } + #pragma omp section + { + #ifdef ELASTICAPP_COMPONENT_THREADS + #pragma omp parallel for simd schedule(static) num_threads(ELASTICAPP_COMPONENT_THREADS) + #endif + for (IndexType k = 0; k < n_nodes; ++k) { + velocity(2, k) += prefac * acceleration(2, k); + } + } + #pragma omp section + { + #ifdef ELASTICAPP_COMPONENT_THREADS + #pragma omp parallel for simd schedule(static) num_threads(ELASTICAPP_COMPONENT_THREADS) + #endif + for (IndexType k = 0; k < n_nodes; ++k) { + omega(0, k) += prefac * alpha(0, k); + } + } + #pragma omp section + { + #ifdef ELASTICAPP_COMPONENT_THREADS + #pragma omp parallel for simd schedule(static) num_threads(ELASTICAPP_COMPONENT_THREADS) + #endif + for (IndexType k = 0; k < n_nodes; ++k) { + omega(1, k) += prefac * alpha(1, k); + } + } + #pragma omp section + { + #ifdef ELASTICAPP_COMPONENT_THREADS + #pragma omp parallel for simd schedule(static) num_threads(ELASTICAPP_COMPONENT_THREADS) + #endif + for (IndexType k = 0; k < n_nodes; ++k) { + omega(2, k) += prefac * alpha(2, k); + } + } + } + } + +protected: + // Protected constructor to prevent direct instantiation + CosseratRodOperations() = default; + ~CosseratRodOperations() = default; + + // Prevent copying/moving (can be enabled in derived class if needed) + CosseratRodOperations(const CosseratRodOperations&) = default; + CosseratRodOperations(CosseratRodOperations&&) = default; + CosseratRodOperations& operator=(const CosseratRodOperations&) = default; + CosseratRodOperations& operator=(CosseratRodOperations&&) = default; +}; + +} // namespace elasticapp diff --git a/backend/src/py/__init__.py b/backend/src/py/__init__.py new file mode 100644 index 000000000..f4c2086e9 --- /dev/null +++ b/backend/src/py/__init__.py @@ -0,0 +1,31 @@ +"""Elasticapp: A CPP accelerated backend for PyElastica kernels.""" + +# Import version function from version module +from elasticapp.version import version + +# Import BlockRodSystem from C++ module +from elasticapp._memory_block import BlockRodSystem, BlockRodSystemView + +# Import MemoryBlockCosseratRod after BlockRodSystem is defined to avoid circular import +from .module_collision import CollisionEnvironment +from .collision_physics import LinearSpringDashpot +from .memory_block_rod import MemoryBlockCosseratRod + + +__all__ = [ + "BlockRodSystem", + "BlockRodSystemView", + "MemoryBlockCosseratRod", + "version", +] + +# import submodules so that they can be easily accessed +# import elasticapp._linalg +# import elasticapp._rotations + +# Note: These imports are commented out as they depend on blaze, +# which is being removed. They will be refactored in later tasks. +# import elasticapp._PyTags +# import elasticapp._PyArrays +# import elasticapp._PyCosseratRods +# import elasticapp._PyExamples diff --git a/backend/src/py/collision_physics.py b/backend/src/py/collision_physics.py new file mode 100644 index 000000000..0f6170855 --- /dev/null +++ b/backend/src/py/collision_physics.py @@ -0,0 +1,98 @@ +from __future__ import annotations +from abc import ABC, abstractmethod +from elasticapp._memory_block import BlockRodSystem +from elasticapp._collision import CollisionSystem + +from .typing_alias import CoarseDetectionType, FineDetectionType, BatchingType + + +class CollisionPhysics(ABC): + """ + Abstract base class for block-wise collision physics. + """ + + def __init__( + self, + coarse_detection: CoarseDetectionType = "hash_grid", + fine_detection: FineDetectionType = "sphere_sphere", + batching: BatchingType = "union_find", + ) -> None: + super().__init__() + self.coarse_detection = coarse_detection + self.fine_detection = fine_detection + self.batching = batching + + @abstractmethod + def resolve_collision(self, system: "BlockRodSystem") -> None: + pass + + +class LinearSpringDashpot(CollisionPhysics): + """ + Linear spring-dashpot collision physics. + """ + + def __init__( + self, + *args, + k_normal: float = 1.0, + eta_normal: float = 0.1, + k_tangential: float = 0.0, + eta_tangential: float | None = None, + friction: float = 0.5, + detect_every: int = 1, + **kwargs, + ) -> None: + super().__init__(*args, **kwargs) + + from elasticapp._collision import LinearSpringDashpot as Model + + if eta_tangential is None and k_tangential == 0.0: + # Use 3-parameter constructor (eta_tangential = eta_normal, k_tangential = 0.0) + model = Model(k_normal=k_normal, eta_normal=eta_normal, friction=friction) + elif k_tangential == 0.0: + # Use 4-parameter constructor (explicit eta_tangential, k_tangential = 0.0) + model = Model( + k_normal=k_normal, + eta_normal=eta_normal, + eta_tangential=eta_tangential, + friction=friction, + ) + else: + # Use 5-parameter constructor (explicit k_tangential and eta_tangential) + if eta_tangential is None: + eta_tangential = eta_normal # Default to eta_normal if not provided + model = Model( + k_normal=k_normal, + eta_normal=eta_normal, + k_tangential=k_tangential, + eta_tangential=eta_tangential, + friction=friction, + ) + self.collision_system = CollisionSystem(model, detect_every=detect_every) + + def resolve_collision(self, system: "BlockRodSystem") -> None: + self.collision_system.resolve(system) + + +class NoInteraction(CollisionPhysics): + """ + NoInteraction collision physics model for testing purposes. + + This model returns zero force for all contacts, allowing collision detection + to be tested without applying any forces. Useful for: + - Testing collision detection algorithms + - Validating contact geometry + - Debugging collision pipeline without force effects + """ + + def __init__(self, *args, detect_every: int = 1, **kwargs) -> None: + super().__init__(*args, **kwargs) + + from elasticapp._collision import NoInteraction as Model + + model = Model() + self.collision_system = CollisionSystem(model, detect_every=detect_every) + + def resolve_collision(self, system: "BlockRodSystem") -> None: + self.collision_system.resolve(system) diff --git a/backend/src/py/memory_block_rod.py b/backend/src/py/memory_block_rod.py new file mode 100644 index 000000000..737b032b3 --- /dev/null +++ b/backend/src/py/memory_block_rod.py @@ -0,0 +1,167 @@ +"""Create block-structure class for collection of Cosserat rod systems.""" + +import numpy as np + +from elastica.rod.cosserat_rod import CosseratRod +from elastica.rod.data_structures import _RodSymplecticStepperMixin +from elastica.typing import RodType, SystemIdxType + +from elasticapp._memory_block import BlockRodSystem + +# Mapping python CosseratRod attribute to C++ tag that will be block memory allocated. +# Tags are defined in cosserat_rod_system.h +PY2CPP_VARNAMES: dict[str, str] = { + # Node variables + "mass": "mass", + "position_collection": "position", + "velocity_collection": "velocity", + "acceleration_collection": "acceleration", + "internal_forces": "internal_forces", + "external_forces": "external_forces", + # Element variables + "omega_collection": "omega", + "alpha_collection": "alpha", + "director_collection": "director", + "rest_lengths": "rest_lengths", + "density": "density", + "volume": "volume", + "mass_second_moment_of_inertia": "mass_second_moment_of_inertia", + "inv_mass_second_moment_of_inertia": "inv_mass_second_moment_of_inertia", + "internal_torques": "internal_torques", + "external_torques": "external_torques", + "lengths": "lengths", + "tangents": "tangents", + "radius": "radius", + "dilatation": "dilatation", + "dilatation_rate": "dilatation_rate", + "sigma": "sigma", + "rest_sigma": "rest_sigma", + "internal_stress": "internal_stress", + "shear_matrix": "shear_matrix", + # Voronoi variables + "rest_voronoi_lengths": "rest_voronoi_lengths", + "voronoi_dilatation": "voronoi_dilatation", + "kappa": "kappa", + "rest_kappa": "rest_kappa", + "internal_couple": "internal_couple", + "bend_matrix": "bend_matrix", +} + + +class MemoryBlockCosseratRod(CosseratRod, _RodSymplecticStepperMixin): + """ + Memory block class for Cosserat rod equations. + + This class is derived from CosseratRod to inherit all rod methods while providing + a memory-efficient block structure for multiple rod systems. It uses the C++ backend + BlockRodSystem for efficient memory management. + + Parameters + ---------- + systems : list[RodType] + List of CosseratRod objects to be included in the memory block structure. + Currently only straight rods are supported (ring rods are not yet implemented). + system_idx_list : list[SystemIdxType] + List of system indices corresponding to each rod in the `systems` list. + These indices are used to map rods back to their original positions in + the simulator's system collection. + + Attributes + ---------- + n_systems : int + Total number of rod systems in the memory block. + n_rods : int + Total number of rods (same as n_systems). + n_elems : int + Total number of elements across all rods in the block structure. + n_nodes : int + Total number of nodes across all rods (n_elems + 1). + n_voronoi : int + Total number of Voronoi points across all rods (n_elems - 1). + system_idx_list : numpy.ndarray + Array of system indices mapping rods to their original positions. + ghost_nodes_idx : numpy.ndarray + Indices of ghost nodes used for boundary conditions. + ghost_elems_idx : numpy.ndarray + Indices of ghost elements used for boundary conditions. + ghost_voronoi_idx : numpy.ndarray + Indices of ghost Voronoi points used for boundary conditions. + + Notes + ----- + - Currently only straight rods are supported. Ring rod support is planned for future. + - All rod data (positions, directors, velocities, etc.) is stored in contiguous + memory blocks for efficient computation. + """ + + def __init__( + self, systems: list[RodType], system_idx_list: list[SystemIdxType] + ) -> None: + self.n_systems = len(systems) + + # Sorted systems (only straight rods for now) + self.system_idx_list = np.array(system_idx_list, dtype=np.int32) + + n_elems_straight_rods = np.array([x.n_elems for x in systems], dtype=np.int32) + + # Create C++ block with element counts + # BlockRodSystem accepts numpy arrays directly (as well as lists/tuples) + self._block = BlockRodSystem(n_elems_straight_rods) + for py_key, cpp_key in PY2CPP_VARNAMES.items(): + full_block_memory = self._block.get(cpp_key) + setattr(self, py_key, full_block_memory) + + # Get ghost indices from C++ block + self.ghost_nodes_idx = np.array(self._block.ghost_nodes_idx, dtype=np.int32) + self.ghost_elems_idx = np.array(self._block.ghost_elems_idx, dtype=np.int32) + self.ghost_voronoi_idx = np.array(self._block.ghost_voronoi_idx, dtype=np.int32) + + # Compute metadata from block and element counts + # n_elems is total elements including ghost elements + # For n rods, there are (n-1) ghost nodes, and 2*(n-1) ghost elements + self.n_elems = int(np.sum(n_elems_straight_rods) + 2 * (len(systems) - 1)) + self.n_nodes = self.n_elems + 1 + self.n_voronoi = self.n_elems - 1 + self.n_rods = len(systems) + + for idx, system in enumerate(systems): + self.relink_system_properties_to_block_memory(system, idx) + + self._block.reset_ghost() + + # Compute strains for the block + self.compute_strains(0.0) + + def relink_system_properties_to_block_memory( + self, + system: RodType, + system_idx: int, + ) -> None: + """Relink system properties to block memory.""" + for py_key, cpp_key in PY2CPP_VARNAMES.items(): + assert hasattr( + system, py_key + ), f"System {system} does not have attribute {py_key}." + value = getattr(system, py_key).copy() + block_memory = self._block.at(system_idx).get(cpp_key) + block_memory[...] = value + setattr(system, py_key, block_memory) + + # # Override python implementation + def compute_strains(self, time: np.float64) -> None: + self._block.compute_strains(time) + + def compute_internal_forces_and_torques(self, time: np.float64) -> None: + self._block.compute_internal_forces_and_torques(time) + + def zeroed_out_external_forces_and_torques(self, time: np.float64) -> None: + self._block.zeroed_out_external_forces_and_torques(time) + + def update_accelerations(self, time: np.float64) -> None: + self._block.update_accelerations(time) + + def update_kinematics(self, time: np.float64, prefac: np.float64) -> None: + self._block.update_kinematics(time, prefac) + + def update_dynamics(self, time: np.float64, prefac: np.float64) -> None: + self._block.update_dynamics(time, prefac) diff --git a/backend/src/py/module_collision.py b/backend/src/py/module_collision.py new file mode 100644 index 000000000..d01532306 --- /dev/null +++ b/backend/src/py/module_collision.py @@ -0,0 +1,188 @@ +from __future__ import annotations + +__doc__ = """ +CollisionEnvironment +-------------------- + +Provides the collision environment interface to configure collision detection and +detection resolution for Cosserat rods in the simulation. +""" +__all__ = ["CollisionEnvironment"] +from typing import Any, Type +import functools + +from elastica.external_forces import NoForces +from elastica.typing import SystemIdxType +from elastica.modules.protocol import SystemCollectionProtocol, ModuleProtocol + +from .memory_block_rod import MemoryBlockCosseratRod +from .collision_physics import CollisionPhysics + +from .typing_alias import CoarseDetectionType, FineDetectionType, BatchingType + + +class CollisionEnvironment(SystemCollectionProtocol): + """ + The CollisionEnvironment class enables collision detection and resolution for Cosserat rods in the simulation. + By including CollisionEnvironment as a mixin, the simulator gains support for discrete element method (DEM)-based contact handling. + Use this environment together with C++ backend support to activate and configure collision pipeline stages. + + Notes + ----- + Currently, the CollisionEnvironment is only compatible when used with the + C++ backend. + Currently, the feature does not support variations in the collision detection + methods, as it requires re-compilation of the C++ backend. This must be done + by the user. + + Examples + -------- + User can include the CollisionEnvironment in the simulator class to enable + collision detection and reaction-forces for all Cosserat rods. + + >>> import elastica as ea + >>> import elasticapp as epp + ... + >>> # Include the CollisionEnvironment in the simulator class + >>> class Simulator(ea.BaseSystemCollection, epp.CollisionEnvironment, ...): + ... pass + ... + >>> simulator = Simulator() + >>> # Enable the C++ block supports for the Cosserat rods + >>> simulator.enable_block_supports(ea.CosseratRod, epp.MemoryBlockCosseratRod) + + To change the configuration of the collision physics, use `.configure_collision`. + + >>> simulator \ + .configure_collision_detection() \ + .using( + epp.LinearSpringDashpot, + k_normal=1.0, + eta_normal=0.1, + friction=0.5 + ) + + Attributes + ---------- + _ext_forces_torques: list + List of forcing class defined for rod-like objects. + """ + + def __init__(self) -> None: + super().__init__() + self._feature_group_finalize.append(self._finalize_block_collision) + + # Initial configuration + self._collision_coarse_detection: CoarseDetectionType + self._collision_fine_detection: FineDetectionType + self._collision_batching: BatchingType + self._collision_controller: "_CollisionController" + self.configure_collision_detection() + + def configure_collision_detection( + self, + coarse_detection: CoarseDetectionType = "hash_grid", + fine_detection: FineDetectionType = "sphere_sphere", + batching: BatchingType = "union_find", + ) -> ModuleProtocol: + """ + This method applies external forces and torques on the relevant + user-defined system or rod-like object. You must input the system + or rod-like object that you want to apply external forces and torques on. + + Parameters + ---------- + coarse_detection: CoarseDetectionType + The coarse detection algorithm to use. + fine_detection: FineDetectionType + The fine detection algorithm to use. + batching: BatchingType + The batching algorithm to use. + """ + self._collision_coarse_detection = coarse_detection + self._collision_fine_detection = fine_detection + self._collision_batching = batching + + # Create _Constraint object, cache it and return to user + self._collision_controller = _CollisionController() + self._feature_group_synchronize.append_id(self._collision_controller) + + return self._collision_controller + + def _finalize_block_collision(self) -> None: + controller = self._collision_controller.instantiate() + del self._collision_controller + # Find block rods in the system + for sys in self.final_systems(): + if isinstance(sys, MemoryBlockCosseratRod): + block = sys._block + break + else: + raise ValueError( + "No block rods found in the system. Collision requies at least one rod in the simulator." + ) + + compute_collision = functools.partial( + controller.resolve_collision, system=block + ) + + self._feature_group_synchronize.add_operators(controller, [compute_collision]) + + return controller + + +class _CollisionController: + """ + Collision controller module private class + """ + + _forcing_cls: Type[NoForces] + _args: Any + _kwargs: Any + + def using(self, cls: Type[NoForces], *args: Any, **kwargs: Any) -> None: + """ + This method sets which forcing class is used to apply forcing + to user defined rod-like objects. + + Parameters + ---------- + cls: Type[Any] + User defined forcing class. + *args: Any + Variable length argument list. + **kwargs: Any + Arbitrary keyword arguments. + + Returns + ------- + + """ + assert issubclass( + cls, CollisionPhysics + ), "{} is not a valid collision physics. Did you forget to derive from CollisionPhysics?".format( + cls + ) + self._cls = cls + self._args = args + self._kwargs = kwargs + + def id(self) -> SystemIdxType: + return None # type: ignore + + def instantiate(self) -> "CollisionPhysics": + """Constructs a constraint after checks""" + if not hasattr(self, "_cls"): + raise RuntimeError( + "No collision physics provided to act on block rod" + "but a collision physics was registered. Did you forget to call" + "the `using` method" + ) + + try: + return self._cls(*self._args, **self._kwargs) + except (TypeError, IndexError): + raise TypeError( + r"Unable to construct collision physics class.\n" + r"Did you provide all necessary collision physics parameters?" + ) diff --git a/backend/src/py/typing_alias.py b/backend/src/py/typing_alias.py new file mode 100644 index 000000000..982093ee4 --- /dev/null +++ b/backend/src/py/typing_alias.py @@ -0,0 +1,5 @@ +from typing import Literal + +CoarseDetectionType = Literal["hash_grid"] +FineDetectionType = Literal["sphere_sphere"] +BatchingType = Literal["union_find"] # , "single_batch", "hybrid_batch"] diff --git a/backend/src/system.h b/backend/src/system.h new file mode 100644 index 000000000..8e772a4bd --- /dev/null +++ b/backend/src/system.h @@ -0,0 +1,141 @@ +#pragma once + +#include +#include +#include +#include +#include "traits.h" + +namespace elasticapp { + +// +// System is a minimal header-only class representing a collection of physical variables +// and their properties for a mechanical or simulation system. It is used solely for compile- +// time type information and does not allocate memory or hold state. Its purpose is to define, +// via template arguments, which variables are present in a system and to specify their placement +// (e.g., on nodes, elements, voronoi) and data type (e.g., scalar, vector, matrix) using tags. +// System enables static reflection and type-driven logic for Block, BlockView, and other components. +// +// Example: +// using MySystem = System; +// +// - Displacement, Force, BendingMoment are structs inheriting correct placement and data type tags. +// - MySystem::Variables is a type list of the variables, usable in metaprogramming. +// - System does not itself allocate memory or store real data. +// + +// ******************************************************** +// Placement tags +// ******************************************************** + +// Placement tags - indicate where variables are stored +namespace Placement { + struct OnNode {}; + struct OnElement {}; + struct OnVoronoi {}; +} // namespace Placement + +template +concept HasPlacementTag = + std::is_base_of_v || + std::is_base_of_v || + std::is_base_of_v; + +// ******************************************************** +// DataType tags +// ******************************************************** + +namespace DataType { + // Dimension tags - indicate the size of variables + struct Scalar { + static constexpr std::size_t dimension = 1; + // Note: Not constexpr because Eigen dynamic matrices don't have constexpr constructors + // Using inline static (C++17) to define in header + inline static MatrixType ghost_value = MatrixType::Zero(1, 1); + }; + struct Vector { + static constexpr std::size_t dimension = 3; + // Note: Not constexpr because Eigen dynamic matrices don't have constexpr constructors + // Using inline static (C++17) to define in header + inline static MatrixType ghost_value = MatrixType::Zero(3, 1); + }; + struct Matrix { + static constexpr std::size_t dimension = 9; + // Note: Not constexpr because Eigen dynamic matrices don't have constexpr constructors + // Using inline static (C++17) to define in header + inline static MatrixType ghost_value = MatrixType::Zero(9, 1); + }; +} // namespace DataType + +template +concept HasDataTypeTag = + std::is_base_of_v || + std::is_base_of_v || + std::is_base_of_v; + +template +concept ValidVariable = + HasPlacementTag && + HasDataTypeTag && + requires { + { T::name } -> std::convertible_to; + { T::ghost_value }; + }; + +// Helper to extract dimension from a variable type +template +constexpr std::size_t get_dimension_v = + std::is_base_of_v ? DataType::Scalar::dimension : + std::is_base_of_v ? DataType::Vector::dimension : + std::is_base_of_v ? DataType::Matrix::dimension : 0; + +// Helper to sum dimensions across a parameter pack +template +constexpr std::size_t sum_dimensions_v = (0 + ... + get_dimension_v); + +// ******************************************************** +// System class +// ******************************************************** + +// System class - collection of Variables +// Template parameter pack for variable types +// Each Variable must derive from one Placement tag and one DataType tag +template +class System { +public: + // Expose the variable types for template metaprogramming + using Variables = std::tuple; + + static constexpr std::size_t get_depth() { + return sum_dimensions_v; + } + +}; + +// Helper to extract variable types from a System type +template +struct system_variables; + +template +struct system_variables> { + using type = std::tuple; +}; + +template +using system_variables_t = typename system_variables::type; + +// Concept to check if a type is a System +// A type is a SystemModel if: +// 1. It is not instantiable on its own. +// 2. It has a Variables type alias (std::tuple<...>) +// 3. It has a static get_depth() method +// 4. system_variables is specialized (i.e., T is System<...>) +template +concept SystemModel = requires { + typename SystemType::Variables; // Must have Variables type alias + { SystemType::get_depth() } -> std::convertible_to; // Must have static get_depth() + typename system_variables::type; // system_variables must be specialized + requires std::is_same_v::type, typename SystemType::Variables>; +}; + +} // namespace elasticapp diff --git a/backend/src/traits.h b/backend/src/traits.h new file mode 100644 index 000000000..6fafcd78a --- /dev/null +++ b/backend/src/traits.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include + +namespace elasticapp { + +using MatrixType = Eigen::Matrix; +using VectorType = Eigen::Matrix; +using IndexType = Eigen::Index; +constexpr bool IsRowMajor = true; +constexpr bool IsColMajor = false; + +// Helper for static_asserts in templates +template +struct dependent_false : std::false_type {}; + +// Helper functions for computing strides for numpy array views +inline std::pair compute_strides(std::size_t rows, std::size_t cols) { + if constexpr (IsRowMajor) { + // Row-major: stride between rows is cols * sizeof(double) + // stride between columns is sizeof(double) + return {static_cast(cols * sizeof(double)), + static_cast(sizeof(double))}; + } else { + // Column-major: stride between rows is sizeof(double) + // stride between columns is rows * sizeof(double) + return {static_cast(sizeof(double)), + static_cast(rows * sizeof(double))}; + } +} + +// Helper functions for matrix slicing operations +// These encapsulate Eigen-specific operations + +// Get a view into a submatrix (columns for a rod) +// Returns a lightweight view - no copy, just a reference to the columns +inline auto get_column_slice(MatrixType& matrix, std::size_t start_col, std::size_t num_cols) { + return matrix.middleCols(static_cast(start_col), + static_cast(num_cols)); +} + +inline auto get_column_slice(const MatrixType& matrix, std::size_t start_col, std::size_t num_cols) { + return matrix.middleCols(static_cast(start_col), + static_cast(num_cols)); +} + +// Get a view into a submatrix (specific rows and columns) +// Returns a lightweight view - no copy, just a reference to the submatrix +inline auto get_block_slice(MatrixType& matrix, + std::size_t start_row, std::size_t num_rows, + std::size_t start_col, std::size_t num_cols) { + return matrix.block(static_cast(start_row), + static_cast(start_col), + static_cast(num_rows), + static_cast(num_cols)); +} + +inline auto get_block_slice(const MatrixType& matrix, + std::size_t start_row, std::size_t num_rows, + std::size_t start_col, std::size_t num_cols) { + return matrix.block(static_cast(start_row), + static_cast(start_col), + static_cast(num_rows), + static_cast(num_cols)); +} + +} // namespace elasticapp diff --git a/backend/src/utility/hash.h b/backend/src/utility/hash.h new file mode 100644 index 000000000..d7642529f --- /dev/null +++ b/backend/src/utility/hash.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include // For std::pair +#include // For std::hash + +namespace elasticapp { +namespace utility { + +/** + * Hash function for std::pair (needed for unordered_map) + * C++11 doesn't provide std::hash for pairs by default + */ +struct PairHash { + std::size_t operator()(const std::pair& p) const { + // Combine hashes of both elements + // Using a simple hash combination (can be improved with boost::hash_combine) + return std::hash()(p.first) ^ (std::hash()(p.second) << 1); + } +}; + +} // namespace utility +} // namespace elasticapp diff --git a/backend/src/variable_offsets.h b/backend/src/variable_offsets.h new file mode 100644 index 000000000..7c2f5383c --- /dev/null +++ b/backend/src/variable_offsets.h @@ -0,0 +1,80 @@ +#pragma once + +#include "cosserat_rod_system.h" +#include "system.h" +#include +#include +#include +#include + +namespace elasticapp { + +// Generic helpers for computing variable offsets in any System + +// Helper to find the index of a type in a parameter pack +template +struct find_index_impl; // Forward declaration + +// Found the type +template +struct find_index_impl { + static constexpr std::size_t value = CurrentIndex; +}; + +// Recurse to the next type in the pack +template +struct find_index_impl { + static constexpr std::size_t value = find_index_impl::value; +}; + +// Type not found in the pack (empty pack) - this provides a clear error message +template +struct find_index_impl { + static_assert(dependent_false::value, + "Undefined variable requested. Please ensure the variable is defined in the System's variable list."); +}; + +// Helper to sum dimensions of variables up to (but not including) a given index +template +struct sum_dimensions_up_to_index_impl; + +template +struct sum_dimensions_up_to_index_impl { + static constexpr std::size_t value = + (Index == 0) ? 0 : + (get_dimension_v + sum_dimensions_up_to_index_impl::value); +}; + +template +struct sum_dimensions_up_to_index_impl { + static constexpr std::size_t value = 0; +}; + +// Helper to compute offset for a variable in a System +// This finds the variable's position and sums dimensions of all variables before it +template +struct compute_variable_offset_impl { + static constexpr std::size_t index = find_index_impl::value; + static constexpr std::size_t value = sum_dimensions_up_to_index_impl::value; +}; + +// Helper to expand a tuple into a parameter pack for compute_variable_offset_impl +template +struct compute_variable_offset_from_system_impl; + +template +struct compute_variable_offset_from_system_impl> { + static constexpr std::size_t value = compute_variable_offset_impl::value; +}; + +// Generic function to compute variable offset for any System type +// This automatically extracts variables from SystemType::Variables and computes the offset +// SystemType must be a System<...> type +template +constexpr std::size_t compute_variable_offset() { + // Extract variables directly from SystemType's Variables type alias + using VariablesTuple = typename SystemType::Variables; + return compute_variable_offset_from_system_impl::value; +} + +} // namespace elasticapp diff --git a/backend/src/version.h b/backend/src/version.h new file mode 100644 index 000000000..304cf1356 --- /dev/null +++ b/backend/src/version.h @@ -0,0 +1,18 @@ +#ifndef ELASTICAPP_VERSION_H +#define ELASTICAPP_VERSION_H + +#include + +#define STRINGIFY(x) #x +#define MACRO_STRINGIFY(x) STRINGIFY(x) + +namespace elasticapp { + +inline std::string version() { + // VERSION_INFO is defined by CMake and stringified here + return std::string(MACRO_STRINGIFY(VERSION_INFO)); +} + +} // namespace elasticapp + +#endif // ELASTICAPP_VERSION_H diff --git a/backend/tests/cpp/collision/test_collision_system.cpp b/backend/tests/cpp/collision/test_collision_system.cpp new file mode 100644 index 000000000..33bbd9c5c --- /dev/null +++ b/backend/tests/cpp/collision/test_collision_system.cpp @@ -0,0 +1,190 @@ +#include +#include +#include +#include +#include +#include +#include "../../../src/environment/collision/collision_system.h" +#include "../../../src/environment/collision/types.h" +#include "../../../src/api.h" + +using Catch::Approx; + +namespace elasticapp::environment::collision { +namespace testing { + +/** + * Null coarse detection policy for unit testing. + * Returns empty vector (no candidate pairs detected). + */ +struct NullCoarseDetection { + std::vector> detect( + const Eigen::MatrixXd& positions, + const Eigen::MatrixXd& radii + ) const { + (void)positions; + (void)radii; + return {}; + } +}; + +/** + * Null fine detection policy for unit testing. + * Returns empty vector (no contacts detected). + */ +struct NullFineDetection { + template + std::vector detect( + const std::vector>& candidate_pairs, + const Eigen::MatrixXd& positions, + const Eigen::MatrixXd& velocities, + const Eigen::MatrixXd& radii, + const PhysicsModel& physics_model + ) const { + (void)candidate_pairs; + (void)positions; + (void)velocities; + (void)radii; + (void)physics_model; + return {}; + } +}; + +/** + * Null batching policy for unit testing. + * Returns empty batches. + */ +struct NullBatching { + std::vector> batch( + std::vector& contacts + ) const { + (void)contacts; + return {}; + } +}; + +} // namespace testing +} // namespace elasticapp::environment::collision + +// Type alias for test CollisionSystem with null policies +using TestCollisionSystem = elasticapp::environment::collision::CollisionSystem< + elasticapp::environment::collision::testing::NullCoarseDetection, + elasticapp::environment::collision::testing::NullFineDetection, + elasticapp::environment::collision::testing::NullBatching +>; + +TEST_CASE("CollisionSystem construction", "[collision]") { + SECTION("Can be constructed with nullptr_t physics model") { + std::nullptr_t null_model = nullptr; + TestCollisionSystem system(null_model); + + // Should construct successfully + REQUIRE(true); + } + + SECTION("Can be constructed with nullptr_t physics model and detect_every") { + std::nullptr_t null_model = nullptr; + TestCollisionSystem system(null_model, 5); + + REQUIRE(system.detect_every() == 5); + } + + SECTION("Default detect_every is 1") { + std::nullptr_t null_model = nullptr; + TestCollisionSystem system(null_model); + + REQUIRE(system.detect_every() == 1); + } +} + +TEST_CASE("CollisionSystem contact_cache", "[collision]") { + std::nullptr_t null_model = nullptr; + TestCollisionSystem system(null_model); + + SECTION("contact_cache() returns non-const reference") { + auto& cache = system.contact_cache(); + REQUIRE(cache.empty()); + + // Can modify cache + cache.push_back({0, 1}); + cache.push_back({2, 3}); + + REQUIRE(cache.size() == 2); + REQUIRE(cache[0] == std::make_pair(0, 1)); + REQUIRE(cache[1] == std::make_pair(2, 3)); + } + + SECTION("contact_cache() const returns const reference") { + const auto& system_const = system; + const auto& cache = system_const.contact_cache(); + + // Should be empty initially + REQUIRE(cache.empty()); + + // Modify via non-const reference + system.contact_cache().push_back({4, 5}); + + // Const reference should see the change + REQUIRE(cache.size() == 1); + REQUIRE(cache[0] == std::make_pair(4, 5)); + } + + SECTION("contact_cache persists across calls") { + auto& cache1 = system.contact_cache(); + cache1.push_back({10, 20}); + + auto& cache2 = system.contact_cache(); + REQUIRE(cache2.size() == 1); + REQUIRE(cache2[0] == std::make_pair(10, 20)); + } +} + +TEST_CASE("CollisionSystem detect_every", "[collision]") { + std::nullptr_t null_model = nullptr; + TestCollisionSystem system(null_model); + + SECTION("get detect_every returns current value") { + REQUIRE(system.detect_every() == 1); + } + + SECTION("set_detect_every updates the value") { + system.set_detect_every(3); + REQUIRE(system.detect_every() == 3); + + system.set_detect_every(10); + REQUIRE(system.detect_every() == 10); + } + + SECTION("set_detect_every can set to 0") { + system.set_detect_every(0); + REQUIRE(system.detect_every() == 0); + } + + SECTION("set_detect_every can set to large values") { + system.set_detect_every(1000); + REQUIRE(system.detect_every() == 1000); + } +} + +TEST_CASE("CollisionSystem with null policies", "[collision]") { + SECTION("System can be instantiated with all null policies") { + std::nullptr_t null_model = nullptr; + TestCollisionSystem system(null_model); + + // Should construct successfully + REQUIRE(true); + } + + SECTION("System methods work with null policies") { + std::nullptr_t null_model = nullptr; + TestCollisionSystem system(null_model); + + // All public methods should work + auto& cache = system.contact_cache(); + REQUIRE(cache.empty()); + + REQUIRE(system.detect_every() == 1); + system.set_detect_every(2); + REQUIRE(system.detect_every() == 2); + } +} diff --git a/backend/tests/cpp/mock/mock_block_system.h b/backend/tests/cpp/mock/mock_block_system.h new file mode 100644 index 000000000..2aa9a97e3 --- /dev/null +++ b/backend/tests/cpp/mock/mock_block_system.h @@ -0,0 +1,51 @@ +#pragma once + +#include "system.h" +#include "block.h" + +namespace elasticapp::mock { + +// Mock system for testing BlockView with arbitrary variables +// This demonstrates that BlockView can work with any System type + +// Simple test variables +struct MockVar1 : + ::elasticapp::Placement::OnNode, + ::elasticapp::DataType::Vector { + static constexpr std::string_view name = "mock_var1"; + }; +struct MockVar2 : + ::elasticapp::Placement::OnNode, + ::elasticapp::DataType::Scalar { + static constexpr std::string_view name = "mock_var2"; + }; +struct MockVar3 : + ::elasticapp::Placement::OnElement, + ::elasticapp::DataType::Vector { + static constexpr std::string_view name = "mock_var3"; + }; +struct MockVar4 : + ::elasticapp::Placement::OnElement, + ::elasticapp::DataType::Matrix { + static constexpr std::string_view name = "mock_var4"; + }; +struct MockVar5 : + ::elasticapp::Placement::OnVoronoi, + ::elasticapp::DataType::Scalar { + static constexpr std::string_view name = "mock_var5"; + }; + +// MockSystem with a small set of variables for testing +using MockSystem = ::elasticapp::System< + MockVar1, // Node, Vector (3) + MockVar2, // Node, Scalar (1) + MockVar3, // Element, Vector (3) + MockVar4, // Element, Matrix (9) + MockVar5 // Voronoi, Scalar (1) +>; +// Total depth: 3 + 1 + 3 + 9 + 1 = 17 + +using MockBlockSystem = ::elasticapp::Block; +using MockBlockSystemView = ::elasticapp::BlockView; + +} // namespace elasticapp::mock diff --git a/backend/tests/cpp/mock/mock_block_system_with_operation.h b/backend/tests/cpp/mock/mock_block_system_with_operation.h new file mode 100644 index 000000000..2db37f44f --- /dev/null +++ b/backend/tests/cpp/mock/mock_block_system_with_operation.h @@ -0,0 +1,62 @@ +#pragma once + +#include "system.h" +#include "block.h" +#include "operations.h" +#include "mock_block_system.h" // For MockSystem and variable definitions + +namespace elasticapp::mock { + +// Mock operations class that implements addition of two variables +// Adds MockVar1 (Vector) and MockVar2 (Scalar, broadcasted) and stores in MockVar1 +template +class AdditionOperations { +public: + // Operation: result = var1 + var2 (where var2 is broadcasted if scalar) + // This adds MockVar1 (Vector) and MockVar2 (Scalar broadcasted to Vector) + // and stores the result back in MockVar1 + void add_variables() { + auto& block = static_cast(*this); + + // Get views of the variables + auto var1 = block.template get(); // Vector on Node + auto var2 = block.template get(); // Scalar on Node + + // var1 is a matrix view: rows = 3 (vector dimension), cols = width + // var2 is a matrix view: rows = 1 (scalar dimension), cols = width + + // Add var2 (scalar) to each component of var1 (vector) + // var1 has 3 rows, var2 has 1 row + for (Eigen::Index col = 0; col < var1.cols(); ++col) { + double scalar_value = var2(0, col); + for (Eigen::Index row = 0; row < var1.rows(); ++row) { + var1(row, col) = var1(row, col) + scalar_value; + } + } + } + + // Alternative operation: add two vectors (MockVar1 + MockVar3) + // Note: MockVar1 is on Node, MockVar3 is on Element, so we need to handle different sizes + // For simplicity, we'll add MockVar1 to a scaled version of itself + void add_vector_to_itself() { + auto& block = static_cast(*this); + auto var1 = block.template get(); // Vector on Node + + // Add var1 to itself (multiply by 2) + var1 = var1 + var1; + } + +protected: + AdditionOperations() = default; + ~AdditionOperations() = default; + + AdditionOperations(const AdditionOperations&) = default; + AdditionOperations(AdditionOperations&&) = default; + AdditionOperations& operator=(const AdditionOperations&) = default; + AdditionOperations& operator=(AdditionOperations&&) = default; +}; + +// MockBlockSystem with AdditionOperations +using MockBlockSystemWithOperations = ::elasticapp::Block; + +} // namespace elasticapp::mock diff --git a/backend/tests/cpp/test_block.cpp b/backend/tests/cpp/test_block.cpp new file mode 100644 index 000000000..e81c63ace --- /dev/null +++ b/backend/tests/cpp/test_block.cpp @@ -0,0 +1,581 @@ +#include +#include +#include +#include "block.h" +#include "mock/mock_block_system.h" + +using Catch::Approx; + +using MockBlockSystem = elasticapp::mock::MockBlockSystem; + +// Type aliases for convenience in tests +TEST_CASE("Block construction", "[block]") { + SECTION("Can be constructed with list of element counts") { + std::vector n_elems_per_rod = {6}; // 6 elements -> 7 nodes (width) + MockBlockSystem block(n_elems_per_rod); + auto shape = block.shape(); + REQUIRE(shape.first == 17); // MockSystem depth + REQUIRE(shape.second == 7); // 6+1 nodes + 0 ghost = 7 nodes + } + + SECTION("Can be constructed with list of element counts (auto depth)") { + std::vector n_elems_per_rod = {3, 5, 2}; + MockBlockSystem block(n_elems_per_rod); + + // Width = sum of (n_elems + 1) for each rod + 2 ghost = (4 + 6 + 3) + 2 = 15 + // Depth = MockSystem block_depth = 17 + REQUIRE(block.width() == 15); + REQUIRE(block.depth() == 17); + } + + SECTION("Block stores correct starting indices for each rod") { + std::vector n_elems_per_rod = {3, 5, 2}; + MockBlockSystem block(n_elems_per_rod); + + // Rod 0: starts at 0, has 3+1=4 nodes + 1 ghost = 5 total + REQUIRE(block.system_start_index(0) == 0); + // Rod 1: starts at 5, has 5+1=6 nodes + 1 ghost = 7 total + REQUIRE(block.system_start_index(1) == 5); + // Rod 2: starts at 5+7=12, has 2+1=3 nodes + 1 ghost = 4 total + REQUIRE(block.system_start_index(2) == 12); + } + + SECTION("Block calculates width correctly for single rod") { + std::vector n_elems_per_rod = {7}; + MockBlockSystem block(n_elems_per_rod); + + // Single rod: 8 nodes + 0 ghost = 8 nodes + REQUIRE(block.width() == 8); + REQUIRE(block.system_start_index(0) == 0); + } + + SECTION("Block calculates width correctly for empty list") { + std::vector n_elems_per_rod = {}; + REQUIRE_THROWS_AS(MockBlockSystem(n_elems_per_rod), std::invalid_argument); + } + + SECTION("Block rejects input with less than 6 total elements") { + // Test with 5 total elements (should fail) + std::vector n_elems_per_rod = {5}; + REQUIRE_THROWS_AS(MockBlockSystem(n_elems_per_rod), std::invalid_argument); + + // Test with 6 total elements (should succeed) + n_elems_per_rod = {6}; + REQUIRE_NOTHROW(MockBlockSystem(n_elems_per_rod)); + + // Test with multiple rods totaling less than 6 (should fail) + n_elems_per_rod = {2, 3}; // Total = 5 + REQUIRE_THROWS_AS(MockBlockSystem(n_elems_per_rod), std::invalid_argument); + + // Test with multiple rods totaling 6 (should succeed) + n_elems_per_rod = {3, 3}; // Total = 6 + REQUIRE_NOTHROW(MockBlockSystem(n_elems_per_rod)); + } + + SECTION("Block automatically computes depth from System") { + std::vector n_elems_per_rod = {6}; + MockBlockSystem mock_block(n_elems_per_rod); + REQUIRE(mock_block.depth() == 17); // MockSystem depth + } +} + +TEST_CASE("Block shape", "[block]") { + SECTION("Returns correct shape") { + std::vector n_elems_per_rod = {6}; // 6 elements -> 7 nodes (width) + MockBlockSystem block(n_elems_per_rod); + auto shape = block.shape(); + REQUIRE(shape.first == 17); // MockSystem depth + REQUIRE(shape.second == 7); // 7 nodes + 0 ghost = 7 nodes + } +} + +TEST_CASE("Block data access", "[block]") { + SECTION("Data can be accessed and modified") { + std::vector n_elems_per_rod = {6}; // 6 elements -> 7 nodes (width) + MockBlockSystem block(n_elems_per_rod); + auto& data = block.data(); + + // Modify data + data(0, 0) = 1.5; + data(0, 1) = 2.5; + data(1, 0) = 3.5; + + // Verify modifications + REQUIRE(data(0, 0) == 1.5); + REQUIRE(data(0, 1) == 2.5); + REQUIRE(data(1, 0) == 3.5); + } +} + +TEST_CASE("Block CRTP - access to System methods", "[block]") { + SECTION("Block inherits System methods") { + std::vector n_elems_per_rod = {6}; + MockBlockSystem block(n_elems_per_rod); + + // Block inherits from MockSystem, so it has System methods + REQUIRE(block.depth() == 17); // MockSystem depth + } +} + +TEST_CASE("Block get() method", "[block]") { + std::vector n_elems_per_rod = {3, 5, 2}; + MockBlockSystem block(n_elems_per_rod); + // Block has 3 rods: rod 0 has 3 elems (4 nodes), rod 1 has 5 elems (6 nodes), rod 2 has 2 elems (3 nodes) + // Total width = 4 + 6 + 3 + (2 ghost) = 15 + + SECTION("Block get() returns correct shapes for different variable types") { + using namespace elasticapp::mock; + auto& matrix = block.data(); + // Block width = 4 + 6 + 3 + 2 (ghost) = 15 + + // Test MockVar1 (Node, Vector, offset=0, dimension=3) + auto var1_view = block.get(); + REQUIRE(var1_view.rows() == 3); + REQUIRE(var1_view.cols() == 15); // OnNode: full width + + // Test MockVar2 (Node, Scalar, offset=3, dimension=1) + auto var2_view = block.get(); + REQUIRE(var2_view.rows() == 1); + REQUIRE(var2_view.cols() == 15); // OnNode: full width + + // Test MockVar3 (Element, Vector, offset=4, dimension=3) + auto var3_view = block.get(); + REQUIRE(var3_view.rows() == 3); + REQUIRE(var3_view.cols() == 14); // OnElement: width - 1 + + // Test MockVar4 (Element, Matrix, offset=7, dimension=9) + auto var4_view = block.get(); + REQUIRE(var4_view.rows() == 9); + REQUIRE(var4_view.cols() == 14); // OnElement: width - 1 + + // Test MockVar5 (Voronoi, Scalar, offset=16, dimension=1) + auto var5_view = block.get(); + REQUIRE(var5_view.rows() == 1); + REQUIRE(var5_view.cols() == 13); // OnVoronoi: width - 2 + } + + SECTION("Block get() returns writable views that modify underlying data") { + using namespace elasticapp::mock; + auto& matrix = block.data(); + + // Test MockVar1 (Node, Vector) + auto var1_view = block.get(); + + // Modify through the view + var1_view(0, 0) = 10.0; + var1_view(1, 1) = 20.0; + var1_view(2, 2) = 30.0; + + // Verify modifications are reflected in underlying matrix + REQUIRE(matrix(0, 0) == 10.0); + REQUIRE(matrix(1, 1) == 20.0); + REQUIRE(matrix(2, 2) == 30.0); + + // Modify through matrix and verify view sees changes + matrix(0, 3) = 40.0; + REQUIRE(var1_view(0, 3) == 40.0); + + // Get another view - should see the same data + auto var1_view2 = block.get(); + REQUIRE(var1_view2(0, 0) == 10.0); + REQUIRE(var1_view2(1, 1) == 20.0); + REQUIRE(var1_view2(2, 2) == 30.0); + + // Modify through second view + var1_view2(0, 4) = 50.0; + REQUIRE(var1_view(0, 4) == 50.0); // Should be reflected in first view + REQUIRE(matrix(0, 4) == 50.0); // And in underlying matrix + } + + SECTION("Block get() works for different variable types") { + using namespace elasticapp::mock; + auto& matrix = block.data(); + + // Test MockVar1 (OnNode, Vector) + auto var1_view = block.get(); + var1_view(0, 0) = 100.0; + REQUIRE(matrix(0, 0) == 100.0); + + // Test MockVar2 (OnNode, Scalar) + auto var2_view = block.get(); + var2_view(0, 0) = 200.0; + REQUIRE(matrix(3, 0) == 200.0); // offset = 3 + + // Test MockVar3 (OnElement, Vector) + auto var3_view = block.get(); + var3_view(0, 0) = 300.0; + REQUIRE(matrix(4, 0) == 300.0); // offset = 4 + + // Test MockVar4 (OnElement, Matrix) + auto var4_view = block.get(); + var4_view(0, 0) = 400.0; + REQUIRE(matrix(7, 0) == 400.0); // offset = 7 + + // Test MockVar5 (OnVoronoi, Scalar) + auto var5_view = block.get(); + var5_view(0, 0) = 500.0; + REQUIRE(matrix(16, 0) == 500.0); // offset = 16 + } + + SECTION("Block get() works across multiple rods") { + using namespace elasticapp::mock; + auto& matrix = block.data(); + + // Rod 0: starts at column 0, has 4 nodes + // Rod 1: starts at column 4, has 6 nodes + // Rod 2: starts at column 10, has 3 nodes + + auto var1_view = block.get(); + + // Modify data for different rods + var1_view(0, 0) = 1.0; // Rod 0, first node + var1_view(0, 3) = 2.0; // Rod 0, last node + var1_view(0, 4) = 3.0; // Rod 1, first node + var1_view(0, 9) = 4.0; // Rod 1, last node + var1_view(0, 10) = 5.0; // Rod 2, first node + var1_view(0, 12) = 6.0; // Rod 2, last node + + // Verify all modifications + REQUIRE(matrix(0, 0) == 1.0); + REQUIRE(matrix(0, 3) == 2.0); + REQUIRE(matrix(0, 4) == 3.0); + REQUIRE(matrix(0, 9) == 4.0); + REQUIRE(matrix(0, 10) == 5.0); + REQUIRE(matrix(0, 12) == 6.0); + } + + SECTION("Block get() validates VariableTag is in System") { + using namespace elasticapp::mock; + + // All valid variables from MockSystem should compile and work + STATIC_REQUIRE(elasticapp::tuple_contains_v); + STATIC_REQUIRE(elasticapp::tuple_contains_v); + STATIC_REQUIRE(elasticapp::tuple_contains_v); + STATIC_REQUIRE(elasticapp::tuple_contains_v); + STATIC_REQUIRE(elasticapp::tuple_contains_v); + + // Test that non-MockVar is not in MockSystem::Variables + class NonMockVar : elasticapp::Placement::OnNode, elasticapp::DataType::Vector {}; + STATIC_REQUIRE(!elasticapp::tuple_contains_v); + + // Verify get() works for all valid variables (compile-time check) + auto v1 = block.get(); + auto v2 = block.get(); + auto v3 = block.get(); + auto v4 = block.get(); + auto v5 = block.get(); + + // Use the variables to ensure they compile + (void)v1; + (void)v2; + (void)v3; + (void)v4; + (void)v5; + } + + SECTION("Block get() const version works") { + using namespace elasticapp::mock; + const MockBlockSystem& const_block = block; + auto& matrix = block.data(); + + // Set some values + matrix(0, 0) = 42.0; + matrix(3, 1) = 43.0; + matrix(4, 2) = 44.0; + + // Get const views + auto var1_view = const_block.get(); + auto var2_view = const_block.get(); + auto var3_view = const_block.get(); + + // Verify we can read values + REQUIRE(var1_view(0, 0) == 42.0); + REQUIRE(var2_view(0, 1) == 43.0); + REQUIRE(var3_view(0, 2) == 44.0); + + // Verify shapes (with width adjustment) + REQUIRE(var1_view.rows() == 3); + REQUIRE(var1_view.cols() == 15); // OnNode: full width + REQUIRE(var2_view.rows() == 1); + REQUIRE(var2_view.cols() == 15); // OnNode: full width + REQUIRE(var3_view.rows() == 3); + REQUIRE(var3_view.cols() == 14); // OnElement: width - 1 + } + + SECTION("Block get() width adjustment based on placement type") { + using namespace elasticapp::mock; + // Block width = 4 + 6 + 3 + 2 (ghost) = 15 + + // Test OnNode variables get full width + auto var1_view = block.get(); // OnNode + auto var2_view = block.get(); // OnNode + REQUIRE(var1_view.cols() == 15); + REQUIRE(var2_view.cols() == 15); + REQUIRE(var1_view.cols() == block.width()); + REQUIRE(var2_view.cols() == block.width()); + + // Test OnElement variables get width - 1 + auto var3_view = block.get(); // OnElement + auto var4_view = block.get(); // OnElement + REQUIRE(var3_view.cols() == 14); // 15 - 1 + REQUIRE(var4_view.cols() == 14); // 15 - 1 + REQUIRE(var3_view.cols() == block.width() - 1); + REQUIRE(var4_view.cols() == block.width() - 1); + + // Test OnVoronoi variables get width - 2 + auto var5_view = block.get(); // OnVoronoi + REQUIRE(var5_view.cols() == 13); // 15 - 2 + REQUIRE(var5_view.cols() == block.width() - 2); + } + + SECTION("Block get() width adjustment with single rod") { + using namespace elasticapp::mock; + std::vector single_rod = {8}; // 5 elems -> 6 nodes, width = 6 + MockBlockSystem single_block(single_rod); + + // OnNode: full width + auto var1_view = single_block.get(); + REQUIRE(var1_view.cols() == 9); + + // OnElement: width - 1 + auto var3_view = single_block.get(); + REQUIRE(var3_view.cols() == 8); // 6 - 1 + + // OnVoronoi: width - 2 + auto var5_view = single_block.get(); + REQUIRE(var5_view.cols() == 7); // 6 - 2 + } + + SECTION("Block get() width adjustment edge cases") { + using namespace elasticapp::mock; + // Test with small width (minimum 6 elements required) + std::vector small_rod = {6}; // 6 elem -> 7 nodes, width = 7 + MockBlockSystem small_block(small_rod); + + // OnNode: full width + auto var1_view = small_block.get(); + REQUIRE(var1_view.cols() == 7); + + // OnElement: width - 1 + auto var3_view = small_block.get(); + REQUIRE(var3_view.cols() == 6); // 7 - 1 + + // OnVoronoi: width - 2 + auto var5_view = small_block.get(); + REQUIRE(var5_view.cols() == 5); // 7 - 2 = 5 + } +} + +TEST_CASE("Block ghost indices", "[block]") { + using namespace elasticapp::mock; + + SECTION("Ghost nodes indices with multiple rods") { + std::vector n_elems_per_rod = {3, 5, 2}; + MockBlockSystem block(n_elems_per_rod); + // Rod 0: 3 elems -> 4 nodes (indices 0-3) + // Ghost node at 4 + // Rod 1: 5 elems -> 6 nodes (indices 5-10) + // Ghost node at 11 + // Rod 2: 2 elems -> 3 nodes (indices 12-14) + + auto ghost_nodes = block.ghost_nodes_idx(); + REQUIRE(ghost_nodes.size() == 2); // n_rods - 1 + REQUIRE(ghost_nodes[0] == 4); // After rod 0 + REQUIRE(ghost_nodes[1] == 11); // After rod 1 + } + + SECTION("Ghost elements indices with multiple rods") { + std::vector n_elems_per_rod = {3, 5, 2}; + MockBlockSystem block(n_elems_per_rod); + + auto ghost_elems = block.ghost_elems_idx(); + REQUIRE(ghost_elems.size() == 5); // 2 * (n_rods - 1) + // For ghost node at 4: elements at 3 and 4 + REQUIRE(ghost_elems[0] == 3); + REQUIRE(ghost_elems[1] == 4); + // For ghost node at 11: elements at 10 and 11 + REQUIRE(ghost_elems[2] == 10); + REQUIRE(ghost_elems[3] == 11); + } + + SECTION("Ghost voronoi indices with multiple rods") { + std::vector n_elems_per_rod = {3, 5, 2}; + MockBlockSystem block(n_elems_per_rod); + + auto ghost_voronoi = block.ghost_voronoi_idx(); + REQUIRE(ghost_voronoi.size() == 8); // 3 * (n_rods - 1) + // For ghost node at 4: voronoi at 2, 3, 4 + REQUIRE(ghost_voronoi[0] == 2); + REQUIRE(ghost_voronoi[1] == 3); + REQUIRE(ghost_voronoi[2] == 4); + // For ghost node at 11: voronoi at 9, 10, 11 + REQUIRE(ghost_voronoi[3] == 9); + REQUIRE(ghost_voronoi[4] == 10); + REQUIRE(ghost_voronoi[5] == 11); + REQUIRE(ghost_voronoi[6] == 13); + REQUIRE(ghost_voronoi[7] == 14); + } + + SECTION("Ghost indices with single rod") { + std::vector n_elems_per_rod = {6}; + MockBlockSystem block(n_elems_per_rod); + + auto ghost_nodes = block.ghost_nodes_idx(); + auto ghost_elems = block.ghost_elems_idx(); + auto ghost_voronoi = block.ghost_voronoi_idx(); + + REQUIRE(ghost_nodes.empty()); + // Since we have rectangular block, ghost elements and voronoi should not be empty + REQUIRE(ghost_elems.size() == 1); + REQUIRE(ghost_elems[0] == 6); + REQUIRE(ghost_voronoi.size() == 2); + REQUIRE(ghost_voronoi[0] == 5); + REQUIRE(ghost_voronoi[1] == 6); + } + + SECTION("Ghost indices with two rods") { + std::vector n_elems_per_rod = {4, 3}; + MockBlockSystem block(n_elems_per_rod); + // Rod 0: 2 elems -> 3 nodes (indices 0-2) + // Ghost node at 3 + // Rod 1: 3 elems -> 4 nodes (indices 4-7) + + auto ghost_nodes = block.ghost_nodes_idx(); + REQUIRE(ghost_nodes.size() == 1); + REQUIRE(ghost_nodes[0] == 5); + + auto ghost_elems = block.ghost_elems_idx(); + REQUIRE(ghost_elems.size() == 3); + REQUIRE(ghost_elems[0] == 4); // Element before ghost node + REQUIRE(ghost_elems[1] == 5); // Element at ghost node + REQUIRE(ghost_elems[2] == 9); // Element after ghost node + + auto ghost_voronoi = block.ghost_voronoi_idx(); + REQUIRE(ghost_voronoi.size() == 5); + REQUIRE(ghost_voronoi[0] == 3); // Voronoi 2 before ghost node + REQUIRE(ghost_voronoi[1] == 4); // Voronoi 1 before ghost node + REQUIRE(ghost_voronoi[2] == 5); // Voronoi at ghost node + REQUIRE(ghost_voronoi[3] == 8); // Voronoi after ghost node + REQUIRE(ghost_voronoi[4] == 9); // Voronoi after ghost node + } + + SECTION("Ghost indices with empty block") { + std::vector n_elems_per_rod = {}; + REQUIRE_THROWS_AS(MockBlockSystem(n_elems_per_rod), std::invalid_argument); + } +} + +TEST_CASE("Block reset_ghost operations", "[block]") { + using namespace elasticapp::mock; + + SECTION("reset_ghost_for_variable for OnNode variable") { + std::vector n_elems_per_rod = {3, 5}; + MockBlockSystem block(n_elems_per_rod); + // Ghost nodes at indices 4, 10 + + // Get variable view and modify ghost positions + auto var1_view = block.get(); // OnNode, Vector + var1_view(0, 4) = 999.0; // Modify ghost node + var1_view(1, 4) = 888.0; + var1_view(2, 4) = 777.0; + + // Reset ghost for this variable + block.reset_ghost_for_variable(); + + // Verify ghost values are reset (MockVar1 uses DataType::Vector, ghost_value = Zero(3,1)) + REQUIRE(var1_view(0, 4) == Approx(0.0)); + REQUIRE(var1_view(1, 4) == Approx(0.0)); + REQUIRE(var1_view(2, 4) == Approx(0.0)); + } + + SECTION("reset_ghost_for_variable for OnElement variable") { + std::vector n_elems_per_rod = {3, 5}; + MockBlockSystem block(n_elems_per_rod); + // Ghost elements at indices 3, 4, 10, 11 + + // Get variable view and modify ghost positions + auto var3_view = block.get(); // OnElement, Vector + var3_view(0, 3) = 999.0; // Modify ghost element + var3_view(1, 3) = 888.0; + var3_view(2, 3) = 777.0; + + // Reset ghost for this variable + block.reset_ghost_for_variable(); + + // Verify ghost values are reset (MockVar3 uses DataType::Vector, ghost_value = Zero(3,1)) + REQUIRE(var3_view(0, 3) == Approx(0.0)); + REQUIRE(var3_view(1, 3) == Approx(0.0)); + REQUIRE(var3_view(2, 3) == Approx(0.0)); + } + + SECTION("reset_ghost_for_variable for OnVoronoi variable") { + std::vector n_elems_per_rod = {3, 5}; + MockBlockSystem block(n_elems_per_rod); + // Ghost voronoi at indices 2, 3, 4, 9, 10, 11 + + // Get variable view and modify ghost positions + auto var5_view = block.get(); // OnVoronoi, Scalar + var5_view(0, 2) = 999.0; // Modify ghost voronoi + + // Reset ghost for this variable + block.reset_ghost_for_variable(); + + // Verify ghost values are reset (MockVar5 uses DataType::Scalar, ghost_value = Zero(1,1)) + REQUIRE(var5_view(0, 2) == Approx(0.0)); + } + + SECTION("reset_ghost resets all variables") { + std::vector n_elems_per_rod = {4, 3}; + MockBlockSystem block(n_elems_per_rod); + // Ghost nodes at index 3 + + // Modify ghost positions for multiple variables + auto var1_view = block.get(); // OnNode + auto var2_view = block.get(); // OnNode + auto var3_view = block.get(); // OnElement + + var1_view(0, 5) = 999.0; + var2_view(0, 5) = 888.0; + var3_view(0, 4) = 777.0; // Ghost element at 2 (before ghost node at 3) + + // Reset all ghosts + block.reset_ghost(); + + // Verify all ghost values are reset + REQUIRE(var1_view(0, 5) == Approx(0.0)); + REQUIRE(var2_view(0, 5) == Approx(0.0)); + REQUIRE(var3_view(0, 4) == Approx(0.0)); + } + + SECTION("reset_ghost called in constructor") { + std::vector n_elems_per_rod = {3, 5}; + MockBlockSystem block(n_elems_per_rod); + // Constructor should have called reset_ghost() + + // Check that ghost values are already initialized + auto var1_view = block.get(); + auto ghost_nodes = block.ghost_nodes_idx(); + + if (!ghost_nodes.empty()) { + std::size_t ghost_col = ghost_nodes[0]; + // Ghost values should be zero (default ghost_value for Vector) + REQUIRE(var1_view(0, ghost_col) == Approx(0.0)); + REQUIRE(var1_view(1, ghost_col) == Approx(0.0)); + REQUIRE(var1_view(2, ghost_col) == Approx(0.0)); + } + } + + SECTION("reset_ghost with single rod (no ghosts)") { + std::vector n_elems_per_rod = {6}; + MockBlockSystem block(n_elems_per_rod); + + // Should not crash even with no ghost nodes + REQUIRE_NOTHROW(block.reset_ghost()); + REQUIRE_NOTHROW(block.reset_ghost_for_variable()); + } + + SECTION("reset_ghost with empty block") { + std::vector n_elems_per_rod = {}; + REQUIRE_THROWS_AS(MockBlockSystem(n_elems_per_rod), std::invalid_argument); + } +} diff --git a/backend/tests/cpp/test_block_operations.cpp b/backend/tests/cpp/test_block_operations.cpp new file mode 100644 index 000000000..5583964a0 --- /dev/null +++ b/backend/tests/cpp/test_block_operations.cpp @@ -0,0 +1,123 @@ +#include +#include +#include +#include +#include "block.h" +#include "mock/mock_block_system_with_operation.h" + +using MockBlockSystemWithOps = elasticapp::mock::MockBlockSystemWithOperations; +using namespace elasticapp::mock; +using Catch::Approx; + +TEST_CASE("Block with Operations - Addition", "[block][operations]") { + std::vector n_elems_per_rod = {3, 5}; + MockBlockSystemWithOps block(n_elems_per_rod); + + SECTION("Operations can be called on block") { + // Verify the block has the operations method + REQUIRE_NOTHROW(block.add_variables()); + } + + SECTION("Add variables operation - Vector + Scalar") { + // Get variable views + auto var1 = block.template get(); // Vector (3 rows) + auto var2 = block.template get(); // Scalar (1 row) + + // Initialize test data + // Set var1 to some values + var1(0, 0) = 1.0; // First component, first column + var1(1, 0) = 2.0; // Second component, first column + var1(2, 0) = 3.0; // Third component, first column + + var1(0, 1) = 4.0; + var1(1, 1) = 5.0; + var1(2, 1) = 6.0; + + // Set var2 (scalar) values + var2(0, 0) = 10.0; // First column + var2(0, 1) = 20.0; // Second column + + // Perform addition operation + block.add_variables(); + + // Verify results: var1 should now be var1 + var2 (broadcasted) + REQUIRE(var1(0, 0) == Approx(11.0)); // 1.0 + 10.0 + REQUIRE(var1(1, 0) == Approx(12.0)); // 2.0 + 10.0 + REQUIRE(var1(2, 0) == Approx(13.0)); // 3.0 + 10.0 + + REQUIRE(var1(0, 1) == Approx(24.0)); // 4.0 + 20.0 + REQUIRE(var1(1, 1) == Approx(25.0)); // 5.0 + 20.0 + REQUIRE(var1(2, 1) == Approx(26.0)); // 6.0 + 20.0 + } + + SECTION("Add vector to itself operation") { + // Get variable view + auto var1 = block.template get(); // Vector (3 rows) + + // Initialize test data + var1(0, 0) = 1.0; + var1(1, 0) = 2.0; + var1(2, 0) = 3.0; + + var1(0, 1) = 4.0; + var1(1, 1) = 5.0; + var1(2, 1) = 6.0; + + // Perform addition operation (adds var1 to itself) + block.add_vector_to_itself(); + + // Verify results: var1 should now be 2 * original + REQUIRE(var1(0, 0) == Approx(2.0)); // 1.0 * 2 + REQUIRE(var1(1, 0) == Approx(4.0)); // 2.0 * 2 + REQUIRE(var1(2, 0) == Approx(6.0)); // 3.0 * 2 + + REQUIRE(var1(0, 1) == Approx(8.0)); // 4.0 * 2 + REQUIRE(var1(1, 1) == Approx(10.0)); // 5.0 * 2 + REQUIRE(var1(2, 1) == Approx(12.0)); // 6.0 * 2 + } + + SECTION("Operations work across multiple rods") { + // Block has 2 rods: rod 0 with 3 elems (4 nodes), rod 1 with 5 elems (6 nodes) + // Total width = 4 + 6 = 10 + + auto var1 = block.template get(); + auto var2 = block.template get(); + + // Set values for rod 0 (columns 0-3) + var1(0, 0) = 1.0; + var1(1, 0) = 2.0; + var1(2, 0) = 3.0; + var2(0, 0) = 5.0; + + // Set values for rod 1 (columns 4-9) + var1(0, 4) = 10.0; + var1(1, 4) = 20.0; + var1(2, 4) = 30.0; + var2(0, 4) = 50.0; + + // Perform operation + block.add_variables(); + + // Verify rod 0 results + REQUIRE(var1(0, 0) == Approx(6.0)); // 1.0 + 5.0 + REQUIRE(var1(1, 0) == Approx(7.0)); // 2.0 + 5.0 + REQUIRE(var1(2, 0) == Approx(8.0)); // 3.0 + 5.0 + + // Verify rod 1 results + REQUIRE(var1(0, 4) == Approx(60.0)); // 10.0 + 50.0 + REQUIRE(var1(1, 4) == Approx(70.0)); // 20.0 + 50.0 + REQUIRE(var1(2, 4) == Approx(80.0)); // 30.0 + 50.0 + } + + SECTION("Operations can access block data and methods") { + // Verify that operations can access block's public interface + REQUIRE(block.width() > 0); + REQUIRE(block.depth() == 17); // MockSystem depth + REQUIRE(block.n_systems() == 2); + + // Operations should be able to call block methods + auto var1 = block.template get(); + REQUIRE(var1.rows() == 3); // Vector dimension + REQUIRE(var1.cols() == block.width()); + } +} diff --git a/backend/tests/cpp/test_block_view.cpp b/backend/tests/cpp/test_block_view.cpp new file mode 100644 index 000000000..995051420 --- /dev/null +++ b/backend/tests/cpp/test_block_view.cpp @@ -0,0 +1,146 @@ +#include +#include "../../src/block.h" +#include "mock/mock_block_system.h" +#include +#include + +using MockBlockSystem = elasticapp::mock::MockBlockSystem; +using MockBlockSystemView = typename MockBlockSystem::View; + +TEST_CASE("BlockView - Variable Retrieval", "[block_view]") { + std::vector n_elems_per_rod = {3, 5, 2}; + MockBlockSystem block(n_elems_per_rod); + + SECTION("BlockView has correct shape") { + auto&& view1 = block.at(0); + auto&& view2 = block.at(1); + REQUIRE(view1.shape() == std::pair(17UL, 4UL)); + REQUIRE(view2.shape() == std::pair(17UL, 6UL)); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + } + + SECTION("BlockView data read/write access") { + for(size_t rod_index = 0; rod_index < block.n_systems(); ++rod_index) { + auto& matrix = block.data(); + auto&& view = block.at(rod_index); + auto&& view_matrix = view.data(); + size_t start_index = block.system_start_index(rod_index); + + // Read access test + // start_index is a column index, so we access matrix(row, start_index + col) + matrix(0, start_index) = 1.2; + matrix(1, start_index + 1) = 2.3; + matrix(2, start_index + 2) = 3.4; + REQUIRE(matrix(0, start_index) == view_matrix(0, 0)); + REQUIRE(matrix(1, start_index + 1) == view_matrix(1, 1)); + REQUIRE(matrix(2, start_index + 2) == view_matrix(2, 2)); + + // Write access test + view_matrix(0, 0) = 4.2; + view_matrix(1, 1) = 5.3; + view_matrix(2, 2) = 6.4; + REQUIRE(matrix(0, start_index) == 4.2); + REQUIRE(matrix(1, start_index + 1) == 5.3); + REQUIRE(matrix(2, start_index + 2) == 6.4); + } + } + + SECTION("BlockView get() method for specific variables") { + using namespace elasticapp::mock; + auto&& view = block.at(0); + auto& matrix = block.data(); + + // Test MockVar1 (Node, Vector, offset=0, dimension=3) + auto var1_view = view.get(); + REQUIRE(var1_view.rows() == 3); + REQUIRE(var1_view.cols() == 4); // rod_n_nodes_ = 3 + 1 = 4 + + // Verify it's a view (no copy) - modify through view and check original + var1_view(0, 0) = 10.0; + var1_view(1, 1) = 20.0; + var1_view(2, 2) = 30.0; + size_t start_index = block.system_start_index(0); + REQUIRE(matrix(0, start_index) == 10.0); + REQUIRE(matrix(1, start_index + 1) == 20.0); + REQUIRE(matrix(2, start_index + 2) == 30.0); + + // Test MockVar2 (Node, Scalar, offset=3, dimension=1) + auto var2_view = view.get(); + REQUIRE(var2_view.rows() == 1); + REQUIRE(var2_view.cols() == 4); // rod_n_nodes_ = 4 + var2_view(0, 0) = 100.0; + REQUIRE(matrix(3, start_index) == 100.0); + + // Test MockVar3 (Element, Vector, offset=4, dimension=3) + auto var3_view = view.get(); + REQUIRE(var3_view.rows() == 3); + REQUIRE(var3_view.cols() == 3); // rod_n_elems_ = 3 + var3_view(0, 0) = 200.0; + REQUIRE(matrix(4, start_index) == 200.0); + + // Test MockVar4 (Element, Matrix, offset=7, dimension=9) + auto var4_view = view.get(); + REQUIRE(var4_view.rows() == 9); + REQUIRE(var4_view.cols() == 3); // rod_n_elems_ = 3 + var4_view(0, 0) = 300.0; + REQUIRE(matrix(7, start_index) == 300.0); + + // Test MockVar5 (Voronoi, Scalar, offset=16, dimension=1) + auto var5_view = view.get(); + REQUIRE(var5_view.rows() == 1); + REQUIRE(var5_view.cols() == 2); // rod_n_voronoi_ = 3 - 1 = 2 + var5_view(0, 0) = 400.0; + REQUIRE(matrix(16, start_index) == 400.0); + } + + SECTION("BlockView get() validates VariableTag is in System") { + using namespace elasticapp::mock; + auto&& view = block.at(0); + + + // All valid variables from MockSystem should compile and work + STATIC_REQUIRE(elasticapp::tuple_contains_v); + STATIC_REQUIRE(elasticapp::tuple_contains_v); + STATIC_REQUIRE(elasticapp::tuple_contains_v); + STATIC_REQUIRE(elasticapp::tuple_contains_v); + STATIC_REQUIRE(elasticapp::tuple_contains_v); + + // Test that non-MockVar is not in MockSystem::Variables + class NonMockVar : elasticapp::Placement::OnNode, elasticapp::DataType::Vector {}; + STATIC_REQUIRE(!elasticapp::tuple_contains_v); + + // Verify get() works for all valid variables (compile-time check) + auto v1 = view.get(); + auto v2 = view.get(); + auto v3 = view.get(); + auto v4 = view.get(); + auto v5 = view.get(); + + // Use the variables to ensure they compile + (void)v1; + (void)v2; + (void)v3; + (void)v4; + (void)v5; + } + + SECTION("tuple_contains_v trait works correctly") { + using namespace elasticapp::mock; + using MockVars = typename MockSystem::Variables; + + // Test that tuple_contains_v correctly identifies members + STATIC_REQUIRE(elasticapp::tuple_contains_v); + STATIC_REQUIRE(elasticapp::tuple_contains_v); + STATIC_REQUIRE(elasticapp::tuple_contains_v); + STATIC_REQUIRE(elasticapp::tuple_contains_v); + STATIC_REQUIRE(elasticapp::tuple_contains_v); + + // Test with invalid types (should be false) + struct InvalidVar1 : elasticapp::Placement::OnNode, elasticapp::DataType::Vector {}; + struct InvalidVar2 : elasticapp::Placement::OnElement, elasticapp::DataType::Scalar {}; + + STATIC_REQUIRE(!elasticapp::tuple_contains_v); + STATIC_REQUIRE(!elasticapp::tuple_contains_v); + } +} diff --git a/backend/tests/cpp/test_system.cpp b/backend/tests/cpp/test_system.cpp new file mode 100644 index 000000000..219a5e541 --- /dev/null +++ b/backend/tests/cpp/test_system.cpp @@ -0,0 +1,40 @@ +#include +#include +#include "system.h" +#include "cosserat_rod_system.h" +#include "mock/mock_block_system.h" + +TEST_CASE("Variable tags - compile-time only", "[system]") { + // Variables should be tag types, not instantiable + // We can use them as template parameters + + SECTION("Variable placement types exist") { + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + } +} + +TEST_CASE("Dimension types", "[system]") { + SECTION("Dimension types exist") { + static_assert(std::is_empty_v); + static_assert(std::is_empty_v); + static_assert(std::is_empty_v); + } + + SECTION("Dimension types have correct sizes") { + STATIC_REQUIRE(std::is_integral_v); + STATIC_REQUIRE(std::is_integral_v); + STATIC_REQUIRE(std::is_integral_v); + } +} + +TEST_CASE("System block depth computation", "[system]") { + + using MockSystem = elasticapp::mock::MockSystem; + + SECTION("MockSystem has correct depth") { + REQUIRE(MockSystem::get_depth() == 17); + } + +} diff --git a/backend/tests/cpp/test_system_variable_validation.cpp b/backend/tests/cpp/test_system_variable_validation.cpp new file mode 100644 index 000000000..844df5def --- /dev/null +++ b/backend/tests/cpp/test_system_variable_validation.cpp @@ -0,0 +1,78 @@ +#include +#include "../../src/system.h" +#include +#include + +namespace elasticapp { + +// Valid variable examples +struct ValidVar1 : Placement::OnNode, DataType::Vector { + static constexpr std::string_view name = "valid_var1"; +}; +struct ValidVar2 : Placement::OnElement, DataType::Scalar { + static constexpr std::string_view name = "valid_var2"; +}; +struct ValidVar3 : Placement::OnVoronoi, DataType::Matrix { + static constexpr std::string_view name = "valid_var3"; +}; + +// Invalid variable examples (for testing compile-time checks) +struct InvalidVarNoPlacement : DataType::Vector {}; // Missing Placement +struct InvalidVarNoDataType : Placement::OnNode {}; // Missing DataType +struct InvalidVarNeither {}; // Missing both + +// Dummy classes that use System as a mixin +template +class DummySystem : public System { +public: + // Expose block_depth() method that uses System's static get_depth() + std::size_t block_depth() const { + return System::get_depth(); + } +}; + +} // namespace elasticapp + +TEST_CASE("System - Variable Validation", "[system]") { + SECTION("Valid variables compile successfully") { + // These should compile without errors - using System as a mixin + elasticapp::DummySystem system1; + elasticapp::DummySystem system2; + elasticapp::DummySystem system3; + + REQUIRE(system1.block_depth() == 3); // Vector = 3 + REQUIRE(system2.block_depth() == 4); // Vector(3) + Scalar(1) = 4 + REQUIRE(system3.block_depth() == 13); // Vector(3) + Scalar(1) + Matrix(9) = 13 + } + + SECTION("Variable validation concepts work correctly") { + // Test HasPlacementTag concept + static_assert(elasticapp::HasPlacementTag); + static_assert(elasticapp::HasPlacementTag); + static_assert(elasticapp::HasPlacementTag); + static_assert(!elasticapp::HasPlacementTag); + + // Test HasDataTypeTag concept + static_assert(elasticapp::HasDataTypeTag); + static_assert(elasticapp::HasDataTypeTag); + static_assert(elasticapp::HasDataTypeTag); + static_assert(!elasticapp::HasDataTypeTag); + + // Test ValidVariable concept + static_assert(elasticapp::ValidVariable); + static_assert(elasticapp::ValidVariable); + static_assert(elasticapp::ValidVariable); + static_assert(!elasticapp::ValidVariable); + static_assert(!elasticapp::ValidVariable); + static_assert(!elasticapp::ValidVariable); + } +} + +// Uncomment these to verify compile-time errors are caught: +// TEST_CASE("System - Invalid Variables (should not compile)", "[system]") { +// // These should cause compile-time errors +// // elasticapp::System invalid1(10); +// // elasticapp::System invalid2(10); +// // elasticapp::System invalid3(10); +// // elasticapp::System invalid4(10); +// } diff --git a/backend/tests/cpp/test_traits.cpp b/backend/tests/cpp/test_traits.cpp new file mode 100644 index 000000000..5087a87d0 --- /dev/null +++ b/backend/tests/cpp/test_traits.cpp @@ -0,0 +1,339 @@ +#include +#include "traits.h" +#include "block.h" +#include "mock/mock_block_system.h" + +using MockBlockSystem = elasticapp::mock::MockBlockSystem; + +TEST_CASE("Traits - Matrix storage order", "[traits]") { + SECTION("Matrix type is defined") { + std::vector n_elems_per_rod = {8}; // 3 elements -> 4 nodes (width) + MockBlockSystem block(n_elems_per_rod); + auto& matrix = block.data(); + + // Verify matrix has correct dimensions + REQUIRE(matrix.rows() == 17); // MockSystem depth + REQUIRE(matrix.cols() == 9); // (3+1) nodes + 0 ghost = 4 nodes + + // Verify we can access and modify data + matrix(0, 0) = 1.0; + REQUIRE(matrix(0, 0) == 1.0); + } + + SECTION("Stride computation") { + auto strides = elasticapp::compute_strides(3, 4); + + if constexpr (elasticapp::IsRowMajor) { + // Row-major: row stride = cols * sizeof(double), col stride = sizeof(double) + REQUIRE(strides.first == static_cast(4 * sizeof(double))); + REQUIRE(strides.second == static_cast(sizeof(double))); + } else { + // Column-major: row stride = sizeof(double), col stride = rows * sizeof(double) + REQUIRE(strides.first == static_cast(sizeof(double))); + REQUIRE(strides.second == static_cast(3 * sizeof(double))); + } + } +} + +TEST_CASE("Traits - get_column_slice", "[traits]") { + SECTION("Basic column slice - non-const") { + elasticapp::MatrixType matrix(5, 10); // 5 rows, 10 columns + matrix.setZero(); + + // Fill some columns with test data + for (std::size_t col = 0; col < 10; ++col) { + for (std::size_t row = 0; row < 5; ++row) { + matrix(row, col) = static_cast(col * 10 + row); + } + } + + // Get a slice of columns 2-5 (4 columns) + auto slice = elasticapp::get_column_slice(matrix, 2, 4); + + // Verify dimensions + REQUIRE(slice.rows() == 5); + REQUIRE(slice.cols() == 4); + + // Verify data matches original matrix + for (std::size_t col = 0; col < 4; ++col) { + for (std::size_t row = 0; row < 5; ++row) { + REQUIRE(slice(row, col) == matrix(row, col + 2)); + } + } + } + + SECTION("Column slice is a view, not a copy") { + elasticapp::MatrixType matrix(3, 6); + matrix.setZero(); + + // Get a slice + auto slice = elasticapp::get_column_slice(matrix, 1, 3); + + // Modify through the slice + slice(0, 0) = 42.0; + slice(1, 1) = 99.0; + slice(2, 2) = 123.0; + + // Verify original matrix is modified + REQUIRE(matrix(0, 1) == 42.0); // slice(0,0) -> matrix(0,1) + REQUIRE(matrix(1, 2) == 99.0); // slice(1,1) -> matrix(1,2) + REQUIRE(matrix(2, 3) == 123.0); // slice(2,2) -> matrix(2,3) + } + + SECTION("Basic column slice - const") { + elasticapp::MatrixType matrix(4, 8); + matrix.setZero(); + + // Fill with test data + for (std::size_t col = 0; col < 8; ++col) { + for (std::size_t row = 0; row < 4; ++row) { + matrix(row, col) = static_cast(col * 100 + row); + } + } + + // Get a const slice + const elasticapp::MatrixType& const_matrix = matrix; + auto const_slice = elasticapp::get_column_slice(const_matrix, 3, 2); + + // Verify dimensions + REQUIRE(const_slice.rows() == 4); + REQUIRE(const_slice.cols() == 2); + + // Verify data matches + REQUIRE(const_slice(0, 0) == matrix(0, 3)); + REQUIRE(const_slice(3, 1) == matrix(3, 4)); + } + + SECTION("Single column slice") { + elasticapp::MatrixType matrix(5, 7); + matrix.setZero(); + + // Fill column 3 with test data + for (std::size_t row = 0; row < 5; ++row) { + matrix(row, 3) = static_cast(row * 10); + } + + auto slice = elasticapp::get_column_slice(matrix, 3, 1); + + REQUIRE(slice.rows() == 5); + REQUIRE(slice.cols() == 1); + + for (std::size_t row = 0; row < 5; ++row) { + REQUIRE(slice(row, 0) == static_cast(row * 10)); + } + } + + SECTION("Full matrix slice") { + elasticapp::MatrixType matrix(6, 8); + matrix.setZero(); + + // Fill with test data + for (std::size_t col = 0; col < 8; ++col) { + for (std::size_t row = 0; row < 6; ++row) { + matrix(row, col) = static_cast(row * 8 + col); + } + } + + // Get slice of all columns + auto slice = elasticapp::get_column_slice(matrix, 0, 8); + + REQUIRE(slice.rows() == 6); + REQUIRE(slice.cols() == 8); + + // Verify all data matches + for (std::size_t col = 0; col < 8; ++col) { + for (std::size_t row = 0; row < 6; ++row) { + REQUIRE(slice(row, col) == matrix(row, col)); + } + } + } + + SECTION("Slice at end of matrix") { + elasticapp::MatrixType matrix(4, 10); + matrix.setZero(); + + // Fill last 3 columns + for (std::size_t col = 7; col < 10; ++col) { + for (std::size_t row = 0; row < 4; ++row) { + matrix(row, col) = static_cast(col * 100 + row); + } + } + + auto slice = elasticapp::get_column_slice(matrix, 7, 3); + + REQUIRE(slice.rows() == 4); + REQUIRE(slice.cols() == 3); + + // Verify data + REQUIRE(slice(0, 0) == matrix(0, 7)); + REQUIRE(slice(3, 2) == matrix(3, 9)); + } +} + +TEST_CASE("Traits - get_block_slice", "[traits]") { + SECTION("Basic block slice - non-const") { + elasticapp::MatrixType matrix(8, 12); + matrix.setZero(); + + // Fill matrix with test data + for (std::size_t row = 0; row < 8; ++row) { + for (std::size_t col = 0; col < 12; ++col) { + matrix(row, col) = static_cast(row * 100 + col); + } + } + + // Get a block slice: rows 2-5 (4 rows), columns 3-7 (5 columns) + auto slice = elasticapp::get_block_slice(matrix, 2, 4, 3, 5); + + // Verify dimensions + REQUIRE(slice.rows() == 4); + REQUIRE(slice.cols() == 5); + + // Verify data matches original matrix + for (std::size_t row = 0; row < 4; ++row) { + for (std::size_t col = 0; col < 5; ++col) { + REQUIRE(slice(row, col) == matrix(row + 2, col + 3)); + } + } + } + + SECTION("Block slice is a view, not a copy") { + elasticapp::MatrixType matrix(6, 10); + matrix.setZero(); + + // Get a block slice + auto slice = elasticapp::get_block_slice(matrix, 1, 3, 2, 4); + + // Modify through the slice + slice(0, 0) = 42.0; + slice(1, 1) = 99.0; + slice(2, 2) = 123.0; + + // Verify original matrix is modified + REQUIRE(matrix(1, 2) == 42.0); // slice(0,0) -> matrix(1,2) + REQUIRE(matrix(2, 3) == 99.0); // slice(1,1) -> matrix(2,3) + REQUIRE(matrix(3, 4) == 123.0); // slice(2,2) -> matrix(3,4) + } + + SECTION("Basic block slice - const") { + elasticapp::MatrixType matrix(5, 8); + matrix.setZero(); + + // Fill with test data + for (std::size_t row = 0; row < 5; ++row) { + for (std::size_t col = 0; col < 8; ++col) { + matrix(row, col) = static_cast(row * 50 + col); + } + } + + // Get a const block slice + const elasticapp::MatrixType& const_matrix = matrix; + auto const_slice = elasticapp::get_block_slice(const_matrix, 2, 2, 3, 3); + + // Verify dimensions + REQUIRE(const_slice.rows() == 2); + REQUIRE(const_slice.cols() == 3); + + // Verify data matches + REQUIRE(const_slice(0, 0) == matrix(2, 3)); + REQUIRE(const_slice(1, 2) == matrix(3, 5)); + } + + SECTION("Single element block slice") { + elasticapp::MatrixType matrix(5, 7); + matrix.setZero(); + + // Fill specific element + matrix(3, 4) = 42.0; + + auto slice = elasticapp::get_block_slice(matrix, 3, 1, 4, 1); + + REQUIRE(slice.rows() == 1); + REQUIRE(slice.cols() == 1); + REQUIRE(slice(0, 0) == 42.0); + } + + SECTION("Full matrix block slice") { + elasticapp::MatrixType matrix(6, 8); + matrix.setZero(); + + // Fill with test data + for (std::size_t row = 0; row < 6; ++row) { + for (std::size_t col = 0; col < 8; ++col) { + matrix(row, col) = static_cast(row * 10 + col); + } + } + + // Get slice of entire matrix + auto slice = elasticapp::get_block_slice(matrix, 0, 6, 0, 8); + + REQUIRE(slice.rows() == 6); + REQUIRE(slice.cols() == 8); + + // Verify all data matches + for (std::size_t row = 0; row < 6; ++row) { + for (std::size_t col = 0; col < 8; ++col) { + REQUIRE(slice(row, col) == matrix(row, col)); + } + } + } + + SECTION("Block slice at corner of matrix") { + elasticapp::MatrixType matrix(7, 9); + matrix.setZero(); + + // Fill bottom-right corner + for (std::size_t row = 4; row < 7; ++row) { + for (std::size_t col = 6; col < 9; ++col) { + matrix(row, col) = static_cast(row * 100 + col); + } + } + + auto slice = elasticapp::get_block_slice(matrix, 4, 3, 6, 3); + + REQUIRE(slice.rows() == 3); + REQUIRE(slice.cols() == 3); + + // Verify data + REQUIRE(slice(0, 0) == matrix(4, 6)); + REQUIRE(slice(2, 2) == matrix(6, 8)); + } + + SECTION("Block slice with single row") { + elasticapp::MatrixType matrix(5, 8); + matrix.setZero(); + + // Fill row 2 + for (std::size_t col = 0; col < 8; ++col) { + matrix(2, col) = static_cast(col * 10); + } + + auto slice = elasticapp::get_block_slice(matrix, 2, 1, 0, 8); + + REQUIRE(slice.rows() == 1); + REQUIRE(slice.cols() == 8); + + for (std::size_t col = 0; col < 8; ++col) { + REQUIRE(slice(0, col) == static_cast(col * 10)); + } + } + + SECTION("Block slice with single column") { + elasticapp::MatrixType matrix(6, 7); + matrix.setZero(); + + // Fill column 3 + for (std::size_t row = 0; row < 6; ++row) { + matrix(row, 3) = static_cast(row * 20); + } + + auto slice = elasticapp::get_block_slice(matrix, 0, 6, 3, 1); + + REQUIRE(slice.rows() == 6); + REQUIRE(slice.cols() == 1); + + for (std::size_t row = 0; row < 6; ++row) { + REQUIRE(slice(row, 0) == static_cast(row * 20)); + } + } +} diff --git a/backend/tests/cpp/test_version.cpp b/backend/tests/cpp/test_version.cpp new file mode 100644 index 000000000..8d7ccb567 --- /dev/null +++ b/backend/tests/cpp/test_version.cpp @@ -0,0 +1,16 @@ +#include +#include "version.h" +#include + +// Simple test to verify Catch2 is working +TEST_CASE("Catch2 is working", "[basic]") { + REQUIRE(1 + 1 == 2); + REQUIRE(std::string("hello") == "hello"); +} + +// Test version string from version.cpp +TEST_CASE("Version string from version.cpp", "[version]") { + std::string version = elasticapp::version(); + REQUIRE(version.length() > 0); + REQUIRE(version.find('.') != std::string::npos); +} diff --git a/backend/tests/py/test_block.py b/backend/tests/py/test_block.py new file mode 100644 index 000000000..078647aff --- /dev/null +++ b/backend/tests/py/test_block.py @@ -0,0 +1,197 @@ +import numpy as np +import pytest + +from elasticapp import BlockRodSystem + + +def test_block_construction(): + """Test that Block can be constructed with shape.""" + block = BlockRodSystem([3, 4]) + # Rod 0: 3 elems -> 4 nodes, Rod 1: 4 elems -> 5 nodes + # Ghost nodes: 1 (between 2 rods) + # Total width = 4 + 5 + 1 = 10 + shape = block.shape + assert shape[1] == 10 + + +def test_block_as_ref_returns_numpy_array(): + """Test that as_ref() returns a numpy array.""" + block = BlockRodSystem([3, 3]) + # Rod 0: 3 elems -> 4 nodes, Rod 1: 3 elems -> 4 nodes + # Ghost nodes: 1 (between 2 rods) + # Total width = 4 + 4 + 1 = 9 + arr = block.as_ref() + assert isinstance(arr, np.ndarray) + assert arr.shape[1] == 9 + assert arr.flags["OWNDATA"] is False + assert arr.flags["WRITEABLE"] is True + + +def test_block_as_ref_modifies_underlying_memory(): + """Test that modifying the numpy array modifies the underlying C++ memory.""" + block = BlockRodSystem([3, 3]) + arr1 = block.as_ref() + + # Try to modify + arr1[0, 0] = 1.5 + assert arr1[0, 0] == 1.5 + arr1[0, 0] = 2.5 + assert arr1[0, 0] == 2.5 + + +def test_block_as_ref_bidirectional_modification(): + """Test that modifications persist across multiple as_ref() calls.""" + block = BlockRodSystem([3, 3]) + arr1 = block.as_ref() + + # Fill with some values + vals = np.empty(block.shape) + arr1[:] = vals + arr2 = block.as_ref() + np.testing.assert_array_equal(arr2, vals) + + # Modify through second reference + arr2[0, 0] = 100.0 + assert arr1[0, 0] == 100.0 + + +def test_block_rod_start_indices(): + """Test that Block stores correct starting indices for each rod.""" + block = BlockRodSystem([3, 5, 2]) + + # Rod 0: starts at 0, has 3+1=4 nodes + 1 ghost = 5 total + assert block.system_start_index(0) == 0 + # Rod 1: starts at 5, has 5+1=6 nodes + 1 ghost = 7 total + assert block.system_start_index(1) == 5 + # Rod 2: starts at 5+7=12, has 2+1=3 nodes + assert block.system_start_index(2) == 12 + + +def test_block_view_variable_query(): + """Test that BlockRodSystemView can query variables.""" + block = BlockRodSystem([3, 5, 2]) + view = block.at(0) + assert view.get("position") is not None + assert view.get("position").shape == (3, 4) + assert view.get("velocity") is not None + assert view.get("velocity").shape == (3, 4) + assert view.get("director") is not None + + print("") + print(type(view)) + print(type(view.get("director"))) + print(view.get("director")) + print(view.get("director").flags) + print(view.get("director").shape) + assert view.get("director").shape == (3, 3, 3) + + +def test_block_get_shape(): + """Test that Block.get() returns correct shapes for different variable types.""" + block = BlockRodSystem([3, 4]) + # Block has 2 rods: rod 0 has 3 elems (4 nodes), rod 1 has 4 elems (5 nodes) + # Ghost nodes: 1 (between 2 rods) + # Total width = 4 + 5 + 1 = 10 + + # OnNode variables: should have shape (dimension, full width) + position = block.get("position") + assert position.shape == (3, 10) # Vector (3D) x total nodes (full width) + assert position.flags["OWNDATA"] is False + assert position.flags["WRITEABLE"] is True + + velocity = block.get("velocity") + assert velocity.shape == (3, 10) # OnNode: full width + + mass = block.get("mass") + assert mass.shape == (10,) # OnNode: full width + + # OnElement variables: should have shape (dimension, width - 1) + director = block.get("director") + assert director.shape == (3, 3, 9) # Matrix (9D) x (width - 1) = 10 - 1 = 9 + assert director.flags["OWNDATA"] is False + assert director.flags["WRITEABLE"] is True + + omega = block.get("omega") + assert omega.shape == (3, 9) # Vector (3D) x (width - 1) = 10 - 1 = 9 + + # OnVoronoi variables: should have shape (dimension, width - 2) + kappa = block.get("kappa") + assert kappa.shape == (3, 8) # Vector (3D) x (width - 2) = 10 - 2 = 8 + + +def test_block_get_contents(): + """Test that Block.get() returns writable views that modify underlying data.""" + block = BlockRodSystem([3, 3]) + # Rod 0: 3 elems -> 4 nodes, Rod 1: 3 elems -> 4 nodes + # Ghost nodes: 1 (between 2 rods) + # Total width = 4 + 4 + 1 = 9 + + # Get position variable (OnNode: full width) + position = block.get("position") + assert position.shape == (3, 9) + + # Modify through the view + position[0, 0] = 1.5 + position[1, 0] = 2.5 + position[2, 0] = 3.5 + + # Verify modifications persist + assert position[0, 0] == 1.5 + assert position[1, 0] == 2.5 + assert position[2, 0] == 3.5 + + # Get another view - should see the same data + position2 = block.get("position") + assert position2[0, 0] == 1.5 + assert position2[1, 0] == 2.5 + assert position2[2, 0] == 3.5 + + # Modify through second view + position2[0, 1] = 10.0 + assert position[0, 1] == 10.0 # Should be reflected in first view + + +def test_block_get_different_variable_types(): + """Test that Block.get() works for different variable types (OnNode, OnElement, OnVoronoi).""" + block = BlockRodSystem([3, 3]) + # Rod 0: 3 elems -> 4 nodes, Rod 1: 3 elems -> 4 nodes + # Ghost nodes: 1 (between 2 rods) + # Total width = 4 + 4 + 1 = 9 + + # OnNode variable (Vector) - full width + velocity = block.get("velocity") + assert velocity.shape == (3, 9) # OnNode: full width + velocity[0, 0] = 100.0 + assert velocity[0, 0] == 100.0 + + # OnNode variable (Scalar) - full width + mass = block.get("mass") + assert mass.shape == (9,) # OnNode: full width + mass[0] = 5.0 + assert mass[0] == 5.0 + + # OnElement variable (Matrix) - width - 1 + director = block.get("director") + assert director.shape == (3, 3, 8) # OnElement: width - 1 = 9 - 1 = 8 + director[0, 0, 0] = 0.5 + assert director[0, 0, 0] == 0.5 + + # OnElement variable (Vector) + omega = block.get("omega") + assert omega.shape == (3, 8) + omega[0, 0] = 1.0 + assert omega[0, 0] == 1.0 + + # OnVoronoi variable (Vector) - width - 2 + kappa = block.get("kappa") + assert kappa.shape == (3, 7) # OnVoronoi: width - 2 = 9 - 2 = 7 + kappa[0, 0] = 2.0 + assert kappa[0, 0] == 2.0 + + +def test_block_get_invalid_variable(): + """Test that Block.get() raises error for invalid variable names.""" + block = BlockRodSystem([3, 4]) + + with pytest.raises(RuntimeError, match="Unknown variable name"): + block.get("invalid_variable") diff --git a/backend/tests/py/test_block_ghost_indices.py b/backend/tests/py/test_block_ghost_indices.py new file mode 100644 index 000000000..2a58a1d6f --- /dev/null +++ b/backend/tests/py/test_block_ghost_indices.py @@ -0,0 +1,73 @@ +import numpy as np +import pytest + +from elasticapp import BlockRodSystem + + +def test_ghost_nodes_idx(): + """Test ghost nodes indices computation.""" + block = BlockRodSystem([3, 5, 2]) + # Rod 0: 3 elems -> 4 nodes (indices 0-3) + # Ghost node at 4 + # Rod 1: 5 elems -> 6 nodes (indices 5-10) + # Ghost node at 11 + # Rod 2: 2 elems -> 3 nodes (indices 12-14) + + ghost_nodes = block.ghost_nodes_idx + assert ghost_nodes == [4, 11] + + +def test_ghost_elems_idx(): + """Test ghost elements indices computation.""" + block = BlockRodSystem([3, 5, 2]) + + ghost_elems = block.ghost_elems_idx + # Rod 0: 3 elems -> elements 0-2, ghost elements at 3, 4 + # Rod 1: 5 elems -> elements 5-9, ghost elements at 10, 11 + # Rod 2: 2 elems -> elements 12-13, ghost element at 14 (last element) + assert ghost_elems == [3, 4, 10, 11, 14] + + +def test_ghost_voronoi_idx(): + """Test ghost voronoi indices computation.""" + block = BlockRodSystem([3, 5, 2]) + + ghost_voronoi = block.ghost_voronoi_idx + # Rod 0: 3 elems -> voronoi 0-1, ghost voronoi at 2, 3, 4 + # Rod 1: 5 elems -> voronoi 5-8, ghost voronoi at 9, 10, 11 + # Rod 2: 2 elems -> voronoi 12, ghost voronoi at 13, 14 (includes last voronoi) + assert ghost_voronoi == [2, 3, 4, 9, 10, 11, 13, 14] + + +def test_ghost_indices_single_rod(): + """Test ghost indices with single rod (includes last element/voronoi).""" + block = BlockRodSystem([6]) + + ghost_nodes = block.ghost_nodes_idx + ghost_elems = block.ghost_elems_idx + ghost_voronoi = block.ghost_voronoi_idx + + assert ghost_nodes == [] + assert ghost_elems == [6] # Last element is included as ghost + assert ghost_voronoi == [5, 6] # Last two voronoi are included as ghosts + + +def test_ghost_indices_two_rods(): + """Test ghost indices with two rods.""" + block = BlockRodSystem([3, 3]) + # Rod 0: 3 elems -> 4 nodes (indices 0-3) + # Ghost node at 4 + # Rod 1: 3 elems -> 4 nodes (indices 5-8) + + ghost_nodes = block.ghost_nodes_idx + assert ghost_nodes == [4] + ghost_elems = block.ghost_elems_idx + assert ghost_elems == [3, 4, 8] # Includes last element of rod 1 + ghost_voronoi = block.ghost_voronoi_idx + assert ghost_voronoi == [2, 3, 4, 7, 8] # Includes last voronoi of rod 1 + + +def test_ghost_indices_empty_block(): + """Test ghost indices with empty block (should raise error).""" + with pytest.raises(ValueError, match="n_elems_per_rod cannot be empty"): + block = BlockRodSystem([]) diff --git a/backend/tests/py/test_block_reset_ghost.py b/backend/tests/py/test_block_reset_ghost.py new file mode 100644 index 000000000..d4b1a34fa --- /dev/null +++ b/backend/tests/py/test_block_reset_ghost.py @@ -0,0 +1,151 @@ +import numpy as np +import pytest + +from elasticapp import BlockRodSystem + + +def test_reset_ghost_for_variable_onnode(): + """Test reset_ghost_for_variable for OnNode variable.""" + block = BlockRodSystem([3, 5]) + # Ghost nodes at indices 4, 11 + + # Get position variable and modify ghost positions + position = block.get("position") + position[:] = 0.0 + position[0, 4] = 999.0 # Modify ghost node + position[1, 4] = 888.0 + position[2, 4] = 777.0 + + # Reset ghost for position variable + block.reset_ghost_for_variable("position") + + # Verify ghost values are reset (position uses Vector, ghost_value = Zero(3,1)) + assert position[0, 4] == 0.0 + assert position[1, 4] == 0.0 + assert position[2, 4] == 0.0 + + +def test_reset_ghost_for_variable_onelement(): + """Test reset_ghost_for_variable for OnElement variable.""" + block = BlockRodSystem([3, 5]) + # Ghost elements at indices 3, 4, 10, 11 + + # Get director variable and modify ghost positions + director = block.get("director") + director[0, 0, 3] = 999.0 # Modify ghost element + director[1, 0, 3] = 888.0 + director[2, 0, 3] = 777.0 + + # Reset ghost for director variable + block.reset_ghost_for_variable("director") + + # Verify ghost values are reset (director uses Matrix, ghost_value = Zero(3,3)) + print(director.flags) + assert director[0, 0, 3] == 0.0 + assert director[1, 0, 3] == 0.0 + assert director[2, 0, 3] == 0.0 + + +def test_reset_ghost_for_variable_onvoronoi(): + """Test reset_ghost_for_variable for OnVoronoi variable.""" + block = BlockRodSystem([3, 5]) + # Ghost voronoi at indices 2, 3, 4, 9, 10, 11 + + # Get kappa variable and modify ghost positions + kappa = block.get("kappa") + kappa[:, 2] = 999.0 # Modify ghost voronoi + + # Reset ghost for kappa variable + block.reset_ghost_for_variable("kappa") + + # Verify ghost values are reset (kappa uses Vector, ghost_value = Zero(3,1)) + assert kappa[0, 2] == 0.0 + assert kappa[1, 2] == 0.0 + assert kappa[2, 2] == 0.0 + + +def test_reset_ghost_resets_all_variables(): + """Test that reset_ghost resets all variables.""" + block = BlockRodSystem([3, 3]) + # Ghost nodes at index 4 + + # Modify ghost positions for multiple variables + lengths = block.get("lengths") + position = block.get("position") + velocity = block.get("velocity") + director = block.get("director") + + lengths[4] = 999.0 + position[0, 4] = 999.0 + velocity[0, 4] = 888.0 + director[0, 0, 3] = 777.0 # Ghost element at 3 (before ghost node at 4) + + # Reset all ghosts + block.reset_ghost() + + # Verify all ghost values are reset + assert lengths[4] == 0.0 + assert position[0, 4] == 0.0 + assert velocity[0, 4] == 0.0 + assert director[0, 0, 3] == 0.0 + + +def test_reset_ghost_called_in_constructor(): + """Test that reset_ghost is called in constructor.""" + block = BlockRodSystem([3, 5]) + # Constructor should have called reset_ghost() + + # Check that ghost values are already initialized + position = block.get("position") + ghost_nodes = block.ghost_nodes_idx + + if len(ghost_nodes) > 0: + ghost_col = ghost_nodes[0] + # Ghost values should be zero (default ghost_value for Vector) + assert position[0, ghost_col] == 0.0 + assert position[1, ghost_col] == 0.0 + assert position[2, ghost_col] == 0.0 + + +def test_reset_ghost_with_single_rod(): + """Test reset_ghost with single rod (no ghosts).""" + block = BlockRodSystem([6]) + + # Should not crash even with no ghost nodes + block.reset_ghost() + block.reset_ghost_for_variable("position") + + +def test_reset_ghost_with_empty_block(): + """Test reset_ghost with empty block.""" + with pytest.raises(ValueError, match="n_elems_per_rod cannot be empty"): + block = BlockRodSystem([]) + + +def test_reset_ghost_for_variable_invalid_name(): + """Test reset_ghost_for_variable with invalid variable name.""" + block = BlockRodSystem([3, 5]) + + with pytest.raises(RuntimeError, match="Unknown variable name"): + block.reset_ghost_for_variable("invalid_variable") + + +def test_reset_ghost_multiple_ghost_positions(): + """Test reset_ghost with multiple rods (multiple ghost positions).""" + block = BlockRodSystem([3, 3, 3]) + # Ghost nodes at indices 4, 8 + # Rod 0: 3 elems -> 4 nodes (0-3), ghost at 4 + # Rod 1: 3 elems -> 4 nodes (5-8), ghost at 9 + # Rod 2: 3 elems -> 4 nodes (10-13) + + # Modify multiple ghost positions + position = block.get("position") + position[:, 4] = 999.0 # First ghost node + position[:, 9] = 888.0 # Second ghost node + + # Reset all ghosts + block.reset_ghost() + + # Verify all ghost positions are reset + np.testing.assert_array_equal(position[:, 4], 0.0) + np.testing.assert_array_equal(position[:, 9], 0.0) diff --git a/backend/tests/py/test_block_reshape.py b/backend/tests/py/test_block_reshape.py new file mode 100644 index 000000000..a99ba591a --- /dev/null +++ b/backend/tests/py/test_block_reshape.py @@ -0,0 +1,297 @@ +""" +Tests for Block variable reshaping behavior. + +This module tests that: +- Scalar variables return 1D arrays +- Vector variables return 2D arrays +- Matrix variables return 3D arrays that are views (not copies) +""" + +import numpy as np +import pytest + +from elasticapp import BlockRodSystem + + +class TestScalarVariableReshaping: + """Test that Scalar variables return 1D arrays.""" + + def test_scalar_variables_are_1d(self): + """Test that scalar variables return 1D arrays.""" + block = BlockRodSystem([6, 6]) + # Scalar variables: mass, density, volume, etc. + mass = block.get("mass") + density = block.get("density") + volume = block.get("volume") + + # Should be 1D arrays + assert mass.ndim == 1 + assert density.ndim == 1 + assert volume.ndim == 1 + + # Check shapes (accounting for ghost nodes) + # 2 rods with 6 elements each = 7 nodes each = 14 nodes + 1 ghost = 15 total + assert mass.shape == (15,) + # OnElement variables: width - 1 = 15 - 1 = 14 + assert density.shape == (14,) + assert volume.shape == (14,) + + def test_scalar_variables_are_writable(self): + """Test that scalar variables are writable.""" + block = BlockRodSystem([6]) + mass = block.get("mass") + + assert mass.flags.writeable is True + assert mass.flags.owndata is False # Should be a view + + # Modify and verify + original_value = mass[0] + mass[0] = 999.0 + assert mass[0] == 999.0 + + # Verify persistence + mass_new = block.get("mass") + assert mass_new[0] == 999.0 + + +class TestVectorVariableReshaping: + """Test that Vector variables return 2D arrays.""" + + def test_vector_variables_are_2d(self): + """Test that vector variables return 2D arrays.""" + block = BlockRodSystem([6, 6]) + # Vector variables: position, velocity, acceleration, etc. + position = block.get("position") + velocity = block.get("velocity") + acceleration = block.get("acceleration") + + # Should be 2D arrays + assert position.ndim == 2 + assert velocity.ndim == 2 + assert acceleration.ndim == 2 + + # Check shapes: (3, n_cols) + # 2 rods with 6 elements each = 7 nodes each = 14 nodes + 1 ghost = 15 total + assert position.shape == (3, 15) + assert velocity.shape == (3, 15) + assert acceleration.shape == (3, 15) + + def test_vector_variables_are_writable(self): + """Test that vector variables are writable.""" + block = BlockRodSystem([6]) + position = block.get("position") + + assert position.flags.writeable is True + assert position.flags.owndata is False # Should be a view + + # Modify and verify + original_value = position[0, 0].copy() + position[0, 0] = 999.0 + assert position[0, 0] == 999.0 + + # Verify persistence + position_new = block.get("position") + assert position_new[0, 0] == 999.0 + + +class TestMatrixVariableReshaping: + """Test that Matrix variables return 3D arrays that are views.""" + + def test_matrix_variables_are_3d(self): + """Test that matrix variables return 3D arrays.""" + block = BlockRodSystem([6, 6]) + # Matrix variables: director, shear_matrix, bend_matrix, etc. + director = block.get("director") + shear_matrix = block.get("shear_matrix") + bend_matrix = block.get("bend_matrix") + mass_second_moment = block.get("mass_second_moment_of_inertia") + + # Should be 3D arrays + assert director.ndim == 3 + assert shear_matrix.ndim == 3 + assert bend_matrix.ndim == 3 + assert mass_second_moment.ndim == 3 + + # Check shapes: (3, 3, n_cols) + # OnElement variables: width - 1 = 15 - 1 = 14 + assert director.shape == (3, 3, 14) + assert shear_matrix.shape == (3, 3, 14) + assert bend_matrix.shape == (3, 3, 13) # OnVoronoi: width - 2 = 15 - 2 = 13 + assert mass_second_moment.shape == (3, 3, 14) + + def test_matrix_variables_are_views_not_copies(self): + """Test that matrix variables are views, not copies.""" + block = BlockRodSystem([6]) + director = block.get("director") + + # Should be a view, not a copy + assert ( + director.flags.owndata is False + ), "Matrix variables should be views, not copies" + assert director.flags.writeable is True + assert director.base is not None, "View should have a base array" + + def test_matrix_variables_modifications_persist(self): + """Test that modifications to matrix variables persist.""" + block = BlockRodSystem([6]) + director = block.get("director") + + # Modify a value + original_value = director[0, 0, 0].copy() + director[0, 0, 0] = 123.456 + + # Verify the modification + assert director[0, 0, 0] == 123.456 + + # Get a new view and verify it sees the same modification + director_new = block.get("director") + assert ( + director_new[0, 0, 0] == 123.456 + ), "Modifications should persist across views" + + def test_matrix_variables_reference_same_memory(self): + """Test that matrix variables reference the same underlying memory.""" + block = BlockRodSystem([6]) + director1 = block.get("director") + director2 = block.get("director") + + # Modify through first view + director1[0, 0, 0] = 789.0 + + # Check through second view + assert ( + director2[0, 0, 0] == 789.0 + ), "Both views should reference the same memory" + + def test_matrix_variables_correct_strides(self): + """Test that matrix variables have correct strides for non-contiguous view.""" + block = BlockRodSystem([6]) + director = block.get("director") + + # Strides should be non-contiguous (not C or F contiguous) + # For a (3, 3, 6) view of a (9, 6) column-major matrix: + # The underlying matrix is (9, 6) column-major + # - strides[0] (page stride): 3 * inner_stride = 3 * 8 = 24 bytes (distance between pages in 3x3) + # - strides[1] (row stride): inner_stride = 8 bytes (distance between rows) + # - strides[2] (col stride): outer_stride = 9 * 8 = 72 bytes (distance between columns in original) + assert len(director.strides) == 3 + + # Strides should be in bytes + # For column-major (9, 6) matrix viewed as (3, 3, 6): + # The actual stride calculation depends on the underlying matrix layout + # We just verify the strides are positive and the array is non-contiguous + assert director.strides[0] > 0, "Page stride should be positive" + assert director.strides[1] > 0, "Row stride should be positive" + assert director.strides[2] > 0, "Col stride should be positive" + # Verify it's not C or F contiguous (non-contiguous view) + assert not director.flags["C_CONTIGUOUS"], "Should not be C contiguous" + assert not director.flags["F_CONTIGUOUS"], "Should not be F contiguous" + + # Page stride should be outer_stride (depends on underlying matrix) + assert director.strides[0] > 0, "Page stride should be positive" + + +class TestBlockRodSystemViewReshaping: + """Test reshaping behavior for BlockRodSystemView.""" + + def test_blockview_scalar_is_1d(self): + """Test that BlockRodSystemView scalar variables are 1D.""" + block = BlockRodSystem([6]) + view = block.at(0) + + mass = view.get("mass") + assert mass.ndim == 1 + assert mass.shape == (7,) # 6 elements + 1 = 7 nodes + + def test_blockview_vector_is_2d(self): + """Test that BlockRodSystemView vector variables are 2D.""" + block = BlockRodSystem([6]) + view = block.at(0) + + position = view.get("position") + assert position.ndim == 2 + assert position.shape == (3, 7) # 3 rows, 7 nodes + + def test_blockview_matrix_is_3d(self): + """Test that BlockRodSystemView matrix variables are 3D views.""" + block = BlockRodSystem([6]) + view = block.at(0) + + director = view.get("director") + assert director.ndim == 3 + assert director.shape == (3, 3, 6) # 3x3 matrices, 6 elements + assert director.flags.owndata is False, "Should be a view, not a copy" + + def test_blockview_matrix_modifications_persist(self): + """Test that BlockRodSystemView matrix modifications persist.""" + block = BlockRodSystem([6]) + view = block.at(0) + + director = view.get("director") + director[0, 0, 0] = 456.789 + + # Get new view + director_new = view.get("director") + assert director_new[0, 0, 0] == 456.789 + + +class TestReshapingEdgeCases: + """Test edge cases for reshaping.""" + + def test_single_rod_block(self): + """Test reshaping with a single rod.""" + block = BlockRodSystem([6]) + + # Scalar + mass = block.get("mass") + assert mass.ndim == 1 + assert mass.shape == (7,) # 6 elements + 1 = 7 nodes + + # Vector + position = block.get("position") + assert position.ndim == 2 + assert position.shape == (3, 7) + + # Matrix + director = block.get("director") + assert director.ndim == 3 + assert director.shape == (3, 3, 6) # 6 elements + assert director.flags.owndata is False + + def test_multiple_rods_different_sizes(self): + """Test reshaping with multiple rods of different sizes.""" + block = BlockRodSystem([3, 5, 2]) + + # Scalar (OnNode) + mass = block.get("mass") + assert mass.ndim == 1 + # 3 elems -> 4 nodes, 5 elems -> 6 nodes, 2 elems -> 3 nodes + # Ghost nodes: 2 (between 3 rods) + # Total: 4 + 6 + 3 + 2 = 15 + assert mass.shape == (15,) + + # Vector (OnNode) + position = block.get("position") + assert position.ndim == 2 + assert position.shape == (3, 15) + + # Matrix (OnElement) + director = block.get("director") + assert director.ndim == 3 + # OnElement: width - 1 = 15 - 1 = 14 + assert director.shape == (3, 3, 14) + assert director.flags.owndata is False + + def test_voronoi_variables(self): + """Test reshaping for OnVoronoi variables.""" + block = BlockRodSystem([6, 6]) + + # OnVoronoi variables should have width - 2 + kappa = block.get("kappa") + assert kappa.ndim == 2 # Vector, not Matrix + # OnVoronoi: width - 2 = 15 - 2 = 13 + assert kappa.shape == (3, 13) + + # OnVoronoi Matrix variable + # Note: There might not be OnVoronoi Matrix variables in the system + # This test verifies the width adjustment works correctly diff --git a/backend/tests/py/test_block_view_rod.py b/backend/tests/py/test_block_view_rod.py new file mode 100644 index 000000000..7344583a3 --- /dev/null +++ b/backend/tests/py/test_block_view_rod.py @@ -0,0 +1,23 @@ +import numpy as np +import pytest + +from elasticapp import BlockRodSystem + + +def test_at_returns_block_view(): + """Test that at() returns a BlockRodSystemView object.""" + n_elems_per_system = [3, 5, 2] + block = BlockRodSystem(n_elems_per_system) + expected_depth = 124 + # Rod 0: 3 elems -> 4 nodes, Rod 1: 5 elems -> 6 nodes, Rod 2: 2 elems -> 3 nodes + # Ghost nodes: 2 (between 3 rods) + # Total width = 4 + 6 + 3 + 2 = 15 + expected_shape = (expected_depth, 15) + + # assert block.n_systems == len(n_elems_per_system) + assert block.shape[1] == 15 + + for sys_index in range(block.n_systems): + block_view = block.at(sys_index) + # assert block_view.shape[0] == expected_depth # depth may change with dummy variables + assert block_view.shape[1] == n_elems_per_system[sys_index] + 1 diff --git a/backend/tests/py/test_cosserat_rod_operation.py b/backend/tests/py/test_cosserat_rod_operation.py new file mode 100644 index 000000000..0d80ee1ad --- /dev/null +++ b/backend/tests/py/test_cosserat_rod_operation.py @@ -0,0 +1,58 @@ +import numpy as np +import pytest + +from elasticapp import BlockRodSystem + + +def test_compute_internal_forces_and_torques(): + """Test that compute_internal_forces_and_torques can be called.""" + block = BlockRodSystem([3, 4]) + # Should not raise an exception + block.compute_internal_forces_and_torques(0.0) + + +def test_update_accelerations(): + """Test that update_accelerations can be called.""" + block = BlockRodSystem([3, 4]) + # Should not raise an exception + block.update_accelerations(0.0) + + +def test_zeroed_out_external_forces_and_torques(): + """Test that zeroed_out_external_forces_and_torques can be called.""" + block = BlockRodSystem([3, 4]) + # Should not raise an exception + block.zeroed_out_external_forces_and_torques(0.0) + + +def test_update_kinematics(): + """Test that update_kinematics can be called.""" + block = BlockRodSystem([3, 4]) + # Should not raise an exception + block.update_kinematics(0.0, 1.0) + + +def test_update_dynamics(): + """Test that update_dynamics can be called.""" + block = BlockRodSystem([3, 4]) + # Should not raise an exception + block.update_dynamics(0.0, 1.0) + + +def test_operations_are_callable(): + """Test that all operation methods exist and are callable.""" + block = BlockRodSystem([3, 4]) + + # Verify all methods exist + assert hasattr(block, "compute_internal_forces_and_torques") + assert hasattr(block, "update_accelerations") + assert hasattr(block, "zeroed_out_external_forces_and_torques") + assert hasattr(block, "update_kinematics") + assert hasattr(block, "update_dynamics") + + # Verify they are callable + assert callable(block.compute_internal_forces_and_torques) + assert callable(block.update_accelerations) + assert callable(block.zeroed_out_external_forces_and_torques) + assert callable(block.update_kinematics) + assert callable(block.update_dynamics) diff --git a/backend/tests/py/test_version.py b/backend/tests/py/test_version.py new file mode 100644 index 000000000..cd7e7ba85 --- /dev/null +++ b/backend/tests/py/test_version.py @@ -0,0 +1,12 @@ +"""Test for elasticapp.version function.""" + +import pytest +import elasticapp +import importlib.metadata + + +def test_version(): + """Test that elasticapp.version returns the current version.""" + version = elasticapp.version() + assert isinstance(version, str) + assert version == importlib.metadata.version("elasticapp") diff --git a/backend/tests/test_py_cpp_block_operation_match.py b/backend/tests/test_py_cpp_block_operation_match.py new file mode 100644 index 000000000..55c1ec3d1 --- /dev/null +++ b/backend/tests/test_py_cpp_block_operation_match.py @@ -0,0 +1,109 @@ +""" +This script is an integrated test that ensures the C++ and Python +implementations of Cosserat rod operations produce matching results. +It was converted from the benchmarking script: +`benchmarking/memory_block_integrity_check.py`. + +The test covers the following sequence of operations: +1. Creation of Python and C++ rod systems. +2. Initialization of memory blocks for both implementations. +3. Verification of ghost indices. +4. An initial check of memory block consistency. +5. Execution and cross-verification of: + - `compute_internal_forces_and_torques` + - `update_accelerations` + - `update_kinematics` + - `update_dynamics` +""" + +import numpy as np + +import elastica as epy +import elasticapp as epp + + +def test_py_cpp_block_operation_match(): + # Create test rods + n_rods = 5 + n_elems_per_rod = 20 + + keys = list(epp.memory_block_rod.PY2CPP_VARNAMES.keys()) + rods_cpp = [ + epy.CosseratRod.straight_rod( + n_elements=n_elems_per_rod, + start=np.zeros(3), + direction=np.array([0.0, 0.0, 1.0]), + normal=np.array([1.0, 0.0, 0.0]), + base_length=1.0, + base_radius=0.01, + density=3000, + youngs_modulus=1e6, + ) + for _ in range(n_rods) + ] + rods_py = [ + epy.CosseratRod.straight_rod( + n_elements=n_elems_per_rod, + start=np.zeros(3), + direction=np.array([0.0, 0.0, 1.0]), + normal=np.array([1.0, 0.0, 0.0]), + base_length=1.0, + base_radius=0.01, + density=3000, + youngs_modulus=1e6, + ) + for _ in range(n_rods) + ] + rng = np.random.default_rng(43) + for i in range(n_rods): + for key in ["rest_kappa", "rest_sigma"]: + shape = getattr(rods_py[i], key).shape + values = rng.random(size=shape) + getattr(rods_py[i], key)[...] = values.copy() + getattr(rods_cpp[i], key)[...] = values.copy() + + # Create block rod system + block_cpp = epp.MemoryBlockCosseratRod(rods_cpp, range(n_rods)) + block_py = epy.MemoryBlockCosseratRod(rods_py, range(n_rods)) + + # Cross-check ghost indices + np.testing.assert_array_equal(block_cpp.ghost_nodes_idx, block_py.ghost_nodes_idx) + np.testing.assert_array_equal( + block_cpp.ghost_elems_idx[:-1], block_py.ghost_elems_idx + ) + np.testing.assert_array_equal( + block_cpp.ghost_voronoi_idx[:-2], block_py.ghost_voronoi_idx + ) + + def cross_check_block_memory(): + for key in keys: + cpp_value = getattr(block_cpp, key) + py_value = getattr(block_py, key) + assert cpp_value.shape == py_value.shape + assert np.allclose(cpp_value, py_value), f"{key} is not equal" + + cross_check_block_memory() + + # Cross-check computing internal forces and torques + block_cpp.compute_internal_forces_and_torques(0.0) + block_py.compute_internal_forces_and_torques(0.0) + + cross_check_block_memory() + + # Cross-check updating accelerations + block_cpp.update_accelerations(0.0) + block_py.update_accelerations(0.0) + + cross_check_block_memory() + + # Cross-check updating kinematics + block_cpp.update_kinematics(0.0, 1.4) + block_py.update_kinematics(0.0, 1.4) + + cross_check_block_memory() + + # Cross-check updating dynamics + block_cpp.update_dynamics(0.0, 1.6) + block_py.update_dynamics(0.0, 1.6) + + cross_check_block_memory() diff --git a/docs/Makefile b/docs/Makefile index d4bb2cbb9..5887be21b 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,7 @@ # You can set these variables from the command line, and also # from the environment for the first two. -SPHINXOPTS ?= +SPHINXOPTS ?= -v -T SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build diff --git a/docs/README.md b/docs/README.md index ca199fe80..094b63cee 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,8 +8,8 @@ In addition, we utilize the following extensions to enhance the documentation :c ## Build documentation -The `sphinx` is already initialized in `docs` directory. -In order to build the documentation, you will need additional +The `sphinx` is already initialized in `docs` directory. +In order to build the documentation, you will need additional packages listed in extra dependencies. ```bash @@ -27,7 +27,7 @@ Use `make help` for other options. # Contribution -The documentation-related commits will be collected in the branch `doc_**` separate from `master` branch, and merged into `master` collectively. Ideally, the updates in the documentation branch will seek upcoming version update (i.e. `update-**` branch) and merged shortly after the version release. If an update is critical and urgent, create PR directly to `master` branch. +Documentation-related commits should be pushed to the appropriate `update-**` branch. These changes will be incorporated into the `master` branch upon the corresponding release. For urgent or critical documentation updates, you may create a pull request directly to `master`. ## User Guide @@ -39,7 +39,7 @@ These files will be managed in `docs` directory. ## API documentation The docstring for function or modules are automatically parsed using `sphinx`+`numpydoc`. -Any inline function description, such as +Any inline function description, such as ```py """ This is the form of a docstring. @@ -62,4 +62,4 @@ will be parsed and displayed in API documentation. See `numpydoc` for more detai `ReadtheDocs` runs `sphinx` internally and maintain the documentation website. We will always activate the `stable` and `latest` version, and few past-documentations will also be available for the support. -@nmnaughton and @skim449 has access to the `ReadtheDocs` account. +@nmnaughton and [@skim0119](https://github.com/skim0119) has access to the `ReadtheDocs` account. diff --git a/docs/advanced/PackageDesign.md b/docs/advanced/PackageDesign.md index 302577c82..e885adcd3 100644 --- a/docs/advanced/PackageDesign.md +++ b/docs/advanced/PackageDesign.md @@ -20,43 +20,35 @@ Elastica package uses [structural subtyping](https://peps.python.org/pep-0544/) direction RL subgraph Systems Protocol direction RL - SLBD(SlenderBodyGeometryProtool) - SymST["SymplecticSystem:\n• KinematicStates/Rates\n• DynamicStates/Rates"] + SymST["SymplecticSystemProtocol
(Mixin for timestepper)
• update_kinematics
• update_dynamics"] style SymST text-align:left - ExpST["ExplicitSystem:\n• States (Unused)"] - style ExpST text-align:left - P((position\nvelocity\nacceleration\n..)) --> SLBD - subgraph StaticSystemType - Surface - Mesh - end - subgraph SystemType - direction TB - Rod - RigidBody - end - SLBD --> SymST + StaticSystemType["Static System Type"
• Plane] + SystemType["(Dynamic) System Type
• CosseratRod (Rod)
• Sphere (RigidBody)
• Cylinder (RigidBody)"] + SystemType --> SymST - SLBD --> ExpST - SystemType --> ExpST end + + subgraph System Collection + SysColl["SystemCollectionProtocol"] + end + subgraph Timestepper Protocol - direction TB - StP["StepperProtocol\n• step(SystemCollection, time, dt)"] - style StP text-align:left - SymplecticStepperProtocol["SymplecticStepperProtocol\n• PositionVerlet"] + direction LR + SymplecticStepperProtocol["SymplecticStepperProtocol
• PositionVerlet"] style SymplecticStepperProtocol text-align:left - ExpplicitStepperProtocol["ExpplicitStepperProtocol\n(Unused)"] end - subgraph SystemCollection + SymST --> SysColl -->|Symplectic systems only| SymplecticStepperProtocol + StaticSystemType --> SysColl +``` - end - SymST --> SystemCollection --> SymplecticStepperProtocol - ExpST --> SystemCollection --> ExpplicitStepperProtocol - StaticSystemType --> SystemCollection +#### Key takeaways: + +- Any object that conforms to `StaticSystemProtocol` can be added to the system collection. + - If you want to add custom type to the system, you can use `append_allowed_types` to add it to the system collection. To add associated block support, you can use `enable_block_supports`. +- Among the systems added to the system collection, only objects that conform to `SymplecticSystemProtocol` will be integrated by the symplectic timestepper. This protocol requires `update_kinematics(time, prefac)` and `update_dynamics(time, prefac)` methods to be implemented. +- If block support is available for a system, they will be collected together during the `finalize` step, and passed to the timestepper. -``` ### System Collection (Build memory block) @@ -66,15 +58,15 @@ Elastica package uses [structural subtyping](https://peps.python.org/pep-0544/) St((Stepper)) subgraph SystemCollectionType direction LR - StSys["StaticSystem:\n• Surface\n• Mesh"] + StSys["StaticSystem:
• Plane"] style StSys text-align:left - DynSys["DynamicSystem:\n• Rod\n  • CosseratRod\n• RigidBody\n  • Sphere\n  • Cylinder"] + DynSys["DynamicSystem:
• CosseratRod
• Sphere
• Cylinder"] style DynSys text-align:left - BlDynSys["BlockSystemType:\n• BlockCosseratRod\n• BlockRigidBody"] + BlDynSys["BlockSystem:
• MemoryBlockCosseratRod
• MemoryBlockRigidBody"] style BlDynSys text-align:left - F{{"Feature Group (OperatorGroup):\n• Synchronize\n• Constrain values\n• Constrain rates\n• Callback"}} + F{{"Feature Group (OperatorGroup):
• Synchronize
• Constrain values
• Constrain rates
• Callback"}} style F text-align:left end Sys --> StSys --> F @@ -91,9 +83,9 @@ Elastica package uses [structural subtyping](https://peps.python.org/pep-0544/) St((Stepper)) subgraph SystemCollectionType direction LR - StSys["StaticSystem:\n• Surface\n• Mesh"] + StSys["StaticSystem:
• Plane"] style StSys text-align:left - DynSys["DynamicSystem:\n• Rod\n  • CosseratRod\n• RigidBody\n  • Sphere\n  • Cylinder"] + DynSys["DynamicSystem:
• Rod
  • CosseratRod
• RigidBody
  • Sphere
  • Cylinder"] style DynSys text-align:left subgraph Feature @@ -104,7 +96,7 @@ Elastica package uses [structural subtyping](https://peps.python.org/pep-0544/) Contact -->|detect_contact_between| Synchronize Connection -->|connect| Synchronize Damping -->|dampen| ConstrainRates - Callback -->|collect_diagnosis| CallbackGroup + Callback -->|collect_diagnostics| CallbackGroup end end Sys --> StSys --> Feature diff --git a/docs/api/callback.rst b/docs/api/callback.rst index 59eaac274..7142d77a2 100644 --- a/docs/api/callback.rst +++ b/docs/api/callback.rst @@ -1,14 +1,12 @@ Callback Functions =================== -.. _constraints: - .. automodule:: elastica.callback_functions Description ----------- -The frequency at which you have your callback function save data will depend on what information you need from the simulation. Excessive call backs can cause performance penalties, however, it is rarely necessary to make call backs at a frequency that this becomes a problem. We have found that making a call back roughly every 100 iterations has a negligible performance penalty. +The frequency at which you have your callback function save data will depend on what information you need from the simulation. Excessive call backs can cause performance penalties, however, it is rarely necessary to make call backs at a frequency that this becomes a problem. We have found that making a call back roughly every 100 iterations has a negligible performance penalty. Currently, all data saved from call back functions is saved in memory. If you have many rods or are running for a long time, you may want to consider editing the call back function to write the saved data to disk so you do not run out of memory during the simulation. @@ -19,8 +17,8 @@ Currently, all data saved from call back functions is saved in memory. If you ha ExportCallBack MyCallBack -Built-in Constraints --------------------- +Built-in Callbacks +------------------ .. autoclass:: CallBackBaseClass :special-members: __init__ @@ -30,4 +28,3 @@ Built-in Constraints .. autoclass:: MyCallBack :special-members: __init__ - diff --git a/docs/api/connections.rst b/docs/api/connections.rst index b616b4cad..16ea324f9 100644 --- a/docs/api/connections.rst +++ b/docs/api/connections.rst @@ -13,6 +13,7 @@ Description .. autosummary:: :nosignatures: + ConnectionBase FreeJoint FixedJoint HingeJoint @@ -21,8 +22,9 @@ Compatibility ~~~~~~~~~~~~~ =============================== ==== =========== -Connection / Joints Rod Rigid Body +Connection / Joints Rod Rigid Body =============================== ==== =========== +ConnectionBase ✅ ✅ FreeJoint ✅ ❌ FixedJoint ✅ ❌ HingeJoint ✅ ❌ @@ -31,6 +33,9 @@ HingeJoint ✅ ❌ Built-in Connection / Joint ------------------------------------- +.. autoclass:: ConnectionBase + :special-members: __init__,apply_forces,apply_torques + .. autoclass:: FreeJoint :special-members: __init__,apply_forces,apply_torques diff --git a/docs/api/constraints.rst b/docs/api/constraints.rst index 32308e94d..a366c1870 100644 --- a/docs/api/constraints.rst +++ b/docs/api/constraints.rst @@ -5,6 +5,7 @@ Constraints .. automodule:: elastica.boundary_conditions + Description ----------- @@ -21,8 +22,6 @@ Constraints are equivalent to displacement boundary condition. GeneralConstraint FixedConstraint HelicalBucklingBC - FreeRod - OneEndFixedRod Compatibility ~~~~~~~~~~~~~ @@ -72,7 +71,3 @@ Built-in Constraints .. autoclass:: HelicalBucklingBC :special-members: __init__ - -.. autoclass:: FreeRod - -.. autoclass:: OneEndFixedRod diff --git a/docs/api/contact.rst b/docs/api/contact.rst index e3b2b0099..a029dff90 100644 --- a/docs/api/contact.rst +++ b/docs/api/contact.rst @@ -1,5 +1,5 @@ Contact -============================== +======= .. _contact: @@ -8,6 +8,12 @@ Contact Description ----------- +.. note:: + (CAUTION) + The contact is recommended to be added at last. This is because contact forces often includes + friction that may depend on other normal forces and constraints to be calculated accurately. + Be careful on the order of adding interactions. + .. rubric:: Available Contact Classes .. autosummary:: @@ -24,7 +30,7 @@ Description Built-in Contact Classes -------------------------------------- +------------------------ .. autoclass:: NoContact :special-members: __init__,apply_contact diff --git a/docs/api/damping.rst b/docs/api/damping.rst index bd2b87a3c..0940c257e 100644 --- a/docs/api/damping.rst +++ b/docs/api/damping.rst @@ -28,8 +28,8 @@ LaplaceDissipationFilter ✅ ❌ =============================== ==== =========== -Built-in Constraints --------------------- +Built-in Dampers +---------------- .. autoclass:: DamperBase :inherited-members: diff --git a/docs/api/external_forces.rst b/docs/api/external_forces.rst index f34c05352..f67627c56 100644 --- a/docs/api/external_forces.rst +++ b/docs/api/external_forces.rst @@ -30,8 +30,6 @@ External force and environmental interaction are represented as force/torque bou .. autosummary:: :nosignatures: - AnisotropicFrictionalPlane - InteractionPlane SlenderBodyTheory Compatibility @@ -52,8 +50,6 @@ EndpointForcesSinusoidal ✅ ❌ ========================== ======= ============ Interaction Rod Rigid Body ========================== ======= ============ -AnisotropicFrictionalPlane ✅ ❌ -InteractionPlane ✅ ❌ SlenderBodyTheory ✅ ❌ ========================== ======= ============ @@ -88,11 +84,5 @@ Built-in Environment Interactions .. automodule:: elastica.interaction :noindex: -.. autoclass:: AnisotropicFrictionalPlane - :special-members: __init__ - -.. autoclass:: InteractionPlane - :special-members: __init__ - .. autoclass:: SlenderBodyTheory :special-members: __init__ diff --git a/docs/api/rigidbody.rst b/docs/api/rigidbody.rst index e7b863cb4..1f28c324f 100644 --- a/docs/api/rigidbody.rst +++ b/docs/api/rigidbody.rst @@ -9,9 +9,9 @@ Rigid Body | Sphere | | +----------+----+ -.. automodule:: elastica.rigidbody.rigid_body +.. automodule:: elastica.rigidbody.rigid_body_base :members: - :exclude-members: __weakref__, update_acceleration, zeroed_out_external_forces_and_torques + :exclude-members: __weakref__, update_accelerations, zeroed_out_external_forces_and_torques .. automodule:: elastica.rigidbody.cylinder :members: diff --git a/docs/api/surface.rst b/docs/api/surface.rst index 55d4615d2..6b68f3283 100644 --- a/docs/api/surface.rst +++ b/docs/api/surface.rst @@ -1,15 +1,18 @@ Surface ========== -+----------+----+ -| type | | -+==========+====+ -| plane | | -+----------+----+ +Description +----------- -.. automodule:: elastica.surface.surface_base - :members: - :exclude-members: __weakref__ +Surface is a static system that does not change by the timestepping. + +Available Surface Types +----------------------- ++----------+---------------------+ +| type | description | ++==========+=====================+ +| plane | Plane static system.| ++----------+---------------------+ .. automodule:: elastica.surface.plane :members: diff --git a/docs/api/utility.rst b/docs/api/utility.rst index 53c03539c..079a2b5db 100644 --- a/docs/api/utility.rst +++ b/docs/api/utility.rst @@ -1,16 +1,19 @@ Utility Functions -================== +================= Here, we provide some useful functions that we often use along with elastica. Transformations ------------------ +--------------- + .. automodule:: elastica.transformations :members: :exclude-members: __weakref__ + Math ---- + .. automodule:: elastica._calculus :members: :exclude-members: __weakref__ @@ -25,6 +28,7 @@ Math Miscellaneous ------------- + .. automodule:: elastica.utils :members: :exclude-members: __weakref__ diff --git a/docs/archive/NCSA-NVIDIA-AI-Hackathon-2020.md b/docs/archive/NCSA-NVIDIA-AI-Hackathon-2020.md index 3cb9928ed..6908866fd 100644 --- a/docs/archive/NCSA-NVIDIA-AI-Hackathon-2020.md +++ b/docs/archive/NCSA-NVIDIA-AI-Hackathon-2020.md @@ -6,46 +6,46 @@ The objective is to train a model to move a (cyber)-octopus with two soft arms and a head to reach a target location, and then grab an object. The octopus is modeled as an assembly of Cosserat rods and is activated by muscles surrounding its arms. Input to the mechanical model is the activation signals to the surrounding muscles, which causes it to contract, thus moving the arms. The output of the model comes from the octopus' environment. The mechanical model will be provided both for the octopus and its interaction with its environment. The goal is to find the correct muscle activation signals that make the octopus crawl to reach the target location and then make one arm to grab the object. ## Progression of specific goals -These goals build on each other, you need to successfully accomplish all prior goals to get credit for later goals. +These goals build on each other, you need to successfully accomplish all prior goals to get credit for later goals. 1) Make octopus crawl towards some direction. (5 points) -2) Make your octopus crawl to the target location. (7.5 points) +2) Make your octopus crawl to the target location. (7.5 points) 3) Make octopus to move the object using its arms. (7.5 points) -4) Have your octopus grab the object by wrapping one arm around the object. (10 points) +4) Have your octopus grab the object by wrapping one arm around the object. (10 points) 5) Make your octopus return to its starting location with the object. (20 points) -6) Generalize your policy to perform these tasks for an arbitrarily located object. (50 points) +6) Generalize your policy to perform these tasks for an arbitrarily located object. (50 points) ## Problem Context -Octopuses have flexible limbs made up of muscles with no internal bone structure. These limbs, know as muscular hydrostats, have an almost infinite number of degrees of freedom, allowing an octopus to perform complex actions with its arms, but also making them difficult to mathematically model. Attempts to model octopus arms are motivated not only by a desire to understand them biologically, but also to adapt their control ability and decision making processes to the rapidly developing field of soft robotics. We have developed a simulation package Elastica that models flexible 1-d rods, which can be used to represent octopus arms as a long, slender rod. We now want to learn methods for controlling these arms. +Octopuses have flexible limbs made up of muscles with no internal bone structure. These limbs, know as muscular hydrostats, have an almost infinite number of degrees of freedom, allowing an octopus to perform complex actions with its arms, but also making them difficult to mathematically model. Attempts to model octopus arms are motivated not only by a desire to understand them biologically, but also to adapt their control ability and decision making processes to the rapidly developing field of soft robotics. We have developed a simulation package Elastica that models flexible 1-d rods, which can be used to represent octopus arms as a long, slender rod. We now want to learn methods for controlling these arms. -You are being provided with a model of an octopus that consists of two arms connected by a head. Each arm can be controlled independently. These arms are actuated through the contraction of muscles in the arms. This muscle activation produces a torque profile along the arm, resulting in movement of the arm. The arms interact with the ground through friction. Your goal is to teach the octopus to crawl towards an object, grab it, and bring it back to where the octopus started. +You are being provided with a model of an octopus that consists of two arms connected by a head. Each arm can be controlled independently. These arms are actuated through the contraction of muscles in the arms. This muscle activation produces a torque profile along the arm, resulting in movement of the arm. The arms interact with the ground through friction. Your goal is to teach the octopus to crawl towards an object, grab it, and bring it back to where the octopus started. ## Controlling octopus arms with hierarchical basis functions -For this problem, we abstract the activation of the octopus muscles to the generation of a torque profile defined by the activation of a set of hierarchical radial basis function. Here we are using Gaussian basis functions. +For this problem, we abstract the activation of the octopus muscles to the generation of a torque profile defined by the activation of a set of hierarchical radial basis function. Here we are using Gaussian basis functions. image name image name -There are three levels of these basis functions, with 1 basis function in the first level, 2 in the second level and 4 in the third, leading to 7 basis functions in set. These levels have different maximum levels of activation. The lower levels have larger magnitudes than the higher levels, meaning they represent bulk motion of the rod while the higher levels allow finer control of the rod along the interval. In the code, the magnitude of each level will be fixed but you can choose the amount of activation at each level by setting the activation level between -1 and 1. +There are three levels of these basis functions, with 1 basis function in the first level, 2 in the second level and 4 in the third, leading to 7 basis functions in set. These levels have different maximum levels of activation. The lower levels have larger magnitudes than the higher levels, meaning they represent bulk motion of the rod while the higher levels allow finer control of the rod along the interval. In the code, the magnitude of each level will be fixed but you can choose the amount of activation at each level by setting the activation level between -1 and 1. -There are two bending modes (in the normal and binormal directions) and a twisting mode (in the tangent direction), so we define torques in these three different directions and independently for each arm. This yields six different sets of basis functions that can be activated for a total of 42 inputs. +There are two bending modes (in the normal and binormal directions) and a twisting mode (in the tangent direction), so we define torques in these three different directions and independently for each arm. This yields six different sets of basis functions that can be activated for a total of 42 inputs. ## Overview of provided Elastica code -We are providing you the Elastica software package which is written in Python. Elastica simulates the dynamics and kinematics of 1-d slender rods. We have set up the model for you such that you do not need to worry about the details of the model, only the activation patterns of the muscle. -In the provided `examples/ArmWithBasisFunctions/two_arm_octopus_ai_imp.py` file you will import the `Environment` class which will define and setup the simulation. +We are providing you the Elastica software package which is written in Python. Elastica simulates the dynamics and kinematics of 1-d slender rods. We have set up the model for you such that you do not need to worry about the details of the model, only the activation patterns of the muscle. +In the provided `examples/ArmWithBasisFunctions/two_arm_octopus_ai_imp.py` file you will import the `Environment` class which will define and setup the simulation. -`Environment` has three relevant functions: -* `Environment.reset(self)`: setups and initializes the simulation environment. Call this prior to running any simulations. -* `Environment.step(self, activation_array_list, time)`: takes one timestep for muscle activations defined in `activation_array_list`. -* `Environment.post_processing(self, filename_video)`: Makes 3D video based on saved data from simulation. Requires `ffmpeg`. -We do not suggest changing `Environment` as it may cause unintended consequences to the simulation. +`Environment` has three relevant functions: +* `Environment.reset(self)`: setups and initializes the simulation environment. Call this prior to running any simulations. +* `Environment.step(self, activation_array_list, time)`: takes one timestep for muscle activations defined in `activation_array_list`. +* `Environment.post_processing(self, filename_video)`: Makes 3D video based on saved data from simulation. Requires `ffmpeg`. +We do not suggest changing `Environment` as it may cause unintended consequences to the simulation. You will want to work within `main()` to interface with the simulations and develop your learning model. In `main()`, the first thing you need to define is the length of your simulation and initialize the environment. `final_time` is the length of time that your simulation will run unless exited early. You want to give your octopus enough time to complete the task, but too much time will lead to excessively long simulation times. -```python +```python # Set simulation final time final_time = 10.0 @@ -55,22 +55,22 @@ You will want to work within `main()` to interface with the simulations and deve total_steps, systems = env.reset() ``` -With your system initialized, you are now ready to perform the simulation. To perform the simulation there are two steps: +With your system initialized, you are now ready to perform the simulation. To perform the simulation there are two steps: 1) Evaluate the reward function and define the basis function activations -2) Perform time step +2) Perform time step -There is also a user defined stopping condition. When met, this will immediately end the simulation. This can be useful to end the simulation if the octopus successfully complete the task early, or has a sufficiently low reward function that there is no point continuing the simulation. +There is also a user defined stopping condition. When met, this will immediately end the simulation. This can be useful to end the simulation if the octopus successfully complete the task early, or has a sufficiently low reward function that there is no point continuing the simulation. ```python for i_sim in tqdm(range(total_steps)): """ Learning loop """ if i_sim % 200: """ Add your learning algorithm here to define activation """ - # This will be based on your observations of the system and - # evaluation of your reward function. + # This will be based on your observations of the system and + # evaluation of your reward function. shearable_rod = systems[0] - rigid_body = systems[1] - reward = reward_function() + rigid_body = systems[1] + reward = reward_function() activation = segment_activation_function() """ Perform time step """ @@ -85,7 +85,7 @@ There is also a user defined stopping condition. When met, this will immediately The state of the octopus is available in `shearable_rod`. The octopus consists of a series of 121 nodes. Nodes 0-49 relate to one arm, nodes 50-70 relate to the head, and nodes 71-120 relate to the second arm. `shearable_rod.position_collection` returns an array with entries relating to the position of each node. The state of the target object is available in `rigid_body`. -It is important to properly define the activation function. It consists of a list of lists defining the activation of the two arms in each of the the three modes of deformation. The activation function should be a list with three entries for the three modes of deformation. Each of these entries is in turn a list with two entries, which are arrays of the basis function activations for the two arms. +It is important to properly define the activation function. It consists of a list of lists defining the activation of the two arms in each of the the three modes of deformation. The activation function should be a list with three entries for the three modes of deformation. Each of these entries is in turn a list with two entries, which are arrays of the basis function activations for the two arms. ```python activation = [ @@ -97,22 +97,17 @@ It is important to properly define the activation function. It consists of a lis Each activation array has 7 entries that relate to the activation of different basis functions. The ordering goes from the top level to the bottom level of the hierarchy. Each entry can vary from -1 to 1. -`activation_array[0] ` -- One top level muscle segment -`activation_array[1:3]` -- Two mid level muscle segment -`activation_array[3:7]` -- Four bottom level muscle segment +`activation_array[0] ` -- One top level muscle segment +`activation_array[1:3]` -- Two mid level muscle segment +`activation_array[3:7]` -- Four bottom level muscle segment + - ## A few practical notes -1) To save a video of the octopus with `Environment.post_processing()`, you need to install `ffmeg`. You can download and install it [here](https://www.ffmpeg.org/). +1) To save a video of the octopus with `Environment.post_processing()`, you need to install `ffmpeg`. You can download and install it [here](https://www.ffmpeg.org/). 2) The timestep size is set to 40 μs. This is necessary to keep the simulation stable, however, you may not need to update your muscle activations that often. Varying the learning time step will change how often your octopus updates its behaviour. -3) There is a 15-20 second startup delay while the simulation is initialized. This is a one time cost whenever the Python script is run and resetting the simulation using `.rest()` does not incur this delay for subsequent simulations. +3) There is a 15-20 second startup delay while the simulation is initialized. This is a one time cost whenever the Python script is run and resetting the simulation using `.reset()` does not incur this delay for subsequent simulations. 4) We suggest installing `requirements.txt` and `optional-requirements.txt`, to run Elastica without any problem. - - - - - diff --git a/docs/conf.py b/docs/conf.py index 0a3db5386..3cde77d26 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,19 +13,20 @@ import os import sys +import datetime -sys.path.insert(0, os.path.abspath('../')) +sys.path.insert(0, os.path.abspath("../")) from elastica.version import VERSION # -- Project information ----------------------------------------------------- -project = 'PyElastica' -copyright = '2025, Gazzola Lab' -author = 'Gazzola Lab' +YEAR = datetime.datetime.now().year -# The full version, including alpha/beta/rc tags +project = "PyElastica" +copyright = f"{YEAR}, Gazzola Lab" +author = "Gazzola Lab" release = VERSION @@ -35,17 +36,40 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosectionlabel', - 'sphinx_autodoc_typehints', + "sphinx.ext.autodoc", + "sphinx.ext.autosectionlabel", + "sphinx_autodoc_typehints", #'sphinx.ext.napoleon', - 'sphinx.ext.viewcode', - 'sphinx.ext.mathjax', + "sphinx.ext.viewcode", + "sphinx.ext.mathjax", + "sphinxcontrib.video", "sphinxcontrib.mermaid", - 'numpydoc', - 'myst_parser', + "numpydoc", + "myst_parser", + "sphinx_gallery.gen_gallery", ] +# To add example in gallery, +# 1. run script should start with GALLERY_KEY +# 2. Some README(.rst/.md/.txt) file should be in the directory +GALLERY_KEY = "run" +sphinx_gallery_conf = { + "examples_dirs": "../examples", + "subsection_order": [ + "../examples/TimoshenkoBeamCase", + "*", + "../examples/ContinuumSnakeCase", + ], + "gallery_dirs": "_gallery", + "backreferences_dir": "gen_modules/backreferences", + "example_extensions": ".py", + "ignore_pattern": rf"^(?!.*{GALLERY_KEY})[^/\\]+\.py$", + "filename_pattern": f"/{GALLERY_KEY}_.*", + # 'nested_sections': True, + "first_notebook_cell": ("# PyElastica installation\n" "# !pip install pyelastica"), + # "parallel": 2, +} + myst_enable_extensions = [ "amsmath", "colon_fence", @@ -57,48 +81,52 @@ ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [ - "README.md", # File reserved to explain how documentationing works. - ] + "README.md", # File reserved to explain how documentationing works. +] -autodoc_default_flags = ['members', 'private-members', 'special-members', 'show-inheritance'] +autodoc_default_flags = [ + "members", + "private-members", + "special-members", + "show-inheritance", +] autosectionlabel_prefix_document = True -source_parsers = { -} -source_suffix = ['.rst', '.md'] +source_parsers = {} +source_suffix = [".rst", ".md"] -master_doc = 'index' +master_doc = "index" # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinx_book_theme' +html_theme = "sphinx_book_theme" html_theme_options = { "repository_url": "https://github.com/GazzolaLab/PyElastica", "use_repository_button": True, } html_title = "PyElastica" html_logo = "_static/assets/Logo.png" -#pygments_style = "sphinx" +# pygments_style = "sphinx" # 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'] -html_css_files = ['css/*', 'css/logo.css'] +html_static_path = ["_static"] +html_css_files = ["css/*", "css/logo.css"] # -- Options for autodoc --------------------------------------------------- -autodoc_member_order = 'bysource' +autodoc_member_order = "bysource" # -- Options for numpydoc --------------------------------------------------- numpydoc_show_class_members = False # -- Mermaid configuration --------------------------------------------------- -mermaid_params = ['--theme', 'neutral'] +mermaid_params = ["--theme", "neutral"] diff --git a/docs/guide/binder.md b/docs/guide/binder.md deleted file mode 100644 index 54393f0da..000000000 --- a/docs/guide/binder.md +++ /dev/null @@ -1,16 +0,0 @@ -# Binder Tutorials - - - -We have created several Jupyter notebooks and Python scripts to help get users started with using PyElastica. The Jupyter notebooks are available on Binder, allowing you to try out some of the tutorials without having to install PyElastica. - -[![](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/GazzolaLab/PyElastica/master?filepath=examples%2FBinder%2F0_PyElastica_Tutorials_Overview.ipynb) - -:::{note} -Additional examples are also available in the examples folder of PyElastica's [Github repo](https://github.com/GazzolaLab/PyElastica/tree/master/examples). -::: - diff --git a/docs/guide/discretization.md b/docs/guide/discretization.md index 851ba5c70..5d9a56248 100644 --- a/docs/guide/discretization.md +++ b/docs/guide/discretization.md @@ -1,23 +1,23 @@ # Discretization -To help get you started building initial intuition about PyElastica, here are some general rules of thumb to follow. +To help get you started building initial intuition about PyElastica, here are some general rules of thumb to follow. :::{important} These are based on general observations of how simulations tend to behave and are not guaranteed to always hold. Particularly for choosing dx and dt, it is important to perform a separate convergence study for your specific case. ::: ## Number of elements per rod -Generally, the more flexible your rod, the more elements you need. It is important to always perform a convergence test for your simulation, however, 30-50 elements per rod is a good starting point. +Generally, the more flexible your rod, the more elements you need. It is important to always perform a convergence test for your simulation, however, 30-50 elements per rod is a good starting point. ## Choosing your dx and dt -Generally you will set your dx and then choose a stable dt. Your dx will be a combination of your problems length scale and the number of elements you want. Recall that units can be rescaled as long as they are consistent. If you have have a small rod, selecting a dx on the order of nm without scaling is 1e-9. This small value can cause numerical issues, so it is better to rescale your units so that nm $\sim O(1)$. +Generally you will set your dx and then choose a stable dt. Your dx will be a combination of your problems length scale and the number of elements you want. Recall that units can be rescaled as long as they are consistent. If you have a small rod, selecting a dx on the order of nm without scaling is 1e-9. This small value can cause numerical issues, so it is better to rescale your units so that nm $\sim O(1)$. When choosing your time step, there are a number of different conditions that can affect your choice. The most important consideration is that the time stepping algorithm remain stable. As a useful heuristic, we have found that dt = 0.01 dx $s/m$ tends to yield stable time steps, but depending on your problem this may not hold. If you wish to be able to resolve the propagation of different waves, then you need to make sure your dt is able to capture their propagation ($dt = dx \sqrt{\rho/G}$ for shear waves or $dt = dx \sqrt{\rho/E}$ for flexural waves). ## Run time scaling -PyElastica will scale linearly with the number of time steps, so if you halve your time step, your simulation will take twice as long to finish. +PyElastica will scale linearly with the number of time steps, so if you halve your time step, your simulation will take twice as long to finish. -The algorithms that PyElastica is based on scale linearly with the number of elements. However, due to overhead from calling functions in Python, PyElastica does not currently have a strong dependence on the number of nodes. Doubling the number of nodes may only lead to a 10-20% increase in run time. While this means you can decrease your dx without a large run time penalty, remember that you also need to adjust your dt, which will affect the run time. +The algorithms that PyElastica is based on scale linearly with the number of elements. However, due to overhead from calling functions in Python, PyElastica does not currently have a strong dependence on the number of nodes. Doubling the number of nodes may only lead to a 10-20% increase in run time. While this means you can decrease your dx without a large run time penalty, remember that you also need to adjust your dt, which will affect the run time. Adding additional interactions with the environment, such as friction or gravity, will increase run time. Most of these interactions only have a small effect on run time except for rod collision and/or self-intersection. As implemented, these are expensive routines ($O(N^2)$) and should be avoided if possible as they will substantially lengthen your run time. diff --git a/docs/guide/example_cases.rst b/docs/guide/example_cases.rst deleted file mode 100644 index 4100a33f2..000000000 --- a/docs/guide/example_cases.rst +++ /dev/null @@ -1,31 +0,0 @@ -************* -Example Cases -************* - -Example cases are the demonstration of physical example with known analytical solution or well-studied phenomenon. -Each cases follows the recommended workflow, shown :ref:`here `. Feel free to use them as an initial template to build your own case study. - -Axial Stretching -~~~~~~~~~~~~~~~~ -.. literalinclude:: ../../examples/AxialStretchingCase/axial_stretching.py - :linenos: - -Timoshenko -~~~~~~~~~~ -.. literalinclude:: ../../examples/TimoshenkoBeamCase/timoshenko.py - :linenos: - -Butterfly -~~~~~~~~~ -.. literalinclude:: ../../examples/ButterflyCase/butterfly.py - :linenos: - -Helical Buckling -~~~~~~~~~~~~~~~~ -.. literalinclude:: ../../examples/HelicalBucklingCase/helicalbuckling.py - :linenos: - -Continuum Snake -~~~~~~~~~~~~~~~ -.. literalinclude:: ../../examples/ContinuumSnakeCase/continuum_snake.py - :linenos: diff --git a/docs/guide/visualization.md b/docs/guide/visualization.md index 0710e6cc5..af7b68711 100644 --- a/docs/guide/visualization.md +++ b/docs/guide/visualization.md @@ -6,7 +6,7 @@ If you wish to visualize your system, make sure you define your callback functio ## POVray -For high-quality visualization, we suggest [POVray](http://povray.com). See [this tutorial](https://github.com/GazzolaLab/PyElastica/tree/master/examples/Visualization) for examples of different ways of visualizing the system. +For high-quality visualization, we suggest [POVray](http://povray.com). ## Rhino diff --git a/docs/guide/workflow.md b/docs/guide/workflow.md index 0a823c904..68be7e18c 100644 --- a/docs/guide/workflow.md +++ b/docs/guide/workflow.md @@ -6,7 +6,7 @@ When using PyElastica, users will setup a simulation in which they define a syst **A note on notation:** Like other FEA packages such as Abaqus, PyElastica does not enforce units. This means that you are required to make sure that all units for your input variables are consistent. When in doubt, SI units are always safe, however, if you have a very small length scale ($\sim$ nm), then you may need to rescale your units to avoid needing prohibitively small time steps and/or roundoff errors. ::: -

1. Setup Simulation

+## 1. Setup Simulation ```python from elastica.modules import ( @@ -38,15 +38,15 @@ Available components are: | [Constraints](../api/constraints.rst) | | | [Forcing](../api/external_forces.rst) | | | [Connections](../api/connections.rst) | | -| [CallBacks](../api/callback.rst) | | -| [Damping](../api/damping.rst) | | +| [CallBacks](../api/callback.rst) | | +| [Damping](../api/damping.rst) | | :::{Note} We adopted a composition and mixin design paradigm in building elastica. The detail of the implementation is not important in using the package, but we left some references to read [here](../advanced/PackageDesign.md). ::: -

2. Create Rods

+## 2. Create Rods Each rod has a number of physical parameters that need to be defined. These values then need to be assigned to the rod to create the object, and the rod needs to be added to the simulator. ```python @@ -92,7 +92,7 @@ This can be repeated to create multiple rods. Supported geometries are listed in The number of element (`n_elements`) and `base_length` determines the spatial discretization `dx`. More detail discussion is included [here](discretization.md). ::: -

3.a Define Boundary Conditions, Forcings, and Connections

+## 3.a Define Boundary Conditions, Forcings, and Connections Now that we have added all our rods to `simulator`, we need to apply relevant boundary conditions. @@ -153,7 +153,7 @@ Version 0.3.3: The order of the operation is defined by the order of the definit For example, friction should be defined after contact, since contact will define the normal force applied to the surface, which friction depends on. Contact should be defined before any other boundary conditions, since aggregated normal force is used to calculate the repelling force. ::: -

3.b Define Damping

+## 3.b Define Damping Next, if required, in order to numerically stabilize the simulation, we can apply damping to the rods. @@ -178,7 +178,7 @@ simulator.dampen(rod2).using( ) ``` -

4. Add Callback Functions (optional)

+## 4. Add Callback Functions (optional) If you want to know what happens to the rod during the course of the simulation, you must collect data during the simulation. Here, we demonstrate how the callback function can be defined to export the data you need. There is a base class `CallBackBaseClass` that can help with this. @@ -220,7 +220,33 @@ simulator.collect_diagnostics(rod2).using( You can define different callback functions for different rods and also have different data outputted at different time step intervals depending on your needs. See [this page](../api/callback.rst) for more in-depth documentation. -

5. Finalize Simulator

+:::{note} +During setting up callbacks, `.collect_diagnostics` can take python-collection (list, dict, tuple) of rods/systems: +```python +... + def make_callback(self, system, time, current_step): + rod1 = system[key1] + rod2 = system[key2] +... +simulator.collect_diagnostics({key1: rod1, key2: rod2}).using( + # custom callback class +) +``` +to handle data from multiple rods at the same time. +Additionally, you can pass ellipsis (...) to collect data from all rods/systems in the simulator. +```python +... + def make_callback(self, system, time, current_step): + for rod in system: + pass +... +simulator.collect_diagnostics(...).using( + # custom callback class +) +``` +::: + +## 5. Finalize Simulator Now that we have finished defining our rods, the different boundary conditions and connections between them, and how often we want to save data, we have finished setting up the simulation. We now need to finalize the simulator by calling @@ -230,24 +256,30 @@ simulator.finalize() This goes through and collects all the rods and applied conditions, preparing the system for the simulation. -

6. Set Timestepper

+## 6. Set Timestepper -With our system now ready to be run, we need to define which time stepping algorithm to use. Currently, we suggest using the position Verlet algorithm. We also need to define how much time we want to simulate as well as either the time step (dt) or the number of total time steps we want to take. Once we have defined these things, we can run the simulation by calling `integrate()`, which will start the simulation. +With our system now ready to be run, we need to define which time stepping algorithm to use. Currently, we suggest using the Position Verlet algorithm. We also need to define how much time we want to simulate as well as either the time step (dt) or the number of total time steps we want to take. Once we have defined these things, we can run the simulation using an a timestepper loop. ->> We are still actively testing different integration and time-stepping techniques, `PositionVerlet` is the best default at this moment. +:::{note} +We are still actively testing different integration and time-stepping techniques, `PositionVerlet` is the best default at this moment. +::: ```python from elastica.timestepper.symplectic_steppers import PositionVerlet -from elastica.timestepper import integrate timestepper = PositionVerlet() final_time = 10 # seconds total_steps = int(final_time / dt) -integrate(timestepper, simulator, final_time, total_steps) + +# Timestepper loop +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(simulator, time, dt) ``` More documentation on timestepper and integrator is included [here](../api/time_steppers.rst) -

7. Post Process

+## 7. Post Process Once the simulation ends, it is time to analyze the data. If you defined a callback function, the data you outputted in available there (i.e. `callback_data_rod1`), otherwise you can access the final configuration of your system through your rod objects. For example, if you want the final position of one of your rods, you can get it from `rod1.position_collection[:]`. diff --git a/docs/index.rst b/docs/index.rst index 59e639ad9..8434cd510 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,9 +18,9 @@ Community :alt: on gitter We mainly use `git-issue`_ to communicate the roadmap, updates, helps, and bug fixes. -If you have problem using PyElastica, check if similar issue is reported in `git-issue`_. +If you have a problem using PyElastica, check if similar issue is reported in `git-issue`_. -We also opened `gitter` channel for short and immediate feedbacks. +We also opened `gitter` channel for short and immediate feedback. Contributing @@ -44,10 +44,14 @@ If you are interested to contribute, please read `contribution-guide`_ first. guide/workflow guide/discretization - guide/example_cases - guide/binder guide/visualization +.. toctree:: + :maxdepth: 3 + :caption: Case Examples + + _gallery/index + .. toctree:: :maxdepth: 2 :caption: API Documentation @@ -65,12 +69,6 @@ If you are interested to contribute, please read `contribution-guide`_ first. api/simulator api/utility -.. api/elastica++ - -.. toctree:: - :maxdepth: 2 - :caption: Gallary - .. toctree:: :maxdepth: 2 :caption: Advanced Guide diff --git a/docs/overview/installation.md b/docs/overview/installation.md index 3887aade5..1c28afe18 100644 --- a/docs/overview/installation.md +++ b/docs/overview/installation.md @@ -1,6 +1,6 @@ # Installation -## Instruction +## Instructions PyElastica requires Python 3.10+, which needs to be installed prior to using PyElastica. For information on installing Python, see [here](https://realpython.com/installing-python/). If you are interested in using a package manager like Conda, see [here](https://docs.conda.io/projects/conda/en/latest/user-guide/getting-started.html). diff --git a/elastica/__init__.py b/elastica/__init__.py index 9297af16a..9f93e7fb8 100644 --- a/elastica/__init__.py +++ b/elastica/__init__.py @@ -1,4 +1,7 @@ -from collections import defaultdict +from elastica.systems.protocol import ( + StaticSystemProtocol, + SystemProtocol, +) from elastica.rod.knot_theory import ( compute_link, compute_twist, @@ -6,7 +9,7 @@ ) from elastica.rod.rod_base import RodBase from elastica.rod.cosserat_rod import CosseratRod -from elastica.rigidbody.rigid_body import RigidBodyBase +from elastica.rigidbody.rigid_body_base import RigidBodyBase from elastica.rigidbody.cylinder import Cylinder from elastica.rigidbody.sphere import Sphere from elastica.surface.plane import Plane @@ -28,13 +31,14 @@ EndpointForcesSinusoidal, ) from elastica.interaction import ( - AnisotropicFrictionalPlane, - InteractionPlane, SlenderBodyTheory, ) from elastica.joint import ( + ConnectionBase, FreeJoint, FixedJoint, + BallJoint, # Alias + SphericalJoint, # Alias HingeJoint, ) from elastica.contact_forces import ( @@ -52,6 +56,7 @@ DamperBase, AnalyticalLinearDamper, LaplaceDissipationFilter, + RayleighDissipation, ) from elastica.modules.base_system import BaseSystemCollection from elastica.modules.callbacks import CallBacks diff --git a/elastica/_contact_functions.py b/elastica/_contact_functions.py index c8c9670c2..264861a68 100644 --- a/elastica/_contact_functions.py +++ b/elastica/_contact_functions.py @@ -4,6 +4,7 @@ _dot_product, _norm, _find_min_dist, + _find_min_dist_cylinder_sphere, _find_slipping_elements, _node_to_element_mass_or_force, _elements_to_nodes_inplace, @@ -49,6 +50,13 @@ def _calculate_contact_forces_rod_cylinder( velocity_damping_coefficient: np.float64, friction_coefficient: np.float64, ) -> None: + """ + Calculates the contact forces between a rod and a cylinder. + + This function computes the linear and angular contact forces acting on both the rod and the cylinder + when they come into contact. It considers spring-damper-based contact forces as well as friction. + The forces are applied to the nodes of the rod and the center of mass of the cylinder. + """ # We already pass in only the first n_elem x n_points = x_collection_rod.shape[1] cylinder_total_contact_forces = np.zeros((3)) @@ -174,6 +182,9 @@ def _calculate_contact_forces_rod_rod( contact_k: np.float64, contact_nu: np.float64, ) -> None: + """ + Calculates the contact forces between two rods. + """ # We already pass in only the first n_elem x n_points_rod_one = x_collection_rod_one.shape[1] n_points_rod_two = x_collection_rod_two.shape[1] @@ -283,6 +294,12 @@ def _calculate_contact_forces_self_rod( contact_k: np.float64, contact_nu: np.float64, ) -> None: + """ + Calculates the self-contact forces within a single rod. + + This function prevents self-penetration of a rod by calculating contact forces between different elements + of the same rod. A skip parameter is used to avoid checking adjacent elements. + """ # We already pass in only the first n_elem x n_points_rod = x_collection_rod.shape[1] edge_collection_rod_one = _batch_product_k_ik_to_ik(length_rod, tangent_rod) @@ -364,16 +381,11 @@ def _calculate_contact_forces_self_rod( def _calculate_contact_forces_rod_sphere( x_collection_rod: NDArray[np.float64], edge_collection_rod: NDArray[np.float64], - x_sphere_center: NDArray[np.float64], - x_sphere_tip: NDArray[np.float64], - edge_sphere: NDArray[np.float64], + x_sphere: NDArray[np.float64], radii_sum: NDArray[np.float64], length_sum: NDArray[np.float64], - internal_forces_rod: NDArray[np.float64], external_forces_rod: NDArray[np.float64], external_forces_sphere: NDArray[np.float64], - external_torques_sphere: NDArray[np.float64], - sphere_director_collection: NDArray[np.float64], velocity_rod: NDArray[np.float64], velocity_sphere: NDArray[np.float64], contact_k: np.float64, @@ -381,15 +393,21 @@ def _calculate_contact_forces_rod_sphere( velocity_damping_coefficient: np.float64, friction_coefficient: np.float64, ) -> None: + """ + Calculates the contact forces between a rod and a sphere. + + This function computes the linear and angular contact forces acting on both the rod and the sphere + when they come into contact. It considers spring-damper-based contact forces as well as friction. + The forces are applied to the nodes of the rod and the center of mass of the sphere. + """ # We already pass in only the first n_elem x n_points = x_collection_rod.shape[1] sphere_total_contact_forces = np.zeros((3)) - sphere_total_contact_torques = np.zeros((3)) for i in range(n_points): # Element-wise bounding box x_selected = x_collection_rod[..., i] # x_sphere is already a (,) array from outside - del_x = x_selected - x_sphere_tip + del_x = x_selected - x_sphere norm_del_x = _norm(del_x) # If outside then don't process @@ -397,8 +415,10 @@ def _calculate_contact_forces_rod_sphere( continue # find the shortest line segment between the two centerline - distance_vector, x_sphere_contact_point, _ = _find_min_dist( - x_selected, edge_collection_rod[..., i], x_sphere_tip, edge_sphere + distance_vector, _, _ = _find_min_dist_cylinder_sphere( + x_selected, + edge_collection_rod[..., i], + x_sphere, ) distance_vector_length = _norm(distance_vector) distance_vector /= distance_vector_length @@ -453,37 +473,22 @@ def _calculate_contact_forces_rod_sphere( # Update contact force net_contact_force += friction_force - # Torques acting on the cylinder - moment_arm = x_sphere_contact_point - x_sphere_center - # Add it to the rods at the end of the day if i == 0: external_forces_rod[..., i] -= 2 / 3 * net_contact_force external_forces_rod[..., i + 1] -= 4 / 3 * net_contact_force sphere_total_contact_forces += 2.0 * net_contact_force - sphere_total_contact_torques += np.cross( - moment_arm, 2.0 * net_contact_force - ) elif i == n_points - 1: external_forces_rod[..., i] -= 4 / 3 * net_contact_force external_forces_rod[..., i + 1] -= 2 / 3 * net_contact_force sphere_total_contact_forces += 2.0 * net_contact_force - sphere_total_contact_torques += np.cross( - moment_arm, 2.0 * net_contact_force - ) else: external_forces_rod[..., i] -= net_contact_force external_forces_rod[..., i + 1] -= net_contact_force sphere_total_contact_forces += 2.0 * net_contact_force - sphere_total_contact_torques += np.cross( - moment_arm, 2.0 * net_contact_force - ) # Update the cylinder external forces and torques external_forces_sphere[..., 0] += sphere_total_contact_forces - external_torques_sphere[..., 0] += ( - sphere_director_collection @ sphere_total_contact_torques - ) @njit(cache=True) # type: ignore @@ -501,17 +506,16 @@ def _calculate_contact_forces_rod_plane( external_forces: NDArray[np.float64], ) -> tuple[NDArray[np.float64], NDArray[np.intp]]: """ - This function computes the plane force response on the element, in the - case of contact. Contact model given in Eqn 4.8 Gazzola et. al. RSoS 2018 paper - is used. + Calculates the contact forces between a rod and a plane. + + This function computes the contact force exerted by a flat plane on a rod. + It includes a linear spring-damper model for the normal force to handle penetration + and a response to prevent the rod from passing through the plane. + It returns the magnitude of the plane response force and indices of elements not in contact. - Parameters - ---------- - system + Contact model given in Eqn 4.8 Gazzola et. al. RSoS 2018 paper + is used. - Returns - ------- - magnitude of the plane response """ # Compute plane response force @@ -597,6 +601,9 @@ def _calculate_contact_forces_rod_plane_with_anisotropic_friction( internal_torques: NDArray[np.float64], external_torques: NDArray[np.float64], ) -> None: + """ + Calculates contact forces between a rod and a plane with anisotropic friction. + """ ( plane_response_force_mag, no_contact_point_idx, @@ -796,6 +803,9 @@ def _calculate_contact_forces_cylinder_plane( velocity_collection: NDArray[np.float64], external_forces: NDArray[np.float64], ) -> tuple[NDArray[np.float64], NDArray[np.intp]]: + """ + Calculates the contact forces between a cylinder and a plane. + """ # Compute plane response force # total_forces = system.internal_forces + system.external_forces diff --git a/elastica/_rotations.py b/elastica/_rotations.py index bbc2b336f..0feddabb9 100644 --- a/elastica/_rotations.py +++ b/elastica/_rotations.py @@ -4,14 +4,12 @@ from itertools import combinations import numpy as np -from numpy import sin -from numpy import cos -from numpy import sqrt -from numpy import arccos +from numpy import sin, cos, sqrt, arccos from numpy.typing import NDArray from numba import njit +from elastica.typing import RodType, RigidBodyType, ConnectionIndex from elastica._linalg import _batch_matmul @@ -19,6 +17,29 @@ def _get_rotation_matrix( scale: np.float64, axis_collection: NDArray[np.float64] ) -> NDArray[np.float64]: + """ + Compute rotation matrices from axis-angle representation using Rodrigues' formula. + + Parameters + ---------- + scale : float + Scale factor applied to rotation angles. The actual rotation angle for each + axis is scale * ||axis||. + axis_collection : numpy.ndarray + 2D array of shape (dim, blocksize) containing rotation axes. Each column + represents an axis of rotation. + + Returns + ------- + rot_mat : numpy.ndarray + 3D array of shape (dim, dim, blocksize) containing rotation matrices computed + using Rodrigues' rotation formula. + + Notes + ----- + The axes are normalized before computing the rotation matrices. A small epsilon + (1e-14) is added to prevent division by zero for zero-length axes. + """ blocksize = axis_collection.shape[1] rot_mat = np.empty((3, 3, blocksize)) @@ -58,19 +79,42 @@ def _rotate( axis_collection: NDArray[np.float64], ) -> NDArray[np.float64]: """ - Does alibi rotations - https://en.wikipedia.org/wiki/Rotation_matrix#Ambiguities + Rotate director collection by specified axes and scale (alibi rotation). + + Performs alibi (active) rotations on a collection of director frames. + Each director frame is rotated around its corresponding axis by an angle + proportional to the scale factor. The rotation is applied using Rodrigues' + rotation formula via `_get_rotation_matrix`. Parameters ---------- - director_collection - scale - axis_collection + director_collection : numpy.ndarray + 3D array of shape (dim, dim, blocksize) containing rotation matrices + (director frames) to be rotated. + scale : float + Scale factor for rotation angles. The actual rotation angle for each + frame is scale * ||axis||, where ||axis|| is the magnitude of the + corresponding axis vector. + axis_collection : numpy.ndarray + 2D array of shape (dim, blocksize) containing rotation axes for each + director frame. Each column represents the axis of rotation for the + corresponding director frame. Returns ------- + rotated_directors : numpy.ndarray + 3D array of shape (dim, dim, blocksize) containing the rotated director + frames. Each frame is rotated around its corresponding axis by the + scaled angle. + + Notes + ----- + This function performs alibi (active) rotations, meaning the coordinate + system is rotated. For more information on rotation matrix ambiguities, see: + https://en.wikipedia.org/wiki/Rotation_matrix#Ambiguities - TODO Finish documentation + The rotation is computed as: R(scale * axis) @ director, where R is the + rotation matrix computed from the axis-angle representation. """ # return _batch_matmul( # director_collection, _get_rotation_matrix(scale, axis_collection) @@ -83,22 +127,30 @@ def _rotate( @njit(cache=True) # type: ignore def _inv_rotate(director_collection: NDArray[np.float64]) -> NDArray[np.float64]: """ - Calculated rate of change using Rodrigues' formula + Compute rotation axes between consecutive director frames using Rodrigues' formula. + + Calculates the rotation axis (in axis-angle representation) that transforms + each director frame to the next one. This is the inverse operation of rotating + directors and is used to extract the relative rotation between consecutive + elements. Parameters ---------- - director_collection : The collection of frames/directors at every element, - numpy.ndarray of shape (dim, dim, n) + director_collection : numpy.ndarray + The collection of frames/directors at every element, of shape (dim, dim, n) + where n is the number of director frames. Returns ------- - vector_collection : The collection of axes around which the body rotates - numpy.ndarray of shape (dim, n) - - Note - ---- - TODO: Benchmark missing - + vector_collection : numpy.ndarray + The collection of rotation axes, of shape (dim, n-1). Each column represents + the axis of rotation (scaled by angle) that transforms director[k] to director[k+1]. + + Notes + ----- + The output has n-1 elements because it computes the relative rotation between + consecutive pairs of directors. The rotation axis is computed using the trace + of the relative rotation matrix Q_{k+1} @ Q_k^T. """ blocksize = director_collection.shape[2] - 1 vector_collection = np.empty((3, blocksize)) @@ -173,7 +225,22 @@ def _inv_rotate(director_collection: NDArray[np.float64]) -> NDArray[np.float64] # TODO: Below contains numpy-only implementations @functools.lru_cache(maxsize=1) def _generate_skew_map(dim: int) -> list[tuple[int, int, int]]: - # TODO Documentation + """ + Generate mapping indices for converting vectors to skew-symmetric matrices. + + Creates a mapping that defines how vector elements are arranged in a + flattened skew-symmetric matrix representation. This is used for efficient + conversion between vector and matrix forms in dimension-agnostic operations. + + Notes + ----- + The mapping handles the conversion from a vector v = [x, y, z] to a + skew-symmetric matrix M where M[i,j] = -M[j,i] and the off-diagonal + elements correspond to vector components. + + The formula used (dim - (i + j)) works correctly for dimensions 2 and 3, + but may need verification for higher dimensions. + """ # Preallocate mapping_list = [_generate_skew_map_sentinel] * ((dim**2 - dim) // 2) # Indexing (i,j), j is the fastest changing @@ -223,7 +290,19 @@ def _get_skew_map(dim: int) -> tuple[tuple[int, int, int], ...]: @functools.lru_cache(maxsize=1) def _get_inv_skew_map(dim: int) -> tuple[tuple[int, int, int], ...]: - # TODO Documentation + """ + Generate inverse mapping for extracting vectors from skew-symmetric matrices. + + Creates a mapping that defines how to extract vector elements from a + flattened skew-symmetric matrix representation. This is the inverse + operation of `_generate_skew_map`. + + Notes + ----- + This mapping is used to extract vector components from skew-symmetric + matrices. The mapping is generated by inverting the tuple element order + from `_generate_skew_map`. + """ # (vec_src, mat_i, mat_j, sign) mapping_list = _generate_skew_map(dim) @@ -250,18 +329,10 @@ def _get_diag_map(dim: int) -> tuple[int, ...]: def _skew_symmetrize(vector: NDArray[np.float64]) -> NDArray[np.float64]: """ + Convert vector collection to skew-symmetric matrix collection. - Parameters - ---------- - vector : numpy.ndarray of shape (dim, blocksize) - - Returns - ------- - output : numpy.ndarray of shape (dim*dim, blocksize) corresponding to - [0, -z, y, z, 0, -x, -y , x, 0] - - Note - ---- + Notes + ----- Gets close to the hard-coded implementation in time but with slightly high memory requirement for iteration. @@ -285,16 +356,24 @@ def _skew_symmetrize(vector: NDArray[np.float64]) -> NDArray[np.float64]: # While calculating u^2, use u with einsum instead, as it is tad bit faster def _skew_symmetrize_sq(vector: NDArray[np.float64]) -> NDArray[np.float64]: """ - Generate the square of an orthogonal matrix from vector elements + Generate the square of a skew-symmetric matrix from vector elements. + + Computes u^2 where u is the skew-symmetric matrix corresponding to the input + vector. This is used in Rodrigues' rotation formula. Parameters ---------- - vector : numpy.ndarray of shape (dim, blocksize) + vector : numpy.ndarray + Input vector collection of shape (dim, blocksize). Returns ------- - output : numpy.ndarray of shape (dim*dim, blocksize) corresponding to - [-(y^2+z^2), xy, xz, yx, -(x^2+z^2), yz, zx, zy, -(x^2+y^2)] + output : numpy.ndarray + Square of skew-symmetric matrices of shape (dim, dim, blocksize). + For a 3D vector [x, y, z], the corresponding matrix u^2 is: + [[-(y^2+z^2), xy, xz], + [yx, -(x^2+z^2), yz], + [zx, zy, -(x^2+y^2)]] Note ---- @@ -345,13 +424,11 @@ def _get_skew_symmetric_pair( vector_collection: NDArray[np.float64], ) -> tuple[NDArray[np.float64], NDArray[np.float64]]: """ + Compute both the skew-symmetric matrix and its square from vector collection. - Parameters - ---------- - vector_collection - - Returns - ------- + This is a convenience function that computes both u and u^2 where u is the + skew-symmetric matrix corresponding to the input vectors. These are commonly + used together in Rodrigues' rotation formula. """ u = _skew_symmetrize(vector_collection) @@ -361,20 +438,22 @@ def _get_skew_symmetric_pair( def _inv_skew_symmetrize(matrix: NDArray[np.float64]) -> NDArray[np.float64]: """ - Return the vector elements from a skew-symmetric matrix M + Return the vector elements from a skew-symmetric matrix M. Parameters ---------- - matrix : np.ndarray of dimension (dim, dim, blocksize) + matrix : numpy.ndarray + 3D (dim, dim, blocksize) array containing skew-symmetric matrices. Returns ------- - vector : np.ndarray of dimension (dim, blocksize) + vector : numpy.ndarray + 2D (dim, blocksize) array containing the extracted vector elements. - Note - ---- - Harcoded : 2.28 µs ± 63.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) - This : 2.91 µs ± 58.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) + Notes + ----- + Hardcoded: 2.28 µs ± 63.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) + This: 2.91 µs ± 58.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) """ dim, dim, blocksize = matrix.shape @@ -388,3 +467,55 @@ def _inv_skew_symmetrize(matrix: NDArray[np.float64]) -> NDArray[np.float64]: vector[tgt_index] = matrix[src_i, src_j] return vector + + +def get_relative_rotation_two_systems( + system_one: "RodType | RigidBodyType", + index_one: "ConnectionIndex", + system_two: "RodType | RigidBodyType", + index_two: "ConnectionIndex", +) -> NDArray[np.float64]: + """ + Compute the relative rotation matrix C_12 between system one and system two at the specified elements. + + Examples + -------- + How to get the relative rotation between two systems (e.g. the rotation from end of rod one to base of rod two): + + >>> rel_rot_mat = get_relative_rotation_two_systems(system1, -1, system2, 0) + + How to initialize a FixedJoint with a rest rotation between the two systems, + which is enforced throughout the simulation: + + >>> simulator.connect( + ... first_rod=system1, second_rod=system2, first_connect_idx=-1, second_connect_idx=0 + ... ).using( + ... FixedJoint, + ... ku=1e6, nu=0.0, kt=1e3, nut=0.0, + ... rest_rotation_matrix=get_relative_rotation_two_systems(system1, -1, system2, 0) + ... ) + + See Also + -------- + FixedJoint + + Parameters + ---------- + system_one : RodType | RigidBodyType + Rod or rigid-body object + index_one : ConnectionIndex + Index of first system for connection. + system_two : RodType | RigidBodyType + Rod or rigid-body object + index_two : ConnectionIndex + Index of second system for connection. + + Returns + ------- + relative_rotation_matrix : numpy.ndarray + 2D (3, 3) array containing the relative rotation matrix C_12 between the two systems + for their current state. + """ + director_one = system_one.director_collection[..., index_one] + director_two = system_two.director_collection[..., index_two] + return director_one @ director_two.T diff --git a/elastica/boundary_conditions.py b/elastica/boundary_conditions.py index 1075821cf..f5cbbc3f7 100644 --- a/elastica/boundary_conditions.py +++ b/elastica/boundary_conditions.py @@ -1,4 +1,4 @@ -__doc__ = """ Built-in boundary condition implementationss """ +__doc__ = """ Built-in boundary condition implementations """ from typing import Any, Optional, TypeVar, Generic @@ -14,7 +14,7 @@ from elastica.typing import SystemType, RodType, RigidBodyType, ConstrainingIndex -S = TypeVar("S") +S = TypeVar("S", bound=SystemType) class ConstraintBase(ABC, Generic[S]): @@ -22,15 +22,13 @@ class ConstraintBase(ABC, Generic[S]): Notes ----- - Constraint class must inherit BaseConstraint class. - - - Attributes - ---------- - system : RodBase or RigidBodyBase - node_indices : None or numpy.ndarray - element_indices : None or numpy.ndarray + Constraint class must inherit ConstraintBase class. + Attributes + ---------- + _system : RodType or RigidBodyType + _constrained_position_idx : NDArray[np.int32] + _constrained_director_idx : NDArray[np.int32] """ _system: S @@ -40,13 +38,14 @@ class ConstraintBase(ABC, Generic[S]): def __init__( self, *args: Any, + _system: S, constrained_position_idx: ConstrainingIndex = (), constrained_director_idx: ConstrainingIndex = (), **kwargs: Any, ) -> None: """Initialize boundary condition""" try: - self._system = kwargs["_system"] + self._system = _system self._constrained_position_idx = np.array( constrained_position_idx, dtype=np.int32 ) @@ -85,7 +84,6 @@ def constrain_values(self, system: S, time: np.float64) -> None: time : float The time of simulation. """ - pass @abstractmethod def constrain_rates(self, system: S, time: np.float64) -> None: @@ -100,7 +98,6 @@ def constrain_rates(self, system: S, time: np.float64) -> None: The time of simulation. """ - pass class FreeBC(ConstraintBase): @@ -115,13 +112,11 @@ def constrain_values( self, system: "RodType | RigidBodyType", time: np.float64 ) -> None: """In FreeBC, this routine simply passes.""" - pass def constrain_rates( self, system: "RodType | RigidBodyType", time: np.float64 ) -> None: """In FreeBC, this routine simply passes.""" - pass class OneEndFixedBC(ConstraintBase): @@ -134,13 +129,17 @@ class OneEndFixedBC(ConstraintBase): Examples -------- - How to fix one ends of the rod: + How to fix one end of the rod: >>> simulator.constrain(rod).using( ... OneEndFixedBC, - ... constrained_position_idx=(0,), - ... constrained_director_idx=(0,) + ... constrained_position_idx=(0,), # Specify node to fix + ... constrained_director_idx=(0,), # Specify element to fix ... ) + + See Also + -------- + :class:`GeneralConstraint`: For fixing multiple node/element with specific degrees-of-freedom. """ def __init__( @@ -195,22 +194,18 @@ def compute_constrain_values( fixed_directors_collection: NDArray[np.float64], ) -> None: """ - Computes constrain values in numba njit decorator + Computes constrain values in numba njit decorator. Parameters ---------- position_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. - fixed_position : numpy.ndarray + 2D (dim, blocksize) array containing data with 'float' type. + fixed_position_collection : numpy.ndarray 2D (dim, 1) array containing data with 'float' type. director_collection : numpy.ndarray - 3D (dim, dim, blocksize) array containing data with `float` type. - fixed_directors : numpy.ndarray + 3D (dim, dim, blocksize) array containing data with 'float' type. + fixed_directors_collection : numpy.ndarray 3D (dim, dim, 1) array containing data with 'float' type. - - Returns - ------- - """ position_collection[..., 0] = fixed_position_collection director_collection[..., 0] = fixed_directors_collection @@ -222,18 +217,14 @@ def compute_constrain_rates( omega_collection: NDArray[np.float64], ) -> None: """ - Compute contrain rates in numba njit decorator + Compute constrain rates in numba njit decorator Parameters ---------- velocity_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. + 2D (dim, blocksize) array containing data with 'float' type. omega_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. - - Returns - ------- - + 2D (dim, blocksize) array containing data with 'float' type. """ velocity_collection[..., 0] = 0.0 omega_collection[..., 0] = 0.0 @@ -288,22 +279,22 @@ def __init__( np.array of type bool indicating which translational degrees of freedom (dof) to constrain. If entry is True, the corresponding dof will be constrained. If None, we constrain all dofs. rotational_constraint_selector: Optional[np.ndarray] - np.array of type bool indicating which translational degrees of freedom (dof) to constrain. + np.array of type bool indicating which rotational degrees of freedom (dof) to constrain. If entry is True, the corresponding dof will be constrained. """ super().__init__(**kwargs) pos, dir = [], [] - for data in fixed_data: + for idx, data in enumerate(fixed_data): if isinstance(data, np.ndarray) and data.shape == (3,): pos.append(data) - elif isinstance(data, np.ndarray) and data.shape == ( - 3, - 3, - ): + elif isinstance(data, np.ndarray) and data.shape == (3, 3): dir.append(data) else: - # TODO: This part is prone to error. - break + raise ValueError( + f"Invalid data at position {idx} in fixed_data. " + f"Expected numpy array with shape (3,) for position or (3, 3) for director, " + f"but got {type(data).__name__} with value: {data}." + ) if len(pos) > 0: # transpose from (blocksize, dim) to (dim, blocksize) @@ -371,14 +362,14 @@ def nb_constrain_translational_values( constraint_selector: NDArray[np.int32], ) -> None: """ - Computes constrain values in numba njit decorator + Computes constrain values in numba njit decorator. Parameters ---------- position_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. + 2D (dim, blocksize) array containing data with 'float' type. fixed_position_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. + 2D (dim, blocksize) array containing data with 'float' type. indices : numpy.ndarray 1D array containing the index of constraining nodes constraint_selector: numpy.ndarray @@ -413,7 +404,7 @@ def nb_constrain_translational_rates( Parameters ---------- velocity_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. + 2D (dim, blocksize) array containing data with 'float' type. indices : numpy.ndarray 1D array containing the index of constraining nodes constraint_selector: numpy.ndarray @@ -445,9 +436,9 @@ def nb_constrain_rotational_rates( Parameters ---------- director_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. + 2D (dim, blocksize) array containing data with 'float' type. omega_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. + 2D (dim, blocksize) array containing data with 'float' type. indices : numpy.ndarray 1D array containing the index of constraining nodes constraint_selector: numpy.ndarray @@ -561,15 +552,16 @@ def nb_constraint_rotational_values( indices: NDArray[np.int32], ) -> None: """ - Computes constrain values in numba njit decorator + Computes constrain values in numba njit decorator. + Parameters ---------- director_collection : numpy.ndarray - 3D (dim, dim, blocksize) array containing data with `float` type. + 3D (dim, dim, blocksize) array containing data with 'float' type. fixed_director_collection : numpy.ndarray - 3D (dim, dim, blocksize) array containing data with `float` type. + 3D (dim, dim, blocksize) array containing data with 'float' type. indices : numpy.ndarray - 1D array containing the index of constraining nodes + 1D array containing the index of constraining nodes. """ block_size = indices.size for i in range(block_size): @@ -584,15 +576,16 @@ def nb_constrain_translational_values( indices: NDArray[np.int32], ) -> None: """ - Computes constrain values in numba njit decorator + Computes constrain values in numba njit decorator. + Parameters ---------- position_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. + 2D (dim, blocksize) array containing data with 'float' type. fixed_position_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. + 2D (dim, blocksize) array containing data with 'float' type. indices : numpy.ndarray - 1D array containing the index of constraining nodes + 1D array containing the index of constraining nodes. """ block_size = indices.size for i in range(block_size): @@ -605,13 +598,14 @@ def nb_constrain_translational_rates( velocity_collection: NDArray[np.float64], indices: NDArray[np.int32] ) -> None: """ - Compute constrain rates in numba njit decorator + Compute constrain rates in numba njit decorator. + Parameters ---------- velocity_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. + 2D (dim, blocksize) array containing data with 'float' type. indices : numpy.ndarray - 1D array containing the index of constraining nodes + 1D array containing the index of constraining nodes. """ block_size = indices.size @@ -627,13 +621,14 @@ def nb_constrain_rotational_rates( omega_collection: NDArray[np.float64], indices: NDArray[np.int32] ) -> None: """ - Compute constrain rates in numba njit decorator + Compute constrain rates in numba njit decorator. + Parameters ---------- omega_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. + 2D (dim, blocksize) array containing data with 'float' type. indices : numpy.ndarray - 1D array containing the index of constraining nodes + 1D array containing the index of constraining nodes. """ block_size = indices.size @@ -653,30 +648,28 @@ class HelicalBucklingBC(ConstraintBase): `Example case (helical buckling) `_ - Attributes - ---------- - twisting_time: float - Time to complete twist. - final_start_position: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - Position of first node of rod after twist completed. - final_end_position: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - Position of last node of rod after twist completed. - ang_vel: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - Angular velocity of rod during twisting time. - shrink_vel: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - Shrink velocity of rod during twisting time. - final_start_directors: numpy.ndarray - 3D (dim, dim, 1) array containing data with 'float' type. - Directors of first element of rod after twist completed. - final_end_directors: numpy.ndarray - 3D (dim, dim, 1) array containing data with 'float' type. - Directors of last element of rod after twist completed. - - + Attributes + ---------- + twisting_time: float + Time to complete twist. + final_start_position: numpy.ndarray + 2D (dim, 1) array containing data with 'float' type. + Position of first node of rod after twist completed. + final_end_position: numpy.ndarray + 2D (dim, 1) array containing data with 'float' type. + Position of last node of rod after twist completed. + ang_vel: numpy.ndarray + 2D (dim, 1) array containing data with 'float' type. + Angular velocity of rod during twisting time. + shrink_vel: numpy.ndarray + 2D (dim, 1) array containing data with 'float' type. + Shrink velocity of rod during twisting time. + final_start_directors: numpy.ndarray + 3D (dim, dim, 1) array containing data with 'float' type. + Directors of first element of rod after twist completed. + final_end_directors: numpy.ndarray + 3D (dim, dim, 1) array containing data with 'float' type. + Directors of last element of rod after twist completed. """ def __init__( @@ -719,7 +712,7 @@ def __init__( super().__init__(**kwargs) self.twisting_time = np.float64(twisting_time) - angel_vel_scalar = np.float64( + angle_vel_scalar = np.float64( (2.0 * number_of_rotations * np.pi / self.twisting_time) / 2.0 ) shrink_vel_scalar = np.float64(slack / (self.twisting_time * 2.0)) @@ -731,7 +724,7 @@ def __init__( self.final_start_position = position_start + slack / 2.0 * direction self.final_end_position = position_end - slack / 2.0 * direction - self.ang_vel = angel_vel_scalar * direction + self.ang_vel = angle_vel_scalar * direction self.shrink_vel = shrink_vel_scalar * direction theta = np.float64(number_of_rotations * np.pi) diff --git a/elastica/callback_functions.py b/elastica/callback_functions.py index 50c13cf73..2bcd42e1a 100644 --- a/elastica/callback_functions.py +++ b/elastica/callback_functions.py @@ -26,7 +26,12 @@ class CallBackBaseClass(Generic[T]): """ - def make_callback(self, system: T, time: np.float64, current_step: int) -> None: + def make_callback( + self, + system: T, + time: np.float64, + current_step: int, + ) -> None: """ This method is called every time step. Users can define which parameters are called back and recorded. Also users @@ -35,7 +40,7 @@ def make_callback(self, system: T, time: np.float64, current_step: int) -> None: Parameters ---------- - system : object + system : T (SystemType | tuple[SystemType] | dict[Any, SystemType] | list[SystemType]) System is a rod-like object. time : float The time of the simulation. @@ -43,7 +48,12 @@ def make_callback(self, system: T, time: np.float64, current_step: int) -> None: Simulation step. """ - pass + + def on_close(self) -> None: + """ + This method is called collectively when when .close() is + called by the system collection. + """ class MyCallBack(CallBackBaseClass): @@ -52,12 +62,12 @@ class MyCallBack(CallBackBaseClass): This is just an example of a callback class, this class as an example/template to write new call back classes in your client file. - Attributes - ---------- - sample_every: int - Collect data using make_callback method every sampling step. - callback_params: dict - Collected callback data is saved in this dictionary. + Attributes + ---------- + sample_every: int + Collect data using make_callback method every sampling step. + callback_params: dict + Collected callback data is saved in this dictionary. """ def __init__(self, step_skip: int, callback_params: dict) -> None: @@ -75,7 +85,7 @@ def __init__(self, step_skip: int, callback_params: dict) -> None: self.callback_params = callback_params def make_callback( - self, system: "RodType | RigidBodyType", time: np.float64, current_step: int + self, system: RodType, time: np.float64, current_step: int ) -> None: if current_step % self.sample_every == 0: @@ -97,16 +107,16 @@ class ExportCallBack(CallBackBaseClass): If one wants to customize the saving data, we recommend to override `make_callback` method. - Attributes - ---------- - AVAILABLE_METHOD - Supported method to save the file. We recommend - binary save to maintain the tensor structure of - data. - FILE_SIZE_CUTOFF - Maximum buffer size for each file. If the buffer - size exceed, new file is created. Actual size of - the file is expected to be marginally larger. + Attributes + ---------- + AVAILABLE_METHOD + Supported method to save the file. We recommend + binary save to maintain the tensor structure of + data. + FILE_SIZE_CUTOFF + Maximum buffer size for each file. If the buffer + size exceed, new file is created. Actual size of + the file is expected to be marginally larger. """ AVAILABLE_METHOD = ["pickle", "npz", "tempfile"] @@ -198,15 +208,16 @@ def make_callback( self, system: "RodType | RigidBodyType", time: np.float64, current_step: int ) -> None: """ + Collect simulation data at specified intervals. Parameters ---------- - system : - Each part of the system (i.e. rod, rigid body, etc) - time : - simulation time unit + system : RodType | RigidBodyType + Each part of the system (i.e. rod, rigid body, etc). + time : float + Simulation time. current_step : int - simulation step + Current simulation step. """ if current_step % self.step_skip == 0: position = system.position_collection.copy() @@ -264,15 +275,10 @@ def get_last_saved_path(self) -> Optional[str]: else: return self.save_path.format(self.file_count - 1, self._ext) - def close(self) -> None: + def on_close(self) -> None: """ - Save residual buffer + Save residual buffer. + Can be called using `simulator.close()`. """ if self.buffer_size: self._dump() - - def clear(self) -> None: - """ - Alias to `close` - """ - self.close() diff --git a/elastica/contact_forces.py b/elastica/contact_forces.py index 93889b43f..3ddad53e9 100644 --- a/elastica/contact_forces.py +++ b/elastica/contact_forces.py @@ -1,13 +1,14 @@ -__doc__ = """ Numba implementation module containing contact between rods and rigid bodies and other rods rigid bodies or surfaces.""" +__doc__ = """ +Numba implementation module containing contact between rods and rigid bodies and other rods rigid bodies or surfaces. +""" from typing import TypeVar, Generic, Type -from elastica.typing import RodType, SystemType, SurfaceType +from elastica.typing import RodType, SystemType, StaticSystemType from elastica.rod.rod_base import RodBase from elastica.rigidbody.cylinder import Cylinder from elastica.rigidbody.sphere import Sphere from elastica.surface.plane import Plane -from elastica.surface.surface_base import SurfaceBase from elastica.contact_utils import ( _prune_using_aabbs_rod_cylinder, _prune_using_aabbs_rod_rod, @@ -26,8 +27,8 @@ from numpy.typing import NDArray -S1 = TypeVar("S1") # TODO: Find bound -S2 = TypeVar("S2") +S1 = TypeVar("S1", bound=StaticSystemType) +S2 = TypeVar("S2", bound=StaticSystemType) class NoContact(Generic[S1, S2]): @@ -79,14 +80,17 @@ def apply_contact( time: np.float64 = np.float64(0.0), ) -> None: """ - Apply contact forces and torques between two system object.. - + Apply contact forces and torques between two system objects. In NoContact class, this routine simply passes. Parameters ---------- - system_one - system_two + system_one : SystemType + First system object. + system_two : SystemType + Second system object. + time : float + The time of simulation. """ @@ -130,8 +134,12 @@ def apply_contact( Parameters ---------- - system_one: RodType - system_two: RodType + system_one : RodType + First rod object. + system_two : RodType + Second rod object. + time : float + The time of simulation. """ # First, check for a global AABB bounding box, and see whether that @@ -174,7 +182,7 @@ def apply_contact( class RodCylinderContact(NoContact): """ This class is for applying contact forces between rod-cylinder. - If you are want to apply contact forces between rod and cylinder, first system is always rod and second system + If you want to apply contact forces between rod and cylinder, first system is always rod and second system is always cylinder. In addition to the contact forces, user can define apply friction forces between rod and cylinder that are in contact. For details on friction model refer to this [1]_. @@ -194,7 +202,6 @@ class RodCylinderContact(NoContact): ... nu=10, ... ) - .. [1] Preclik T., Popa Constantin., Rude U., Regularizing a Time-Stepping Method for Rigid Multibody Dynamics, Multibody Dynamics 2011, ECCOMAS. URL: https://www10.cs.fau.de/publications/papers/2011/Preclik_Multibody_Ext_Abstr.pdf """ @@ -236,6 +243,19 @@ def apply_contact( system_two: Cylinder, time: np.float64 = np.float64(0.0), ) -> None: + """ + Apply contact forces and torques between RodType object and Cylinder object. + + Parameters + ---------- + system_one : RodType + Rod object. + system_two : Cylinder + Cylinder object. + time : float + The time of simulation. + + """ # First, check for a global AABB bounding box, and see whether that # intersects if _prune_using_aabbs_rod_cylinder( @@ -333,8 +353,12 @@ def apply_contact( Parameters ---------- - system_one: RodType - system_two: RodType + system_one : RodType + Rod object. + system_two : RodType + Same rod object as system_one (self-contact). + time : float + The time of simulation. """ _calculate_contact_forces_self_rod( @@ -358,6 +382,8 @@ class RodSphereContact(NoContact): In addition to the contact forces, user can define apply friction forces between rod and sphere that are in contact. For details on friction model refer to this [1]_. + .. [1] Preclik T., Popa Constantin., Rude U., Regularizing a Time-Stepping Method for Rigid Multibody Dynamics, Multibody Dynamics 2011, ECCOMAS. URL: https://www10.cs.fau.de/publications/papers/2011/Preclik_Multibody_Ext_Abstr.pdf + Notes ----- The `velocity_damping_coefficient` is set to a high value (e.g. 1e4) to minimize slip and simulate stiction @@ -373,7 +399,6 @@ class RodSphereContact(NoContact): ... nu=10, ... ) - .. [1] Preclik T., Popa Constantin., Rude U., Regularizing a Time-Stepping Method for Rigid Multibody Dynamics, Multibody Dynamics 2011, ECCOMAS. URL: https://www10.cs.fau.de/publications/papers/2011/Preclik_Multibody_Ext_Abstr.pdf """ def __init__( @@ -384,6 +409,7 @@ def __init__( friction_coefficient: float = 0.0, ) -> None: """ + Parameters ---------- k : float @@ -417,8 +443,12 @@ def apply_contact( Parameters ---------- - system_one: RodType - system_two: Sphere + system_one : RodType + Rod object. + system_two : Sphere + Sphere object. + time : float + The time of simulation. """ # First, check for a global AABB bounding box, and see whether that @@ -433,10 +463,7 @@ def apply_contact( ): return - x_sph = ( - system_two.position_collection[..., 0] - - system_two.radius * system_two.director_collection[2, :, 0] - ) + sphere_position = system_two.position_collection[..., 0] rod_element_position = 0.5 * ( system_one.position_collection[..., 1:] @@ -445,16 +472,11 @@ def apply_contact( _calculate_contact_forces_rod_sphere( rod_element_position, system_one.lengths * system_one.tangents, - system_two.position_collection[..., 0], - x_sph, - system_two.radius * system_two.director_collection[2, :, 0], + sphere_position, system_one.radius + system_two.radius, system_one.lengths + 2 * system_two.radius, - system_one.internal_forces, system_one.external_forces, system_two.external_forces, - system_two.external_torques, - system_two.director_collection[:, :, 0], system_one.velocity_collection, system_two.velocity_collection, self.k, @@ -502,12 +524,12 @@ def __init__( @property def _allowed_system_two(self) -> list[Type]: - return [SurfaceBase] + return [Plane] def apply_contact( self, system_one: RodType, - system_two: SurfaceType, + system_two: Plane, time: np.float64 = np.float64(0.0), ) -> None: """ @@ -515,10 +537,12 @@ def apply_contact( Parameters ---------- - system_one: object + system_one : RodType Rod object. - system_two: object + system_two : Plane Plane object. + time : float + The time of simulation. """ _calculate_contact_forces_rod_plane( @@ -566,6 +590,7 @@ def __init__( kinetic_mu_array: NDArray[np.float64], ) -> None: """ + Parameters ---------- k : float @@ -599,12 +624,12 @@ def __init__( @property def _allowed_system_two(self) -> list[Type]: - return [SurfaceBase] + return [Plane] def apply_contact( self, system_one: RodType, - system_two: SurfaceType, + system_two: Plane, time: np.float64 = np.float64(0.0), ) -> None: """ @@ -612,11 +637,14 @@ def apply_contact( Parameters ---------- - system_one: RodType - system_two: SurfaceType + system_one : RodType + Rod object. + system_two : Plane + Plane object. + time : float + The time of simulation. """ - _calculate_contact_forces_rod_plane_with_anisotropic_friction( system_two.origin, system_two.normal, @@ -660,6 +688,7 @@ class CylinderPlaneContact(NoContact): ... k=1e4, ... nu=10, ... ) + """ def __init__( @@ -668,6 +697,7 @@ def __init__( nu: float, ) -> None: """ + Parameters ---------- k : float @@ -686,23 +716,27 @@ def _allowed_system_one(self) -> list[Type]: @property def _allowed_system_two(self) -> list[Type]: - return [SurfaceBase] + return [Plane] def apply_contact( self, system_one: Cylinder, - system_two: SurfaceType, + system_two: Plane, time: np.float64 = np.float64(0.0), ) -> None: """ - This function computes the plane force response on the cylinder, in the - case of contact. Contact model given in Eqn 4.8 Gazzola et. al. RSoS 2018 paper - is used. + Compute the plane force response on the cylinder in the case of contact. + + Contact model given in Eqn 4.8 Gazzola et al. RSoS 2018 paper is used. Parameters ---------- - system_one: Cylinder - system_two: SurfaceBase + system_one : Cylinder + Cylinder object. + system_two : Plane + Plane object. + time : float + The time of simulation. """ _calculate_contact_forces_cylinder_plane( diff --git a/elastica/contact_utils.py b/elastica/contact_utils.py index bcb577012..bc898720a 100644 --- a/elastica/contact_utils.py +++ b/elastica/contact_utils.py @@ -41,6 +41,9 @@ def _find_min_dist( x2: NDArray[np.float64], e2: NDArray[np.float64], ) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]: + """ + Find the minimum distance between two centerline segments: (x1, edge1) and (x2, edge2). + """ e1e1 = _dot_product(e1, e1) # type: ignore e1e2 = _dot_product(e1, e2) # type: ignore e2e2 = _dot_product(e2, e2) # type: ignore @@ -105,11 +108,32 @@ def _find_min_dist( return x2 + s * e2 - x1 - t * e1, x2 + s * e2, x1 - t * e1 +@numba.njit(cache=True) # type: ignore +def _find_min_dist_cylinder_sphere( + x1: NDArray[np.float64], + e1: NDArray[np.float64], + x2: NDArray[np.float64], +) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]: + """ + Find the minimum distance between centerline segment and point: (x1, edge1) and (x2). + """ + e1e1 = _dot_product(e1, e1) # type: ignore + x1e1 = _dot_product(x1, e1) # type: ignore + x2e1 = _dot_product(e1, x2) # type: ignore + + # Parametrization + t = (x2e1 - x1e1) / e1e1 # Comes from taking dot of e1 with a normal + t = _clip(t, 0.0, 1.0) + + # Return distance, contact point of system 2, contact point of system 1 + return x2 - x1 - t * e1, x2, x1 - t * e1 + + @numba.njit(cache=True) # type: ignore def _aabbs_not_intersecting( aabb_one: NDArray[np.float64], aabb_two: NDArray[np.float64] ) -> Literal[1, 0]: - """Returns true if not intersecting else false""" + """Checks if two axis-aligned bounding boxes (AABBs) are not intersecting.""" if (aabb_one[0, 1] < aabb_two[0, 0]) | (aabb_one[0, 0] > aabb_two[0, 1]): return 1 if (aabb_one[1, 1] < aabb_two[1, 0]) | (aabb_one[1, 0] > aabb_two[1, 1]): @@ -130,6 +154,13 @@ def _prune_using_aabbs_rod_cylinder( cylinder_radius: NDArray[np.float64], cylinder_length: NDArray[np.float64], ) -> Literal[1, 0]: + """ + Prunes broad-phase collision detection between a rod and a cylinder using AABBs. + + This function checks for intersection between the axis-aligned bounding boxes (AABBs) + of a rod and a cylinder. It's a quick way to rule out collision without performing + a more detailed and expensive check. + """ max_possible_dimension = np.zeros((3,)) aabb_rod = np.empty((3, 2)) aabb_cylinder = np.empty((3, 2)) @@ -171,6 +202,13 @@ def _prune_using_aabbs_rod_rod( rod_two_radius_collection: NDArray[np.float64], rod_two_length_collection: NDArray[np.float64], ) -> Literal[1, 0]: + """ + Prunes broad-phase collision detection between two rods using AABBs. + + This function checks for intersection between the axis-aligned bounding boxes (AABBs) + of two rods. It's a quick way to rule out collision without performing + a more detailed and expensive check. + """ max_possible_dimension = np.zeros((3,)) aabb_rod_one = np.empty((3, 2)) aabb_rod_two = np.empty((3, 2)) @@ -209,33 +247,29 @@ def _prune_using_aabbs_rod_sphere( sphere_director: NDArray[np.float64], sphere_radius: NDArray[np.float64], ) -> Literal[1, 0]: - max_possible_dimension = np.zeros((3,)) + """ + Prunes broad-phase collision detection between a rod and a sphere using AABBs. + + This function checks for intersection between the axis-aligned bounding boxes (AABBs) + of a rod and a sphere. It's a quick way to rule out collision without performing + a more detailed and expensive check. + """ + # AABB for rod aabb_rod = np.empty((3, 2)) - aabb_sphere = np.empty((3, 2)) - max_possible_dimension[...] = np.max(rod_one_radius_collection) + np.max( - rod_one_length_collection - ) + # Taking max radius of rod + max_rod_radius = np.max(rod_one_radius_collection) for i in range(3): - aabb_rod[i, 0] = ( - np.min(rod_one_position_collection[i]) - max_possible_dimension[i] - ) - aabb_rod[i, 1] = ( - np.max(rod_one_position_collection[i]) + max_possible_dimension[i] - ) + aabb_rod[i, 0] = np.min(rod_one_position_collection[i]) - max_rod_radius + aabb_rod[i, 1] = np.max(rod_one_position_collection[i]) + max_rod_radius - sphere_dimensions_in_local_FOR = np.array( - [sphere_radius, sphere_radius, sphere_radius] - ) - sphere_dimensions_in_world_FOR = np.zeros_like(sphere_dimensions_in_local_FOR) + # AABB for sphere + aabb_sphere = np.empty((3, 2)) + # A sphere is symmetrical, so its AABB is easy to compute. + # The director is not needed. for i in range(3): - for j in range(3): - sphere_dimensions_in_world_FOR[i] += ( - sphere_director[j, i, 0] * sphere_dimensions_in_local_FOR[j] - ) + aabb_sphere[i, 0] = sphere_position[i, 0] - sphere_radius + aabb_sphere[i, 1] = sphere_position[i, 0] + sphere_radius - max_possible_dimension = np.abs(sphere_dimensions_in_world_FOR) - aabb_sphere[..., 0] = sphere_position[..., 0] - max_possible_dimension - aabb_sphere[..., 1] = sphere_position[..., 0] + max_possible_dimension return _aabbs_not_intersecting(aabb_sphere, aabb_rod) diff --git a/elastica/dissipation.py b/elastica/dissipation.py index 22f618fc7..f1a691ece 100644 --- a/elastica/dissipation.py +++ b/elastica/dissipation.py @@ -14,18 +14,20 @@ import numpy as np from numpy.typing import NDArray +from elastica.typing import SystemType -T = TypeVar("T") + +T = TypeVar("T", bound=SystemType) class DamperBase(Generic[T], ABC): - """Base class for damping module implementations. + """ + Base class for damping module implementations. Notes ----- All damper classes must inherit DamperBase class. - Attributes ---------- system : RodBase @@ -34,9 +36,30 @@ class DamperBase(Generic[T], ABC): _system: T - # TODO typing can be made better def __init__(self, *args: Any, **kwargs: Any) -> None: - """Initialize damping module""" + """Initialize damping module + + Parameters + ---------- + *args : Any + Positional arguments (not currently used, reserved for future use). + **kwargs : Any + Keyword arguments. Must include '_system' key containing the system + (rod or rigid body) to be damped. Additional keyword arguments are + passed to derived classes for their specific configuration. + + Raises + ------ + KeyError + If '_system' is not provided in kwargs. This typically indicates + incorrect usage - use simulator.dampen(...).using(...) syntax instead. + + Notes + ----- + The base class extracts the '_system' parameter from kwargs. Derived + damper classes (e.g., AnalyticalLinearDamper, LaplaceDissipationFilter) + may accept additional keyword arguments for their specific configuration. + """ try: self._system = kwargs["_system"] except KeyError: @@ -59,7 +82,6 @@ def system(self) -> T: @abstractmethod def dampen_rates(self, system: T, time: np.float64) -> None: - # TODO: In the future, we can remove rod and use self.system """ Dampen rates (velocity and/or omega) of a rod object. @@ -71,7 +93,6 @@ def dampen_rates(self, system: T, time: np.float64) -> None: The time of simulation. """ - pass DampenType: TypeAlias = Callable[[RodType], None] @@ -117,13 +138,12 @@ class AnalyticalLinearDamper(DamperBase): 3. Damping constant: this protocol follows the original algorithm where the damping constants for translational and rotational velocities are assumed to be numerically identical. This leads to dimensional inconsistencies (see - https://github.com/GazzolaLab/PyElastica/issues/354). Hence, this option will be deprecated - in version 0.4.0. + https://github.com/GazzolaLab/PyElastica/issues/354). >>> simulator.dampen(rod).using( ... AnalyticalLinearDamper, - ... damping_constant=0.1, # To be deprecated in 0.4.0 - ... time_step = 1E-4, # Simulation time-step + ... damping_constant=0.1, + ... time_step=1E-4, ... ) Notes @@ -135,7 +155,7 @@ class AnalyticalLinearDamper(DamperBase): about the simulation becoming unstable. This now leads to a streamlined procedure for tuning the `damping_constant`: - 1. Set a high value for `damping_constant` to first acheive a stable simulation. + 1. Set a high value for `damping_constant` to first achieve a stable simulation. 2. If you feel the simulation is overdamped, reduce `damping_constant` until you feel the simulation is underdamped, and expected dynamics are recovered. """ @@ -150,42 +170,45 @@ def __init__(self, time_step: np.float64, **kwargs: Any) -> None: ) rotational_damping_constant = kwargs.get("rotational_damping_constant", None) + # Count non-None parameters + provided_params = [ + p + for p in [ + damping_constant, + uniform_damping_constant, + translational_damping_constant, + rotational_damping_constant, + ] + if p is not None + ] + self._dampen_rates_protocol: DampenType - if ( - (damping_constant is not None) - and (uniform_damping_constant is None) - and (translational_damping_constant is None) - and (rotational_damping_constant is None) - ): + # Determine which protocol to use based on provided parameters + if len(provided_params) == 1 and damping_constant is not None: + # Deprecated: single damping_constant self._dampen_rates_protocol = self._deprecated_damping_protocol( damping_constant=damping_constant, time_step=time_step ) - - elif ( - (damping_constant is None) - and (uniform_damping_constant is not None) - and (translational_damping_constant is None) - and (rotational_damping_constant is None) - ): + elif len(provided_params) == 1 and uniform_damping_constant is not None: + # Uniform damping: single uniform_damping_constant self._dampen_rates_protocol = self._uniform_damping_protocol( uniform_damping_constant=uniform_damping_constant, time_step=time_step ) - elif ( - (damping_constant is None) - and (uniform_damping_constant is None) - and (translational_damping_constant is not None) - and (rotational_damping_constant is not None) + len(provided_params) == 2 + and translational_damping_constant is not None + and rotational_damping_constant is not None ): + # Physical damping: both translational and rotational constants self._dampen_rates_protocol = self._physical_damping_protocol( translational_damping_constant=translational_damping_constant, rotational_damping_constant=rotational_damping_constant, time_step=time_step, ) - else: - message = ( + # Invalid parameter combination + raise ValueError( "AnalyticalLinearDamper usage:\n" "\tsimulator.dampen(rod).using(\n" "\t\tAnalyticalLinearDamper,\n" @@ -206,7 +229,6 @@ def __init__(self, time_step: np.float64, **kwargs: Any) -> None: "\t\ttime_step=...,\n" "\t)\n" ) - raise ValueError(message) def _deprecated_damping_protocol( self, damping_constant: np.float64, time_step: np.float64 @@ -276,6 +298,102 @@ def dampen_rates(self, system: RodType, time: np.float64) -> None: self._dampen_rates_protocol(system) +class RayleighDissipation(DamperBase): + """ + Rayleigh dissipation model matching the C++ implementation. + + This class implements the C++ force-based damping model for compatibility. + It is deprecated in favor of :class:`AnalyticalLinearDamper` which provides + better numerical stability and unconditional stability. This implementation + is kept for validation for old cases. + + This class implements force-based damping that matches the C++ nest-simulator + implementation. It adds damping forces and torques proportional to velocities: + + .. math:: + + \\mathbf{F}_{damp} = -\\nu \\mathbf{v} + + \\boldsymbol{\\tau}_{damp} = -\\nu \\boldsymbol{\\omega} + + where the damping coefficient :math:`\\nu` can decay exponentially over time. + + The damping forces are added to external forces and integrated through the + time stepper, which may require smaller time steps for large damping values. + + Parameters + ---------- + damping_constant : float + Damping coefficient :math:`\\nu` (per unit length). Units: [1/s] or [kg/(m·s)] + + Examples + -------- + .. code-block:: python + + simulator.dampen(rod).using( + RayleighDissipation, + damping_constant=0.1, + ) + + See Also + -------- + AnalyticalLinearDamper : Recommended alternative with better stability + LaplaceDissipationFilter : Alternative filtering-based dissipation + """ + + def __init__( + self, + damping_constant: np.float64, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + + if damping_constant < 0.0: + raise ValueError("damping_constant must be non-negative") + + _relaxation_time = 0.0 # relaxation: scale damping by exp(-time/relaxation) + + # Pre-compute average element length for rescaling + rest_lengths = self._system.rest_lengths + n_elems = self._system.n_elems + self._average_element_length = np.sum(rest_lengths) / n_elems + + if _relaxation_time > 0.0: + self.get_nu = lambda time: damping_constant * np.exp( + -time / _relaxation_time + ) + else: + self.get_nu = lambda time: damping_constant + + def dampen_rates(self, system: RodType, time: np.float64) -> None: + """ + Apply Rayleigh dissipation forces and torques. + + Parameters + ---------- + system : RodType + Rod system to apply damping to + time : float + Current simulation time + """ + # Rescale since nu is per unit length + nu_now = self.get_nu(time) * self._average_element_length # type: ignore + + # Apply damping forces: F = -nu * v + # Boundary factor: 0.5 at endpoints, 1.0 otherwise (matches C++) + # dampingForces[i] -= (nuNow * factor) * v[i] + for i in range(system.n_nodes): + factor = 0.5 if (i == 0 or i == system.n_nodes - 1) else 1.0 + damping_force = -(nu_now * factor) * system.velocity_collection[:, i] + system.external_forces[:, i] += damping_force + + # Apply damping torques: T = -nu * w + # dampingTorques[i] -= nuNow * w[i] + for i in range(system.n_elems): + damping_torque = -nu_now * system.omega_collection[:, i] + system.external_torques[:, i] += damping_torque + + class LaplaceDissipationFilter(DamperBase): """ Laplace Dissipation Filter class. This class corresponds qualitatively to a @@ -327,7 +445,7 @@ class LaplaceDissipationFilter(DamperBase): def __init__(self, filter_order: int, **kwargs: Any) -> None: """ - Filter damper initializer + Filter damper initializer. Parameters ---------- @@ -335,6 +453,12 @@ def __init__(self, filter_order: int, **kwargs: Any) -> None: Filter order, which corresponds to the number of times the Laplacian operator is applied. Increasing `filter_order` implies higher-order/weaker filtering. + + Raises + ------ + ValueError + If filter_order is not a positive integer. + """ super().__init__(**kwargs) if not (filter_order > 0 and isinstance(filter_order, int)): @@ -376,13 +500,13 @@ def _filter_function_periodic_condition_ring_rod( ) -> None: blocksize = velocity_filter_term.shape[1] - # Transfer velocity to an array which has periodic boundaries and synchornize boundaries + # Transfer velocity to an array which has periodic boundaries and synchronize boundaries velocity_collection_with_periodic_bc = np.empty((3, blocksize)) velocity_collection_with_periodic_bc[:, 1:-1] = velocity_collection[:] velocity_collection_with_periodic_bc[:, 0] = velocity_collection[:, -1] velocity_collection_with_periodic_bc[:, -1] = velocity_collection[:, 0] - # Transfer omega to an array which has periodic boundaries and synchornize boundaries + # Transfer omega to an array which has periodic boundaries and synchronize boundaries omega_collection_with_periodic_bc = np.empty((3, blocksize)) omega_collection_with_periodic_bc[:, 1:-1] = omega_collection[:] omega_collection_with_periodic_bc[:, 0] = omega_collection[:, -1] diff --git a/elastica/experimental/connection_contact_joint/generic_system_type_connection.py b/elastica/experimental/connection_contact_joint/generic_system_type_connection.py index 86c40fd23..7b3551a89 100644 --- a/elastica/experimental/connection_contact_joint/generic_system_type_connection.py +++ b/elastica/experimental/connection_contact_joint/generic_system_type_connection.py @@ -1,7 +1,7 @@ __doc__ = ( """ Module containing joint classes to connect rods and rigid bodies together. """ ) -from elastica.joint import FreeJoint, FixedJoint +from elastica.joint import ConnectionBase, FixedJoint from elastica.typing import SystemType import numpy as np from typing import Optional @@ -20,7 +20,7 @@ # - [x] Examples -class GenericSystemTypeFreeJoint(FreeJoint): +class GenericSystemTypeFreeJoint(ConnectionBase): """ Constrains the relative movement between two nodes by applying restoring forces. @@ -37,28 +37,6 @@ class GenericSystemTypeFreeJoint(FreeJoint): Describes for system two in the local coordinate system the translation from the node `index_two` (for rods) or the center of mass (for rigid bodies) to the joint. - - Examples - -------- - How to connect two Cosserat rods together using a spherical joint with a gap of 0.01 m in between. - - >>> simulator.connect(rod_one, rod_two, first_connect_idx=-1, second_connect_idx=0).using( - ... FreeJoint, - ... k=1e4, - ... nu=1, - ... point_system_one=np.array([0.0, 0.0, 0.005]), - ... point_system_two=np.array([0.0, 0.0, -0.005]), - ... ) - - How to connect the distal end of a CosseratRod with the base of a cylinder using a spherical joint. - - >>> simulator.connect(rod, cylinder, first_connect_idx=-1, second_connect_idx=0).using( - ... FreeJoint, - ... k=1e4, - ... nu=1, - ... point_system_two=np.array([0.0, 0.0, -cylinder.length / 2.]), - ... ) - """ # pass the k and nu for the forces @@ -88,7 +66,8 @@ def __init__( or the center of mass (for rigid bodies) to the joint. (default = np.array([0.0, 0.0, 0.0])) """ - super().__init__(k=k, nu=nu, **kwargs) + self.k = np.float64(k) + self.nu = np.float64(nu) self.point_system_one = ( point_system_one @@ -202,8 +181,6 @@ def apply_torques( """ Apply restoring joint torques to the connected systems. - In FreeJoint class, this routine simply passes. - Parameters ---------- system_one : SystemType @@ -223,51 +200,51 @@ class GenericSystemTypeFixedJoint(GenericSystemTypeFreeJoint): The fixed joint class restricts the relative movement and rotation between two nodes and elements by applying restoring forces and torques. - Attributes - ---------- - k : float - Stiffness coefficient of the joint. - nu : float - Damping coefficient of the joint. - kt : float - Rotational stiffness coefficient of the joint. - nut : float - Rotational damping coefficient of the joint. - point_system_one : numpy.ndarray - Describes for system one in the local coordinate system the translation from the node `index_one` (for rods) - or the center of mass (for rigid bodies) to the joint. - point_system_two : numpy.ndarray - Describes for system two in the local coordinate system the translation from the node `index_two` (for rods) - or the center of mass (for rigid bodies) to the joint. - rest_rotation_matrix : np.ndarray - 2D (3,3) array containing data with 'float' type. - Rest 3x3 rotation matrix from system one to system two at the connected elements. - Instead of aligning the directors of both systems directly, a desired rest rotational matrix labeled C_12* - is enforced. - - Examples - -------- - How to connect two Cosserat rods together using a fixed joint while aligning the tangents (e.g. local z-axis). - - >>> simulator.connect(rod_one, rod_two).using( - ... FixedJoint, - ... k=1e4, - ... nu=1, - ... ) - - How to connect a cosserat rod with the base of a cylinder using a fixed joint, where the cylinder is rotated - by 45 degrees around the y-axis. - - >>> from scipy.spatial.transform import Rotation - ... simulator.connect(rod, cylinder).using( - ... FixedJoint, - ... k=1e5, - ... nu=1e0, - ... kt=1e3, - ... nut=1e-3, - ... point_system_two=np.array([0, 0, -cylinder.length / 2]), - ... rest_rotation_matrix=Rotation.from_euler('y', np.pi / 4, degrees=False).as_matrix(), - ... ) + Attributes + ---------- + k : float + Stiffness coefficient of the joint. + nu : float + Damping coefficient of the joint. + kt : float + Rotational stiffness coefficient of the joint. + nut : float + Rotational damping coefficient of the joint. + point_system_one : numpy.ndarray + Describes for system one in the local coordinate system the translation from the node `index_one` (for rods) + or the center of mass (for rigid bodies) to the joint. + point_system_two : numpy.ndarray + Describes for system two in the local coordinate system the translation from the node `index_two` (for rods) + or the center of mass (for rigid bodies) to the joint. + rest_rotation_matrix : np.ndarray + 2D (3,3) array containing data with 'float' type. + Rest 3x3 rotation matrix from system one to system two at the connected elements. + Instead of aligning the directors of both systems directly, a desired rest rotational matrix labeled C_12* + is enforced. + + Examples + -------- + How to connect two Cosserat rods together using a fixed joint while aligning the tangents (e.g. local z-axis). + + >>> simulator.connect(rod_one, rod_two).using( + ... FixedJoint, + ... k=1e4, + ... nu=1, + ... ) + + How to connect a cosserat rod with the base of a cylinder using a fixed joint, where the cylinder is rotated + by 45 degrees around the y-axis. + + >>> from scipy.spatial.transform import Rotation + ... simulator.connect(rod, cylinder).using( + ... FixedJoint, + ... k=1e5, + ... nu=1e0, + ... kt=1e3, + ... nut=1e-3, + ... point_system_two=np.array([0, 0, -cylinder.length / 2]), + ... rest_rotation_matrix=Rotation.from_euler('y', np.pi / 4, degrees=False).as_matrix(), + ... ) """ def __init__( diff --git a/elastica/experimental/connection_contact_joint/parallel_connection.py b/elastica/experimental/connection_contact_joint/parallel_connection.py index 24980d635..f065beac1 100644 --- a/elastica/experimental/connection_contact_joint/parallel_connection.py +++ b/elastica/experimental/connection_contact_joint/parallel_connection.py @@ -1,7 +1,7 @@ __doc__ = """Contains SurfaceJointSideBySide class which connects two parallel rods .""" import numpy as np from numba import njit -from elastica.joint import FreeJoint +from elastica.joint import ConnectionBase # Join the two rods from elastica._linalg import ( @@ -67,7 +67,7 @@ def get_connection_vector_straight_straight_rod( ) -class SurfaceJointSideBySide(FreeJoint): +class SurfaceJointSideBySide(ConnectionBase): """ TODO: documentation """ diff --git a/elastica/experimental/interaction.py b/elastica/experimental/interaction.py index 293349ee3..d60426747 100644 --- a/elastica/experimental/interaction.py +++ b/elastica/experimental/interaction.py @@ -102,7 +102,9 @@ def anisotropic_friction_numba_rigid_body( velocity_collection, external_forces, ) - # FIXME: In future change the below part we should be able to compute the normal + # NOTE: Currently uses director_collection[0] as axial direction. + # If user anticipate the normal direction to be different from the director_collection[0] + # due to the rolling motion, this function cannot be used. axial_direction = director_collection[0] # rigid_body_normal # system.tangents element_velocity = velocity_collection diff --git a/elastica/experimental/timestepper/explicit_steppers.py b/elastica/experimental/timestepper/explicit_steppers.py deleted file mode 100644 index 3705ccd2d..000000000 --- a/elastica/experimental/timestepper/explicit_steppers.py +++ /dev/null @@ -1,320 +0,0 @@ -__doc__ = """Explicit timesteppers and concepts""" - -from typing import Any - -import numpy as np -from copy import copy - -from elastica.typing import ( - SystemType, - SystemCollectionType, - StepType, - SteppersOperatorsType, - StateType, -) -from elastica.experimental.timestepper.protocol import ( - ExplicitSystemProtocol, - ExplicitStepperProtocol, - MemoryProtocol, -) - - -""" -Developer Note --------------- -## Motivation for choosing _Mixin classes below - -The constraint/problem is that we do not know what -`System` we are integrating apriori. For a single -standalone `System` (which defines a `__call__` -operator and has its own states), we should just -step it like a single system. - -Instead if we get a `SystemCollection` made up of -bunch of smaller systems (like Cosserat Rods), we now -need to loop over all these smaller systems and perform -state updates there. Not only that we may also need -to communicate between such smaller systems. - -One way to solve this issue is to give the integrator -two methods: - -- `do_step`, which does the time-stepping for only a -`System` -- `do_system_step` which does the time-stepping for -a `SystemCollection` - -The problem with this approach is that -1. We have more methods than we actually use -(indeed we can only integrate either a `System` or -a `SystemCollection` but not both) -2. From an interface point of view, its ugly and not -graceful (at least IMO). - -The second approach is what I have chosen here, -which is to create two mixin classes : one for -integrating `System` and one for integrating -`SystemCollection`. And then depending upon the runtime -type of the object to be integrated, we can dynamically -mixin the required class. - -This approach overcomes the disadvantages of the -previous approach (as there's only one `do_step` method -associated with a Stepper at any given point of time), -at the expense of being a tad bit harder to understand -(which this documentation will hopefully fix). In essence, -we "smartly" use a mixin class to define the necessary -`do_step` method, which the `integrate` function then uses. -""" - - -class EulerForwardMemory: - def __init__(self, initial_state: StateType) -> None: - self.initial_state = initial_state - - -class RungeKutta4Memory: - """ - Stores all states of Rk within the time-stepper. Works as long as the states - are all one big numpy array, made possible by carefully using views. - - Convenience wrapper around Stateless that provides memory - """ - - def __init__( - self, - initial_state: StateType, - ) -> None: - self.initial_state = initial_state - self.k_1 = initial_state - self.k_2 = initial_state - self.k_3 = initial_state - self.k_4 = initial_state - - -class ExplicitStepperMixin: - """Base class for all explicit steppers - Can also be used as a mixin with optional cls argument below - """ - - def __init__(self: ExplicitStepperProtocol): - self.steps_and_prefactors = self.step_methods() - - def step_methods(self: ExplicitStepperProtocol) -> SteppersOperatorsType: - stages = self.get_stages() - updates = self.get_updates() - - assert len(stages) == len( - updates - ), "Number of stages and updates should be equal to one another" - return tuple(zip(stages, updates)) - - @property - def n_stages(self: ExplicitStepperProtocol) -> int: - return len(self.steps_and_prefactors) - - def step( - self: ExplicitStepperProtocol, - SystemCollection: SystemCollectionType, - time: np.float64, - dt: np.float64, - ) -> np.float64: - if isinstance( - self, EulerForward - ): # TODO: Cleanup - use depedency injection instead - Memory = EulerForwardMemory - elif isinstance(self, RungeKutta4): - Memory = RungeKutta4Memory # type: ignore[assignment] - else: - raise NotImplementedError(f"Memory class not defined for {self}") - memory_collection = tuple( - [Memory(initial_state=system.state) for system in SystemCollection] - ) - return ExplicitStepperMixin.do_step(self, self.steps_and_prefactors, SystemCollection, memory_collection, time, dt) # type: ignore[attr-defined] - - @staticmethod - def do_step( - TimeStepper: ExplicitStepperProtocol, - steps_and_prefactors: SteppersOperatorsType, - SystemCollection: SystemCollectionType, - MemoryCollection: Any, # TODO - time: np.float64, - dt: np.float64, - ) -> np.float64: - for stage, update in steps_and_prefactors: - SystemCollection.synchronize(time) - for system, memory in zip(SystemCollection[:-1], MemoryCollection[:-1]): - stage(system, memory, time, dt) - _ = update(system, memory, time, dt) - - stage(SystemCollection[-1], MemoryCollection[-1], time, dt) - time = update(SystemCollection[-1], MemoryCollection[-1], time, dt) - return time - - def step_single_instance( - self: ExplicitStepperProtocol, - System: SystemType, - Memory: MemoryProtocol, - time: np.float64, - dt: np.float64, - ) -> np.float64: - for stage, update in self.steps_and_prefactors: - stage(System, Memory, time, dt) - time = update(System, Memory, time, dt) - return time - - -class EulerForward(ExplicitStepperMixin): - """ - Classical Euler Forward stepper. Stateless, coordinates operations only. - """ - - def get_stages(self) -> list[StepType]: - return [self._first_stage] - - def get_updates(self) -> list[StepType]: - return [self._first_update] - - def _first_stage( - self, - System: ExplicitSystemProtocol, - Memory: EulerForwardMemory, - time: np.float64, - dt: np.float64, - ) -> None: - pass - - def _first_update( - self, - System: ExplicitSystemProtocol, - Memory: EulerForwardMemory, - time: np.float64, - dt: np.float64, - ) -> np.float64: - System.state += dt * System(time, dt) # type: ignore[arg-type] - return time + dt - - -class RungeKutta4(ExplicitStepperMixin): - """ - Stateless runge-kutta4. coordinates operations only, memory needs - to be externally managed and allocated. - """ - - def get_stages(self) -> list[StepType]: - return [ - self._first_stage, - self._second_stage, - self._third_stage, - self._fourth_stage, - ] - - def get_updates(self) -> list[StepType]: - return [ - self._first_update, - self._second_update, - self._third_update, - self._fourth_update, - ] - - # These methods should be static, but because we need to enable automatic - # discovery in ExplicitStepper, these are bound to the RungeKutta4 class - # For automatic discovery, the order of declaring stages here is very important - def _first_stage( - self, - System: ExplicitSystemProtocol, - Memory: RungeKutta4Memory, - time: np.float64, - dt: np.float64, - ) -> None: - Memory.initial_state = copy(System.state) - Memory.k_1 = dt * System(time, dt) # type: ignore[operator, assignment] - - def _first_update( - self, - System: ExplicitSystemProtocol, - Memory: RungeKutta4Memory, - time: np.float64, - dt: np.float64, - ) -> np.float64: - # prepare for next stage - System.state = Memory.initial_state + 0.5 * Memory.k_1 # type: ignore[operator] - return time + 0.5 * dt - - def _second_stage( - self, - System: ExplicitSystemProtocol, - Memory: RungeKutta4Memory, - time: np.float64, - dt: np.float64, - ) -> None: - Memory.k_2 = dt * System(time, dt) # type: ignore[operator, assignment] - - def _second_update( - self, - System: ExplicitSystemProtocol, - Memory: RungeKutta4Memory, - time: np.float64, - dt: np.float64, - ) -> np.float64: - # prepare for next stage - System.state = Memory.initial_state + 0.5 * Memory.k_2 # type: ignore[operator] - return time - - def _third_stage( - self, - System: ExplicitSystemProtocol, - Memory: RungeKutta4Memory, - time: np.float64, - dt: np.float64, - ) -> None: - Memory.k_3 = dt * System(time, dt) # type: ignore[operator, assignment] - - def _third_update( - self, - System: ExplicitSystemProtocol, - Memory: RungeKutta4Memory, - time: np.float64, - dt: np.float64, - ) -> np.float64: - # prepare for next stage - System.state = Memory.initial_state + Memory.k_3 # type: ignore[operator] - return time + 0.5 * dt - - def _fourth_stage( - self, - System: ExplicitSystemProtocol, - Memory: RungeKutta4Memory, - time: np.float64, - dt: np.float64, - ) -> None: - Memory.k_4 = dt * System(time, dt) # type: ignore[operator, assignment] - - def _fourth_update( - self, - System: ExplicitSystemProtocol, - Memory: RungeKutta4Memory, - time: np.float64, - dt: np.float64, - ) -> np.float64: - # prepare for next stage - System.state = ( - Memory.initial_state - + (Memory.k_1 + 2.0 * Memory.k_2 + 2.0 * Memory.k_3 + Memory.k_4) / 6.0 # type: ignore[operator] - ) - return time - - -# class ExplicitLinearExponentialIntegrator( -# _LinearExponentialIntegratorMixin, ExplicitStepper -# ): -# def __init__(self): -# _LinearExponentialIntegratorMixin.__init__(self) -# ExplicitStepper.__init__(self, _LinearExponentialIntegratorMixin) -# -# -# class StatefulLinearExponentialIntegrator(_StatefulStepper): -# def __init__(self): -# super(StatefulLinearExponentialIntegrator, self).__init__() -# self.stepper = ExplicitLinearExponentialIntegrator() -# self.linear_operator = None diff --git a/elastica/experimental/timestepper/memory.py b/elastica/experimental/timestepper/memory.py deleted file mode 100644 index b63931aa2..000000000 --- a/elastica/experimental/timestepper/memory.py +++ /dev/null @@ -1,83 +0,0 @@ -from typing import Iterator, TypeVar, Generic, Type -from elastica.typing import SystemCollectionType -from elastica.experimental.timestepper.explicit_steppers import ( - RungeKutta4, - EulerForward, -) -from elastica.experimental.timestepper.protocol import ExplicitStepperProtocol - -from copy import copy - - -# FIXME: Move memory related functions to separate module or as part of the timestepper -# TODO: Use MemoryProtocol -def make_memory_for_explicit_stepper( - stepper: ExplicitStepperProtocol, system: SystemCollectionType -) -> "MemoryCollection": - # TODO Automated logic (class creation, memory management logic) agnostic of stepper details (RK, AB etc.) - - # is_this_system_a_collection = is_system_a_collection(system) - - memory_cls: Type - if RungeKutta4 in stepper.__class__.mro(): - # Bad way of doing it, introduces tight coupling - # this should rather be taken from the class itself - class MemoryRungeKutta4: - def __init__(self) -> None: - self.initial_state = None - self.k_1 = None - self.k_2 = None - self.k_3 = None - self.k_4 = None - - memory_cls = MemoryRungeKutta4 - elif EulerForward in stepper.__class__.mro(): - - class MemoryEulerForward: - def __init__(self) -> None: - self.initial_state = None - self.k = None - - memory_cls = MemoryEulerForward - else: - raise NotImplementedError("Making memory for other types not supported") - - return MemoryCollection(memory_cls(), len(system)) - - -M = TypeVar("M", bound="MemoryCollection") - - -class MemoryCollection(Generic[M]): - """Slots of memories for timestepper in a cohesive unit. - - A `MemoryCollection` object is meant to be used in conjunction - with a `SystemCollection`, where each independent `System` to - be integrated has its own `Memory`. - - Example - ------- - - A RK4 integrator needs to store k_1, k_2, k_3, k_4 (intermediate - results from four stages) for each `System`. The restriction for - having a memory slot arises because the `Systems` are usually - not independent of one another and may need communication after - every stage. - """ - - def __init__(self, memory: M, n_memory_slots: int): - super(MemoryCollection, self).__init__() - - self.__memories: list[M] = [] - for _ in range(n_memory_slots - 1): - self.__memories.append(copy(memory)) - self.__memories.append(memory) - - def __getitem__(self, idx: int) -> M: - return self.__memories[idx] - - def __len__(self) -> int: - return len(self.__memories) - - def __iter__(self) -> Iterator[M]: - return self.__memories.__iter__() diff --git a/elastica/experimental/timestepper/protocol.py b/elastica/experimental/timestepper/protocol.py deleted file mode 100644 index b8d489fab..000000000 --- a/elastica/experimental/timestepper/protocol.py +++ /dev/null @@ -1,86 +0,0 @@ -from typing import Protocol - -from elastica.typing import StepType, StateType -from elastica.systems.protocol import SystemProtocol, SlenderBodyGeometryProtocol -from elastica.timestepper.protocol import StepperProtocol - -import numpy as np - - -class ExplicitSystemProtocol(SystemProtocol, SlenderBodyGeometryProtocol, Protocol): - # TODO: Temporarily made to handle explicit stepper. - # Need to be refactored as the explicit stepper is further developed. - def __call__(self, time: np.float64, dt: np.float64) -> np.float64: ... - @property - def state(self) -> StateType: ... - @state.setter - def state(self, state: StateType) -> None: ... - @property - def n_elems(self) -> int: ... - - -class MemoryProtocol(Protocol): - @property - def initial_state(self) -> bool: ... - - -class ExplicitStepperProtocol(StepperProtocol, Protocol): - """symplectic stepper protocol.""" - - def get_stages(self) -> list[StepType]: ... - - def get_updates(self) -> list[StepType]: ... - - -# class _LinearExponentialIntegratorMixin: -# """ -# Linear Exponential integrator mixin wrapper. -# """ -# -# def __init__(self): -# pass -# -# def _do_stage(self, System, Memory, time, dt): -# # TODO : Make more general, system should not be calculating what the state -# # transition matrix directly is, but rather it should just give -# Memory.linear_operator = System.get_linear_state_transition_operator(time, dt) -# -# def _do_update(self, System, Memory, time, dt): -# # FIXME What's the right formula when doing update? -# # System.linearly_evolving_state = _batch_matmul( -# # System.linearly_evolving_state, -# # Memory.linear_operator -# # ) -# System.linearly_evolving_state = np.einsum( -# "ijk,ljk->ilk", System.linearly_evolving_state, Memory.linear_operator -# ) -# return time + dt -# -# def _first_prefactor(self, dt): -# """Prefactor call to satisfy interface of SymplecticStepper. Should never -# be used in actual code. -# -# Parameters -# ---------- -# dt : the time step of simulation -# -# Raises -# ------ -# RuntimeError -# """ -# raise RuntimeError( -# "Symplectic prefactor of LinearExponentialIntegrator should not be called!" -# ) -# -# # Code repeat! -# # Easy to avoid, but keep for performance. -# def _do_one_step(self, System, time, prefac): -# System.linearly_evolving_state = np.einsum( -# "ijk,ljk->ilk", -# System.linearly_evolving_state, -# System.get_linear_state_transition_operator(time, prefac), -# ) -# return ( -# time # TODO fix hack that treats time separately here. Shuold be time + dt -# ) -# # return time + dt diff --git a/elastica/external_forces.py b/elastica/external_forces.py index 340ba7cd3..54896a042 100644 --- a/elastica/external_forces.py +++ b/elastica/external_forces.py @@ -1,5 +1,4 @@ -__doc__ = """ Numba implementation module for boundary condition implementations that apply -external forces to the system.""" +__doc__ = """Numba implementation module for external forces applied to objects.""" from typing import TypeVar, Generic @@ -69,18 +68,25 @@ class GravityForces(NoForces): """ This class applies a constant gravitational force to the entire rod. - Attributes - ---------- - acc_gravity: numpy.ndarray - 1D (dim) array containing data with 'float' type. Gravitational acceleration vector. + Attributes + ---------- + acc_gravity : numpy.ndarray + 1D (dim) array containing data with 'float' type. Gravitational acceleration vector. + + Examples + -------- + How to apply gravity to a rod: + + >>> simulator.add_forcing_to(rod).using( + ... GravityForces, + ... acc_gravity=np.array([0.0, -9.80665, 0.0]), + ... ) """ def __init__( self, - acc_gravity: NDArray[np.float64] = np.array( - [0.0, -9.80665, 0.0] - ), # FIXME: avoid mutable default + acc_gravity: NDArray[np.float64] | None = None, ) -> None: """ @@ -88,10 +94,14 @@ def __init__( ---------- acc_gravity: numpy.ndarray 1D (dim) array containing data with 'float' type. Gravitational acceleration vector. + Defaults to [0.0, -9.80665, 0.0] if not provided. """ - super(GravityForces, self).__init__() - self.acc_gravity = acc_gravity + super().__init__() + if acc_gravity is None: + acc_gravity = np.array([0.0, -9.80665, 0.0]) + assert len(acc_gravity) == 3, "Gravity acceleration vector must be 3D" + self.acc_gravity = np.array(acc_gravity) def apply_forces( self, system: "RodType | RigidBodyType", time: np.float64 = np.float64(0.0) @@ -108,7 +118,7 @@ def compute_gravity_forces( external_forces: NDArray[np.float64], ) -> None: """ - This function add gravitational forces on the nodes. We are + This function adds gravitational forces on the nodes. We are using njit decorated function to increase the speed. Parameters @@ -128,15 +138,14 @@ class EndpointForces(NoForces): """ This class applies constant forces on the endpoint nodes. - Attributes - ---------- - start_force: numpy.ndarray - 1D (dim) array containing data with 'float' type. Force applied to first node of the system. - end_force: numpy.ndarray - 1D (dim) array containing data with 'float' type. Force applied to last node of the system. - ramp_up_time: float - Applied forces are ramped up until ramp up time. - + Attributes + ---------- + start_force: numpy.ndarray + 1D (dim) array containing data with 'float' type. Force applied to first node of the system. + end_force: numpy.ndarray + 1D (dim) array containing data with 'float' type. Force applied to last node of the system. + ramp_up_time: float + Applied forces are ramped up until ramp up time. """ def __init__( @@ -194,13 +203,14 @@ def compute_end_point_forces( 2D (dim, blocksize) array containing data with 'float' type. External force vector. start_force: numpy.ndarray 1D (dim) array containing data with 'float' type. + Force applied to first node of the system. end_force: numpy.ndarray 1D (dim) array containing data with 'float' type. Force applied to last node of the system. time: float + The time of simulation. ramp_up_time: float Applied forces are ramped up until ramp up time. - """ factor = min(1.0, float(time / ramp_up_time)) external_forces[..., 0] += start_force * factor @@ -211,19 +221,16 @@ class UniformTorques(NoForces): """ This class applies a uniform torque to the entire rod. - Attributes - ---------- - torque: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. Total torque applied to a rod-like object. - + Attributes + ---------- + torque : numpy.ndarray + 2D (dim, 1) array containing data with 'float' type. Total torque applied to a rod-like object. """ def __init__( self, torque: np.float64, - direction: NDArray[np.float64] = np.array( - [0.0, 0.0, 0.0] - ), # FIXME: avoid mutable default + direction: NDArray[np.float64] | None = None, ) -> None: """ @@ -233,9 +240,11 @@ def __init__( Torque magnitude applied to a rod-like object. direction: numpy.ndarray 1D (dim) array containing data with 'float' type. - Direction in which torque applied. + Direction in which torque applied. Defaults to [0.0, 0.0, 0.0] if not provided. """ super(UniformTorques, self).__init__() + if direction is None: + direction = np.array([0.0, 0.0, 0.0]) self.torque = torque * direction def apply_torques( @@ -254,18 +263,16 @@ class UniformForces(NoForces): """ This class applies a uniform force to the entire rod. - Attributes - ---------- - force: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. Total force applied to a rod-like object. + Attributes + ---------- + force : numpy.ndarray + 2D (dim, 1) array containing data with 'float' type. Total force applied to a rod-like object. """ def __init__( self, force: np.float64, - direction: NDArray[np.float64] = np.array( - [0.0, 0.0, 0.0] - ), # FIXME: avoid mutable default + direction: NDArray[np.float64] | None = None, ) -> None: """ @@ -275,9 +282,11 @@ def __init__( Force magnitude applied to a rod-like object. direction: numpy.ndarray 1D (dim) array containing data with 'float' type. - Direction in which force applied. + Direction in which force applied. Defaults to [0.0, 0.0, 0.0] if not provided. """ super(UniformForces, self).__init__() + if direction is None: + direction = np.array([0.0, 0.0, 0.0]) self.force = (force * direction).reshape(3, 1) def apply_forces( @@ -300,26 +309,25 @@ class MuscleTorques(NoForces): as a traveling wave. For implementation details refer to Gazzola et. al. RSoS. (2018). - Attributes - ---------- - direction: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. Muscle torque direction. - angular_frequency: float - Angular frequency of traveling wave. - wave_number: float - Wave number of traveling wave. - phase_shift: float - Phase shift of traveling wave. - ramp_up_time: float - Applied muscle torques are ramped up until ramp up time. - my_spline: numpy.ndarray - 1D (blocksize) array containing data with 'float' type. Generated spline. - + Attributes + ---------- + direction: numpy.ndarray + 2D (dim, 1) array containing data with 'float' type. Muscle torque direction. + angular_frequency: float + Angular frequency of traveling wave. + wave_number: float + Wave number of traveling wave. + phase_shift: float + Phase shift of traveling wave. + ramp_up_time: float + Applied muscle torques are ramped up until ramp up time. + my_spline: numpy.ndarray + 1D (blocksize) array containing data with 'float' type. Generated spline. """ def __init__( self, - base_length: float, # TODO: Is this necessary? + base_length: float, b_coeff: NDArray[np.float64], period: float, wave_number: float, @@ -334,8 +342,10 @@ def __init__( Parameters ---------- base_length: float - Rest length of the rod-like object. - b_coeff: nump.ndarray + Rest length of the rod-like object. This parameter is used to + normalize the spatial coordinate along the rod for the traveling + wave calculation. + b_coeff: numpy.ndarray 1D array containing data with 'float' type. Beta coefficients for beta-spline. period: float @@ -346,7 +356,10 @@ def __init__( Phase shift of traveling wave. direction: numpy.ndarray 1D (dim) array containing data with 'float' type. Muscle torque direction. - ramp_up_time: np.float64 + rest_lengths: numpy.ndarray + 1D (n_elems) array containing data with 'float' type. + Rod element lengths at rest configuration. + ramp_up_time: float Applied muscle torques are ramped up until ramp up time. with_spline: boolean Option to use beta-spline. @@ -427,7 +440,7 @@ def compute_muscle_torques( external_torques[..., 1:], _batch_matvec(director_collection, torque)[..., 1:], ) - inplace_substraction( + inplace_subtraction( external_torques[..., :-1], _batch_matvec(director_collection[..., :-1], torque[..., 1:]), ) @@ -460,23 +473,22 @@ def inplace_addition( @njit(cache=True) # type: ignore -def inplace_substraction( +def inplace_subtraction( external_force_or_torque: NDArray[np.float64], force_or_torque: NDArray[np.float64], ) -> None: """ - This function does inplace substraction. First argument + This function does inplace subtraction. First argument `external_force_or_torque` is the system.external_forces or system.external_torques. Second argument force or torque - vector to be substracted. + vector to be subtracted. + Parameters ---------- external_force_or_torque: numpy.ndarray 2D (dim, blocksize) array containing data with 'float' type. force_or_torque: numpy.ndarray 2D (dim, blocksize) array containing data with 'float' type. - - """ blocksize = force_or_torque.shape[1] for i in range(3): @@ -489,25 +501,24 @@ class EndpointForcesSinusoidal(NoForces): This class applies sinusoidally varying forces to the ends of a rod. Forces are applied in a plane, which is defined by the tangent_direction and normal_direction. - Attributes - ---------- - start_force_mag: float - Magnitude of the force that is applied to the start of the rod (node 0). - end_force_mag: float - Magnitude of the force that is applied to the end of the rod (node -1). - ramp_up_time: float - Applied forces are applied in the normal direction until time reaches ramp_up_time. - normal_direction: np.ndarray - An array (3,) contains type float. - This is the normal direction of the rod. - roll_direction: np.ndarray - An array (3,) contains type float. - This is the direction perpendicular to rod tangent, and rod normal. - - Notes - ----- - In order to see example how to use this class, see joint examples. + Attributes + ---------- + start_force_mag: float + Magnitude of the force that is applied to the start of the rod (node 0). + end_force_mag: float + Magnitude of the force that is applied to the end of the rod (node -1). + ramp_up_time: float + Applied forces are applied in the normal direction until time reaches ramp_up_time. + normal_direction: numpy.ndarray + 1D (3,) array containing data with 'float' type. + This is the normal direction of the rod. + roll_direction: numpy.ndarray + 1D (3,) array containing data with 'float' type. + This is the direction perpendicular to rod tangent, and rod normal. + Notes + ----- + In order to see example how to use this class, see joint examples. """ def __init__( @@ -515,15 +526,10 @@ def __init__( start_force_mag: float, end_force_mag: float, ramp_up_time: float = 0.0, - tangent_direction: NDArray[np.floating] = np.array( - [0.0, 0.0, 1.0] - ), # FIXME: avoid mutable default - normal_direction: NDArray[np.floating] = np.array( - [0.0, 1.0, 0.0] - ), # FIXME: avoid mutable default + tangent_direction: NDArray[np.floating] | None = None, + normal_direction: NDArray[np.floating] | None = None, ) -> None: """ - Parameters ---------- start_force_mag: float @@ -531,13 +537,14 @@ def __init__( end_force_mag: float Magnitude of the force that is applied to the end of the system (node -1). ramp_up_time: float - Applied muscle torques are ramped up until ramp up time. - tangent_direction: np.ndarray - An array (3,) contains type float. + Applied forces are ramped up until ramp up time. + tangent_direction: numpy.ndarray + 1D (3,) array containing data with 'float' type. This is the tangent direction of the system, or normal of the plane that forces applied. - normal_direction: np.ndarray - An array (3,) contains type float. - This is the normal direction of the system. + Defaults to [0.0, 0.0, 1.0] if not provided. + normal_direction: numpy.ndarray + 1D (3,) array containing data with 'float' type. + This is the normal direction of the system. Defaults to [0.0, 1.0, 0.0] if not provided. """ super(EndpointForcesSinusoidal, self).__init__() # Start force @@ -545,6 +552,10 @@ def __init__( self.end_force_mag = np.float64(end_force_mag) # Applied force directions + if normal_direction is None: + normal_direction = np.array([0.0, 1.0, 0.0]) + if tangent_direction is None: + tangent_direction = np.array([0.0, 0.0, 1.0]) self.normal_direction = normal_direction self.roll_direction = np.cross(normal_direction, tangent_direction) diff --git a/elastica/interaction.py b/elastica/interaction.py index 1a72650d0..ab5c8d2d0 100644 --- a/elastica/interaction.py +++ b/elastica/interaction.py @@ -9,8 +9,6 @@ _node_to_element_velocity, ) from elastica._contact_functions import ( - _calculate_contact_forces_rod_plane, - _calculate_contact_forces_rod_plane_with_anisotropic_friction, _calculate_contact_forces_cylinder_plane, ) @@ -19,213 +17,6 @@ from elastica.typing import SystemType, RodType, RigidBodyType -# base class for interaction -# only applies normal force no friction -class InteractionPlane(NoForces): - """ - The interaction plane class computes the plane reaction - force on a rod-like object. For more details regarding the contact module refer to - Eqn 4.8 of Gazzola et al. RSoS (2018). - - Attributes - ---------- - k: float - Stiffness coefficient between the plane and the rod-like object. - nu: float - Dissipation coefficient between the plane and the rod-like object. - plane_origin: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - Origin of the plane. - plane_normal: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - The normal vector of the plane. - surface_tol: float - Penetration tolerance between the plane and the rod-like object. - - """ - - def __init__( - self, - k: float, - nu: float, - plane_origin: NDArray[np.float64], - plane_normal: NDArray[np.float64], - ) -> None: - """ - - Parameters - ---------- - k: float - Stiffness coefficient between the plane and the rod-like object. - nu: float - Dissipation coefficient between the plane and the rod-like object. - plane_origin: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - Origin of the plane. - plane_normal: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - The normal vector of the plane. - """ - self.k = np.float64(k) - self.nu = np.float64(nu) - self.surface_tol = np.float64(1e-4) - self.plane_origin = plane_origin.reshape(3, 1) - self.plane_normal = plane_normal.reshape(3) - - def apply_forces(self, system: RodType, time: np.float64 = np.float64(0.0)) -> None: - """ - In the case of contact with the plane, this function computes the plane reaction force on the element. - - Parameters - ---------- - system: object - Rod-like object. - - Returns - ------- - plane_response_force_mag : numpy.ndarray - 1D (blocksize) array containing data with 'float' type. - Magnitude of plane response force acting on rod-like object. - no_contact_point_idx : numpy.ndarray - 1D (blocksize) array containing data with 'int' type. - Index of rod-like object elements that are not in contact with the plane. - """ - return _calculate_contact_forces_rod_plane( - self.plane_origin, - self.plane_normal, - self.surface_tol, - self.k, - self.nu, - system.radius, - system.mass, - system.position_collection, - system.velocity_collection, - system.internal_forces, - system.external_forces, - ) - - -# class for anisotropic frictional plane -# NOTE: friction coefficients are passed as arrays in the order -# mu_forward : mu_backward : mu_sideways -# head is at x[0] and forward means head to tail -# same convention for kinetic and static -# mu named as to which direction it opposes -class AnisotropicFrictionalPlane(InteractionPlane): - """ - This anisotropic friction plane class is for computing - anisotropic friction forces on rods. - A detailed explanation of the implemented equations - can be found in Gazzola et al. RSoS. (2018). - - Attributes - ---------- - k: float - Stiffness coefficient between the plane and the rod-like object. - nu: float - Dissipation coefficient between the plane and the rod-like object. - plane_origin: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - Origin of the plane. - plane_normal: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - The normal vector of the plane. - slip_velocity_tol: float - Velocity tolerance to determine if the element is slipping or not. - static_mu_array: numpy.ndarray - 1D (3,) array containing data with 'float' type. - [forward, backward, sideways] static friction coefficients. - kinetic_mu_array: numpy.ndarray - 1D (3,) array containing data with 'float' type. - [forward, backward, sideways] kinetic friction coefficients. - """ - - def __init__( - self, - k: float, - nu: float, - plane_origin: NDArray[np.float64], - plane_normal: NDArray[np.float64], - slip_velocity_tol: float, - static_mu_array: NDArray[np.float64], - kinetic_mu_array: NDArray[np.float64], - ) -> None: - """ - - Parameters - ---------- - k: float - Stiffness coefficient between the plane and the rod-like object. - nu: float - Dissipation coefficient between the plane and the rod-like object. - plane_origin: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - Origin of the plane. - plane_normal: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - The normal vector of the plane. - slip_velocity_tol: float - Velocity tolerance to determine if the element is slipping or not. - static_mu_array: numpy.ndarray - 1D (3,) array containing data with 'float' type. - [forward, backward, sideways] static friction coefficients. - kinetic_mu_array: numpy.ndarray - 1D (3,) array containing data with 'float' type. - [forward, backward, sideways] kinetic friction coefficients. - """ - InteractionPlane.__init__(self, k, nu, plane_origin, plane_normal) - self.slip_velocity_tol = np.float64(slip_velocity_tol) - ( - self.static_mu_forward, - self.static_mu_backward, - self.static_mu_sideways, - ) = static_mu_array - ( - self.kinetic_mu_forward, - self.kinetic_mu_backward, - self.kinetic_mu_sideways, - ) = kinetic_mu_array - - # kinetic and static friction should separate functions - # for now putting them together to figure out common variables - def apply_forces( - self, system: "RodType | RigidBodyType", time: np.float64 = np.float64(0.0) - ) -> None: - """ - Call numba implementation to apply friction forces - Parameters - ---------- - system : RodType | RigidBodyType - time - - """ - _calculate_contact_forces_rod_plane_with_anisotropic_friction( - self.plane_origin, - self.plane_normal, - self.surface_tol, - self.slip_velocity_tol, - self.k, - self.nu, - self.kinetic_mu_forward, - self.kinetic_mu_backward, - self.kinetic_mu_sideways, - self.static_mu_forward, - self.static_mu_backward, - self.static_mu_sideways, - system.radius, - system.mass, - system.tangents, - system.position_collection, - system.director_collection, - system.velocity_collection, - system.omega_collection, - system.internal_forces, - system.external_forces, - system.internal_torques, - system.external_torques, - ) - - # Slender body module @njit(cache=True) # type: ignore def sum_over_elements(input: NDArray[np.float64]) -> np.float64: @@ -243,9 +34,7 @@ def sum_over_elements(input: NDArray[np.float64]) -> np.float64: ------- float - """ - """ - Developer Note + Notes ----- Faster than sum(), .sum() and np.sum() @@ -295,7 +84,7 @@ def slender_body_forces( Rod-like object velocity collection. dynamic_viscosity: float Dynamic viscosity of the fluid. - length: numpy.ndarray + lengths: numpy.ndarray 1D (blocksize) array containing data with 'float' type. Rod-like object element lengths. radius: numpy.ndarray @@ -379,11 +168,10 @@ class SlenderBodyTheory(NoForces): forces on the body using the slender body theory given in Eq. 4.13 of Gazzola et al. RSoS (2018). - Attributes - ---------- - dynamic_viscosity: float - Dynamic viscosity of the fluid. - + Attributes + ---------- + dynamic_viscosity: float + Dynamic viscosity of the fluid. """ def __init__(self, dynamic_viscosity: float) -> None: @@ -420,9 +208,14 @@ def apply_forces(self, system: RodType, time: np.float64 = np.float64(0.0)) -> N _elements_to_nodes_inplace(stokes_force, system.external_forces) -# base class for interaction -# only applies normal force no friction class InteractionPlaneRigidBody(NoForces): + """ + Interaction class for applying contact forces between a rigid body and a plane. + + This class applies normal contact forces (no friction) when a rigid body + contacts a plane surface. + """ + def __init__( self, k: float, @@ -430,6 +223,23 @@ def __init__( plane_origin: NDArray[np.float64], plane_normal: NDArray[np.float64], ) -> None: + """ + Initialize the plane-rigid body interaction. + + Parameters + ---------- + k : float + Contact spring constant. + nu : float + Contact damping constant. + plane_origin : numpy.ndarray + 1D (3,) or 2D (3, 1) array containing data with 'float' type. + Origin of the plane. + plane_normal : numpy.ndarray + 1D (3,) array containing data with 'float' type. + Normal vector of the plane. + + """ self.k = np.float64(k) self.nu = np.float64(nu) self.surface_tol = np.float64(1e-4) @@ -440,16 +250,16 @@ def apply_forces( self, system: RigidBodyType, time: np.float64 = np.float64(0.0) ) -> None: """ - This function computes the plane force response on the rigid body, in the - case of contact. Contact model given in Eqn 4.8 Gazzola et. al. RSoS 2018 paper - is used. + Compute the plane force response on the rigid body in the case of contact. + + Contact model given in Eqn 4.8 Gazzola et. al. RSoS 2018 paper is used. + Parameters ---------- - system - - Returns - ------- - magnitude of the plane response + system : RigidBodyType + Rigid body object. + time : float + The time of simulation. """ _calculate_contact_forces_cylinder_plane( self.plane_origin, diff --git a/elastica/joint.py b/elastica/joint.py index 62555cef1..1892d99f7 100644 --- a/elastica/joint.py +++ b/elastica/joint.py @@ -1,5 +1,8 @@ __doc__ = """ Module containing joint classes to connect multiple rods together. """ -__all__ = ["FreeJoint", "HingeJoint", "FixedJoint", "get_relative_rotation_two_systems"] +__all__ = ["FreeJoint", "HingeJoint", "FixedJoint"] + +from typing import TypeVar, Generic +from abc import ABC, abstractmethod from elastica._rotations import _inv_rotate from elastica.typing import SystemType, RodType, ConnectionIndex, RigidBodyType @@ -8,23 +11,79 @@ from numpy.typing import NDArray -class FreeJoint: +S = TypeVar("S", bound=SystemType) + + +class ConnectionBase(ABC, Generic[S]): + """ + This Connection base class is for all system-to-system connections. + Every operator for Connections must be derived from this class. """ - This free joint class is the base class for all joints. Free or spherical - joints constrains the relative movement between two nodes (chosen by the user) + + @abstractmethod + def apply_forces( + self, + system_one: "RodType | RigidBodyType", + index_one: ConnectionIndex, + system_two: "RodType | RigidBodyType", + index_two: ConnectionIndex, + time: np.float64 = np.float64(0.0), + ) -> None: + """ + Apply connection force to the connected objects. + + Parameters + ---------- + system_one : RodType | RigidBodyType + Rod or rigid-body object + index_one : ConnectionIndex + Index of first system for connection. + system_two : RodType | RigidBodyType + Rod or rigid-body object + index_two : ConnectionIndex + Index of second system for connection. + """ + + @abstractmethod + def apply_torques( + self, + system_one: "RodType | RigidBodyType", + index_one: ConnectionIndex, + system_two: "RodType | RigidBodyType", + index_two: ConnectionIndex, + time: np.float64 = np.float64(0.0), + ) -> None: + """ + Apply connection torques to the connected objects. + + Parameters + ---------- + system_one : RodType | RigidBodyType + Rod or rigid-body object + index_one : ConnectionIndex + Index of first system for connection + system_two : RodType | RigidBodyType + Rod or rigid-body object + index_two : ConnectionIndex + Index of second system for connection. + """ + + +class FreeJoint(ConnectionBase): + """ + Free or spherical joints constrains the relative movement between two nodes (chosen by the user) by applying restoring forces. For implementation details, refer to Zhang et al. Nature Communications (2019). Notes ----- - Every new joint class must be derived from the FreeJoint class. - - Attributes - ---------- - k: float - Stiffness coefficient of the joint. - nu: float - Damping coefficient of the joint. + Alias for BallJoint and SphericalJoint + Attributes + ---------- + k: float + Stiffness coefficient of the joint. + nu: float + Damping coefficient of the joint. """ # pass the k and nu for the forces @@ -60,15 +119,11 @@ def apply_forces( system_one : RodType | RigidBodyType Rod or rigid-body object index_one : ConnectionIndex - Index of first rod for joint. + Index of first system for connection. system_two : RodType | RigidBodyType Rod or rigid-body object index_two : ConnectionIndex - Index of second rod for joint. - - Returns - ------- - + Index of second system for connection. """ end_distance_vector = ( system_two.position_collection[..., index_two] @@ -86,8 +141,6 @@ def apply_forces( system_one.external_forces[..., index_one] += contact_force system_two.external_forces[..., index_two] -= contact_force - return - def apply_torques( self, system_one: "RodType | RigidBodyType", @@ -97,26 +150,28 @@ def apply_torques( time: np.float64 = np.float64(0.0), ) -> None: """ - Apply restoring joint torques to the connected rod objects. + Apply joint torques to the connected objects. In FreeJoint class, this routine simply passes. Parameters ---------- system_one : RodType | RigidBodyType - Rod or rigid-body object + Rod or rigid-body object. index_one : ConnectionIndex - Index of first rod for joint. + Index of first system for connection. system_two : RodType | RigidBodyType - Rod or rigid-body object + Rod or rigid-body object. index_two : ConnectionIndex - Index of second rod for joint. + Index of second system for connection. + time : float + The time of simulation. + """ - Returns - ------- - """ - pass +# ALIAS +BallJoint = FreeJoint +SphericalJoint = FreeJoint class HingeJoint(FreeJoint): @@ -127,19 +182,18 @@ class HingeJoint(FreeJoint): implementation details, refer to Zhang et. al. Nature Communications (2019). - Attributes - ---------- - k: float - Stiffness coefficient of the joint. - nu: float - Damping coefficient of the joint. - kt: float - Rotational stiffness coefficient of the joint. - normal_direction: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. Constraint rotation direction. + Attributes + ---------- + k: float + Stiffness coefficient of the joint. + nu: float + Damping coefficient of the joint. + kt: float + Rotational stiffness coefficient of the joint. + normal_direction: numpy.ndarray + 2D (dim, 1) array containing data with 'float' type. Constraint rotation direction. """ - # TODO: IN WRAPPER COMPUTE THE NORMAL DIRECTION OR ASK USER TO GIVE INPUT, IF NOT THROW ERROR def __init__( self, k: float, @@ -215,25 +269,25 @@ class FixedJoint(FreeJoint): For implementation details, refer to Zhang et al. Nature Communications (2019). - Notes - ----- - Issue #131 : Add constraint in twisting, add rest_rotation_matrix (v0.3.0) + Notes + ----- + Issue #131 : Add constraint in twisting, add rest_rotation_matrix (v0.3.0) - Attributes - ---------- - k: float - Stiffness coefficient of the joint. - nu: float - Damping coefficient of the joint. - kt: float - Rotational stiffness coefficient of the joint. - nut: float - Rotational damping coefficient of the joint. - rest_rotation_matrix: np.array - 2D (3,3) array containing data with 'float' type. - Rest 3x3 rotation matrix from system one to system two at the connected elements. - Instead of aligning the directors of both systems directly, a desired rest rotational matrix labeled C_12* - is enforced. + Attributes + ---------- + k: float + Stiffness coefficient of the joint. + nu: float + Damping coefficient of the joint. + kt: float + Rotational stiffness coefficient of the joint. + nut: float + Rotational damping coefficient of the joint. + rest_rotation_matrix: numpy.ndarray + 2D (3, 3) array containing data with 'float' type. + Rest 3x3 rotation matrix from system one to system two at the connected elements. + Instead of aligning the directors of both systems directly, a desired rest rotational matrix labeled C_12* + is enforced. """ def __init__( @@ -245,7 +299,6 @@ def __init__( rest_rotation_matrix: NDArray[np.float64] | None = None, ) -> None: """ - Parameters ---------- k: float @@ -254,10 +307,10 @@ def __init__( Damping coefficient of the joint. kt: float Rotational stiffness coefficient of the joint. - nut: float = 0. + nut: float Rotational damping coefficient of the joint. - rest_rotation_matrix: np.array | None - 2D (3,3) array containing data with 'float' type. + rest_rotation_matrix: numpy.ndarray | None + 2D (3, 3) array containing data with 'float' type. Rest 3x3 rotation matrix from system one to system two at the connected elements. If provided, the rest rotation matrix is enforced between the two systems throughout the simulation. If not provided, `rest_rotation_matrix` is initialized to the identity matrix, @@ -337,55 +390,3 @@ def apply_torques( # The opposite torques will be applied to system one and two after rotating the torques into the local frame system_one.external_torques[..., index_one] -= system_one_director @ torque system_two.external_torques[..., index_two] += system_two_director @ torque - - -def get_relative_rotation_two_systems( - system_one: "RodType | RigidBodyType", - index_one: ConnectionIndex, - system_two: "RodType | RigidBodyType", - index_two: ConnectionIndex, -) -> NDArray[np.float64]: - """ - Compute the relative rotation matrix C_12 between system one and system two at the specified elements. - - Examples - ---------- - How to get the relative rotation between two systems (e.g. the rotation from end of rod one to base of rod two): - - >>> rel_rot_mat = get_relative_rotation_two_systems(system1, -1, system2, 0) - - How to initialize a FixedJoint with a rest rotation between the two systems, - which is enforced throughout the simulation: - - >>> simulator.connect( - ... first_rod=system1, second_rod=system2, first_connect_idx=-1, second_connect_idx=0 - ... ).using( - ... FixedJoint, - ... ku=1e6, nu=0.0, kt=1e3, nut=0.0, - ... rest_rotation_matrix=get_relative_rotation_two_systems(system1, -1, system2, 0) - ... ) - - See Also - --------- - FixedJoint - - Parameters - ---------- - system_one : RodType | RigidBodyType - Rod or rigid-body object - index_one : ConnectionIndex - Index of first rod for joint. - system_two : RodType | RigidBodyType - Rod or rigid-body object - index_two : ConnectionIndex - Index of second rod for joint. - - Returns - ------- - relative_rotation_matrix : np.array - Relative rotation matrix C_12 between the two systems for their current state. - """ - return ( - system_one.director_collection[..., index_one] - @ system_two.director_collection[..., index_two].T - ) diff --git a/elastica/memory_block/memory_block_rod.py b/elastica/memory_block/memory_block_rod.py index a47998bac..8940328f3 100644 --- a/elastica/memory_block/memory_block_rod.py +++ b/elastica/memory_block/memory_block_rod.py @@ -22,12 +22,73 @@ class MemoryBlockCosseratRod(CosseratRod, _RodSymplecticStepperMixin): """ - Memory block class for Cosserat rod equations. This class is derived from Cosserat Rod class in order to inherit - the methods of Cosserat rod class. This class takes the cosserat rod object (systems) and creates big - arrays to store the system data and returns a reference of that data to the systems. - Thus each system is now in contiguous memory, so it is faster to compute Cosserat rod equations. - - TODO: need more documentation! + Memory block class for Cosserat rod equations. + + This class is derived from CosseratRod to inherit all rod methods while providing + a memory-efficient block structure for multiple rod systems. It takes a collection + of CosseratRod objects and creates contiguous memory blocks to store all system data, + allowing for faster computation of Cosserat rod equations through better cache locality + and vectorized operations. + + The class separates rods into straight rods and ring rods (periodic boundary conditions), + and handles ghost nodes, elements, and Voronoi indices for proper boundary conditions. + All rod data is stored in contiguous arrays, with references maintained to the original + rod objects for compatibility. + + Parameters + ---------- + systems : list[RodType] + List of CosseratRod objects to be included in the memory block structure. + Rods are automatically separated into straight rods and ring rods based on + their `ring_rod_flag` attribute. + system_idx_list : list[SystemIdxType] + List of system indices corresponding to each rod in the `systems` list. + These indices are used to map rods back to their original positions in + the simulator's system collection. + + Attributes + ---------- + n_systems : int + Total number of rod systems in the memory block. + n_rods : int + Total number of rods (same as n_systems). + n_elems : int + Total number of elements across all rods in the block structure. + n_nodes : int + Total number of nodes across all rods (n_elems + 1). + n_voronoi : int + Total number of Voronoi points across all rods (n_elems - 1). + ring_rod_flag : bool + Flag indicating if any ring rods are present in the block. + system_idx_list : numpy.ndarray + Array of system indices mapping rods to their original positions. + ghost_nodes_idx : numpy.ndarray + Indices of ghost nodes used for boundary conditions. + ghost_elems_idx : numpy.ndarray + Indices of ghost elements used for boundary conditions. + ghost_voronoi_idx : numpy.ndarray + Indices of ghost Voronoi points used for boundary conditions. + periodic_boundary_nodes_idx : numpy.ndarray + Indices of periodic boundary nodes for ring rods. + periodic_boundary_elems_idx : numpy.ndarray + Indices of periodic boundary elements for ring rods. + periodic_boundary_voronoi_idx : numpy.ndarray + Indices of periodic boundary Voronoi points for ring rods. + + Notes + ----- + - Straight rods are placed first in memory, followed by ring rods. + - Ring rods require additional periodic boundary nodes, elements, and Voronoi points + to maintain compatibility with the block structure implementation. + - Ghost nodes/elements/Voronoi are used to handle boundaries between rods and + periodic boundaries for ring rods. + - All rod data (positions, directors, velocities, etc.) is stored in contiguous + memory blocks for efficient computation. + + See Also + -------- + CosseratRod : Base class for Cosserat rod systems + _RodSymplecticStepperMixin : Mixin providing symplectic stepper interface """ def __init__( @@ -199,9 +260,6 @@ def __init__( self.rest_kappa, self.periodic_boundary_voronoi_idx ) - # Initialize the mixin class for symplectic time-stepper. - _RodSymplecticStepperMixin.__init__(self) - def _allocate_block_variables_in_nodes(self, systems: list[RodType]) -> None: """ This function takes system collection and allocates the variables on diff --git a/elastica/memory_block/memory_block_rod_base.py b/elastica/memory_block/memory_block_rod_base.py deleted file mode 100644 index 87fcdd6d4..000000000 --- a/elastica/memory_block/memory_block_rod_base.py +++ /dev/null @@ -1,8 +0,0 @@ -__doc__ = """Deprecated module. Use memory_blocks.utils instead.""" -import numpy as np -import numpy.typing as npt - -from .utils import ( - make_block_memory_metadata, - make_block_memory_periodic_boundary_metadata, -) diff --git a/elastica/memory_block/protocol.py b/elastica/memory_block/protocol.py index 51cd57d51..4b527f778 100644 --- a/elastica/memory_block/protocol.py +++ b/elastica/memory_block/protocol.py @@ -1,22 +1,22 @@ -from typing import Protocol -from elastica.rod.protocol import CosseratRodProtocol -from elastica.rigidbody.protocol import RigidBodyProtocol +from typing import Protocol, runtime_checkable +from elastica.typing import StaticSystemType, SystemIdxType from elastica.systems.protocol import SystemProtocol -class BlockProtocol(Protocol): +@runtime_checkable +class BlockSystemProtocol(SystemProtocol, Protocol): + """ + Protocol for block systems. + Block systems are systems that are used to store the data of multiple systems. + """ + + def __init__( + self, systems: list[StaticSystemType], system_idx_list: list[SystemIdxType] + ) -> None: + """ + Block initializer takes the list of systems and the list of system indices. + """ + @property def n_systems(self) -> int: """Number of systems in the block.""" - - -class BlockSystemProtocol(SystemProtocol, BlockProtocol, Protocol): - pass - - -class BlockRodProtocol(BlockProtocol, CosseratRodProtocol, Protocol): - pass - - -class BlockRigidBodyProtocol(BlockProtocol, RigidBodyProtocol, Protocol): - pass diff --git a/elastica/modules/base_system.py b/elastica/modules/base_system.py index abd7d8c3c..178f74521 100644 --- a/elastica/modules/base_system.py +++ b/elastica/modules/base_system.py @@ -5,7 +5,7 @@ Basic coordinating for multiple, smaller systems that have an independently integrable interface (i.e. works with symplectic or explicit routines `timestepper.py`.) """ -from typing import TYPE_CHECKING, Type, Generator, Any, overload +from typing import TYPE_CHECKING, Type, Generator, Any, overload, Callable from typing import final from elastica.typing import ( SystemType, @@ -20,34 +20,40 @@ import numpy as np from itertools import chain +from collections import defaultdict from collections.abc import MutableSequence -from elastica.rod.rod_base import RodBase -from elastica.rigidbody.rigid_body import RigidBodyBase -from elastica.surface.surface_base import SurfaceBase +from elastica.systems.protocol import StaticSystemProtocol, SystemProtocol +from elastica.memory_block.protocol import BlockSystemProtocol + +from elastica.memory_block.memory_block_rod import MemoryBlockCosseratRod +from elastica.memory_block.memory_block_rigid_body import MemoryBlockRigidBody from .memory_block import construct_memory_block_structures from .operator_group import OperatorGroupFIFO from .protocol import ModuleProtocol +from ..rod.cosserat_rod import CosseratRod +from ..rigidbody.sphere import Sphere +from ..rigidbody.cylinder import Cylinder class BaseSystemCollection(MutableSequence): """ Base System for simulator classes. Every simulation class written by the user must be derived from the BaseSystemCollection class; otherwise the simulation will - proceed. - - Attributes - ---------- - allowed_sys_types: tuple[Type] - Tuple of allowed type rod-like objects. Here use a base class for objects, i.e. RodBase. - systems: Callable - Returns all system objects. Once finalize, block objects are also included. - blocks: Callable - Returns block objects. Should be called after finalize. - - Note - ---- + not proceed. + + Attributes + ---------- + allowed_sys_types: tuple[Type] + Tuple of allowed type rod-like objects. Here use a base class for objects, i.e. RodBase. + systems: Callable + Returns all system objects. Once finalize, block objects are also included. + blocks: Callable + Returns block objects. Should be called after finalize. + + Notes + ----- We can directly subclass a list for the most part, but this is a bad idea, as List is non abstract https://stackoverflow.com/q/3945940 @@ -72,20 +78,30 @@ def __init__(self) -> None: self._feature_group_callback: OperatorGroupFIFO[ OperatorCallbackType, ModuleProtocol ] = OperatorGroupFIFO() + self._feature_group_on_close: OperatorGroupFIFO[Callable, ModuleProtocol] = ( + OperatorGroupFIFO() + ) self._feature_group_finalize: list[OperatorFinalizeType] = [] # We need to initialize our mixin classes super().__init__() # List of system types/bases that are allowed - self.allowed_sys_types: tuple[Type, ...] = ( - RodBase, - RigidBodyBase, - SurfaceBase, + # By default, any object that is a subclass of StaticSystemProtocol is allowed. + # (Technically, any object that is conforms StaticSystemProtocol is allowed.) + self.allowed_sys_types: tuple[Type, ...] = (StaticSystemProtocol,) + + # Block support for System types. + # If a system type is not in this dictionary, no block will be constructed for it. + # (Note, block support is defined explicitly, without derivation from BaseSystem.) + self._block_supports: dict[Type[BlockSystemType], list[Type[SystemType]]] = ( + defaultdict(list) ) + self._block_supports[MemoryBlockCosseratRod].append(CosseratRod) + self._block_supports[MemoryBlockRigidBody].extend([Sphere, Cylinder]) # List of systems to be integrated self.__systems: list[StaticSystemType] = [] - self.__final_blocks: list[BlockSystemType] = [] + self.__final_systems: list[SystemType] = [] # Flag Finalize: Finalizing twice will cause an error, # but the error message is very misleading @@ -141,18 +157,64 @@ def __str__(self) -> str: """To be readable""" return str(self.__systems) + @final + def append_allowed_types(self, additional_types: Type[SystemType]) -> None: + """ + Append the allowed system types. + In order to add block support, use `enable_block_supports`. + """ + self.allowed_sys_types += (additional_types,) + @final def extend_allowed_types( self, additional_types: tuple[Type[SystemType], ...] ) -> None: + """ + Extend the allowed system types. Typically used for building custom extensions. + In order to add block support, use `enable_block_supports`. + """ self.allowed_sys_types += additional_types @final - def override_allowed_types( + def _override_allowed_types( self, allowed_types: tuple[Type[SystemType], ...] ) -> None: + """ + Override the allowed system types. + Only used for testing purposes. + """ self.allowed_sys_types = allowed_types + @final + def enable_block_supports( + self, + system_type: Type[SystemType], + block_type: Type[BlockSystemType], + ) -> None: + """ + Enable block support for a system type. + If the system type already has block support enabled, it will be overridden. + (In case user wants different implementation of the memory block.) + + + Parameters + ---------- + system_type: Type[SystemType] + System type to enable block support for. + block_type: Type[BlockSystemType] + Block type to enable for the system type. + + Examples + -------- + >>> simulator.append_allowed_types(CustomRod) + >>> simulator.enable_block_supports(CustomRod, CustomMemoryBlock) + """ + for btype in self._block_supports: + if system_type in self._block_supports[btype]: + self._block_supports[btype].remove(system_type) + break + self._block_supports[block_type].append(system_type) + @final def get_system_index( self, system: "SystemType | StaticSystemType" @@ -161,8 +223,8 @@ def get_system_index( Get the index of the system object in the system list. System list is private, so this is the only way to get the index of the system object. - Example - ------- + Examples + -------- >>> system_collection: SystemCollectionProtocol >>> system: SystemType ... @@ -211,11 +273,12 @@ def systems(self) -> Generator[StaticSystemType, None, None]: yield system @final - def block_systems(self) -> Generator[BlockSystemType, None, None]: + def final_systems(self) -> Generator[SystemType, None, None]: """ - Iterate over all block systems in the system collection. + Iterate over all systems in the system collection. + This generator is used to pass the systems to the timestepper. """ - for block in self.__final_blocks: + for block in self.__final_systems: yield block @final @@ -225,16 +288,26 @@ def finalize(self) -> None: all rod-like objects to the simulator as well as all boundary conditions, callbacks, etc., acting on these rod-like objects. After the finalize method called, the user cannot add new features to the simulator class. + + Parameters + ---------- + verbose: bool + If True, will print verbose output. """ assert not self._finalize_flag, "The finalize cannot be called twice." self._finalize_flag = True # Construct memory block - self.__final_blocks = construct_memory_block_structures(self.__systems) - # FIXME: We need this to make ring-rod working. - # But probably need to be refactored - self.__systems.extend(self.__final_blocks) + blocks, non_blocked_systems = construct_memory_block_structures( + self.__systems, + self._block_supports, + ) + self.__systems.extend(blocks) # blocks are also systems + + # Finalize the list of systems to run stepping. + self.__final_systems.extend(blocks) + self.__final_systems.extend(non_blocked_systems) # Recurrent call finalize functions for all components. for finalize in self._feature_group_finalize: @@ -244,6 +317,9 @@ def finalize(self) -> None: self._feature_group_finalize.clear() del self._feature_group_finalize + # First callback execution + self.apply_callbacks(time=np.float64(0.0), current_step=0) + @final def synchronize(self, time: np.float64) -> None: """ @@ -282,6 +358,15 @@ def apply_callbacks(self, time: np.float64, current_step: int) -> None: for func in self._feature_group_callback: func(time=time, current_step=current_step) + @final + def close(self) -> None: + """ + Call close functions for all features. + Features are registered in _feature_group_on_close. + """ + for func in self._feature_group_on_close: + func() + if TYPE_CHECKING: from .protocol import SystemCollectionProtocol diff --git a/elastica/modules/callbacks.py b/elastica/modules/callbacks.py index 5638f8ba0..e3be9e5a3 100644 --- a/elastica/modules/callbacks.py +++ b/elastica/modules/callbacks.py @@ -1,41 +1,62 @@ +from __future__ import annotations + __doc__ = """ CallBacks ----------- Provides the callBack interface to collect data over time (see `callback_functions.py`). """ -from typing import Type, Any -from typing_extensions import Self # 3.11: from typing import Self -from elastica.typing import SystemType, SystemIdxType, OperatorFinalizeType -from .protocol import ModuleProtocol +from types import EllipsisType +from typing import Type, Any, TypeAlias, cast +from elastica.typing import ( + SystemType, + SystemIdxType, +) +from .protocol import SystemCollectionProtocol, ModuleProtocol import functools -import numpy as np - from elastica.callback_functions import CallBackBaseClass -from .protocol import SystemCollectionWithCallbackProtocol +# Callback takes data-structures and collection of SystemType +SystemIdxDSType: TypeAlias = """ +( + SystemIdxType + | tuple[SystemIdxType, ...] + | list[SystemIdxType] + | dict[Any, SystemIdxType] +) +""" -class CallBacks: +SystemDSType: TypeAlias = """ +( + SystemType | tuple[SystemType, ...] | list[SystemType] | dict[Any, SystemType] +) +""" + + +class CallBacks(SystemCollectionProtocol): """ CallBacks class is a module for calling callback functions, set by the user. If the user wants to collect data from the simulation, the simulator class has to be derived from the CallBacks class. - Attributes - ---------- - _callback_list: list - List of call back classes defined for rod-like objects. + Attributes + ---------- + _callback_list: list + List of call back classes defined for rod-like objects. """ - def __init__(self: SystemCollectionWithCallbackProtocol) -> None: - self._callback_list: list[ModuleProtocol] = [] + _callback_list: list[ModuleProtocol] + + def __init__(self) -> None: + self._callback_list = [] super(CallBacks, self).__init__() self._feature_group_finalize.append(self._finalize_callback) def collect_diagnostics( - self: SystemCollectionWithCallbackProtocol, system: SystemType + self, + system: SystemDSType | EllipsisType, ) -> ModuleProtocol: """ This method calls user-defined call-back classes for a @@ -51,48 +72,70 @@ def collect_diagnostics( ------- """ - sys_idx: SystemIdxType = self.get_system_index(system) + sys_idx: SystemIdxDSType + if system is Ellipsis: + sys_idx = tuple([self.get_system_index(sys) for sys in self.systems()]) + elif isinstance(system, list): + sys_idx = [self.get_system_index(sys) for sys in system] + elif isinstance(system, dict): + sys_idx = {key: self.get_system_index(sys) for key, sys in system.items()} + elif isinstance(system, tuple): + sys_idx = tuple([self.get_system_index(sys) for sys in system]) + else: + # Single entity + sys_idx = self.get_system_index(system) # Create _Constraint object, cache it and return to user _callback: ModuleProtocol = _CallBack(sys_idx) self._callback_list.append(_callback) self._feature_group_callback.append_id(_callback) + self._feature_group_on_close.append_id(_callback) return _callback - def _finalize_callback(self: SystemCollectionWithCallbackProtocol) -> None: + def _finalize_callback(self) -> None: # dev : the first index stores the rod index to collect data. for callback in self._callback_list: sys_id = callback.id() callback_instance = callback.instantiate() + system: SystemDSType + if isinstance(sys_id, (tuple, list)): + _T = type(sys_id) + system = _T([self[sys_id_] for sys_id_ in sys_id]) + elif isinstance(sys_id, dict): + sys_id = cast(dict[Any, SystemIdxType], sys_id) + system = {key: self[sys_id_] for key, sys_id_ in sys_id.items()} + else: + system = self[sys_id] + callback_operator = functools.partial( - callback_instance.make_callback, system=self[sys_id] + callback_instance.make_callback, system=system ) self._feature_group_callback.add_operators(callback, [callback_operator]) + self._feature_group_on_close.add_operators( + callback, [callback_instance.on_close] + ) self._callback_list.clear() del self._callback_list - # First callback execution - self.apply_callbacks(time=np.float64(0.0), current_step=0) - class _CallBack: """ CallBack module private class - Attributes - ---------- - _sys_idx: rod object index - _callback_cls: list - *args - Variable length argument list. - **kwargs - Arbitrary keyword arguments. + Attributes + ---------- + _sys_idx: rod object index + _callback_cls: list + *args + Variable length argument list. + **kwargs + Arbitrary keyword arguments. """ - def __init__(self, sys_idx: SystemIdxType): + def __init__(self, sys_idx: SystemIdxDSType): """ Parameters @@ -100,7 +143,7 @@ def __init__(self, sys_idx: SystemIdxType): sys_idx: int rod object index """ - self._sys_idx: SystemIdxType = sys_idx + self._sys_idx: SystemIdxDSType = sys_idx self._callback_cls: Type[CallBackBaseClass] self._args: Any self._kwargs: Any @@ -110,7 +153,7 @@ def using( cls: Type[CallBackBaseClass], *args: Any, **kwargs: Any, - ) -> Self: + ) -> None: """ This method is a module to set which callback class is used to collect data from user defined rod-like object. @@ -132,9 +175,8 @@ def using( self._callback_cls = cls self._args = args self._kwargs = kwargs - return self - def id(self) -> SystemIdxType: + def id(self) -> SystemIdxDSType: return self._sys_idx def instantiate(self) -> CallBackBaseClass: diff --git a/elastica/modules/connections.py b/elastica/modules/connections.py index 2b92377ce..17f9d7742 100644 --- a/elastica/modules/connections.py +++ b/elastica/modules/connections.py @@ -6,40 +6,40 @@ rigid bodies) using joints (see `joints.py`). """ from typing import Type, cast, Any -from typing_extensions import Self from elastica.typing import ( SystemIdxType, - OperatorFinalizeType, ConnectionIndex, RodType, RigidBodyType, ) import numpy as np import functools -from elastica.joint import FreeJoint +from elastica.joint import ConnectionBase -from .protocol import ConnectedSystemCollectionProtocol, ModuleProtocol +from .protocol import SystemCollectionProtocol, ModuleProtocol -class Connections: +class Connections(SystemCollectionProtocol): """ The Connections class is a module for connecting rod-like objects using joints selected by the user. To connect two rod-like objects, the simulator class must be derived from the Connections class. - Attributes - ---------- - _connections: list - List of joint classes defined for rod-like objects. + Attributes + ---------- + _connections: list + List of joint classes defined for rod-like objects. """ - def __init__(self: ConnectedSystemCollectionProtocol) -> None: - self._connections: list[ModuleProtocol] = [] + _connections: list[ModuleProtocol] + + def __init__(self) -> None: + self._connections = [] super(Connections, self).__init__() self._feature_group_finalize.append(self._finalize_connections) def connect( - self: ConnectedSystemCollectionProtocol, + self, first_rod: "RodType | RigidBodyType", second_rod: "RodType | RigidBodyType", first_connect_idx: ConnectionIndex = (), @@ -81,7 +81,7 @@ def connect( return _connect - def _finalize_connections(self: ConnectedSystemCollectionProtocol) -> None: + def _finalize_connections(self) -> None: # From stored _Connect objects, instantiate the joints and store it # dev : the first indices stores the # (first rod index, second_rod_idx, connection_idx_on_first_rod, connection_idx_on_second_rod) @@ -91,7 +91,7 @@ def _finalize_connections(self: ConnectedSystemCollectionProtocol) -> None: first_sys_idx, second_sys_idx, first_connect_idx, second_connect_idx = ( connection.id() ) - connect_instance: FreeJoint = connection.instantiate() + connect_instance: ConnectionBase = connection.instantiate() func_force = functools.partial( connect_instance.apply_forces, @@ -133,10 +133,6 @@ class _Connect: _connect_class: list first_sys_connection_idx: ConnectionIndex second_sys_connection_idx: ConnectionIndex - *args - Variable length argument list. - **kwargs - Arbitrary keyword arguments. """ def __init__( @@ -161,7 +157,7 @@ def __init__( self._second_sys_n_lim: int = second_sys_nlim self.first_sys_connection_idx: ConnectionIndex = () self.second_sys_connection_idx: ConnectionIndex = () - self._connect_cls: Type[FreeJoint] + self._connect_cls: Type[ConnectionBase] def set_index( self, first_idx: ConnectionIndex, second_idx: ConnectionIndex @@ -244,10 +240,10 @@ def set_index( def using( self, - cls: Type[FreeJoint], + cls: Type[ConnectionBase], *args: Any, **kwargs: Any, - ) -> Self: + ) -> None: """ This method is a module to set which joint class is used to connect user defined rod-like objects. @@ -266,14 +262,13 @@ def using( """ assert issubclass( - cls, FreeJoint - ), "{} is not a valid joint class. Did you forget to derive from FreeJoint?".format( + cls, ConnectionBase + ), "{} is not a valid connection class. Did you forget to derive from ConnectionBase?".format( cls ) self._connect_cls = cls self._args = args self._kwargs = kwargs - return self def id( self, @@ -285,7 +280,7 @@ def id( self.second_sys_connection_idx, ) - def instantiate(self) -> FreeJoint: + def instantiate(self) -> ConnectionBase: if not hasattr(self, "_connect_cls"): raise RuntimeError( "No connections provided to link rod id {0}" @@ -299,5 +294,5 @@ def instantiate(self) -> FreeJoint: except (TypeError, IndexError): raise TypeError( r"Unable to construct connection class.\n" - r"Did you provide all necessary joint properties?" + r"Did you provide all necessary connection properties?" ) diff --git a/elastica/modules/constraints.py b/elastica/modules/constraints.py index 3686a1354..f3abfb694 100644 --- a/elastica/modules/constraints.py +++ b/elastica/modules/constraints.py @@ -5,7 +5,6 @@ Provides the constraints interface to enforce displacement boundary conditions (see `boundary_conditions.py`). """ from typing import Any, Type, cast -from typing_extensions import Self import functools @@ -16,33 +15,33 @@ from elastica.typing import ( SystemIdxType, ConstrainingIndex, - RigidBodyType, RodType, + RigidBodyType, ) -from elastica.memory_block.protocol import BlockRodProtocol -from .protocol import ConstrainedSystemCollectionProtocol, ModuleProtocol +from elastica.typing import RodType # noqa: F811 +from .protocol import SystemCollectionProtocol, ModuleProtocol -class Constraints: +class Constraints(SystemCollectionProtocol): """ The Constraints class is a module for enforcing displacement boundary conditions. To enforce boundary conditions on rod-like objects, the simulator class must be derived from Constraints class. - Attributes - ---------- - _constraints: list - List of boundary condition classes defined for rod-like objects. + Attributes + ---------- + _constraints: list + List of boundary condition classes defined for rod-like objects. """ - def __init__(self: ConstrainedSystemCollectionProtocol) -> None: - self._constraints_list: list[ModuleProtocol] = [] + _constraints_list: list[ModuleProtocol] + + def __init__(self) -> None: + self._constraints_list = [] super(Constraints, self).__init__() self._feature_group_finalize.append(self._finalize_constraints) - def constrain( - self: ConstrainedSystemCollectionProtocol, system: "RodType | RigidBodyType" - ) -> ModuleProtocol: + def constrain(self, system: "RodType | RigidBodyType") -> ModuleProtocol: """ This method enforces a displacement boundary conditions to the relevant user-defined system or rod-like object. You must input the system or rod-like @@ -67,16 +66,16 @@ def constrain( return _constraint - def _finalize_constraints(self: ConstrainedSystemCollectionProtocol) -> None: + def _finalize_constraints(self) -> None: """ In case memory block have ring rod, then periodic boundaries have to be synched. In order to synchronize periodic boundaries, a new constrain for memory block rod added called as _ConstrainPeriodicBoundaries. This constrain will synchronize the only periodic boundaries of position, director, velocity and omega variables. """ - for block in self.block_systems(): + for block in self.final_systems(): # append the memory block to the simulation as a system. Memory block is the final system in the simulation. - if hasattr(block, "ring_rod_flag"): + if hasattr(block, "ring_rod_flag") and block.ring_rod_flag: from elastica._synchronize_periodic_boundary import ( _ConstrainPeriodicBoundaries, ) @@ -84,7 +83,7 @@ def _finalize_constraints(self: ConstrainedSystemCollectionProtocol) -> None: # Apply the constrain to synchronize the periodic boundaries of the memory rod. Find the memory block # sys idx among other systems added and then apply boundary conditions. memory_block_idx = self.get_system_index(block) - block_system = cast(BlockRodProtocol, self[memory_block_idx]) + block_system = cast(RodType, self[memory_block_idx]) self.constrain(block_system).using( _ConstrainPeriodicBoundaries, ) @@ -166,7 +165,7 @@ def using( constrained_position_idx: ConstrainingIndex = (), constrained_director_idx: ConstrainingIndex = (), **kwargs: Any, - ) -> Self: + ) -> None: """ This method is a module to set which boundary condition class is used to enforce boundary condition from user defined rod-like objects. @@ -194,7 +193,6 @@ def using( self.constrained_director_idx = constrained_director_idx self._args = args self._kwargs = kwargs - return self def id(self) -> SystemIdxType: return self._sys_idx diff --git a/elastica/modules/contact.py b/elastica/modules/contact.py index 81fdd2703..93c60636e 100644 --- a/elastica/modules/contact.py +++ b/elastica/modules/contact.py @@ -6,48 +6,38 @@ (rods, rigid bodies, surfaces). """ from typing import Type, Any -from typing_extensions import Self import functools from elastica.typing import ( SystemIdxType, - OperatorType, - StaticSystemType, SystemType, + StaticSystemType, ) -from .protocol import ContactedSystemCollectionProtocol, ModuleProtocol - -import logging - -import numpy as np +from .protocol import SystemCollectionProtocol, ModuleProtocol from elastica.contact_forces import NoContact -logger = logging.getLogger(__name__) - - -def warnings() -> None: - logger.warning("Contact features should be instantiated lastly.") - -class Contact: +class Contact(SystemCollectionProtocol): """ - The Contact class is a module for applying contact between rod-like objects . To apply contact between rod-like objects, + The Contact class is a module for applying contact between rod-like objects. To apply contact between rod-like objects, the simulator class must be derived from the Contact class. - Attributes - ---------- - _contacts: list - List of contact classes defined for rod-like objects. + Attributes + ---------- + _contacts: list + List of contact classes defined for rod-like objects. """ - def __init__(self: ContactedSystemCollectionProtocol) -> None: - self._contacts: list[ModuleProtocol] = [] + _contacts: list[ModuleProtocol] + + def __init__(self) -> None: + self._contacts = [] super(Contact, self).__init__() self._feature_group_finalize.append(self._finalize_contact) def detect_contact_between( - self: ContactedSystemCollectionProtocol, + self, first_system: SystemType, second_system: "SystemType | StaticSystemType", ) -> ModuleProtocol: @@ -74,7 +64,7 @@ def detect_contact_between( return _contact - def _finalize_contact(self: ContactedSystemCollectionProtocol) -> None: + def _finalize_contact(self) -> None: # dev : the first indices stores the # (first_rod_idx, second_rod_idx) @@ -97,9 +87,6 @@ def _finalize_contact(self: ContactedSystemCollectionProtocol) -> None: self._feature_group_synchronize.add_operators(contact, [func]) - if not self._feature_group_synchronize.is_last(contact): - warnings() - self._contacts = [] del self._contacts @@ -137,7 +124,7 @@ def __init__( self._args: Any self._kwargs: Any - def using(self, cls: Type[NoContact], *args: Any, **kwargs: Any) -> Self: + def using(self, cls: Type[NoContact], *args: Any, **kwargs: Any) -> None: """ This method is a module to set which contact class is used to apply contact between user defined rod-like objects. @@ -163,7 +150,6 @@ def using(self, cls: Type[NoContact], *args: Any, **kwargs: Any) -> Self: self._contact_cls = cls self._args = args self._kwargs = kwargs - return self def id(self) -> Any: return ( diff --git a/elastica/modules/damping.py b/elastica/modules/damping.py index b7abd7541..a45e0beb4 100644 --- a/elastica/modules/damping.py +++ b/elastica/modules/damping.py @@ -10,37 +10,35 @@ """ from typing import Any, Type, List -from typing_extensions import Self import functools -import numpy as np - from elastica.dissipation import DamperBase -from elastica.typing import RodType, SystemType, SystemIdxType -from .protocol import DampenedSystemCollectionProtocol, ModuleProtocol +from elastica.typing import RodType, SystemIdxType +from elastica.rod.rod_base import RodBase +from .protocol import SystemCollectionProtocol, ModuleProtocol -class Damping: +class Damping(SystemCollectionProtocol): """ The Damping class is a module for applying damping on rod-like objects, the simulator class must be derived from Damping class. - Attributes - ---------- - _dampers: list - List of damper classes defined for rod-like objects. + Attributes + ---------- + _dampers: list + List of damper classes defined for rod-like objects. """ - def __init__(self: DampenedSystemCollectionProtocol) -> None: - self._damping_list: List[ModuleProtocol] = [] + _damping_list: List[ModuleProtocol] + + def __init__(self) -> None: + self._damping_list = [] super().__init__() self._feature_group_finalize.append(self._finalize_dampers) - def dampen( - self: DampenedSystemCollectionProtocol, system: RodType - ) -> ModuleProtocol: + def dampen(self, system: "RodType") -> ModuleProtocol: """ This method applies damping on relevant user-defined system or rod-like object. You must input the system or rod-like @@ -64,7 +62,7 @@ def dampen( return _damper - def _finalize_dampers(self: DampenedSystemCollectionProtocol) -> None: + def _finalize_dampers(self) -> None: # From stored _Damping objects, instantiate the dissipation/damping # inplace : https://stackoverflow.com/a/1208792 @@ -114,7 +112,7 @@ def __init__(self, sys_idx: SystemIdxType) -> None: self._args: Any self._kwargs: Any - def using(self, cls: Type[DamperBase], *args: Any, **kwargs: Any) -> Self: + def using(self, cls: Type[DamperBase], *args: Any, **kwargs: Any) -> None: """ This method is a module to set which damper class is used to enforce damping from user defined rod-like objects. @@ -140,12 +138,11 @@ def using(self, cls: Type[DamperBase], *args: Any, **kwargs: Any) -> Self: self._damper_cls = cls self._args = args self._kwargs = kwargs - return self def id(self) -> SystemIdxType: return self._sys_idx - def instantiate(self, rod: SystemType) -> DamperBase: + def instantiate(self, rod: "RodType") -> DamperBase: """Constructs a Damper class object after checks""" if not hasattr(self, "_damper_cls"): raise RuntimeError( diff --git a/elastica/modules/forcing.py b/elastica/modules/forcing.py index d267a7feb..3af1b19a8 100644 --- a/elastica/modules/forcing.py +++ b/elastica/modules/forcing.py @@ -8,37 +8,35 @@ import logging import functools from typing import Any, Type, List -from typing_extensions import Self - -import numpy as np from elastica.external_forces import NoForces from elastica.typing import SystemType, SystemIdxType -from .protocol import ForcedSystemCollectionProtocol, ModuleProtocol +from elastica.systems.protocol import SystemProtocol +from .protocol import SystemCollectionProtocol, ModuleProtocol logger = logging.getLogger(__name__) -class Forcing: +class Forcing(SystemCollectionProtocol): """ The Forcing class is a module for applying boundary conditions that consist of applied external forces. To apply forcing on rod-like objects, the simulator class must be derived from the Forcing class. - Attributes - ---------- - _ext_forces_torques: list - List of forcing class defined for rod-like objects. + Attributes + ---------- + _ext_forces_torques: list + List of forcing class defined for rod-like objects. """ - def __init__(self: ForcedSystemCollectionProtocol) -> None: - self._ext_forces_torques: List[ModuleProtocol] = [] + _ext_forces_torques: List[ModuleProtocol] + + def __init__(self) -> None: + self._ext_forces_torques = [] super().__init__() self._feature_group_finalize.append(self._finalize_forcing) - def add_forcing_to( - self: ForcedSystemCollectionProtocol, system: SystemType - ) -> ModuleProtocol: + def add_forcing_to(self, system: "SystemType") -> ModuleProtocol: """ This method applies external forces and torques on the relevant user-defined system or rod-like object. You must input the system @@ -62,7 +60,7 @@ def add_forcing_to( return _ext_force_torque - def _finalize_forcing(self: ForcedSystemCollectionProtocol) -> None: + def _finalize_forcing(self) -> None: # From stored _ExtForceTorque objects, and instantiate a Force # inplace : https://stackoverflow.com/a/1208792 @@ -111,7 +109,7 @@ def __init__(self, sys_idx: SystemIdxType) -> None: self._args: Any self._kwargs: Any - def using(self, cls: Type[NoForces], *args: Any, **kwargs: Any) -> Self: + def using(self, cls: Type[NoForces], *args: Any, **kwargs: Any) -> None: """ This method sets which forcing class is used to apply forcing to user defined rod-like objects. @@ -137,7 +135,6 @@ def using(self, cls: Type[NoForces], *args: Any, **kwargs: Any) -> Self: self._forcing_cls = cls self._args = args self._kwargs = kwargs - return self def id(self) -> SystemIdxType: return self._sys_idx diff --git a/elastica/modules/memory_block.py b/elastica/modules/memory_block.py index fa75f744b..9936214ef 100644 --- a/elastica/modules/memory_block.py +++ b/elastica/modules/memory_block.py @@ -2,87 +2,77 @@ This function is a module to construct memory blocks for different types of systems, such as Cosserat Rods, Rigid Body etc. """ -from typing import cast -from elastica.typing import ( - RodType, - RigidBodyType, - SurfaceType, - StaticSystemType, - SystemIdxType, - BlockSystemType, -) -from elastica.rod.rod_base import RodBase -from elastica.rigidbody.rigid_body import RigidBodyBase -from elastica.surface.surface_base import SurfaceBase -from elastica.memory_block.memory_block_rod import MemoryBlockCosseratRod -from elastica.memory_block.memory_block_rigid_body import MemoryBlockRigidBody +from typing import Type, TYPE_CHECKING +from collections import defaultdict +from elastica.systems.protocol import SystemProtocol + +if TYPE_CHECKING: + from elastica.typing import ( + SystemType, + StaticSystemType, + SystemIdxType, + BlockSystemType, + ) def construct_memory_block_structures( - systems: list[StaticSystemType], -) -> list[BlockSystemType]: + systems: list["StaticSystemType"], + block_supports: dict[Type["BlockSystemType"], list[Type["SystemType"]]], +) -> tuple[list["BlockSystemType"], list["SystemType"]]: """ - This function takes the systems (rod or rigid body) appended to the simulator class and - separates them into lists depending on if system is Cosserat rod or rigid body. Then using - these separated out systems it creates the memory blocks for Cosserat rods and rigid bodies. + This function takes the systems appended to the simulator class and + separates them into groups based on their block support. Then using + these grouped systems it creates the memory blocks. + + Parameters + ---------- + systems : list[StaticSystemType] + List of systems to be grouped into memory blocks. + block_supports : dict[Type[BlockSystemType], list[Type[SystemType]]] + Dictionary mapping block types to the list of system types that support it. Returns ------- + list[BlockSystemType] + List of memory block structures created from the systems. + Notes + ----- + Systems that don't have an associated block type in the dictionary will be + skipped (no block constructed), but they are still allowed to be appended + to the system collection. """ - _memory_blocks: list[BlockSystemType] = [] - temp_list_for_cosserat_rod_systems: list[RodType] = [] - temp_list_for_rigid_body_systems: list[RigidBodyType] = [] - temp_list_for_cosserat_rod_systems_idx: list[SystemIdxType] = [] - temp_list_for_rigid_body_systems_idx: list[SystemIdxType] = [] - - for system_idx, sys_to_be_added in enumerate(systems): - - if isinstance(sys_to_be_added, RodBase): - rod_system = cast(RodType, sys_to_be_added) - temp_list_for_cosserat_rod_systems.append(rod_system) - temp_list_for_cosserat_rod_systems_idx.append(system_idx) - - elif isinstance(sys_to_be_added, RigidBodyBase): - rigid_body_system = cast(RigidBodyType, sys_to_be_added) - temp_list_for_rigid_body_systems.append(rigid_body_system) - temp_list_for_rigid_body_systems_idx.append(system_idx) + _memory_blocks: list["BlockSystemType"] = [] + _non_blocked_systems: list[SystemProtocol] = [] - elif isinstance(sys_to_be_added, SurfaceBase): - pass - # surface_system = cast(SurfaceType, sys_to_be_added) - # raise NotImplementedError( - # "Surfaces are not yet implemented in memory block construction." - # ) + # Group systems by their block type + system_list: dict[Type["BlockSystemType"], list["StaticSystemType"]] = defaultdict( + list + ) + index_list: dict[Type["BlockSystemType"], list["SystemIdxType"]] = defaultdict(list) - else: - raise TypeError( - "{0}\n" - "is not a system passing validity\n" - "checks for constructing block structure. If you are sure that\n" - "{0}\n" - "satisfies all criteria for being a system, please add\n" - "it here with correct memory block implementation.\n" - "The allowed types are\n" - "{1} {2} {3}".format( - sys_to_be_added.__class__, RodBase, RigidBodyBase, SurfaceBase - ) - ) + for system_idx, system in enumerate(systems): + # Find the matching system type in block_supports + block_type = None + for bt, system_types in block_supports.items(): + if ( + type(system) in system_types + ): # Explicit check for *exact* system type, not subclasses. + block_type = bt + break - if temp_list_for_cosserat_rod_systems: - _memory_blocks.append( - MemoryBlockCosseratRod( - temp_list_for_cosserat_rod_systems, - temp_list_for_cosserat_rod_systems_idx, - ) - ) + if block_type is not None: + # If block type found, group the system + system_list[block_type].append(system) + index_list[block_type].append(system_idx) + elif isinstance(system, SystemProtocol): + _non_blocked_systems.append(system) - if temp_list_for_rigid_body_systems: - _memory_blocks.append( - MemoryBlockRigidBody( - temp_list_for_rigid_body_systems, temp_list_for_rigid_body_systems_idx - ) - ) + # Create blocks for each block type + for block_type, systems_for_block in system_list.items(): + # block_type is a concrete class with constructor (systems, system_idx_list) + block: BlockSystemType = block_type(systems_for_block, index_list[block_type]) + _memory_blocks.append(block) - return list(_memory_blocks) + return _memory_blocks, _non_blocked_systems diff --git a/elastica/modules/protocol.py b/elastica/modules/protocol.py index 4670a3011..4011f9848 100644 --- a/elastica/modules/protocol.py +++ b/elastica/modules/protocol.py @@ -1,6 +1,5 @@ -from typing import Protocol, Generator, TypeVar, Any, Type, overload, Iterator +from typing import Protocol, Generator, Any, Type, Callable, overload from typing import TYPE_CHECKING -from typing_extensions import Self # python 3.11: from typing import Self from elastica.typing import ( SystemIdxType, @@ -9,15 +8,8 @@ OperatorFinalizeType, StaticSystemType, SystemType, - RodType, - RigidBodyType, BlockSystemType, - ConnectionIndex, ) -from elastica.joint import FreeJoint -from elastica.callback_functions import CallBackBaseClass -from elastica.boundary_conditions import ConstraintBase -from elastica.dissipation import DamperBase import numpy as np @@ -25,120 +17,51 @@ from .operator_group import OperatorGroupFIFO -class MixinProtocol(Protocol): - # def finalize(self) -> None: ... - ... +class ModuleProtocol(Protocol): + """Protocol for module handles (e.g., _Connect, _Constraint, _Damper, etc.).""" + def using(self, cls: Type[Any], *args: Any, **kwargs: Any) -> None: ... -M = TypeVar("M", bound=MixinProtocol) - - -class ModuleProtocol(Protocol[M]): - def using(self, cls: Type[M], *args: Any, **kwargs: Any) -> Self: ... - - def instantiate(self, *args: Any, **kwargs: Any) -> M: ... + def instantiate(self, *args: Any, **kwargs: Any) -> Any: ... def id(self) -> Any: ... class SystemCollectionProtocol(Protocol): - def __len__(self) -> int: ... + """ + Protocol for system collections. - def systems(self) -> Generator[StaticSystemType, None, None]: ... - - def block_systems(self) -> Generator[BlockSystemType, None, None]: ... + This protocol defines the interface for system collections including + container operations, lifecycle methods, and internal feature groups + used for operator registration. + """ + # Container access @overload def __getitem__(self, i: slice) -> list[SystemType]: ... @overload def __getitem__(self, i: int) -> SystemType: ... def __getitem__(self, i: slice | int) -> "list[SystemType] | SystemType": ... - def __delitem__(self, i: slice | int) -> None: ... - def __setitem__(self, i: slice | int, value: SystemType) -> None: ... - def insert(self, i: int, value: SystemType) -> None: ... - def __iter__(self) -> Iterator[SystemType]: ... + def systems(self) -> Generator[StaticSystemType, None, None]: ... + + def final_systems(self) -> Generator[SystemType, None, None]: ... def get_system_index( self, sys_to_be_added: "SystemType | StaticSystemType" ) -> SystemIdxType: ... - # Operator Group - _feature_group_synchronize: "OperatorGroupFIFO[OperatorType, ModuleProtocol]" - _feature_group_constrain_values: "OperatorGroupFIFO[OperatorType, ModuleProtocol]" - _feature_group_constrain_rates: "OperatorGroupFIFO[OperatorType, ModuleProtocol]" - _feature_group_damping: "OperatorGroupFIFO[OperatorType, ModuleProtocol]" - _feature_group_callback: "OperatorGroupFIFO[OperatorCallbackType, ModuleProtocol]" - + # Lifecycle methods def synchronize(self, time: np.float64) -> None: ... def constrain_values(self, time: np.float64) -> None: ... def constrain_rates(self, time: np.float64) -> None: ... def apply_callbacks(self, time: np.float64, current_step: int) -> None: ... - # Finalize Operations + # Internal feature groups for operator registration + _feature_group_synchronize: "OperatorGroupFIFO[OperatorType, ModuleProtocol]" + _feature_group_constrain_values: "OperatorGroupFIFO[OperatorType, ModuleProtocol]" + _feature_group_constrain_rates: "OperatorGroupFIFO[OperatorType, ModuleProtocol]" + _feature_group_damping: "OperatorGroupFIFO[OperatorType, ModuleProtocol]" + _feature_group_callback: "OperatorGroupFIFO[OperatorCallbackType, ModuleProtocol]" _feature_group_finalize: list[OperatorFinalizeType] - - def finalize(self) -> None: ... - - -# Mixin Protocols (Used to type Self) -class ConnectedSystemCollectionProtocol(SystemCollectionProtocol, Protocol): - # Connection API - _connections: list[ModuleProtocol] - - def _finalize_connections(self) -> None: ... - - def connect( - self, - first_rod: "RodType | RigidBodyType", - second_rod: "RodType | RigidBodyType", - first_connect_idx: ConnectionIndex, - second_connect_idx: ConnectionIndex, - ) -> ModuleProtocol: ... - - -class ForcedSystemCollectionProtocol(SystemCollectionProtocol, Protocol): - # Forcing API - _ext_forces_torques: list[ModuleProtocol] - - def _finalize_forcing(self) -> None: ... - - def add_forcing_to(self, system: SystemType) -> ModuleProtocol: ... - - -class ContactedSystemCollectionProtocol(SystemCollectionProtocol, Protocol): - # Contact API - _contacts: list[ModuleProtocol] - - def _finalize_contact(self) -> None: ... - - def detect_contact_between( - self, first_system: SystemType, second_system: SystemType - ) -> ModuleProtocol: ... - - -class ConstrainedSystemCollectionProtocol(SystemCollectionProtocol, Protocol): - # Constraints API - _constraints_list: list[ModuleProtocol] - - def _finalize_constraints(self) -> None: ... - - def constrain(self, system: "RodType | RigidBodyType") -> ModuleProtocol: ... - - -class SystemCollectionWithCallbackProtocol(SystemCollectionProtocol, Protocol): - # CallBack API - _callback_list: list[ModuleProtocol] - - def _finalize_callback(self) -> None: ... - - def collect_diagnostics(self, system: SystemType) -> ModuleProtocol: ... - - -class DampenedSystemCollectionProtocol(SystemCollectionProtocol, Protocol): - # Damping API - _damping_list: list[ModuleProtocol] - - def _finalize_dampers(self) -> None: ... - - def dampen(self, system: RodType) -> ModuleProtocol: ... + _feature_group_on_close: "OperatorGroupFIFO[Callable, ModuleProtocol]" diff --git a/elastica/restart.py b/elastica/restart.py index 8c414ec9d..0445d7106 100644 --- a/elastica/restart.py +++ b/elastica/restart.py @@ -6,9 +6,9 @@ import json from itertools import groupby -from .memory_block import MemoryBlockCosseratRod, MemoryBlockRigidBody +from .memory_block.protocol import BlockSystemProtocol -from .typing import SystemType, SystemCollectionType +from .typing import SystemCollectionType def all_equal(iterable: Iterable[Any]) -> bool: @@ -53,7 +53,7 @@ def save_state( os.makedirs(directory, exist_ok=True) # Save system state - for idx, system in enumerate(simulator): + for idx, system in enumerate(simulator.systems()): name = system.__class__.__name__ path = os.path.join(directory, f"{name}_{idx}.npz") np.savez(path, **system.__dict__) # type: ignore @@ -94,9 +94,9 @@ def load_state( time = meta["time"] # Load system state - for idx, system in enumerate(simulator): + for idx, system in enumerate(simulator.systems()): # TODO: Not exactly sure why this condition is necessary. - if isinstance(system, (MemoryBlockCosseratRod, MemoryBlockRigidBody)): + if isinstance(system, BlockSystemProtocol): continue name = system.__class__.__name__ # type: ignore path = os.path.join(directory, f"{name}_{idx}.npz") diff --git a/elastica/rigidbody/__init__.py b/elastica/rigidbody/__init__.py index 732577df2..1b3e8745e 100644 --- a/elastica/rigidbody/__init__.py +++ b/elastica/rigidbody/__init__.py @@ -1,3 +1,3 @@ -from .rigid_body import RigidBodyBase +from .rigid_body_base import RigidBodyBase from .cylinder import Cylinder from .sphere import Sphere diff --git a/elastica/rigidbody/cylinder.py b/elastica/rigidbody/cylinder.py index cde0155d6..b4a1adfa0 100644 --- a/elastica/rigidbody/cylinder.py +++ b/elastica/rigidbody/cylinder.py @@ -1,14 +1,36 @@ __doc__ = """ Implementation of a rigid body cylinder. """ -from typing import TYPE_CHECKING import numpy as np from numpy.typing import NDArray from elastica._linalg import _batch_cross from elastica.utils import MaxDimension -from elastica.rigidbody.rigid_body import RigidBodyBase +from elastica.rigidbody.rigid_body_base import RigidBodyBase + + +def _assert_check_array_size( + to_check: NDArray[np.float64], name: str, expected: int = 3 +) -> None: + """ + Validate that an array has the expected size. + """ + array_size = to_check.size + assert array_size == expected, ( + f"Invalid size of '{name}'. " f"Expected: {expected}, but got: {array_size}" + ) + + +def _assert_check_lower_bound( + to_check: float, name: str, lower_bound: float = 0.0 +) -> None: + """ + Validate that a value is greater than a lower bound. + """ + assert ( + to_check > lower_bound + ), f"Value for '{name}' ({to_check}) must be at least {lower_bound}. " class Cylinder(RigidBodyBase): @@ -33,32 +55,13 @@ def __init__( base_radius : float density : float """ + _assert_check_array_size(start, "start") + _assert_check_array_size(direction, "direction") + _assert_check_array_size(normal, "normal") - # FIXME: Refactor - def assert_check_array_size( - to_check: NDArray[np.float64], name: str, expected: int = 3 - ) -> None: - array_size = to_check.size - assert array_size == expected, ( - f"Invalid size of '{name}'. " - f"Expected: {expected}, but got: {array_size}" - ) - - # FIXME: Refactor - def assert_check_lower_bound( - to_check: float, name: str, lower_bound: float = 0.0 - ) -> None: - assert ( - to_check > lower_bound - ), f"Value for '{name}' ({to_check}) must be at lease {lower_bound}. " - - assert_check_array_size(start, "start") - assert_check_array_size(direction, "direction") - assert_check_array_size(normal, "normal") - - assert_check_lower_bound(base_length, "base_length") - assert_check_lower_bound(base_radius, "base_radius") - assert_check_lower_bound(density, "density") + _assert_check_lower_bound(base_length, "base_length") + _assert_check_lower_bound(base_radius, "base_radius") + _assert_check_lower_bound(density, "density") super().__init__() @@ -114,16 +117,3 @@ def assert_check_lower_bound( self.director_collection[0, ...] = normal self.director_collection[1, ...] = binormal self.director_collection[2, ...] = tangents - - -if TYPE_CHECKING: - from .protocol import RigidBodyProtocol - - _: RigidBodyProtocol = Cylinder( - start=np.zeros(3), - direction=np.ones(3), - normal=np.ones(3), - base_length=1.0, - base_radius=1.0, - density=1.0, - ) diff --git a/elastica/rigidbody/data_structures.py b/elastica/rigidbody/data_structures.py index 95944f759..f7e9ae658 100644 --- a/elastica/rigidbody/data_structures.py +++ b/elastica/rigidbody/data_structures.py @@ -2,44 +2,5 @@ from elastica.rod.data_structures import _RodSymplecticStepperMixin -pass -""" -# FIXME : Explicit Stepper doesn't work as States lose the -# views they initially had when working with a timestepper. -class _RigidRodExplicitStepperMixin: - def __init__(self): - ( - self.state, - self.__deriv_state, - self.position_collection, - self.director_collection, - self.velocity_collection, - self.omega_collection, - self.acceleration_collection, - self.alpha_collection, # angular acceleration - ) = _bootstrap_from_data( - "explicit", self.n_elems, self._vector_states, self._matrix_states - ) - - # def __setattr__(self, name, value): - # np.copy(self.__dict__[name], value) - - def __call__(self, time, *args, **kwargs): - self.update_accelerations(time) # Internal, external - - # print("KRC", self.state.kinematic_rate_collection) - # print("DEr", self.__deriv_state.rate_collection) - if np.shares_memory( - self.state.kinematic_rate_collection, - self.velocity_collection - # self.__deriv_state.rate_collection - ): - print("Shares memory") - else: - print("Explicit states does not share memory") - return self.__deriv_state -""" - -# TODO: Temporary solution as the structure for RigidBody is similar to Rod _RigidRodSymplecticStepperMixin = _RodSymplecticStepperMixin diff --git a/elastica/rigidbody/protocol.py b/elastica/rigidbody/protocol.py deleted file mode 100644 index 7e6f0664e..000000000 --- a/elastica/rigidbody/protocol.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Protocol - -import numpy as np -from numpy.typing import NDArray - -from elastica.systems.protocol import SystemProtocol, SlenderBodyGeometryProtocol - - -class RigidBodyProtocol(SystemProtocol, SlenderBodyGeometryProtocol, Protocol): - - mass: np.float64 - volume: np.float64 - length: np.float64 - tangents: NDArray[np.float64] - radius: np.float64 - - mass_second_moment_of_inertia: NDArray[np.float64] - inv_mass_second_moment_of_inertia: NDArray[np.float64] diff --git a/elastica/rigidbody/rigid_body.py b/elastica/rigidbody/rigid_body_base.py similarity index 92% rename from elastica/rigidbody/rigid_body.py rename to elastica/rigidbody/rigid_body_base.py index 999208546..f3b5e31b1 100644 --- a/elastica/rigidbody/rigid_body.py +++ b/elastica/rigidbody/rigid_body_base.py @@ -1,4 +1,4 @@ -__doc__ = """""" +__doc__ = """Base class for rigid body implementations""" from typing import Type @@ -6,10 +6,12 @@ import numpy as np from numpy.typing import NDArray + from elastica._linalg import _batch_matvec, _batch_cross +from elastica.systems.protocol import SystemProtocol -class RigidBodyBase(ABC): +class RigidBodyBase(ABC, SystemProtocol): """ Base class for rigid body classes. @@ -23,7 +25,7 @@ class RigidBodyBase(ABC): def __init__(self) -> None: # rigid body does not have elements it only has one node. We are setting n_elems to - # make code to work. _bootstrap_from_data requires n_elems to be define + # make code to work. self.n_elems: int = 1 self.n_nodes: int = 1 @@ -49,7 +51,7 @@ def __init__(self) -> None: self.mass_second_moment_of_inertia: NDArray[np.float64] self.inv_mass_second_moment_of_inertia: NDArray[np.float64] - def update_accelerations(self, time: np.float64) -> None: + def update_accelerations(self, time: np.float64, dt: np.float64) -> None: np.copyto( self.acceleration_collection, (self.external_forces) / self.mass, diff --git a/elastica/rigidbody/sphere.py b/elastica/rigidbody/sphere.py index 154cdd489..2e8392a50 100644 --- a/elastica/rigidbody/sphere.py +++ b/elastica/rigidbody/sphere.py @@ -1,17 +1,20 @@ __doc__ = """ Implementation of a sphere rigid body. """ -from typing import TYPE_CHECKING import numpy as np from numpy.typing import NDArray from elastica._linalg import _batch_cross from elastica.utils import MaxDimension -from elastica.rigidbody.rigid_body import RigidBodyBase +from elastica.rigidbody.rigid_body_base import RigidBodyBase class Sphere(RigidBodyBase): + """ + Rigid sphere class. + """ + def __init__( self, center: NDArray[np.float64], @@ -23,7 +26,7 @@ def __init__( Parameters ---------- - center : NDArray[np.float64] + center : numpy.ndarray base_radius : float density : float """ @@ -81,13 +84,3 @@ def __init__( self.director_collection[0, ...] = normal self.director_collection[1, ...] = binormal self.director_collection[2, ...] = tangents - - -if TYPE_CHECKING: - from .protocol import RigidBodyProtocol - - _: RigidBodyProtocol = Sphere( - center=np.zeros(3), - base_radius=1.0, - density=1.0, - ) diff --git a/elastica/rod/cosserat_rod.py b/elastica/rod/cosserat_rod.py index 932da09b9..66169d742 100644 --- a/elastica/rod/cosserat_rod.py +++ b/elastica/rod/cosserat_rod.py @@ -1,16 +1,14 @@ __doc__ = """ Rod classes and implementation details """ -from typing import TYPE_CHECKING, Any, Optional, Type +from typing import Any, Optional, Type from typing_extensions import Self -from elastica.typing import RodType -from .protocol import CosseratRodProtocol - from numpy.typing import NDArray import numpy as np import functools import numba from elastica.rod import RodBase +from elastica.systems.protocol import SystemProtocol from elastica._linalg import ( _batch_cross, _batch_norm, @@ -25,7 +23,6 @@ _average, ) from .factory_function import allocate -from .knot_theory import KnotTheory position_difference_kernel = _difference position_average = _average @@ -36,7 +33,7 @@ def _get_z_vector() -> NDArray[np.float64]: return np.array([0.0, 0.0, 1.0]).reshape(3, -1) -def _compute_sigma_kappa_for_blockstructure(memory_block: RodType) -> None: +def _compute_sigma_kappa_for_blockstructure(memory_block: RodBase) -> None: """ This function is a wrapper to call functions which computes shear stretch, strain and bending twist and strain. @@ -70,90 +67,90 @@ def _compute_sigma_kappa_for_blockstructure(memory_block: RodType) -> None: ) -class CosseratRod(RodBase, KnotTheory): +class CosseratRod(RodBase, SystemProtocol): """ Cosserat Rod class. This is the preferred class for rods because it is derived from some of the essential base classes. - Attributes - ---------- - n_elems: int - The number of elements of the rod. - position_collection: NDArray[np.float64] - 2D (dim, n_nodes) array containing data with 'float' type. - Array containing node position vectors. - velocity_collection: NDArray[np.float64] - 2D (dim, n_nodes) array containing data with 'float' type. - Array containing node velocity vectors. - acceleration_collection: NDArray[np.float64] - 2D (dim, n_nodes) array containing data with 'float' type. - Array containing node acceleration vectors. - omega_collection: NDArray[np.float64] - 2D (dim, n_elems) array containing data with 'float' type. - Array containing element angular velocity vectors. - alpha_collection: NDArray[np.float64] - 2D (dim, n_elems) array containing data with 'float' type. - Array contining element angular acceleration vectors. - director_collection: NDArray[np.float64] - 3D (dim, dim, n_elems) array containing data with 'float' type. - Array containing element director matrices. - rest_lengths: NDArray[np.float64] - 1D (n_elems) array containing data with 'float' type. - Rod element lengths at rest configuration. - density: NDArray[np.float64] - 1D (n_elems) array containing data with 'float' type. - Rod elements densities. - volume: NDArray[np.float64] - 1D (n_elems) array containing data with 'float' type. - Rod element volumes. - mass: NDArray[np.float64] - 1D (n_nodes) array containing data with 'float' type. - Rod node masses. Note that masses are stored on the nodes, not on elements. - mass_second_moment_of_inertia: NDArray[np.float64] - 3D (dim, dim, n_elems) array containing data with 'float' type. - Rod element mass second moment of interia. - inv_mass_second_moment_of_inertia: NDArray[np.float64] - 3D (dim, dim, n_elems) array containing data with 'float' type. - Rod element inverse mass moment of inertia. - rest_voronoi_lengths: NDArray[np.float64] - 1D (n_voronoi) array containing data with 'float' type. - Rod lengths on the voronoi domain at the rest configuration. - internal_forces: NDArray[np.float64] - 2D (dim, n_nodes) array containing data with 'float' type. - Rod node internal forces. Note that internal forces are stored on the node, not on elements. - internal_torques: NDArray[np.float64] - 2D (dim, n_elems) array containing data with 'float' type. - Rod element internal torques. - external_forces: NDArray[np.float64] - 2D (dim, n_nodes) array containing data with 'float' type. - External forces acting on rod nodes. - external_torques: NDArray[np.float64] - 2D (dim, n_elems) array containing data with 'float' type. - External torques acting on rod elements. - lengths: NDArray[np.float64] - 1D (n_elems) array containing data with 'float' type. - Rod element lengths. - tangents: NDArray[np.float64] - 2D (dim, n_elems) array containing data with 'float' type. - Rod element tangent vectors. - radius: NDArray[np.float64] - 1D (n_elems) array containing data with 'float' type. - Rod element radius. - dilatation: NDArray[np.float64] - 1D (n_elems) array containing data with 'float' type. - Rod element dilatation. - voronoi_dilatation: NDArray[np.float64] - 1D (n_voronoi) array containing data with 'float' type. - Rod dilatation on voronoi domain. - dilatation_rate: NDArray[np.float64] - 1D (n_elems) array containing data with 'float' type. - Rod element dilatation rates. + Attributes + ---------- + n_elems: int + The number of elements of the rod. + position_collection: NDArray[np.float64] + 2D (dim, n_nodes) array containing data with 'float' type. + Array containing node position vectors. + velocity_collection: NDArray[np.float64] + 2D (dim, n_nodes) array containing data with 'float' type. + Array containing node velocity vectors. + acceleration_collection: NDArray[np.float64] + 2D (dim, n_nodes) array containing data with 'float' type. + Array containing node acceleration vectors. + omega_collection: NDArray[np.float64] + 2D (dim, n_elems) array containing data with 'float' type. + Array containing element angular velocity vectors. + alpha_collection: NDArray[np.float64] + 2D (dim, n_elems) array containing data with 'float' type. + Array containing element angular acceleration vectors. + director_collection: NDArray[np.float64] + 3D (dim, dim, n_elems) array containing data with 'float' type. + Array containing element director matrices. + rest_lengths: NDArray[np.float64] + 1D (n_elems) array containing data with 'float' type. + Rod element lengths at rest configuration. + density: NDArray[np.float64] + 1D (n_elems) array containing data with 'float' type. + Rod element densities. + volume: NDArray[np.float64] + 1D (n_elems) array containing data with 'float' type. + Rod element volumes. + mass: NDArray[np.float64] + 1D (n_nodes) array containing data with 'float' type. + Rod node masses. Note that masses are stored on the nodes, not on elements. + mass_second_moment_of_inertia: NDArray[np.float64] + 3D (dim, dim, n_elems) array containing data with 'float' type. + Rod element mass second moment of inertia. + inv_mass_second_moment_of_inertia: NDArray[np.float64] + 3D (dim, dim, n_elems) array containing data with 'float' type. + Rod element inverse mass moment of inertia. + rest_voronoi_lengths: NDArray[np.float64] + 1D (n_voronoi) array containing data with 'float' type. + Rod lengths on the voronoi domain at the rest configuration. + internal_forces: NDArray[np.float64] + 2D (dim, n_nodes) array containing data with 'float' type. + Rod node internal forces. Note that internal forces are stored on the node, not on elements. + internal_torques: NDArray[np.float64] + 2D (dim, n_elems) array containing data with 'float' type. + Rod element internal torques. + external_forces: NDArray[np.float64] + 2D (dim, n_nodes) array containing data with 'float' type. + External forces acting on rod nodes. + external_torques: NDArray[np.float64] + 2D (dim, n_elems) array containing data with 'float' type. + External torques acting on rod elements. + lengths: NDArray[np.float64] + 1D (n_elems) array containing data with 'float' type. + Rod element lengths. + tangents: NDArray[np.float64] + 2D (dim, n_elems) array containing data with 'float' type. + Rod element tangent vectors. + radius: NDArray[np.float64] + 1D (n_elems) array containing data with 'float' type. + Rod element radius. + dilatation: NDArray[np.float64] + 1D (n_elems) array containing data with 'float' type. + Rod element dilatation. + voronoi_dilatation: NDArray[np.float64] + 1D (n_voronoi) array containing data with 'float' type. + Rod dilatation on voronoi domain. + dilatation_rate: NDArray[np.float64] + 1D (n_elems) array containing data with 'float' type. + Rod element dilatation rates. """ REQUISITE_MODULES: list[Type] = [] def __init__( - self: CosseratRodProtocol, + self, n_elements: int, position: NDArray[np.float64], velocity: NDArray[np.float64], @@ -295,7 +292,7 @@ def straight_rod( Damping coefficient for Rayleigh damping youngs_modulus : float Young's modulus - **kwargs : dict, optional + kwargs : dict, optional The "position" and/or "directors" can be overrided by passing "position" and "directors" argument. Remember, the shape of the "position" is (3,n_elements+1) and the shape of the "directors" is (3,3,n_elements). Returns @@ -413,7 +410,7 @@ def ring_rod( **kwargs: Any, ) -> Self: """ - Cosserat rod constructor for straight-rod geometry. + Cosserat rod constructor for ring-rod geometry. Notes @@ -425,7 +422,7 @@ def ring_rod( Parameters ---------- n_elements : int - Number of element. Must be greater than 3. Generarally recommended to start with 40-50, and adjust the resolution. + Number of element. Must be greater than 3. Generally recommended to start with 40-50, and adjust the resolution. ring_center_position : NDArray[np.float64] Center coordinate for ring rod in 3D direction : NDArray[np.float64] @@ -442,7 +439,7 @@ def ring_rod( Damping coefficient for Rayleigh damping youngs_modulus : float Young's modulus - **kwargs : dict, optional + kwargs : dict, optional The "position" and/or "directors" can be overrided by passing "position" and "directors" argument. Remember, the shape of the "position" is (3,n_elements+1) and the shape of the "directors" is (3,3,n_elements). Returns @@ -461,7 +458,7 @@ def ring_rod( "For reference see the class elastica.dissipation.AnalyticalLinearDamper(),\n" "and for usage check examples/axial_stretching.py" ) - # Straight rod is not ring rod set flag to false + # Ring rod flag set to true ring_rod_flag = True ( n_elements, @@ -547,9 +544,7 @@ def ring_rod( rod.REQUISITE_MODULES.append(Constraints) return rod - def compute_internal_forces_and_torques( - self: CosseratRodProtocol, time: np.float64 - ) -> None: + def compute_internal_forces_and_torques(self, time: np.float64) -> None: """ Compute internal forces and torques. We need to compute internal forces and torques before the acceleration because they are used in interaction. Thus in order to speed up simulation, we will compute internal forces and torques @@ -604,7 +599,7 @@ def compute_internal_forces_and_torques( ) # Interface to time-stepper mixins (Symplectic, Explicit), which calls this method - def update_accelerations(self: CosseratRodProtocol, time: np.float64) -> None: + def update_accelerations(self, time: np.float64, dt: np.float64) -> None: """ Updates the acceleration variables @@ -612,6 +607,8 @@ def update_accelerations(self: CosseratRodProtocol, time: np.float64) -> None: ---------- time: np.float64 current time + dt: np.float64 + timestep. (may involve designing implicit solver) """ _update_accelerations( @@ -626,88 +623,11 @@ def update_accelerations(self: CosseratRodProtocol, time: np.float64) -> None: self.dilatation, ) - def zeroed_out_external_forces_and_torques( - self: CosseratRodProtocol, time: np.float64 - ) -> None: + def zeroed_out_external_forces_and_torques(self, time: np.float64) -> None: _zeroed_out_external_forces_and_torques( self.external_forces, self.external_torques ) - def compute_translational_energy(self: CosseratRodProtocol) -> NDArray[np.float64]: - """ - Compute total translational energy of the rod at the instance. - """ - return ( - 0.5 - * ( - self.mass - * np.einsum( - "ij, ij-> j", self.velocity_collection, self.velocity_collection - ) - ).sum() - ) - - def compute_rotational_energy(self: CosseratRodProtocol) -> NDArray[np.float64]: - """ - Compute total rotational energy of the rod at the instance. - """ - J_omega_upon_e = ( - _batch_matvec(self.mass_second_moment_of_inertia, self.omega_collection) - / self.dilatation - ) - return 0.5 * np.einsum("ik,ik->k", self.omega_collection, J_omega_upon_e).sum() - - def compute_velocity_center_of_mass( - self: CosseratRodProtocol, - ) -> NDArray[np.float64]: - """ - Compute velocity center of mass of the rod at the instance. - """ - mass_times_velocity = np.einsum("j,ij->ij", self.mass, self.velocity_collection) - sum_mass_times_velocity = np.einsum("ij->i", mass_times_velocity) - - return sum_mass_times_velocity / self.mass.sum() - - def compute_position_center_of_mass( - self: CosseratRodProtocol, - ) -> NDArray[np.float64]: - """ - Compute position center of mass of the rod at the instance. - """ - mass_times_position = np.einsum("j,ij->ij", self.mass, self.position_collection) - sum_mass_times_position = np.einsum("ij->i", mass_times_position) - - return sum_mass_times_position / self.mass.sum() - - def compute_bending_energy(self: CosseratRodProtocol) -> NDArray[np.float64]: - """ - Compute total bending energy of the rod at the instance. - """ - - kappa_diff = self.kappa - self.rest_kappa - bending_internal_torques = _batch_matvec(self.bend_matrix, kappa_diff) - - return ( - 0.5 - * ( - _batch_dot(kappa_diff, bending_internal_torques) - * self.rest_voronoi_lengths - ).sum() - ) - - def compute_shear_energy(self: CosseratRodProtocol) -> NDArray[np.float64]: - """ - Compute total shear energy of the rod at the instance. - """ - - sigma_diff = self.sigma - self.rest_sigma - shear_internal_forces = _batch_matvec(self.shear_matrix, sigma_diff) - - return ( - 0.5 - * (_batch_dot(sigma_diff, shear_internal_forces) * self.rest_lengths).sum() - ) - # Below is the numba-implementation of Cosserat Rod equations. They don't need to be visible by users. @@ -761,11 +681,11 @@ def _compute_all_dilatations( for k in range(lengths.shape[0]): dilatation[k] = lengths[k] / rest_lengths[k] - # Cmopute eq (3.4) from 2018 RSOS paper + # Compute eq (3.4) from 2018 RSOS paper # Note : we can use trapezoidal kernel, but it has padding and will be slower voronoi_lengths = position_average(lengths) - # Cmopute eq (3.45 from 2018 RSOS paper + # Compute eq (3.4) from 2018 RSOS paper for k in range(voronoi_lengths.shape[0]): voronoi_dilatation[k] = voronoi_lengths[k] / rest_voronoi_lengths[k] @@ -781,7 +701,6 @@ def _compute_dilatation_rate( """ Update dilatation_rate given position, velocity, length, and rest_length """ - # TODO Use the vector formula rather than separating it out # self.lengths = l_i = |r^{i+1} - r^{i}| r_dot_v = _batch_dot(position_collection, velocity_collection) r_plus_one_dot_v = _batch_dot( @@ -1130,26 +1049,3 @@ def _zeroed_out_external_forces_and_torques( for i in range(3): for k in range(n_elems): external_torques[i, k] = 0.0 - - -if TYPE_CHECKING: - _: CosseratRodProtocol = CosseratRod.straight_rod( - 3, - np.zeros(3), - np.array([0, 1, 0]), - np.array([0, 0, 1]), - 1.0, - 0.1, - 1.0, - youngs_modulus=1.0, - ) - _: CosseratRodProtocol = CosseratRod.ring_rod( # type: ignore[no-redef] - 3, - np.zeros(3), - np.array([0, 1, 0]), - np.array([0, 0, 1]), - 1.0, - 0.1, - 1.0, - youngs_modulus=1.0, - ) diff --git a/elastica/rod/data_structures.py b/elastica/rod/data_structures.py index f7b248bcc..9ca3247c7 100644 --- a/elastica/rod/data_structures.py +++ b/elastica/rod/data_structures.py @@ -1,11 +1,17 @@ -__doc__ = "Data structure wrapper for rod components" +""" +Data structures and Numba-jitted operators for handling rod components +and their integration in a symplectic time-stepping scheme. + +This module provides the `_RodSymplecticStepperMixin` for managing +kinematic and dynamic states of rods, and optimized functions for +their in-place updates. +""" -from typing import TYPE_CHECKING, Optional -from typing_extensions import Self +from typing import TYPE_CHECKING import numpy as np from numpy.typing import NDArray from numba import njit -from elastica._rotations import _get_rotation_matrix, _rotate +from elastica._rotations import _get_rotation_matrix from elastica._linalg import _batch_matmul if TYPE_CHECKING: @@ -13,549 +19,146 @@ else: SymplecticSystemProtocol = "SymplecticSystemProtocol" -# FIXME : Explicit Stepper doesn't work as States lose the -# views they initially had when working with a timestepper. -# class _RodExplicitStepperMixin: -# def __init__(self) -> None: -# ( -# self.state, -# self.__deriv_state, -# self.position_collection, -# self.director_collection, -# self.velocity_collection, -# self.omega_collection, -# self.acceleration_collection, -# self.alpha_collection, # angular acceleration -# ) = _bootstrap_from_data( -# "explicit", self.n_elems, self._vector_states, self._matrix_states -# ) -# -# # def __setattr__(self, name, value): -# # np.copy(self.__dict__[name], value) -# -# def __call__(self, time, *args, **kwargs): -# self.update_accelerations(time) # Internal, external -# -# # print("KRC", self.state.kinematic_rate_collection) -# # print("DEr", self.__deriv_state.rate_collection) -# if np.shares_memory( -# self.state.kinematic_rate_collection, -# self.velocity_collection -# # self.__deriv_state.rate_collection -# ): -# print("Shares memory") -# else: -# print("Explicit states does not share memory") -# return self.__deriv_state - class _RodSymplecticStepperMixin: - def __init__(self: SymplecticSystemProtocol) -> None: - self.kinematic_states = _KinematicState( - self.position_collection, self.director_collection - ) - self.dynamic_states = _DynamicState( - self.v_w_collection, - self.dvdt_dwdt_collection, - self.velocity_collection, - self.omega_collection, - ) - - # Expose rate returning functions in the interface - # to be used by the time-stepping algorithm - # dynamic rates needs to call update_accelerations and henc - # is another function - self.kinematic_rates = self.dynamic_states.kinematic_rates - - def dynamic_rates( - self: SymplecticSystemProtocol, - time: np.float64, - prefac: np.float64, - ) -> NDArray[np.float64]: - self.update_accelerations(time) - return self.dynamic_states.dynamic_rates(time, prefac) - - -def _bootstrap_from_data( - stepper_type: str, - n_elems: int, - vector_states: NDArray[np.float64], - matrix_states: NDArray[np.float64], -) -> Optional[ - tuple[ - "_State", - "_DerivativeState", - NDArray[np.float64], - NDArray[np.float64], - NDArray[np.float64], - NDArray[np.float64], - NDArray[np.float64], - NDArray[np.float64], - ] -]: - """Returns states wrapping numpy arrays based on the time-stepping algorithm - - Convenience method that takes in rod internal (raw np.ndarray) data, create views - (references) from it, and outputs State classes that are used in the time-stepping - algorithm. This means that modifying the state modifies the internal data! - - Parameters - ---------- - stepper_type : str (likely to change in future), representing stepper type - Allowed parameters are ['explicit', 'symplectic'] - n_elems : int, number of rod elements - vector_states : np.ndarray of shape (dim, *) with the following structure - `vector_states` = [`position`,`velocity`,`omega`,`acceleration`,`angular acceleration`] - `n_nodes = n_elems + 1` - `position = 0 -> n_nodes , size = n_nodes` - `velocity = n_nodes -> 2 * n_nodes, size = n_nodes` - `omega = 2 * n_nodes -> 2 * n_nodes + nelem, size = nelem` - `acceleration = 2 * n_nodes + nelem -> 3 * n_nodes + nelem, size = n_nodes` - `angular acceleration = 3 * n_nodes + nelem -> 3 * n_nodes + 2 * nelem, size = n_elems` - matrix_states : np.ndarray of shape (dim, dim, n_elems) containing the directors - - Returns - ------- - output : tuple of len 8 containing - (state, derivative_state, position, directors, velocity, omega, acceleration, alpha) - derivative_state carries rate information - """ - n_nodes = n_elems + 1 - position = np.ndarray.view(vector_states[..., :n_nodes]) - directors = np.ndarray.view(matrix_states) - v_w_dvdt_dwdt = np.ndarray.view(vector_states[..., n_nodes:]) - output: tuple = () - if stepper_type == "explicit": - v_w_states = np.ndarray.view(vector_states[..., n_nodes : 3 * n_nodes - 1]) - output += ( - _State(n_elems, position, directors, v_w_states), - _DerivativeState(n_elems, v_w_dvdt_dwdt), - ) - elif stepper_type == "symplectic": - # TODO: Consider removing. - # output += ( - # _KinematicState(n_elems, position, directors), - # _DynamicState(n_elems, v_w_dvdt_dwdt), - # ) - raise NotImplementedError - else: - return None - - n_velocity_end = n_nodes + n_nodes - velocity = np.ndarray.view(vector_states[..., n_nodes:n_velocity_end]) - - n_omega_end = n_velocity_end + n_elems - omega = np.ndarray.view(vector_states[..., n_velocity_end:n_omega_end]) - - n_acceleration_end = n_omega_end + n_nodes - acceleration = np.ndarray.view(vector_states[..., n_omega_end:n_acceleration_end]) - - n_alpha_end = n_acceleration_end + n_elems - alpha = np.ndarray.view(vector_states[..., n_acceleration_end:n_alpha_end]) - - return output + (position, directors, velocity, omega, acceleration, alpha) - - -""" -Explicit stepper interface -""" + Mixin class providing necessary methods for integration of the kinematic and + dynamic equations of the rod. - -class _State: - """State for explicit steppers. - - Wraps data as state, with overloaded methods for explicit steppers - (steppers that integrate all states in one-step/stage). - Allows for separating implementation of stepper from actual - addition/multiplication/other formulae used. + This mixin manages the rod's posture (position and directors), velocity + (linear and angular), and acceleration states. It provides `update_kinematics` + and `update_dynamics` methods to apply updates to these states, typically + called by a symplectic time-stepper. """ - # TODO : args, kwargs instead of hardcoding types - def __init__( + n_nodes: int + + # Posture state + position_collection: NDArray[np.float64] + director_collection: NDArray[np.float64] + # Velocity state + velocity_collection: NDArray[np.float64] + omega_collection: NDArray[np.float64] + v_w_collection: NDArray[np.float64] # Rate collection + # Acceleration state + # acceleration_collection: NDArray[np.float64] + # alpha_collection: NDArray[np.float64] + dvdt_dwdt_collection: NDArray[np.float64] # Second derivative collection + + def update_kinematics( self, - n_elems: int, - position_collection_view: NDArray[np.float64], - director_collection_view: NDArray[np.float64], - kinematic_rate_collection_view: NDArray[np.float64], + time: np.float64, + prefac: np.float64, ) -> None: """ - Parameters - ---------- - n_elems : int, number of rod elements - position_collection_view : view of positions (or) x - director_collection_view : view of directors (or) Q - kinematic_rate_collection_view : view of velocity and omega (or) (v,ω) - """ - super(_State, self).__init__() - self.n_nodes = n_elems + 1 - self.n_kinematic_rates = self.n_nodes + n_elems # start of (v,ω) in (x,Q,v,ω) - self.position_collection = position_collection_view - self.director_collection = director_collection_view - self.kinematic_rate_collection = kinematic_rate_collection_view - - def __iadd__(self, scaled_deriv_array: NDArray[np.float64]) -> Self: - """overloaded += operator + Update kinematic state. - The add for directors is customized to reflect Rodrigues' rotation - formula. + Typically called after velocity and omega (angular velocity) have been updated. Parameters ---------- - scaled_deriv_array : np.ndarray containing dt * (v, ω, dv/dt, dω/dt) - ,as returned from _DerivativeState's __mul__ method - - Returns - ------- - self : _State with inplace modified data - - """ - # x += v*dt - self.position_collection += scaled_deriv_array[..., : self.n_nodes] - # TODO : Verify the math in this note - r""" - Developer Note - -------------- - Here the overloaded `+=` operator is exploited to perform - matrix multiplication for the directors, which is counter- - intutive at first. While this provides a stable interface - to interact the rod states with the timesteppers and the - rest of the world, the reasons behind including it here also has - a depper mathematical significance. - - Firstly, position lies in the vector space corresponding to R^{3} - and update is done this space (with the + and * operators defined - as usual), hence the `+=` operator (or `__iadd__`) is reflected - as `+=` operator in the position update (line 163 above). - - For directors rather, which lie in a restricteed R^{3} \otimes - R^{3} tensorial space, the space with Q^T.Q = Q.Q^T = I, the + - operator can be thought of as an equivalent `*=` update for a - 'exponential' multiplication with a rotation matrix (e^{At}). - . This does not correspond to the position update. However, if - we view this in a logarithmic space the `*=` becomse the '+=' - operator once again! After performing this `+=` operation, we - bring it back into its original space using the exponential - operator. So we are still indirectly doing the '+=' - update. - - To avoid all this hassle with the operators and spaces, we simply define - '+=' or '__iadd__' in the case of directors as an equivalent - '*=' (matrix multiply) with the RHS below. - """ - # TODO Q *= exp(w*dt) , whats' the formua again? - # TODO the scale factor 1.0 does not seem to be necessary, although - # we perform more work in the present framework (muliply dt to entire vector, then take - # norm) rather than vector norm then multiple by dt (1/3 operation costs) - # TODO optimize (somehow) extra copy away : if we don't make a copy - # its even more slower, maybe due to aliasing effects - np.einsum( - "ijk,jlk->ilk", - _get_rotation_matrix( - 1.0, scaled_deriv_array[..., self.n_nodes : self.n_kinematic_rates] - ), - self.director_collection.copy(), - out=self.director_collection, - ) - # (v,ω) += (dv/dt, dω/dt)*dt - self.kinematic_rate_collection += scaled_deriv_array[ - ..., self.n_kinematic_rates : - ] - return self - - def __add__(self, scaled_derivative_state: NDArray[np.float64]) -> "_State": - """overloaded + operator, useful in state.k1 = state + dt * deriv_state - - The add for directors is customized to reflect Rodrigues' rotation - formula. - - Parameters - ---------- - scaled_derivative_state : np.ndarray with dt * (v, ω, dv/dt, dω/dt) - ,as returned from _DerivativeState's __mul__ method - - Returns - ------- - state : new _State object with modified data (copied) - - Caveats - ------- - Note that the argument is not a `other` _State object but is rather - assumed to be a `np.ndarray` from calling _DerivativeState's __mul__ - method. This reflects the most common use-case in time-steppers - - """ - # x += v*dt - position_collection = ( - self.position_collection + scaled_derivative_state[..., : self.n_nodes] - ) - # Devs : see `_State.__iadd__` for reasons why we do matmul here - director_collection = _rotate( + time : float + Current time. + prefac : float + Integration prefactor. + """ + overload_operator_kinematic_numba( + prefac, + self.position_collection, self.director_collection, - 1.0, - scaled_derivative_state[..., self.n_nodes : self.n_kinematic_rates], - ) - # (v,ω) += (dv/dt, dω/dt)*dt - kinematic_rate_collection = ( - self.kinematic_rate_collection - + scaled_derivative_state[..., self.n_kinematic_rates :] - ) - return _State( - self.n_nodes - 1, - position_collection, - director_collection, - kinematic_rate_collection, + self.velocity_collection, + self.omega_collection, ) - -class _DerivativeState: - """TimeDerivative of States for explicit steppers. - - Wraps time-derivative data as state, with overloaded methods for - explicit steppers (steppers that integrate all states in one-step/stage). - Allows for separating implementation of stepper from actual addition - /multiplication used. - """ - - def __init__( - self, _unused_n_elems: int, rate_collection_view: NDArray[np.float64] + def update_dynamics( + self, + time: np.float64, + prefac: np.float64, ) -> None: """ - Parameters - ---------- - _unused_n_elems : int, number of elements (unused, kept for - compatibility with `_bootstrap_from_data`) - rate_collection_view : np.ndarray containing (v, ω, dv/dt, dω/dt) - """ - super(_DerivativeState, self).__init__() - self.rate_collection = rate_collection_view - - def __rmul__(self, scalar: np.float64) -> NDArray[np.float64]: # type: ignore - """overloaded scalar * self, - - Parameters - ---------- - scalar : float, typically dt (the time-step) - - Returns - ------- - output : np.ndarray containing (v*dt, ω*dt, dv/dt*dt, dω/dt*dt) - - Caveats - ------- - Returns a np.ndarray and not a State object (as one expects). - Returning a State here with (v*dt, ω*dt, dv/dt*dt, dω/dt*dt) as members - is possible but it's less efficient, especially because this is hot - piece of code - """ - """ - Developer Note - -------------- - - Q : Why do we need to overload operators here? - - The Derivative class naturally doesn't have a `mul` overloaded - operator. That means if this method is not present, - doing something like - ``` - ds = _DerivativeState(...) - new_state = 2 * ds - ``` - will throw an error. Note that you can do something like - ``` - ds = _DerivativeState(...) - new_state = 2 * ds.rate_collection - ``` - but this is hacky, as we are exposing the members outside, - in the calling scope (defeats encapsulation and hiding). - The point of having this class is that it works - well with the time-stepper (where we only use `+` and `*` - operations on the State/DerivativeState like above, - i.e. `state = dt * derivative_state` and not something like - `state = dt * derivative_state.rate_collection`). - It also provides an interface for anything outside - the `Rod` system as a whole. - """ - return scalar * self.rate_collection + Update dynamic state. - def __mul__(self, scalar: np.float64) -> NDArray[np.float64]: - """overloaded self * scalar - - TODO Check if this pattern (forwarding to __mul__) has - any disdvantages apart from extra function call penalty + Typically called after acceleration and alpha (angular acceleration) have been updated. Parameters ---------- - scalar : float, typically dt (the time-step) - - Returns - ------- - output : np.ndarray containing (v*dt, ω*dt, dv/dt*dt, dω/dt*dt) - + time : float + Current time. + prefac : float + Integration prefactor. """ - return self.__rmul__(scalar) + overload_operator_dynamic_numba( + prefac, + self.v_w_collection, + self.dvdt_dwdt_collection, + ) """ -Symplectic stepper interface +Symplectic stepper operation """ -class _KinematicState: - """State storing (x,Q) for symplectic steppers. - Wraps data as state, with overloaded methods for symplectic steppers. - Allows for separating implementation of stepper from actual - addition/multiplication/other formulae used. - - Symplectic steppers rely only on in-place modifications to state and so - only these methods are provided. - """ - - def __init__( - self, - position_collection_view: NDArray[np.float64], - director_collection_view: NDArray[np.float64], - ) -> None: - """ - Parameters - ---------- - position_collection_view : view of positions (or) x - director_collection_view : view of directors (or) Q - """ - # super(_KinematicState, self).__init__() - - self.position_collection = position_collection_view - self.director_collection = director_collection_view - - @njit(cache=True) # type: ignore def overload_operator_kinematic_numba( - n_nodes: int, prefac: np.float64, position_collection: NDArray[np.float64], director_collection: NDArray[np.float64], velocity_collection: NDArray[np.float64], omega_collection: NDArray[np.float64], ) -> None: - """overloaded += operator + """Performs in-place update of kinematic states (position and director) using Numba. - The add for directors is customized to reflect Rodrigues' rotation + This operator updates the position and director collections of a rod based on + its velocity and angular velocity. The director update uses Rodrigues' rotation formula. + Parameters ---------- - scaled_deriv_array : np.ndarray containing dt * (v, ω), - as retured from _DynamicState's `kinematic_rates` method - Returns - ------- - self : _KinematicState instance with inplace modified data - Caveats - ------- - Takes a np.ndarray and not a _KinematicState object (as one expects). - This is done for efficiency reasons, see _DynamicState's `kinematic_rates` - method + prefac : numpy.float64 + Pre-factor (e.g., time step `dt`) to scale the velocity and angular velocity. + position_collection : numpy.ndarray + Position of the rod nodes. Modified in-place. + director_collection : numpy.ndarray + Director (orientation) of the rod elements. Modified in-place. + velocity_collection : numpy.ndarray + Linear velocity of the rod nodes. + omega_collection : numpy.ndarray + Angular velocity of the rod elements. """ # x += v*dt + blocksize = position_collection.shape[1] for i in range(3): - for k in range(n_nodes): + for k in range(blocksize): position_collection[i, k] += prefac * velocity_collection[i, k] rotation_matrix = _get_rotation_matrix(1.0, prefac * omega_collection) director_collection[:] = _batch_matmul(rotation_matrix, director_collection) - return - - -class _DynamicState: - """State storing (v,ω, dv/dt, dω/dt) for symplectic steppers. - - Wraps data as state, with overloaded methods for symplectic steppers. - Allows for separating implementation of stepper from actual - addition/multiplication/other formulae used. - Symplectic steppers rely only on in-place modifications to state and so - only these methods are provided. - """ - - def __init__( - self, - v_w_collection: NDArray[np.float64], - dvdt_dwdt_collection: NDArray[np.float64], - velocity_collection: NDArray[np.float64], - omega_collection: NDArray[np.float64], - ) -> None: - """ - Parameters - ---------- - n_elems : int, number of rod elements - rate_collection_view : np.ndarray containing (v, ω, dv/dt, dω/dt) - v_w_collection : numpy.ndarray - - """ - super(_DynamicState, self).__init__() - # Limit at which (v, w) end - # Create views for dynamic state - self.rate_collection = v_w_collection - self.dvdt_dwdt_collection = dvdt_dwdt_collection - self.velocity_collection = velocity_collection - self.omega_collection = omega_collection - - def kinematic_rates( - self, time: np.float64, prefac: np.float64 - ) -> tuple[NDArray[np.float64], NDArray[np.float64]]: - """Yields kinematic rates to interact with _KinematicState - - Returns - ------- - v_and_omega : np.ndarray consisting of (v,ω) - Caveats - ------- - Doesn't return a _KinematicState with (dt*v, dt*w) as members, - as one expects the _Kinematic __add__ operator to interact - with another _KinematicState. This is done for efficiency purposes. - """ - # RHS functino call, gives v,w so that - # Comes from kin_state -> (x,Q) += dt * (v,w) <- First part of dyn_state - return self.velocity_collection, self.omega_collection - - def dynamic_rates( - self, time: np.float64, prefac: np.float64 - ) -> NDArray[np.float64]: - """Yields dynamic rates to add to with _DynamicState - Returns - ------- - acc_and_alpha : np.ndarray consisting of (dv/dt,dω/dt) - Caveats - ------- - Doesn't return a _DynamicState with (dt*v, dt*w) as members, - as one expects the _Dynamic __add__ operator to interact - with another _DynamicState. This is done for efficiency purposes. - """ - return prefac * self.dvdt_dwdt_collection - @njit(cache=True) # type: ignore def overload_operator_dynamic_numba( + prefac: np.float64, rate_collection: NDArray[np.float64], - scaled_second_deriv_array: NDArray[np.float64], + second_deriv_array: NDArray[np.float64], ) -> None: - """overloaded += operator, updating dynamic_rates + """Performs in-place update of dynamic states (linear and angular velocities) using Numba. + + This operator updates the rate collection (which stores linear and angular velocities) + of a rod based on the second derivative array (linear and angular accelerations). + Parameters ---------- - scaled_second_deriv_array : np.ndarray containing dt * (dvdt, dωdt), - as retured from _DynamicState's `dynamic_rates` method - Returns - ------- - self : _DynamicState instance with inplace modified data - Caveats - ------- - Takes a np.ndarray and not a _DynamicState object (as one expects). - This is done for efficiency reasons, see `dynamic_rates`. + prefac : numpy.float64 + Pre-factor (e.g., time step `dt`) to scale the second derivative terms. + rate_collection : numpy.ndarray + Collection of linear and angular velocities of the rod. Modified in-place. + second_deriv_array : numpy.ndarray + Collection of linear and angular accelerations (dv/dt, dω/dt) of the rod. """ # Always goes in LHS : that means the update is on the rates alone - # (v,ω) += dt * (dv/dt, dω/dt) -> self.dynamic_rates - # rate_collection[..., : n_kinematic_rates] += scaled_second_deriv_array - blocksize = scaled_second_deriv_array.shape[1] - + # (v,ω) += dt * (dv/dt, dω/dt) + # rate_collection[..., : n_kinematic_rates] += second_deriv_aray + blocksize = second_deriv_array.shape[1] for i in range(2): for k in range(blocksize): - rate_collection[i, k] += scaled_second_deriv_array[i, k] - - return + rate_collection[i, k] += prefac * second_deriv_array[i, k] diff --git a/elastica/rod/energy.py b/elastica/rod/energy.py new file mode 100644 index 000000000..4b5be68b6 --- /dev/null +++ b/elastica/rod/energy.py @@ -0,0 +1,117 @@ +""" +Energy computation mixin for Cosserat rods. + +This mixin provides methods to compute various energy quantities +(translational, rotational, bending, shear) and center of mass properties. +""" + +import numpy as np +from numpy.typing import NDArray + +from elastica._linalg import _batch_matvec, _batch_dot + + +class RodEnergy: + """ + Mixin class providing energy computation methods for rods. + + This mixin should be used with RodBase-derived classes that have + the required attributes (mass, velocity, omega, etc.). + + Example usage:: + + class MyRod(RodBase, RodEnergy): + ... + + rod = MyRod(...) + kinetic_energy = rod.compute_translational_energy() + bending_energy = rod.compute_bending_energy() + + """ + + # Required attributes (provided by RodBase-derived class) + mass: NDArray[np.float64] + velocity_collection: NDArray[np.float64] + position_collection: NDArray[np.float64] + omega_collection: NDArray[np.float64] + mass_second_moment_of_inertia: NDArray[np.float64] + dilatation: NDArray[np.float64] + kappa: NDArray[np.float64] + rest_kappa: NDArray[np.float64] + bend_matrix: NDArray[np.float64] + rest_voronoi_lengths: NDArray[np.float64] + sigma: NDArray[np.float64] + rest_sigma: NDArray[np.float64] + shear_matrix: NDArray[np.float64] + rest_lengths: NDArray[np.float64] + + def compute_translational_energy(self) -> NDArray[np.float64]: + """ + Compute total translational energy of the rod at the instance. + """ + return ( + 0.5 + * ( + self.mass + * np.einsum( + "ij, ij-> j", self.velocity_collection, self.velocity_collection + ) + ).sum() + ) + + def compute_rotational_energy(self) -> NDArray[np.float64]: + """ + Compute total rotational energy of the rod at the instance. + """ + J_omega_upon_e = ( + _batch_matvec(self.mass_second_moment_of_inertia, self.omega_collection) + / self.dilatation + ) + return 0.5 * np.einsum("ik,ik->k", self.omega_collection, J_omega_upon_e).sum() + + def compute_velocity_center_of_mass(self) -> NDArray[np.float64]: + """ + Compute velocity center of mass of the rod at the instance. + """ + mass_times_velocity = np.einsum("j,ij->ij", self.mass, self.velocity_collection) + sum_mass_times_velocity = np.einsum("ij->i", mass_times_velocity) + + return sum_mass_times_velocity / self.mass.sum() + + def compute_position_center_of_mass(self) -> NDArray[np.float64]: + """ + Compute position center of mass of the rod at the instance. + """ + mass_times_position = np.einsum("j,ij->ij", self.mass, self.position_collection) + sum_mass_times_position = np.einsum("ij->i", mass_times_position) + + return sum_mass_times_position / self.mass.sum() + + def compute_bending_energy(self) -> NDArray[np.float64]: + """ + Compute total bending energy of the rod at the instance. + """ + + kappa_diff = self.kappa - self.rest_kappa + bending_internal_torques = _batch_matvec(self.bend_matrix, kappa_diff) + + return ( + 0.5 + * ( + _batch_dot(kappa_diff, bending_internal_torques) + * self.rest_voronoi_lengths + ).sum() + ) + + def compute_shear_energy(self) -> NDArray[np.float64]: + """ + Compute total shear energy of the rod at the instance. + """ + + sigma_diff = self.sigma - self.rest_sigma + shear_internal_forces = _batch_matvec(self.shear_matrix, sigma_diff) + + return ( + 0.5 + * (_batch_dot(sigma_diff, shear_internal_forces) * self.rest_lengths).sum() + ) diff --git a/elastica/rod/factory_function.py b/elastica/rod/factory_function.py index 2765e8726..792c2ea5d 100644 --- a/elastica/rod/factory_function.py +++ b/elastica/rod/factory_function.py @@ -62,7 +62,7 @@ def allocate( log = logging.getLogger() if "poisson_ratio" in kwargs: - # Deprecation warning for poission_ratio + # Deprecation warning for poisson_ratio raise NameError( "Poisson's ratio is deprecated for Cosserat Rod for clarity. Please provide shear_modulus instead." ) @@ -333,42 +333,6 @@ def allocate( ) -""" -Cosserat rod constructor for straight-rod or ring rod geometry. - - -Notes ------ -Since we expect the Cosserat Rod to simulate soft rod, Poisson's ratio is set to 0.5 by default. -It is possible to give additional argument "shear_modulus" or "poisson_ratio" to specify extra modulus. - - -Parameters ----------- -n_elements : int - Number of element. Must be greater than 3. Generally recommended to start with 40-50, and adjust the resolution. -direction : NDArray[3, float] - Direction of the rod in 3D -normal : NDArray[3, float] - Normal vector of the rod in 3D -base_length : float - Total length of the rod -base_radius : float - Uniform radius of the rod -density : float - Density of the rod -youngs_modulus : float - Young's modulus -**kwargs : dict, optional - The "position" and/or "directors" can be overrided by passing "position" and "directors" argument. - Remember, the shape of the "position" is (3,n_elements+1) and the shape of the "directors" is (3,3,n_elements). - -Returns -------- - -""" - - def _assert_dim(vector: np.ndarray, max_dim: int, name: str) -> None: assert vector.ndim < max_dim, ( f"Input {name} dimension is not correct {vector.shape}" diff --git a/elastica/rod/knot_theory.py b/elastica/rod/knot_theory.py index 64b89e882..9f9c15691 100644 --- a/elastica/rod/knot_theory.py +++ b/elastica/rod/knot_theory.py @@ -14,11 +14,8 @@ from numpy.typing import NDArray from numba import njit -from elastica.rod.rod_base import RodBase from elastica._linalg import _batch_norm, _batch_dot, _batch_cross -from .protocol import CosseratRodProtocol - class KnotTheory: """ @@ -35,30 +32,34 @@ def __init__(self) -> None: total_twist = rod.compute_twist() total_link = rod.compute_link() - There are few alternative way of handling edge-condition in computing Link and Writhe. - Here, we provide three methods: "next_tangent", "end_to_end", and "net_tangent". - The default *type_of_additional_segment* is set to "next_tangent." - - ========================== ===================================== - type_of_additional_segment Description - ========================== ===================================== - next_tangent | Adds a two new point at the begining and end of the center line. - | Distance of these points are given in segment_length. - | Direction of these points are computed using the rod tangents at - | the begining and end. - end_to_end | Adds a two new point at the begining and end of the center line. - | Distance of these points are given in segment_length. - | Direction of these points are computed using the rod node end - | positions. - net_tangent | Adds a two new point at the begining and end of the center line. - | Distance of these points are given in segment_length. Direction of - | these points are point wise avarege of nodes at the first and - | second half of the rod. - ========================== ===================================== + There are a few alternative ways of handling edge-conditions in computing Link and Writhe. + The `type_of_additional_segment` parameter, which defaults to ``"next_tangent"``, can be set to one of the following: + + ``"next_tangent"`` + Adds two new points at the beginning and end of the center line. + The distance of these points is given by `segment_length`. + The direction of these points is computed using the rod tangents at + the beginning and end. + ``"end_to_end"`` + Adds two new points at the beginning and end of the center line. + The distance of these points is given by `segment_length`. + The direction of these points is computed using the rod node end + positions. + ``"net_tangent"`` + Adds two new points at the beginning and end of the center line. + The distance of these points is given by `segment_length`. The direction of + these points is the point-wise average of nodes in the first and + second half of the rod.= """ - def compute_twist(self: CosseratRodProtocol) -> NDArray[np.float64]: + # Required attributes (provided by RodBase-derived class) + position_collection: NDArray[np.float64] + director_collection: NDArray[np.float64] + rest_lengths: NDArray[np.float64] + radius: NDArray[np.float64] + + def compute_twist(self) -> NDArray[np.float64]: """ See :ref:`api/rods:Knot Theory (Mixin)` for the detail. """ @@ -69,7 +70,7 @@ def compute_twist(self: CosseratRodProtocol) -> NDArray[np.float64]: return total_twist[0] def compute_writhe( - self: CosseratRodProtocol, + self, type_of_additional_segment: str = "next_tangent", alpha: float = 1.0, ) -> NDArray[np.float64]: @@ -92,7 +93,7 @@ def compute_writhe( )[0] def compute_link( - self: CosseratRodProtocol, + self, type_of_additional_segment: str = "next_tangent", alpha: float = 1.0, ) -> NDArray[np.float64]: @@ -723,6 +724,9 @@ def _compute_additional_segment( # Direction of the additional point at the end of the rod direction_of_rod_end = center_line[i, :, -1] - center_line[i, :, -2] direction_of_rod_end /= np.linalg.norm(direction_of_rod_end) + + beginning_direction[i, :] = direction_of_rod_begin + end_direction[i, :] = direction_of_rod_end elif type_of_additional_segment == "end_to_end": for i in range(timesize): # Direction of the additional point at the beginning of the rod @@ -731,6 +735,9 @@ def _compute_additional_segment( # Direction of the additional point at the end of the rod direction_of_rod_end = -direction_of_rod_begin + + beginning_direction[i, :] = direction_of_rod_begin + end_direction[i, :] = direction_of_rod_end elif type_of_additional_segment == "net_tangent": for i in range(timesize): # Direction of the additional point at the beginning of the rod @@ -745,19 +752,18 @@ def _compute_additional_segment( direction_of_rod_begin = average_begin - average_end direction_of_rod_begin /= np.linalg.norm(direction_of_rod_begin) direction_of_rod_end = -direction_of_rod_begin + + beginning_direction[i, :] = direction_of_rod_begin + end_direction[i, :] = direction_of_rod_end else: raise NotImplementedError("unavailable type_of_additional_segment is given") # Compute new centerline and beginning/end direction for i in range(timesize): - first_point = center_line[i, :, 0] + segment_length * direction_of_rod_begin - last_point = center_line[i, :, -1] + segment_length * direction_of_rod_end - + first_point = center_line[i, :, 0] + segment_length * beginning_direction[i, :] + last_point = center_line[i, :, -1] + segment_length * end_direction[i, :] new_center_line[i, :, 1:-1] = center_line[i, :, :] new_center_line[i, :, 0] = first_point new_center_line[i, :, -1] = last_point - beginning_direction[i, :] = direction_of_rod_begin - end_direction[i, :] = direction_of_rod_end - return new_center_line, beginning_direction, end_direction diff --git a/elastica/rod/protocol.py b/elastica/rod/protocol.py deleted file mode 100644 index cfc19a825..000000000 --- a/elastica/rod/protocol.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import Protocol - -import numpy as np -from numpy.typing import NDArray - -from elastica.systems.protocol import SystemProtocol, SlenderBodyGeometryProtocol - - -class _CosseratRodEnergy(Protocol): - def compute_bending_energy(self) -> NDArray[np.float64]: ... - - def compute_shear_energy(self) -> NDArray[np.float64]: ... - - def compute_translational_energy(self) -> NDArray[np.float64]: ... - - def compute_rotational_energy(self) -> NDArray[np.float64]: ... - - -class CosseratRodProtocol( - SystemProtocol, SlenderBodyGeometryProtocol, _CosseratRodEnergy, Protocol -): - - mass: NDArray[np.float64] - volume: NDArray[np.float64] - radius: NDArray[np.float64] - tangents: NDArray[np.float64] - lengths: NDArray[np.float64] - rest_lengths: NDArray[np.float64] - rest_voronoi_lengths: NDArray[np.float64] - kappa: NDArray[np.float64] - sigma: NDArray[np.float64] - rest_kappa: NDArray[np.float64] - rest_sigma: NDArray[np.float64] - - internal_stress: NDArray[np.float64] - internal_couple: NDArray[np.float64] - dilatation: NDArray[np.float64] - dilatation_rate: NDArray[np.float64] - voronoi_dilatation: NDArray[np.float64] - - bend_matrix: NDArray[np.float64] - shear_matrix: NDArray[np.float64] - - mass_second_moment_of_inertia: NDArray[np.float64] - inv_mass_second_moment_of_inertia: NDArray[np.float64] - - ghost_voronoi_idx: NDArray[np.int32] - ghost_elems_idx: NDArray[np.int32] - - ring_rod_flag: bool - periodic_boundary_nodes_idx: NDArray[np.int32] - periodic_boundary_elems_idx: NDArray[np.int32] - periodic_boundary_voronoi_idx: NDArray[np.int32] diff --git a/elastica/rod/rod_base.py b/elastica/rod/rod_base.py index 3bd846b7b..2ce303311 100644 --- a/elastica/rod/rod_base.py +++ b/elastica/rod/rod_base.py @@ -4,8 +4,11 @@ import numpy as np from numpy.typing import NDArray +from elastica.rod.energy import RodEnergy +from elastica.rod.knot_theory import KnotTheory -class RodBase: + +class RodBase(RodEnergy, KnotTheory): """ Base class for all rods. @@ -17,22 +20,59 @@ class RodBase: REQUISITE_MODULES: list[Type] = [] - def __init__(self) -> None: - """ - RodBase does not take any arguments. - """ - self.position_collection: NDArray[np.float64] - self.velocity_collection: NDArray[np.float64] - self.acceleration_collection: NDArray[np.float64] - self.director_collection: NDArray[np.float64] - self.omega_collection: NDArray[np.float64] - self.alpha_collection: NDArray[np.float64] - self.external_forces: NDArray[np.float64] - self.external_torques: NDArray[np.float64] - - self.ghost_voronoi_idx: NDArray[np.int32] - self.ghost_elems_idx: NDArray[np.int32] - - self.periodic_boundary_nodes_idx: NDArray[np.int32] - self.periodic_boundary_elems_idx: NDArray[np.int32] - self.periodic_boundary_voronoi_idx: NDArray[np.int32] + # Geometry + n_elems: int + n_nodes: int + + # State arrays + position_collection: NDArray[np.float64] + velocity_collection: NDArray[np.float64] + acceleration_collection: NDArray[np.float64] + director_collection: NDArray[np.float64] + omega_collection: NDArray[np.float64] + alpha_collection: NDArray[np.float64] + + # External forces/torques + external_forces: NDArray[np.float64] + external_torques: NDArray[np.float64] + + # Internal forces/torques + internal_forces: NDArray[np.float64] + internal_torques: NDArray[np.float64] + + # Rod-specific properties + mass: NDArray[np.float64] + volume: NDArray[np.float64] + radius: NDArray[np.float64] + tangents: NDArray[np.float64] + lengths: NDArray[np.float64] + rest_lengths: NDArray[np.float64] + rest_voronoi_lengths: NDArray[np.float64] + kappa: NDArray[np.float64] + sigma: NDArray[np.float64] + rest_kappa: NDArray[np.float64] + rest_sigma: NDArray[np.float64] + + internal_stress: NDArray[np.float64] + internal_couple: NDArray[np.float64] + dilatation: NDArray[np.float64] + dilatation_rate: NDArray[np.float64] + voronoi_dilatation: NDArray[np.float64] + + bend_matrix: NDArray[np.float64] + shear_matrix: NDArray[np.float64] + + mass_second_moment_of_inertia: NDArray[np.float64] + inv_mass_second_moment_of_inertia: NDArray[np.float64] + + # Ring rod / periodic boundary + ring_rod_flag: bool + ghost_voronoi_idx: NDArray[np.int32] + ghost_elems_idx: NDArray[np.int32] + periodic_boundary_nodes_idx: NDArray[np.int32] + periodic_boundary_elems_idx: NDArray[np.int32] + periodic_boundary_voronoi_idx: NDArray[np.int32] + + # Symplectic stepper state + v_w_collection: NDArray[np.float64] + dvdt_dwdt_collection: NDArray[np.float64] diff --git a/elastica/surface/__init__.py b/elastica/surface/__init__.py index efe78d06c..5504e1f47 100644 --- a/elastica/surface/__init__.py +++ b/elastica/surface/__init__.py @@ -1,3 +1,2 @@ __doc__ = """Surface classes""" -from elastica.surface.surface_base import SurfaceBase from elastica.surface.plane import Plane diff --git a/elastica/surface/plane.py b/elastica/surface/plane.py index b12a3feaf..700357103 100644 --- a/elastica/surface/plane.py +++ b/elastica/surface/plane.py @@ -1,26 +1,39 @@ -__doc__ = """""" +__doc__ = """Module containing plane surface implementation for contact interactions.""" +from typing import Type -from elastica.surface.surface_base import SurfaceBase import numpy as np from numpy.typing import NDArray from elastica.utils import Tolerance -class Plane(SurfaceBase): +class Plane: + """ + Plane static system. Static system does not change by the timestepping. + + Attributes + ---------- + normal : numpy.ndarray + 1D (3,) array containing the normal vector of the plane. + origin : numpy.ndarray + 2D (3, 1) array containing the origin of the plane. + """ + + REQUISITE_MODULES: list[Type] = [] + def __init__( self, plane_origin: NDArray[np.float64], plane_normal: NDArray[np.float64] ): """ - Plane surface initializer. + Plane initializer. Parameters ---------- - plane_origin: np.ndarray + plane_origin: numpy.ndarray + 1D (3,) or 2D (3, 1) array containing data with 'float' type. Origin of the plane. - Expect (3,1)-shaped array. - plane_normal: np.ndarray + plane_normal: numpy.ndarray + 1D (3,) or 2D (3, 1) array containing data with 'float' type. The normal vector of the plane, must be normalized. - Expect (3,1)-shaped array. """ assert np.allclose( diff --git a/elastica/surface/surface_base.py b/elastica/surface/surface_base.py deleted file mode 100644 index 37812ea7d..000000000 --- a/elastica/surface/surface_base.py +++ /dev/null @@ -1,31 +0,0 @@ -__doc__ = """Base class for surfaces""" -from typing import TYPE_CHECKING, Type - -import numpy as np -from numpy.typing import NDArray - - -class SurfaceBase: - """ - Base class for all surfaces. - - Notes - ----- - All new surface classes must be derived from this SurfaceBase class. - - """ - - REQUISITE_MODULES: list[Type] = [] - - def __init__(self) -> None: - """ - SurfaceBase does not take any arguments. - """ - self.normal: NDArray[np.float64] # (3,) - self.origin: NDArray[np.float64] # (3, 1) - - -if TYPE_CHECKING: - from elastica.systems.protocol import StaticSystemProtocol - - _: StaticSystemProtocol = SurfaceBase() diff --git a/elastica/systems/protocol.py b/elastica/systems/protocol.py index 89d52d928..9af4ddea9 100644 --- a/elastica/systems/protocol.py +++ b/elastica/systems/protocol.py @@ -1,70 +1,62 @@ __doc__ = """Base class for elastica system""" -from typing import Protocol, Type -from elastica.typing import StateType, SystemType +from typing import Protocol, Type, runtime_checkable -from elastica.rod.data_structures import _KinematicState, _DynamicState +from abc import abstractmethod import numpy as np from numpy.typing import NDArray +@runtime_checkable class StaticSystemProtocol(Protocol): + """ + Protocol for all static elastica system. Minimal requirement interface + to be included in the simulator. + """ + REQUISITE_MODULES: list[Type] +@runtime_checkable class SystemProtocol(StaticSystemProtocol, Protocol): """ - Protocol for all dynamic elastica system + Protocol for all dynamic elastica system. """ + @abstractmethod def compute_internal_forces_and_torques(self, time: np.float64) -> None: ... - def update_accelerations(self, time: np.float64) -> None: ... + @abstractmethod + def update_accelerations(self, time: np.float64, dt: np.float64) -> None: ... + @abstractmethod def zeroed_out_external_forces_and_torques(self, time: np.float64) -> None: ... -class SlenderBodyGeometryProtocol(Protocol): - @property - def n_nodes(self) -> int: ... - - @property - def n_elems(self) -> int: ... - - position_collection: NDArray[np.float64] - velocity_collection: NDArray[np.float64] - acceleration_collection: NDArray[np.float64] - - omega_collection: NDArray[np.float64] - alpha_collection: NDArray[np.float64] - director_collection: NDArray[np.float64] +class SymplecticSystemProtocol(SystemProtocol, Protocol): + """ + Protocol defining the required interface for symplectic time integration. + Typically, implementation of these properties are provided in data_structures.py + for the specific system, and use to build the block structure. - external_forces: NDArray[np.float64] - external_torques: NDArray[np.float64] + Any class used with the symplectic timesteppers in :mod:`elastica.timestepper` + (e.g., :class:`PositionVerlet`, :class:`PEFRL`) must satisfy this protocol. - internal_forces: NDArray[np.float64] - internal_torques: NDArray[np.float64] + The symplectic stepper accesses: + - ``update_kinematics`` and ``update_dynamics``: called by the timestepper + See Also + -------- + elastica.timestepper.symplectic_steppers : Symplectic stepper implementations + elastica.rod.CosseratRod : A concrete implementation satisfying this protocol -class SymplecticSystemProtocol(SystemProtocol, SlenderBodyGeometryProtocol, Protocol): - """ - Protocol for system with symplectic state variables """ - v_w_collection: NDArray[np.float64] - dvdt_dwdt_collection: NDArray[np.float64] - - @property - def kinematic_states(self) -> _KinematicState: ... - - @property - def dynamic_states(self) -> _DynamicState: ... - - def kinematic_rates( - self, time: np.float64, prefac: np.float64 - ) -> tuple[NDArray[np.float64], NDArray[np.float64]]: ... + def update_kinematics(self, time: np.float64, prefac: np.float64) -> None: + """Update kinematic state. Typically called after compute_internal_forces_and_torques.""" + ... - def dynamic_rates( - self, time: np.float64, prefac: np.float64 - ) -> NDArray[np.float64]: ... + def update_dynamics(self, time: np.float64, prefac: np.float64) -> None: + """Update dynamic state. Typically called after ``update_accelerations``.""" + ... diff --git a/elastica/timestepper/__init__.py b/elastica/timestepper/__init__.py index 264c9d8b8..f061967eb 100644 --- a/elastica/timestepper/__init__.py +++ b/elastica/timestepper/__init__.py @@ -11,8 +11,8 @@ from .protocol import StepperProtocol -# Deprecated: Remove in the future version -# Many script still uses this method to control timestep. Keep it for backward compatibility +# Deprecated: Kept for backward compatibility. +# Many script still uses this method to control timestep. def extend_stepper_interface( stepper: StepperProtocol, system_collection: SystemCollectionType ) -> tuple[ @@ -22,13 +22,15 @@ def extend_stepper_interface( SteppersOperatorsType, ]: try: - stepper_methods: SteppersOperatorsType = stepper.steps_and_prefactors + stepper_methods: SteppersOperatorsType = stepper.steps_and_prefactors # type: ignore do_step_method: Callable = stepper.do_step # type: ignore[attr-defined] except AttributeError as e: raise NotImplementedError(f"{stepper} stepper is not supported.") from e return do_step_method, stepper_methods +# Deprecated: Kept for backward compatibility. +# Recommended to call integration loop explicitly by users. def integrate( stepper: StepperProtocol, systems: SystemCollectionType, diff --git a/elastica/timestepper/protocol.py b/elastica/timestepper/protocol.py index 18a92fc42..b1287b195 100644 --- a/elastica/timestepper/protocol.py +++ b/elastica/timestepper/protocol.py @@ -3,8 +3,7 @@ from typing import Protocol from elastica.typing import ( - SteppersOperatorsType, - StepType, + SystemType, SystemCollectionType, ) from elastica.systems.protocol import SymplecticSystemProtocol @@ -15,27 +14,13 @@ class StepperProtocol(Protocol): """Protocol for all time-steppers""" - steps_and_prefactors: SteppersOperatorsType - - def __init__(self) -> None: ... - - @property - def n_stages(self) -> int: ... - - def step_methods(self) -> SteppersOperatorsType: ... - def step( - self, SystemCollection: SystemCollectionType, time: np.float64, dt: np.float64 + self, + SystemCollection: SystemCollectionType, + time: np.float64 | float, + dt: np.float64 | float, ) -> np.float64: ... def step_single_instance( - self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 + self, System: SystemType, time: np.float64, dt: np.float64 ) -> np.float64: ... - - -class SymplecticStepperProtocol(StepperProtocol, Protocol): - """symplectic stepper protocol.""" - - def get_steps(self) -> list[StepType]: ... - - def get_prefactors(self) -> list[StepType]: ... diff --git a/elastica/timestepper/symplectic_steppers.py b/elastica/timestepper/symplectic_steppers.py index 4bd355af7..8796e99f5 100644 --- a/elastica/timestepper/symplectic_steppers.py +++ b/elastica/timestepper/symplectic_steppers.py @@ -1,23 +1,20 @@ __doc__ = """Symplectic time steppers and concepts for integrating the kinematic and dynamic equations of rod-like objects. """ -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Callable from itertools import zip_longest from elastica.typing import ( SystemCollectionType, + SystemType, StepType, SteppersOperatorsType, ) import numpy as np -from elastica.rod.data_structures import ( - overload_operator_kinematic_numba, - overload_operator_dynamic_numba, -) from elastica.systems.protocol import SymplecticSystemProtocol -from .protocol import SymplecticStepperProtocol +from .protocol import StepperProtocol """ Developer Note @@ -29,10 +26,13 @@ class SymplecticStepperMixin: - def __init__(self: SymplecticStepperProtocol): + get_steps: Callable[[], list[StepType]] + get_prefactors: Callable[[], list[StepType]] + + def __init__(self) -> None: self.steps_and_prefactors: SteppersOperatorsType = self.step_methods() - def step_methods(self: SymplecticStepperProtocol) -> SteppersOperatorsType: + def step_methods(self) -> SteppersOperatorsType: # Let the total number of steps for the Symplectic method # be (2*n + 1) (for time-symmetry). _steps: list[StepType] = self.get_steps() @@ -60,28 +60,14 @@ def no_operation(*args: Any) -> None: ) @property - def n_stages(self: SymplecticStepperProtocol) -> int: + def n_stages(self) -> int: return len(self.steps_and_prefactors) def step( - self: SymplecticStepperProtocol, - SystemCollection: SystemCollectionType, - time: np.float64, - dt: np.float64, - ) -> np.float64: - return SymplecticStepperMixin.do_step( - self, self.steps_and_prefactors, SystemCollection, time, dt - ) - - # TODO: Merge with .step method in the future. - # DEPRECATED: Use .step instead. - @staticmethod - def do_step( - TimeStepper: SymplecticStepperProtocol, - steps_and_prefactors: SteppersOperatorsType, + self, SystemCollection: SystemCollectionType, - time: np.float64, - dt: np.float64, + time: np.float64 | float, + dt: np.float64 | float, ) -> np.float64: """ Function for doing symplectic stepper over the user defined rods (system). @@ -92,54 +78,73 @@ def do_step( The time after the integration step. """ - for kin_prefactor, kin_step, dyn_step in steps_and_prefactors[:-1]: + simulation_time = np.float64(time) + simulation_dt = np.float64(dt) - for system in SystemCollection.block_systems(): - kin_step(system, time, dt) + for kin_prefactor, kin_step, dyn_step in self.steps_and_prefactors[:-1]: + for system in SystemCollection.final_systems(): + kin_step(system, simulation_time, simulation_dt) - time += kin_prefactor(dt) + simulation_time += kin_prefactor(simulation_dt) # Constrain only values - SystemCollection.constrain_values(time) + SystemCollection.constrain_values(simulation_time) # We need internal forces and torques because they are used by interaction module. - for system in SystemCollection.block_systems(): - system.compute_internal_forces_and_torques(time) - # system.update_internal_forces_and_torques() + for system in SystemCollection.final_systems(): + system.compute_internal_forces_and_torques(simulation_time) # Add external forces, controls etc. - SystemCollection.synchronize(time) + SystemCollection.synchronize(simulation_time) - for system in SystemCollection.block_systems(): - dyn_step(system, time, dt) + for system in SystemCollection.final_systems(): + dyn_step(system, simulation_time, simulation_dt) # Constrain only rates - SystemCollection.constrain_rates(time) + SystemCollection.constrain_rates(simulation_time) # Peel the last kinematic step and prefactor alone - last_kin_prefactor = steps_and_prefactors[-1][0] - last_kin_step = steps_and_prefactors[-1][1] + last_kin_prefactor = self.steps_and_prefactors[-1][0] + last_kin_step = self.steps_and_prefactors[-1][1] - for system in SystemCollection.block_systems(): - last_kin_step(system, time, dt) - time += last_kin_prefactor(dt) - SystemCollection.constrain_values(time) + for system in SystemCollection.final_systems(): + last_kin_step(system, simulation_time, simulation_dt) + simulation_time += last_kin_prefactor(simulation_dt) + SystemCollection.constrain_values(simulation_time) # Call back function, will call the user defined call back functions and store data - SystemCollection.apply_callbacks(time, round(time / dt)) + SystemCollection.apply_callbacks( + simulation_time, round(simulation_time / simulation_dt) + ) # Zero out the external forces and torques - for system in SystemCollection.block_systems(): - system.zeroed_out_external_forces_and_torques(time) + for system in SystemCollection.final_systems(): + system.zeroed_out_external_forces_and_torques(simulation_time) - return time + return simulation_time + + @staticmethod + def do_step( + TimeStepper: StepperProtocol, + steps_and_prefactors: SteppersOperatorsType, + SystemCollection: SystemCollectionType, + time: np.float64, + dt: np.float64, + ) -> np.float64: # pragma: no cover + from warning import warn + + warn("This method is deprecated. Use the instance method .step instead.") + return TimeStepper.step(SystemCollection, time, dt) # type: ignore def step_single_instance( - self: SymplecticStepperProtocol, - System: SymplecticSystemProtocol, + self, + System: SystemType, time: np.float64, dt: np.float64, ) -> np.float64: + """ + (The function is used for single system instance, mainly for testing purposes.) + """ for kin_prefactor, kin_step, dyn_step in self.steps_and_prefactors[:-1]: kin_step(System, time, dt) @@ -181,22 +186,14 @@ def _first_kinematic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: prefac = self._first_prefactor(dt) - overload_operator_kinematic_numba( - System.n_nodes, - prefac, - System.kinematic_states.position_collection, - System.kinematic_states.director_collection, - System.velocity_collection, - System.omega_collection, - ) + System.update_kinematics(time, prefac) def _first_dynamic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: - overload_operator_dynamic_numba( - System.dynamic_states.rate_collection, - System.dynamic_rates(time, dt), - ) + prefac = dt + System.update_accelerations(time, prefac) + System.update_dynamics(time, prefac) class PEFRL(SymplecticStepperMixin): @@ -241,25 +238,16 @@ def _first_kinematic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: prefac = self._first_kinematic_prefactor(dt) - overload_operator_kinematic_numba( - System.n_nodes, - prefac, - System.kinematic_states.position_collection, - System.kinematic_states.director_collection, - System.velocity_collection, - System.omega_collection, - ) + System.update_kinematics(time, prefac) # System.kinematic_states += prefac * System.kinematic_rates(time, prefac) def _first_dynamic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: - prefac = self.lambda_dash_coeff * dt - overload_operator_dynamic_numba( - System.dynamic_states.rate_collection, - System.dynamic_rates(time, prefac), - ) # System.dynamic_states += prefac * System.dynamic_rates(time, prefac) + prefac = self.lambda_dash_coeff * dt + System.update_accelerations(time, prefac) + System.update_dynamics(time, prefac) def _second_kinematic_prefactor(self, dt: np.float64) -> np.float64: return self.χ * dt @@ -268,25 +256,16 @@ def _second_kinematic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: prefac = self._second_kinematic_prefactor(dt) - overload_operator_kinematic_numba( - System.n_nodes, - prefac, - System.kinematic_states.position_collection, - System.kinematic_states.director_collection, - System.velocity_collection, - System.omega_collection, - ) + System.update_kinematics(time, prefac) # System.kinematic_states += prefac * System.kinematic_rates(time, prefac) def _second_dynamic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: - prefac = self.λ * dt - overload_operator_dynamic_numba( - System.dynamic_states.rate_collection, - System.dynamic_rates(time, prefac), - ) # System.dynamic_states += prefac * System.dynamic_rates(time, prefac) + prefac = self.λ * dt + System.update_accelerations(time, prefac) + System.update_dynamics(time, prefac) def _third_kinematic_prefactor(self, dt: np.float64) -> np.float64: return self.xi_chi_dash_coeff * dt @@ -295,15 +274,7 @@ def _third_kinematic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: prefac = self._third_kinematic_prefactor(dt) - # Need to fill in - overload_operator_kinematic_numba( - System.n_nodes, - prefac, - System.kinematic_states.position_collection, - System.kinematic_states.director_collection, - System.velocity_collection, - System.omega_collection, - ) + System.update_kinematics(time, prefac) # System.kinematic_states += prefac * System.kinematic_rates(time, prefac) diff --git a/elastica/transformations.py b/elastica/transformations.py index 3a2e10b5a..f98edf4a4 100644 --- a/elastica/transformations.py +++ b/elastica/transformations.py @@ -12,8 +12,6 @@ from numpy.typing import NDArray -# TODO Complete, but nicer interface, evolve it eventually - def format_vector_shape( vector_collection: NDArray[np.float64], diff --git a/elastica/typing.py b/elastica/typing.py index 255961431..9e1be93c7 100644 --- a/elastica/typing.py +++ b/elastica/typing.py @@ -1,3 +1,5 @@ +from __future__ import annotations + __doc__ = """ This module contains aliases of type-hints for elastica. @@ -12,37 +14,29 @@ if TYPE_CHECKING: # Used for type hinting without circular imports # NEVER BACK-IMPORT ANY ELASTICA MODULES HERE - from .rod.protocol import CosseratRodProtocol - from .rigidbody.protocol import RigidBodyProtocol - from .surface.surface_base import SurfaceBase + from .rod.rod_base import RodBase + from .rigidbody.rigid_body_base import RigidBodyBase from .modules.base_system import BaseSystemCollection from .modules.protocol import SystemCollectionProtocol - from .rod.data_structures import _State as State from .systems.protocol import ( - SystemProtocol, StaticSystemProtocol, + SystemProtocol, SymplecticSystemProtocol, ) - from .timestepper.protocol import ( - StepperProtocol, - SymplecticStepperProtocol, - ) + from .timestepper.protocol import StepperProtocol from .memory_block.protocol import BlockSystemProtocol else: - CosseratRodProtocol = "CosseratRodProtocol" - RigidBodyProtocol = "RigidBodyProtocol" - SurfaceBase = "SurfaceBase" + RodBase = "RodBase" + RigidBodyType = "RigidBodyBase" BaseSystemCollection = "BaseSystemCollection" SystemCollectionProtocol = "SystemCollectionProtocol" - State = "State" SystemProtocol = "SystemProtocol" StaticSystemProtocol = "StaticSystemProtocol" SymplecticSystemProtocol = "SymplecticSystemProtocol" StepperProtocol = "StepperProtocol" - SymplecticStepperProtocol = "SymplecticStepperProtocol" BlockSystemProtocol = "BlockSystemProtocol" @@ -51,23 +45,15 @@ SystemIdxType: TypeAlias = int BlockSystemType: TypeAlias = "BlockSystemProtocol" - -# Mostly used in explicit stepper: for symplectic, use kinetic and dynamic state -StateType: TypeAlias = "State" - -# TODO: Maybe can be more specific. Up for discussion. StepType: TypeAlias = Callable[..., Any] SteppersOperatorsType: TypeAlias = tuple[tuple[StepType, ...], ...] - -RodType: TypeAlias = "CosseratRodProtocol" -RigidBodyType: TypeAlias = "RigidBodyProtocol" -SurfaceType: TypeAlias = "SurfaceBase" +RodType: TypeAlias = "RodBase" +RigidBodyType: TypeAlias = "RigidBodyBase" SystemCollectionType: TypeAlias = "SystemCollectionProtocol" # Indexing types -# TODO: Maybe just use slice?? ConstrainingIndex: TypeAlias = tuple[int, ...] ConnectionIndex: TypeAlias = ( int | np.int32 | list[int] | tuple[int, ...] | np.typing.NDArray[np.int32] diff --git a/elastica/utils.py b/elastica/utils.py index 39e23a65f..0e3ac70a7 100644 --- a/elastica/utils.py +++ b/elastica/utils.py @@ -1,4 +1,4 @@ -"""Handy utilities""" +__doc__ = """Handy utilities""" from typing import Generator, Iterable, Any, Literal, TypeVar import functools @@ -32,11 +32,9 @@ def isqrt(num: int) -> int: Notes ----- - - Doesn't handle edge-cases of negative numbers by design - - Doesn't type-check for integers by design, although it is hinted at + Doesn't handle edge-cases of negative numbers by design - Examples - -------- + Doesn't type-check for integers by design, although it is hinted at """ if num > 0: @@ -97,18 +95,17 @@ def perm_parity(lst: list[int]) -> int: """ Given a permutation of the digits 0..N in order as a list, returns its parity (or sign): +1 for even parity; -1 for odd. + Code obtained with thanks from https://code.activestate.com/recipes/578227-generate-the-parity-or-sign-of-a-permutation/ + licensed with a MIT License Parameters ---------- - lst + lst : list[int] Returns ------- + int - Credits - ------- - Code obtained with thanks from https://code.activestate.com/recipes/578227-generate-the-parity-or-sign-of-a-permutation/ - licensed with a MIT License """ parity = 1 for i in range(0, len(lst) - 1): @@ -123,24 +120,18 @@ def perm_parity(lst: list[int]) -> int: def grouper(iterable: Iterable[_T], n: int) -> Generator[tuple[_T, ...], None, None]: - """Collect data into fixed-length chunks or blocks" + """ + Collect data into fixed-length chunks or blocks" + https://docs.python.org/3/library/itertools.html#itertools-recipes + https://stackoverflow.com/a/10791887 + + grouper('ABCDEFG', 3) --> ABC DEF G" Parameters ---------- iterable : input collection n : size of chunk - Returns - ------- - - Example - ------- - grouper('ABCDEFG', 3) --> ABC DEF G" - - Credits - ------- - https://docs.python.org/3/library/itertools.html#itertools-recipes - https://stackoverflow.com/a/10791887 """ it = iter(iterable) @@ -153,23 +144,16 @@ def grouper(iterable: Iterable[_T], n: int) -> Generator[tuple[_T, ...], None, N def extend_instance(obj: Any, cls: Any) -> None: """ - - Apply mixins to a class instance after creation + Apply mixins to a class instance after creation. + https://stackoverflow.com/a/31075641 Parameters ---------- - obj : object (not class!) targeted for interface extension - Interface carries throughout its lifetime. - cls : class (not object!) to dynamically mixin - - Returns - ------- - None - - Credits - ------- - https://stackoverflow.com/a/31075641 - + obj : + object (not class!) targeted for interface extension + Interface carries throughout its lifetime. + cls : + class (not object!) to dynamically mixin """ base_cls = obj.__class__ base_cls_name = obj.__class__.__name__ @@ -186,17 +170,21 @@ def _bspline( # type: ignore[no-any-unimported] Parameters ---------- - t_coeff : np.array + t_coeff : numpy.ndarray The spline coefficients, denoted by :math:`beta_i`. Note that the first and the last values are set to zero by default. - l_centreline : float + l_centerline : float The length of the centerline in meters. Returns ------- - spline : scipy.interpolate.Bspline class + spline : scipy.interpolate.BSpline A spline class that can be called as spline(x), where x are the points at which the spline needs to be evaluated. + ctr_pts : numpy.ndarray + Control points. + ctr_coeffs : numpy.ndarray + Control coefficients. """ # Divide into n_control_pts number of points (n_ctr_pts-1) regions control_pts = l_centerline * np.linspace(0.0, 1.0, t_coeff.shape[0] - 2) @@ -210,8 +198,6 @@ def _bspline( # type: ignore[no-any-unimported] def __bspline_impl__( # type: ignore[no-any-unimported] x_pts: NDArray, t_c: NDArray, degree: int ) -> tuple[BSpline, NDArray, NDArray]: - """""" - # Update the knots n_upd = t_c.shape[0] + (degree + 1) diff --git a/elastica/version.py b/elastica/version.py index 4bfcae0e5..8a1604bb9 100644 --- a/elastica/version.py +++ b/elastica/version.py @@ -1,6 +1,6 @@ import importlib.metadata try: - VERSION = importlib.metadata.version("elastica") + VERSION = importlib.metadata.version("pyelastica") except importlib.metadata.PackageNotFoundError: VERSION = "unknown" diff --git a/examples/.gitignore b/examples/.gitignore deleted file mode 100644 index ed6a54379..000000000 --- a/examples/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*local* diff --git a/examples/AxialStretchingCase/.gitignore b/examples/AxialStretchingCase/.gitignore deleted file mode 100644 index 13833d4b9..000000000 --- a/examples/AxialStretchingCase/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -axial_stretching_data.dat -axial_stretching.pdf diff --git a/examples/AxialStretchingCase/README.md b/examples/AxialStretchingCase/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/examples/AxialStretchingCase/axial_stretching.py b/examples/AxialStretchingCase/run_axial_stretching.py similarity index 51% rename from examples/AxialStretchingCase/axial_stretching.py rename to examples/AxialStretchingCase/run_axial_stretching.py index a3d733f60..18ecae232 100644 --- a/examples/AxialStretchingCase/axial_stretching.py +++ b/examples/AxialStretchingCase/run_axial_stretching.py @@ -1,34 +1,28 @@ -"""Axial stretching test-case - -Assume we have a rod lying aligned in the x-direction, with high internal -damping. - -We fix one end (say, the left end) of the rod to a wall. On the right -end we apply a force directed axially pulling the rods tip. Linear -theory (assuming small displacements) predict that the net displacement -experienced by the rod tip is Δx = FL/AE where the symbols carry their -usual meaning (the rod is just a linear spring). We compare our results -with the above result. - -We can "improve" the theory by having a better estimate for the rod's -spring constant by assuming that it equilibriates under the new position, -with -Δx = F * (L + Δx)/ (A * E) -which results in Δx = (F*l)/(A*E - F). Our rod reaches equilibrium wrt to -this position. - -Note that if the damping is not high, the rod oscillates about the eventual -resting position (and this agrees with the theoretical predictions without -any damping : we should see the rod oscillating simple-harmonically in time). - -isort:skip_file """ +Axial Stretching +================ + +This case tests the axial stretching of a rod. +The expected behavior is supposed to be like a spring-gravity motion, but +with a rod. A rod is fixed at one end and a force is applied at the other +end. The rod stretches and the displacement of the tip is compared with +the analytical solution. +""" + +# isort:skip_file import numpy as np +from collections import defaultdict from matplotlib import pyplot as plt import elastica as ea +# %% +# Simulation Setup +# ---------------- +# We define a simulator class that inherits from the necessary mixins. +# This makes constraints, forces, and damping available to the system. + class StretchingBeamSimulator( ea.BaseSystemCollection, ea.Constraints, ea.Forcing, ea.Damping, ea.CallBacks @@ -39,10 +33,14 @@ class StretchingBeamSimulator( stretch_sim = StretchingBeamSimulator() final_time = 200.0 -# Options -PLOT_FIGURE = True -SAVE_FIGURE = False -SAVE_RESULTS = False +# %% +# Rod Setup +# --------- +# Next, we set up the test parameters for the simulating rods. This includes the +# number of elements, the start position, direction, normal, length, radius, +# density, and Young's modulus of the rod. +# For this case, we have fixed boundary condition at one end, and we apply external +# force at the other end. # setting up test params n_elem = 19 @@ -71,6 +69,7 @@ class StretchingBeamSimulator( ) stretch_sim.append(stretchable_rod) + stretch_sim.constrain(stretchable_rod).using( ea.OneEndFixedBC, constrained_position_idx=(0,), constrained_director_idx=(0,) ) @@ -81,6 +80,10 @@ class StretchingBeamSimulator( ea.EndpointForces, 0.0 * end_force, end_force, ramp_up_time=1e-2 ) +# %% +# Damping is added to the system to help it reach a steady state. We use an +# `AnalyticalLinearDamper` to add damping to the rod. + # add damping dl = base_length / n_elem dt = 0.1 * dl @@ -92,6 +95,13 @@ class StretchingBeamSimulator( ) +# %% +# Callbacks +# --------- +# A callback object is passed to the simulator to record states of the rod +# during the simulation. This is useful for post-processing the results. + + # Add call backs class AxialStretchingCallBack(ea.CallBackBaseClass): """ @@ -120,56 +130,47 @@ def make_callback( return -recorded_history: dict[str, list] = ea.defaultdict(list) +recorded_history: dict[str, list] = defaultdict(list) stretch_sim.collect_diagnostics(stretchable_rod).using( AxialStretchingCallBack, step_skip=200, callback_params=recorded_history ) +# %% +# Finalize and Run +# ---------------- +# We finalize the simulator and create the time-stepper. The `PositionVerlet` +# time-stepper is used to integrate the system. + stretch_sim.finalize() timestepper: ea.typing.StepperProtocol = ea.PositionVerlet() # timestepper = PEFRL() total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, stretch_sim, final_time, total_steps) - -if PLOT_FIGURE: - # First-order theory with base-length - expected_tip_disp = end_force_x * base_length / base_area / youngs_modulus - # First-order theory with modified-length, gives better estimates - expected_tip_disp_improved = ( - end_force_x * base_length / (base_area * youngs_modulus - end_force_x) - ) - - fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150) - ax = fig.add_subplot(111) - ax.plot(recorded_history["time"], recorded_history["position"], lw=2.0) - ax.hlines(base_length + expected_tip_disp, 0.0, final_time, "k", "dashdot", lw=1.0) - ax.hlines( - base_length + expected_tip_disp_improved, 0.0, final_time, "k", "dashed", lw=2.0 - ) - if SAVE_FIGURE: - fig.savefig("axial_stretching.pdf") - plt.show() - -if SAVE_RESULTS: - import pickle - - filename = "axial_stretching_data.dat" - file = open(filename, "wb") - pickle.dump(stretchable_rod, file) - file.close() - - tv = ( - np.asarray(recorded_history["time"]), - np.asarray(recorded_history["velocity_norms"]), - ) - - def as_time_series(v: np.ndarray) -> np.ndarray: - return v.T - - np.savetxt( - "velocity_norms.csv", - as_time_series(np.stack(tv)), - delimiter=",", - ) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(stretch_sim, time, dt) + +# %% +# Post-Processing +# --------------- +# Finally, we plot the results and compare them with the analytical solution. +# The analytical solution is calculated using the first-order theory with +# both the base length and the modified length. + +# First-order theory with base-length +expected_tip_disp = end_force_x * base_length / base_area / youngs_modulus +# First-order theory with modified-length, gives better estimates +expected_tip_disp_improved = ( + end_force_x * base_length / (base_area * youngs_modulus - end_force_x) +) + +fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150) +ax = fig.add_subplot(111) +ax.plot(recorded_history["time"], recorded_history["position"], lw=2.0) +ax.hlines(base_length + expected_tip_disp, 0.0, final_time, "k", "dashdot", lw=1.0) +ax.hlines( + base_length + expected_tip_disp_improved, 0.0, final_time, "k", "dashed", lw=2.0 +) +plt.show() diff --git a/examples/Binder/0_PyElastica_Tutorials_Overview.ipynb b/examples/Binder/0_PyElastica_Tutorials_Overview.ipynb deleted file mode 100644 index c7de2423c..000000000 --- a/examples/Binder/0_PyElastica_Tutorials_Overview.ipynb +++ /dev/null @@ -1,68 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "# PyElastica Tutorials\n", - "\n", - "We have developed a number of different Jupyter notebook tutorials to explain how to use Elastica to simulate Cosserat rods in a number of different cases. Thanks to BinderHub, you can run these tutorials in directly in your web browser without needing to first download and install PyElastica. \n", - "\n", - "We suggest beginning with the Timoshenko beam tutorial available [here](./1_Timoshenko_Beam.ipynb). It walks through how to set up and simulate a very simple Cossert rod model and explains the basics of how to use Elastica. \n", - "\"timoshenko_beam_figure\"\n", - "\n", - "After this, for a tutorial covering more complicated use cases of a single Cosserat rods, check out the slithering snake tutorial, available [here](./2_Slithering_Snake.ipynb). This tutorial covers a possible use case of Cosserat rods and shows how to post-process the simulation to get quantitative data about the system as well as visualize the output. \n", - "\n", - "
\n", - " \n", - "
\n", - "\n", - "A list of all the available Jupyter notebook tutorials is [here](./). We are working to add more. If you think you have an interesting use case of Cosserat rods and Elastica and would like to showcase please make a pull request so we can add it! \n", - "\n", - "There are also a number of example Python scripts available [here](https://github.com/GazzolaLab/PyElastica/tree/master/examples) that cover convergence testing, parameter optimization and other more complex use cases. As a warning, these more complex cases take a much longer time to run. \n", - "\n", - "## More about PyElastica\n", - "If you want to learn more bout PyElastica and Cosserat rods, visit the [project website](https://cosseratrods.org). Or visit the [PyElastica GitHub repo](https://github.com/GazzolaLab/PyElastica).\n", - "\n", - "## PyElastica Documentation\n", - "Documentation of PyElastica is available online [here](https://docs.cosseratrods.org). There is also a getting started guide on the project website [here](https://cosseratrods.org/software/pyelastica).\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.7" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/Binder/1_Timoshenko_Beam.ipynb b/examples/Binder/1_Timoshenko_Beam.ipynb deleted file mode 100644 index fa2a389f0..000000000 --- a/examples/Binder/1_Timoshenko_Beam.ipynb +++ /dev/null @@ -1,765 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "# Timoshenko Beam Example\n", - "\n", - "This Elastica tutorial explains the basics of setting up and running a simple simulation of rods in Elastica. Elastica simulates Cosserat Rods, which are thin, 1-dimensional rods that undergo all possible modes of deformation. This example considers a Timoshenko beam, which is the deformation of a beam under a constant applied force while accounting for shear deforation and rotational bending. This is a good example of the capabilities of Elastica and Cosserat Rods as it requires accounting for the effects of shear deformation, something that the classical Euler-Bernoulli beam solution does not.\n", - "\n", - "![timoshenko_beam_figure.png](../../assets/timoshenko_beam_figure.png)\n", - "\n", - "## Getting Started\n", - "To set up the simulation, the first thing you need to do is import the necessary classes. Here we will only import the classes that we need. The `elastica.modules` classes make it easy to construct different simulation systems. Along with these modules, we need to import a rod class, classes for the boundary conditions, and time-stepping functions. As a note, this method of explicitly importing all classes can be a bit cumbersome. Future releases will simplify this step." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "!pip install pyelastica" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "# Import modules\n", - "from elastica.modules import BaseSystemCollection, Constraints, Forcing, Damping\n", - "\n", - "# Import Cosserat Rod Class\n", - "from elastica.rod.cosserat_rod import CosseratRod\n", - "\n", - "# Import Damping Class\n", - "from elastica.dissipation import AnalyticalLinearDamper\n", - "\n", - "# Import Boundary Condition Classes\n", - "from elastica.boundary_conditions import OneEndFixedRod, FreeRod\n", - "from elastica.external_forces import EndpointForces\n", - "\n", - "# Import Timestepping Functions\n", - "from elastica.timestepper.symplectic_steppers import PositionVerlet\n", - "from elastica.timestepper import integrate" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "Now that we have imported all the necessary classes, we want to create our beam system. We do this by combining all the modules we need to represent the physics that we to include in the simulation. In this case, that is the `BaseSystemCollection`, `Constraint`, `Forcings` and `Damping` because the simulation will consider a rod that is fixed in place on one end, and subject to an applied force on the other end." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "class TimoshenkoBeamSimulator(BaseSystemCollection, Constraints, Forcing, Damping):\n", - " pass\n", - "\n", - "\n", - "timoshenko_sim = TimoshenkoBeamSimulator()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Creating Rods\n", - "With our simulator set up, we can now define the numerical, material, and geometric properties. \n", - "\n", - "First we define the number of elements in the rod. Next, the material properties are defined for every rod. These are the Young's modulus, the Poisson ratio, the density and the viscous damping coefficient. Finally, the geometry of the rod also needs to be defined by specifying the location of the rod and its orientation, length and radius. \n", - "\n", - "All of the values defined here are done in SI units, though this is not strictly necessary. You can rescale properties however you want, as long as you use consistent units throughout the simulation. See [here](https://info.simuleon.com/blog/units-in-abaqus) for an example of consistent units.\n", - "\n", - "In order to make the difference between a shearable and unshearable rod more clear, we are using a Poisson ratio of 99. This is an unphysical value, as Poisson ratios can not exceed 0.5, however, it is used here for demonstration purposes. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# setting up test params\n", - "n_elem = 100\n", - "\n", - "density = 1000\n", - "nu = 1e-4\n", - "E = 1e6\n", - "# For shear modulus of 1e4, nu is 99!\n", - "poisson_ratio = 99\n", - "shear_modulus = E / (poisson_ratio + 1.0)\n", - "\n", - "start = np.zeros((3,))\n", - "direction = np.array([0.0, 0.0, 1.0])\n", - "normal = np.array([0.0, 1.0, 0.0])\n", - "base_length = 3.0\n", - "base_radius = 0.25\n", - "base_area = np.pi * base_radius ** 2" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "With all of the rod's parameters set, we can now create a rod with the specificed properties and add the rod to the simulator system. **Important:** Make sure that any rods you create get added to the simulator system (`timoshenko_sim`), otherwise they will not be included in your simulation. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "shearable_rod = CosseratRod.straight_rod(\n", - " n_elem,\n", - " start,\n", - " direction,\n", - " normal,\n", - " base_length,\n", - " base_radius,\n", - " density,\n", - " youngs_modulus=E,\n", - " shear_modulus=shear_modulus,\n", - ")\n", - "\n", - "timoshenko_sim.append(shearable_rod)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Adding Damping\n", - "With the rod added to the simulator, we can add damping to the rod. We do this using the `.dampen()` option and the `AnalyticalLinearDamper`. We are modifying `timoshenko_sim` simulator to `dampen` the `shearable_rod` object using `AnalyticalLinearDamper` type of dissipation (damping) model.\n", - "\n", - "We also need to define `damping_constant` and simulation `time_step` and pass in `.using()` method." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "\n", - "dl = base_length / n_elem\n", - "dt = 0.01 * dl\n", - "timoshenko_sim.dampen(shearable_rod).using(\n", - " AnalyticalLinearDamper,\n", - " damping_constant=nu,\n", - " time_step=dt,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Adding Boundary Conditions\n", - "With the rod added to the system, we need to apply boundary conditions. The first condition we will apply is fixing the location of one end of the rod. We do this using the `.constrain()` option and the `OneEndFixedRod` boundary condition. We are modifying the `timoshenko_sim` simulator to `constrain` the `shearable_rod` object using the `OneEndFixedRod` type of constraint. \n", - "\n", - "We also need to define which node of the rod is being constrained. We do this by passing the index of the nodes that we want to constain to `constrained_position_idx`. Here we are fixing the first node in the rod. In order to keep the rod from rotating around the fixed node, we also need to constrain an element between two nodes. This fixes the orientation of the rod. We do this by passing the index of the element that we want to fix to `constrained_director_idx`. Like with the position, we are fixing the first element of the rod. Together, this contrains the position and orientation of the rod at the origin. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "timoshenko_sim.constrain(shearable_rod).using(\n", - " OneEndFixedRod, constrained_position_idx=(0,), constrained_director_idx=(0,)\n", - ")\n", - "print(\"One end of the rod is now fixed in place\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "The next boundary condition that we want to apply is the endpoint force. Similarly to how we constrained one of the points, we want the `timoshenko_sim` simulator to `add_forcing_to` the `shearable_rod` object using the `EndpointForces` type of forcing. This `EndpointForces` applies forces to both ends of the rod. We want to apply a negative force in the $d_1$ direction, but only at the end of the rod. We do this by specifying the force vector to be applied at each end as `origin_force` and `end_force`. We also want to ramp up the force over time, so we make the force take some `ramp_up_time` to reach its steady-state value. This helps avoid numerical errors due to discontinuities in the applied force. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "origin_force = np.array([0.0, 0.0, 0.0])\n", - "end_force = np.array([-10.0, 0.0, 0.0])\n", - "ramp_up_time = 5.0\n", - "\n", - "timoshenko_sim.add_forcing_to(shearable_rod).using(\n", - " EndpointForces, origin_force, end_force, ramp_up_time=ramp_up_time\n", - ")\n", - "print(\"Forces added to the rod\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Add Unshearable Rod\n", - "\n", - "Along with the shearable rod, we also want to add an unshearable rod to be able to compare the difference between the two. We do this the same way we did for the first rod, however, because this rod is unsherable, we need to change the Poisson ratio to make the rod unsherable. For a truely unsheraable rod, you would need a Poisson ratio of -1.0, however, this causes the system to be numerically unstable, so instead we make the system nearly unshearable by using a Poisson ratio of -0.85. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Start into the plane\n", - "unshearable_start = np.array([0.0, -1.0, 0.0])\n", - "unshearable_rod = CosseratRod.straight_rod(\n", - " n_elem,\n", - " unshearable_start,\n", - " direction,\n", - " normal,\n", - " base_length,\n", - " base_radius,\n", - " density,\n", - " youngs_modulus=E,\n", - " # Unshearable rod needs G -> inf, which is achievable with a poisson ratio of -1.0\n", - " shear_modulus=E / (-0.85 + 1.0),\n", - ")\n", - "\n", - "timoshenko_sim.append(unshearable_rod)\n", - "\n", - "timoshenko_sim.dampen(unshearable_rod).using(\n", - " AnalyticalLinearDamper,\n", - " damping_constant=nu,\n", - " time_step=dt,\n", - ")\n", - "\n", - "timoshenko_sim.constrain(unshearable_rod).using(\n", - " OneEndFixedRod, constrained_position_idx=(0,), constrained_director_idx=(0,)\n", - ")\n", - "\n", - "timoshenko_sim.add_forcing_to(unshearable_rod).using(\n", - " EndpointForces, origin_force, end_force, ramp_up_time=ramp_up_time\n", - ")\n", - "print(\"Unshearable rod set up\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## System Finalization\n", - "\n", - "We have now added all the necessary rods and boundary conditions to our system. The last thing we need to do is finalize the system. This goes through the system, rearranges things, and precomputes useful quantities to prepare the system for simulation. \n", - "\n", - "As a note, if you make any changes to the rod after calling finalize, you will need to re-setup the system. This requires rerunning all cells above this point. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "timoshenko_sim.finalize()\n", - "print(\"System finalized\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Define Simulation Time\n", - "\n", - "The last thing we need to do deceide how long we want the simulation to run for and what timestepping method to use. Currently, the PositionVerlet algorithim is suggested default method. \n", - "\n", - "In this example, we are trying to match a steady-state solution by temporally evolving our system to reach equillibrium. As such, there is a tradeoff between letting the simulation run long enough to each the equillibrium and waiting around for the simulation to be done. Here we are running the simulation for 10 seconds, this produces reasonable agreement with the analytical solution without taking to long to finish. If you run the simulation for longer, you will get better agreement with the analytical solution. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "final_time = 10.0\n", - "total_steps = int(final_time / dt)\n", - "print(\"Total steps to take\", total_steps)\n", - "\n", - "timestepper = PositionVerlet()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Run Simulation\n", - "We are now ready to perform the simulation. To run the simulation, we `integrate` the `timoshenko_sim` system using the `timestepper` method until `final_time` by taking `total_steps`. As currently setup, the beam simulation takes about 1 minute to run. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "integrate(timestepper, timoshenko_sim, final_time, total_steps)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Post Processing Results\n", - "Now that we have finished the simulation, we want to post-process the results. We will do this by comparing the solutions for the shearable and unshearable beams with the analytical Timoshenko and Euler-Bernoulli beam results. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Compute beam position for sherable and unsherable beams.\n", - "def analytical_result(arg_rod, arg_end_force, shearing=True, n_elem=500):\n", - " base_length = np.sum(arg_rod.rest_lengths)\n", - " arg_s = np.linspace(0.0, base_length, n_elem)\n", - " if type(arg_end_force) is np.ndarray:\n", - " acting_force = arg_end_force[np.nonzero(arg_end_force)]\n", - " else:\n", - " acting_force = arg_end_force\n", - " acting_force = np.abs(acting_force)\n", - " linear_prefactor = -acting_force / arg_rod.shear_matrix[0, 0, 0]\n", - " quadratic_prefactor = (\n", - " -acting_force\n", - " / 2.0\n", - " * np.sum(arg_rod.rest_lengths / arg_rod.bend_matrix[0, 0, 0])\n", - " )\n", - " cubic_prefactor = (acting_force / 6.0) / arg_rod.bend_matrix[0, 0, 0]\n", - " if shearing:\n", - " return (\n", - " arg_s,\n", - " arg_s * linear_prefactor\n", - " + arg_s ** 2 * quadratic_prefactor\n", - " + arg_s ** 3 * cubic_prefactor,\n", - " )\n", - " else:\n", - " return arg_s, arg_s ** 2 * quadratic_prefactor + arg_s ** 3 * cubic_prefactor" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "Now we want to plot the results. The one thing to point out in this function is how to access the position of the rods. They are located in `rod.position_collection[dim, n_elem]`. In this case, we are plotting the x- and z-dimensions. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def plot_timoshenko(shearable_rod, unshearable_rod, end_force):\n", - " import matplotlib.pyplot as plt\n", - "\n", - " analytical_shearable_positon = analytical_result(\n", - " shearable_rod, end_force, shearing=True\n", - " )\n", - " analytical_unshearable_positon = analytical_result(\n", - " unshearable_rod, end_force, shearing=False\n", - " )\n", - "\n", - " fig = plt.figure(figsize=(5, 4), frameon=True, dpi=150)\n", - " ax = fig.add_subplot(111)\n", - " ax.grid(which=\"major\", color=\"grey\", linestyle=\"-\", linewidth=0.25)\n", - "\n", - " ax.plot(\n", - " analytical_shearable_positon[0],\n", - " analytical_shearable_positon[1],\n", - " \"k--\",\n", - " label=\"Timoshenko\",\n", - " )\n", - " ax.plot(\n", - " analytical_unshearable_positon[0],\n", - " analytical_unshearable_positon[1],\n", - " \"k-.\",\n", - " label=\"Euler-Bernoulli\",\n", - " )\n", - "\n", - " ax.plot(\n", - " shearable_rod.position_collection[2, :],\n", - " shearable_rod.position_collection[0, :],\n", - " \"b-\",\n", - " label=\"n=\" + str(shearable_rod.n_elems),\n", - " )\n", - " ax.plot(\n", - " unshearable_rod.position_collection[2, :],\n", - " unshearable_rod.position_collection[0, :],\n", - " \"r-\",\n", - " label=\"n=\" + str(unshearable_rod.n_elems),\n", - " )\n", - "\n", - " ax.legend(prop={\"size\": 12})\n", - " ax.set_ylabel(\"Y Position (m)\", fontsize=12)\n", - " ax.set_xlabel(\"X Position (m)\", fontsize=12)\n", - " plt.show()\n", - "\n", - "\n", - "plot_timoshenko(shearable_rod, unshearable_rod, end_force)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "For the sake of time, we are stopping this simulation early. This leads to some disagreement between the analytical solution and the Elastica solution as there are still some transient effects in the Elastica solution. Allowing the simulation to run longer will lead to a closer result between the analytical and Elastica solutions. \n", - "\n", - "## Dynamic Plotting \n", - "To illustrate how the system evolves over time, we can also plot the system in time. To do this, we need to recreate the system, which we now call `BeamSimulator`. It is the same as the previous system so we will just write everything very compactly to save space. We also slightly modify our plotting and integrating functions to allow the output to be plotting during the simulation. \n", - "\n", - "Since we will be plotting the system over time, we also need to initalize the time at 0.0" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "time = 0.0\n", - "\n", - "\n", - "class BeamSimulator(BaseSystemCollection, Constraints, Forcing, Damping):\n", - " pass\n", - "\n", - "\n", - "dynamic_update_sim = BeamSimulator()\n", - "\n", - "shearable_rod_new = CosseratRod.straight_rod(\n", - " n_elem,\n", - " start,\n", - " direction,\n", - " normal,\n", - " base_length,\n", - " base_radius,\n", - " density,\n", - " youngs_modulus=E,\n", - " shear_modulus=shear_modulus,\n", - ")\n", - "dynamic_update_sim.append(shearable_rod_new)\n", - "dynamic_update_sim.dampen(shearable_rod_new).using(\n", - " AnalyticalLinearDamper,\n", - " damping_constant=nu,\n", - " time_step=dt,\n", - ")\n", - "dynamic_update_sim.constrain(shearable_rod_new).using(\n", - " OneEndFixedRod, constrained_position_idx=(0,), constrained_director_idx=(0,)\n", - ")\n", - "dynamic_update_sim.add_forcing_to(shearable_rod_new).using(\n", - " EndpointForces, origin_force, end_force, ramp_up_time=ramp_up_time\n", - ")\n", - "\n", - "unshearable_rod_new = CosseratRod.straight_rod(\n", - " n_elem,\n", - " unshearable_start,\n", - " direction,\n", - " normal,\n", - " base_length,\n", - " base_radius,\n", - " density,\n", - " youngs_modulus=E,\n", - " # Unshearable rod needs G -> inf, which is achievable with a poisson ratio of -1.0\n", - " shear_modulus=E / (-0.85 + 1.0),\n", - ")\n", - "dynamic_update_sim.append(unshearable_rod_new)\n", - "dynamic_update_sim.dampen(unshearable_rod_new).using(\n", - " AnalyticalLinearDamper,\n", - " damping_constant=nu,\n", - " time_step=dt,\n", - ")\n", - "dynamic_update_sim.constrain(unshearable_rod_new).using(\n", - " OneEndFixedRod, constrained_position_idx=(0,), constrained_director_idx=(0,)\n", - ")\n", - "dynamic_update_sim.add_forcing_to(unshearable_rod_new).using(\n", - " EndpointForces, origin_force, end_force, ramp_up_time=ramp_up_time\n", - ")\n", - "\n", - "dynamic_update_sim.finalize()\n", - "\n", - "\n", - "def run_and_update_plot(simulator, dt, start_time, stop_time, ax):\n", - " from elastica.timestepper.symplectic_steppers import PositionVerlet\n", - "\n", - " timestepper = PositionVerlet()\n", - "\n", - " n_steps = int((stop_time - start_time) / dt)\n", - " time = start_time\n", - " for i in range(n_steps):\n", - " time = timestepper.step(simulator, time, dt)\n", - " plot_timoshenko_dynamic(shearable_rod_new, unshearable_rod_new, end_force, time, ax)\n", - " return time\n", - "\n", - "\n", - "def plot_timoshenko_dynamic(shearable_rod, unshearable_rod, end_force, time, ax):\n", - " import matplotlib.pyplot as plt\n", - " from IPython import display\n", - "\n", - " analytical_shearable_positon = analytical_result(\n", - " shearable_rod, end_force, shearing=True\n", - " )\n", - " analytical_unshearable_positon = analytical_result(\n", - " unshearable_rod, end_force, shearing=False\n", - " )\n", - "\n", - " ax.clear()\n", - " ax.grid(which=\"major\", color=\"grey\", linestyle=\"-\", linewidth=0.25)\n", - " ax.plot(\n", - " analytical_shearable_positon[0],\n", - " analytical_shearable_positon[1],\n", - " \"k--\",\n", - " label=\"Timoshenko\",\n", - " )\n", - " ax.plot(\n", - " analytical_unshearable_positon[0],\n", - " analytical_unshearable_positon[1],\n", - " \"k-.\",\n", - " label=\"Euler-Bernoulli\",\n", - " )\n", - "\n", - " ax.plot(\n", - " shearable_rod.position_collection[2, :],\n", - " shearable_rod.position_collection[0, :],\n", - " \"b-\",\n", - " label=\"shearable rod\",\n", - " )\n", - " ax.plot(\n", - " unshearable_rod.position_collection[2, :],\n", - " unshearable_rod.position_collection[0, :],\n", - " \"r-\",\n", - " label=\"unshearable rod\",\n", - " )\n", - "\n", - " ax.legend(prop={\"size\": 12}, loc=\"lower left\")\n", - " ax.set_ylabel(\"Y Position (m)\", fontsize=12)\n", - " ax.set_xlabel(\"X Position (m)\", fontsize=12)\n", - " ax.set_title(\"Simulation Time: %0.2f seconds\" % time)\n", - " ax.set_xlim([-0.1, 3.1])\n", - " ax.set_ylim([-0.045, 0.002])" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "Now we can run the simulation for a time interval `evolve_for_time` and have the system be plotted every `update_interval`. If you run the cell multiple times in a row, you will see that the that the system continues to evolve in time, this is because you are continually updating `dynamic_update_sim`. If you want to reset the system back to its original configuration, run the cell above this one. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "%matplotlib inline\n", - "import matplotlib.pyplot as plt\n", - "from IPython import display\n", - "\n", - "evolve_for_time = 10.0\n", - "update_interval = 1.0e-1\n", - "\n", - "# update the plot every 1 second\n", - "fig = plt.figure(figsize=(5, 4), frameon=True, dpi=150)\n", - "ax = fig.add_subplot(111)\n", - "first_interval_time = update_interval + time\n", - "last_interval_time = time + evolve_for_time\n", - "for stop_time in np.arange(\n", - " first_interval_time, last_interval_time + dt, update_interval\n", - "):\n", - " time = run_and_update_plot(dynamic_update_sim, dt, time, stop_time, ax)\n", - " display.clear_output(wait=True)\n", - " display.display(plt.gcf())\n", - "plt.close()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### Important note on saving data:\n", - "This current method of plotting data during the simulation is helpful for visualizing how the system evolves, but it is computationally inefficient as we are constantly pausing the simulation to plot. It also does not save data for additional post-processing later. A better method for saving data from a simulation is to use call-back functions. There is information on how to use these functions in the [snake tutorial](./2_Slithering_Snake.ipynb)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.4" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/Binder/2_Slithering_Snake.ipynb b/examples/Binder/2_Slithering_Snake.ipynb deleted file mode 100644 index cec14b8f2..000000000 --- a/examples/Binder/2_Slithering_Snake.ipynb +++ /dev/null @@ -1,751 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "# Slithering Snake Example\n", - "\n", - "This Elastica tutorial explains how to setup a Cosserat rod simulation to simulate a slithering snake. It is a more complex use case than the Timoshenko Beam example. If you have not done so, we strongly suggest you start with [this beam example](./1_Timoshenko_Beam.ipynb) as it covers many of the basics of setting up and running simulations with Elastica. \n", - "\n", - "This slithering snake example includes gravitational forces, friction forces, and internal muscle torques. It also introduces the use of call back functions to allow logging of simulations data for post-processing after the simulation is over. \n", - "\n", - "\n", - "## Getting Started\n", - "To set up the simulation, the first thing you need to do is import the necessary classes. As with the Timoshenko bean, we need to import modules which allow us to more easily construct different simulation systems. We also need to import a rod class, all the necessary forces to be applied, timestepping functions, and callback classes. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pip install pyelastica\n", - "!conda install -c conda-forge ffmpeg -y" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "# import modules\n", - "from elastica.modules import BaseSystemCollection, Constraints, Forcing, CallBacks, Damping\n", - "\n", - "# import rod class, damping and forces to be applied\n", - "from elastica.rod.cosserat_rod import CosseratRod\n", - "from elastica.dissipation import AnalyticalLinearDamper\n", - "from elastica.external_forces import GravityForces, MuscleTorques\n", - "from elastica.interaction import AnisotropicFrictionalPlane\n", - "\n", - "# import timestepping functions\n", - "from elastica.timestepper.symplectic_steppers import PositionVerlet\n", - "from elastica.timestepper import integrate\n", - "\n", - "# import call back functions\n", - "from elastica.callback_functions import CallBackBaseClass\n", - "from collections import defaultdict" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Initialize System and Add Rod\n", - "The first thing to do is initialize the simulator class by combining all the imported modules. After initializing, we will generate a rod and add it to the simulation. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "class SnakeSimulator(BaseSystemCollection, Constraints, Forcing, CallBacks, Damping):\n", - " pass\n", - "\n", - "\n", - "snake_sim = SnakeSimulator()\n", - "\n", - "# Define rod parameters\n", - "n_elem = 50\n", - "start = np.array([0.0, 0.0, 0.0])\n", - "direction = np.array([0.0, 0.0, 1.0])\n", - "normal = np.array([0.0, 1.0, 0.0])\n", - "base_length = 0.35\n", - "base_radius = base_length * 0.011\n", - "base_area = np.pi * base_radius ** 2\n", - "density = 1000\n", - "nu = 2e-3\n", - "E = 1e6\n", - "poisson_ratio = 0.5\n", - "shear_modulus = E / (poisson_ratio + 1.0)\n", - "\n", - "# Create rod\n", - "shearable_rod = CosseratRod.straight_rod(\n", - " n_elem,\n", - " start,\n", - " direction,\n", - " normal,\n", - " base_length,\n", - " base_radius,\n", - " density,\n", - " youngs_modulus=E,\n", - " shear_modulus=shear_modulus,\n", - ")\n", - "\n", - "# Add rod to the snake system\n", - "snake_sim.append(shearable_rod)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Adding Damping\n", - "With the rod added to the simulator, we can add damping to the rod. We do this using the `.dampen()` option and the `AnalyticalLinearDamper`. We are modifying `snake_sim` simulator to `dampen` the `shearable_rod` object using `AnalyticalLinearDamper` type of dissipation (damping) model.\n", - "\n", - "We also need to define `damping_constant` and simulation `time_step` and pass in `.using()` method." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "dt = 1e-4\n", - "snake_sim.dampen(shearable_rod).using(\n", - " AnalyticalLinearDamper,\n", - " damping_constant=nu,\n", - " time_step=dt,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Add Forces to Rod\n", - "With our rod added to the system, we need to specify the relevant forces that will be acting on the rod. For all the forces, the method of adding forces is `system_name.add_forcing_to(name_of_rod).using(type_of_force, *kwargs)` where `*kwargs` are the parameters specific to each type of force. \n", - "\n", - "### Gravity\n", - "The first force to add is gravity. We specify the strength of gravity and also the direction it is pointing. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Add gravitational forces\n", - "gravitational_acc = -9.80665\n", - "snake_sim.add_forcing_to(shearable_rod).using(\n", - " GravityForces, acc_gravity=np.array([0.0, gravitational_acc, 0.0])\n", - ")\n", - "print(\"Gravity now acting on shearable rod\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### Muscle Torques\n", - "A snake generates torque throughout its body through muscle activations. While these muscle activations are generated internally by the snake, it is simpler to treat them as applied external forces, allowing us to apply them to the rod in the same manner as the other external forces. \n", - "\n", - "You may notice that the muscle torque parameters appear to have special values. These are optimized coefficients for a snake gait. For information about how to do this optimization, see the [snake optimization example script](../ContinuumSnakeCase/continuum_snake.py)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Define muscle torque parameters\n", - "period = 2.0\n", - "wave_length = 1.0\n", - "b_coeff = np.array([3.4e-3, 3.3e-3, 4.2e-3, 2.6e-3, 3.6e-3, 3.5e-3])\n", - "\n", - "# Add muscle torques to the rod\n", - "snake_sim.add_forcing_to(shearable_rod).using(\n", - " MuscleTorques,\n", - " base_length=base_length,\n", - " b_coeff=b_coeff,\n", - " period=period,\n", - " wave_number=2.0 * np.pi / (wave_length),\n", - " phase_shift=0.0,\n", - " rest_lengths=shearable_rod.rest_lengths,\n", - " ramp_up_time=period,\n", - " direction=normal,\n", - " with_spline=True,\n", - ")\n", - "print(\"Muscle torques added to the rod\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### Anisotropic Friction Forces\n", - "The last force that needs to be added is the friction force between the snake and the ground. Snakes exhibits anisotropic friction where the friction coefficient is different in different directions. You can also define both static and kinematic friction coefficients. This is accomplished by defining some small velocity threshold `slip_velocity_tol` that defines the transitions between static and kinematic friction. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Define friction force parameters\n", - "origin_plane = np.array([0.0, -base_radius, 0.0])\n", - "normal_plane = normal\n", - "slip_velocity_tol = 1e-8\n", - "froude = 0.1\n", - "mu = base_length / (period * period * np.abs(gravitational_acc) * froude)\n", - "kinetic_mu_array = np.array(\n", - " [1.0 * mu, 1.5 * mu, 2.0 * mu]\n", - ") # [forward, backward, sideways]\n", - "static_mu_array = 2 * kinetic_mu_array\n", - "\n", - "# Add friction forces to the rod\n", - "snake_sim.add_forcing_to(shearable_rod).using(\n", - " AnisotropicFrictionalPlane,\n", - " k=1.0,\n", - " nu=1e-6,\n", - " plane_origin=origin_plane,\n", - " plane_normal=normal_plane,\n", - " slip_velocity_tol=slip_velocity_tol,\n", - " static_mu_array=static_mu_array,\n", - " kinetic_mu_array=kinetic_mu_array,\n", - ")\n", - "print(\"Friction forces added to the rod\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Add Callback Function\n", - "The simulation is now setup, but before it is run, we want to define a callback function. A callback function allows us to record time-series data throughout the simulation. If you do not define a callback function, you will only have access to the final configuration of the system. If you want to be able to analyze how the system evolves over time, it is critical that you record the appropriate quantities. \n", - "\n", - "To create a callback function, begin with the `CallBackBaseClass`. You can then define which state quantities you wish to record by having them appended to the `self.callback_params` dictionary as well as how often you wish to save the data by defining `skip_step`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Add call backs\n", - "class ContinuumSnakeCallBack(CallBackBaseClass):\n", - " \"\"\"\n", - " Call back function for continuum snake\n", - " \"\"\"\n", - "\n", - " def __init__(self, step_skip: int, callback_params: dict):\n", - " CallBackBaseClass.__init__(self)\n", - " self.every = step_skip\n", - " self.callback_params = callback_params\n", - "\n", - " def make_callback(self, system, time, current_step: int):\n", - "\n", - " if current_step % self.every == 0:\n", - "\n", - " self.callback_params[\"time\"].append(time)\n", - " self.callback_params[\"step\"].append(current_step)\n", - " self.callback_params[\"position\"].append(system.position_collection.copy())\n", - " self.callback_params[\"velocity\"].append(system.velocity_collection.copy())\n", - " self.callback_params[\"avg_velocity\"].append(\n", - " system.compute_velocity_center_of_mass()\n", - " )\n", - "\n", - " self.callback_params[\"center_of_mass\"].append(\n", - " system.compute_position_center_of_mass()\n", - " )\n", - " self.callback_params[\"curvature\"].append(system.kappa.copy())\n", - "\n", - " return\n", - "\n", - "\n", - "pp_list = defaultdict(list)\n", - "snake_sim.collect_diagnostics(shearable_rod).using(\n", - " ContinuumSnakeCallBack, step_skip=100, callback_params=pp_list\n", - ")\n", - "print(\"Callback function added to the simulator\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "With the callback function added, we can now finalize the system and also define the time stepping parameters of the simulation such as the time step, final time, and time stepping algorithm to use. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "snake_sim.finalize()\n", - "\n", - "final_time = 5.0 * period\n", - "total_steps = int(final_time / dt)\n", - "print(\"Total steps\", total_steps)\n", - "\n", - "timestepper = PositionVerlet()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "Now all that is left is to run the simulation. Using the default parameters the simulation takes about 2-3 minutes to complete. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "integrate(timestepper, snake_sim, final_time, total_steps)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Post-Process Data\n", - "With the simulation complete, we want to analyze the simulation. Because we added a callback function, we can analyze how the snake evolves over time. All of the data from the callback function is located in the `pp_list` dictionary. Here we will use this information to compute and plot the velocity of the snake in the forward, lateral, and normal directions. We do this by using a pre-written analysis function `compute_projected_velocity`.\n", - "\n", - "In the plotted graph, you can see that it takes about one period for the snake to begin moving before rapidly reaching a steady gait over just 2-3 periods. We also see that the normal velocity is zero since we are only actuating the snake in a 2D plane. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def compute_projected_velocity(plot_params: dict, period):\n", - " import numpy as np\n", - "\n", - " time_per_period = np.array(plot_params[\"time\"]) / period\n", - " avg_velocity = np.array(plot_params[\"avg_velocity\"])\n", - " center_of_mass = np.array(plot_params[\"center_of_mass\"])\n", - "\n", - " # Compute rod velocity in rod direction. We need to compute that because,\n", - " # after snake starts to move it chooses an arbitrary direction, which does not\n", - " # have to be initial tangent direction of the rod. Thus we need to project the\n", - " # snake velocity with respect to its new tangent and roll direction, after that\n", - " # we will get the correct forward and lateral speed. After this projection\n", - " # lateral velocity of the snake has to be oscillating between + and - values with\n", - " # zero mean.\n", - "\n", - " # Number of steps in one period.\n", - " period_step = int(1.0 / (time_per_period[-1] - time_per_period[-2]))\n", - " number_of_period = int(time_per_period[-1])\n", - "\n", - " # Center of mass position averaged in one period\n", - " center_of_mass_averaged_over_one_period = np.zeros((number_of_period - 2, 3))\n", - " for i in range(1, number_of_period - 2):\n", - " # position of center of mass averaged over one period\n", - " center_of_mass_averaged_over_one_period[i - 1] = np.mean(\n", - " center_of_mass[(i + 1) * period_step : (i + 2) * period_step]\n", - " - center_of_mass[(i + 0) * period_step : (i + 1) * period_step],\n", - " axis=0,\n", - " )\n", - " # Average the rod directions over multiple periods and get the direction of the rod.\n", - " direction_of_rod = np.mean(center_of_mass_averaged_over_one_period, axis=0)\n", - " direction_of_rod /= np.linalg.norm(direction_of_rod, ord=2)\n", - "\n", - " # Compute the projected rod velocity in the direction of the rod\n", - " velocity_mag_in_direction_of_rod = np.einsum(\n", - " \"ji,i->j\", avg_velocity, direction_of_rod\n", - " )\n", - " velocity_in_direction_of_rod = np.einsum(\n", - " \"j,i->ji\", velocity_mag_in_direction_of_rod, direction_of_rod\n", - " )\n", - "\n", - " # Get the lateral or roll velocity of the rod after subtracting its projected\n", - " # velocity in the direction of rod\n", - " velocity_in_rod_roll_dir = avg_velocity - velocity_in_direction_of_rod\n", - "\n", - " # Compute the average velocity over the simulation, this can be used for optimizing snake\n", - " # for fastest forward velocity. We start after first period, because of the ramping up happens\n", - " # in first period.\n", - " average_velocity_over_simulation = np.mean(\n", - " velocity_in_direction_of_rod[period_step * 2 :], axis=0\n", - " )\n", - "\n", - " return (\n", - " velocity_in_direction_of_rod,\n", - " velocity_in_rod_roll_dir,\n", - " average_velocity_over_simulation[2],\n", - " average_velocity_over_simulation[0],\n", - " )\n", - "\n", - "\n", - "def compute_and_plot_velocity(plot_params: dict, period):\n", - " from matplotlib import pyplot as plt\n", - " from matplotlib.colors import to_rgb\n", - "\n", - " time_per_period = np.array(plot_params[\"time\"]) / period\n", - " avg_velocity = np.array(plot_params[\"avg_velocity\"])\n", - "\n", - " [\n", - " velocity_in_direction_of_rod,\n", - " velocity_in_rod_roll_dir,\n", - " _,\n", - " _,\n", - " ] = compute_projected_velocity(plot_params, period)\n", - "\n", - " fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150)\n", - " plt.rcParams.update({\"font.size\": 16})\n", - " ax = fig.add_subplot(111)\n", - " ax.grid(which=\"minor\", color=\"k\", linestyle=\"--\")\n", - " ax.grid(which=\"major\", color=\"k\", linestyle=\"-\")\n", - " ax.plot(\n", - " time_per_period[:], velocity_in_direction_of_rod[:, 2], \"r-\", label=\"forward\"\n", - " )\n", - " ax.plot(\n", - " time_per_period[:],\n", - " velocity_in_rod_roll_dir[:, 0],\n", - " c=to_rgb(\"xkcd:bluish\"),\n", - " label=\"lateral\",\n", - " )\n", - " ax.plot(time_per_period[:], avg_velocity[:, 1], \"k-\", label=\"normal\")\n", - " ax.set_ylabel(\"Velocity [m/s]\", fontsize=16)\n", - " ax.set_xlabel(\"Time [s]\", fontsize=16)\n", - " fig.legend(prop={\"size\": 20})\n", - " plt.show()\n", - " plt.close(plt.gcf())\n", - "\n", - "\n", - "compute_and_plot_velocity(pp_list, period)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "We can plot the curvature along the snake at different time instance and compare it with the sterotypical snake curvature function $7cos(2 \\pi s)$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def plot_curvature(\n", - " plot_params: dict,\n", - " rest_lengths,\n", - " period,\n", - "):\n", - " from matplotlib import pyplot as plt\n", - " from matplotlib.colors import to_rgb\n", - "\n", - " s = np.cumsum(rest_lengths)\n", - " L0 = s[-1]\n", - " s = s / L0\n", - " s = s[:-1].copy()\n", - " x = np.linspace(0, 1, 100)\n", - " curvature = np.array(plot_params[\"curvature\"])\n", - " time = np.array(plot_params[\"time\"])\n", - " peak_time = period * 0.125\n", - " dt = time[1] - time[0]\n", - " peak_idx = int(peak_time / (dt))\n", - " plt.rcParams.update({\"font.size\": 16})\n", - " fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150)\n", - " ax = fig.add_subplot(111)\n", - " try:\n", - " for i in range(peak_idx * 8, peak_idx * 8 * 2, peak_idx):\n", - " ax.plot(s, curvature[i, 0, :] * L0, \"k\")\n", - " except:\n", - " print(\"Simulation time not long enough to plot curvature\")\n", - " ax.plot(\n", - " x, 7 * np.cos(2 * np.pi * x - 0.80), \"--\", label=\"stereotypical snake curvature\"\n", - " )\n", - " ax.set_ylabel(r\"$\\kappa$\", fontsize=16)\n", - " ax.set_xlabel(\"s\", fontsize=16)\n", - " ax.set_xlim(0, 1)\n", - " ax.set_ylim(-10, 10)\n", - " fig.legend(prop={\"size\": 16})\n", - " plt.show()\n", - "\n", - " plt.close(plt.gcf())\n", - "\n", - "\n", - "plot_curvature(pp_list, shearable_rod.rest_lengths, period)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### Make Video of Snake Gait\n", - "Because we saved data of the snake's behavior, we can make a video of its movement. The easiest way to do this is to do this is to plot the snake's position at each time that the data was recorded and then stitch these plots together to form a video. \n", - "\n", - "note: ffmpeg is required for matplotlib to be able to create a video. More info on ffmepg [here](https://www.ffmpeg.org/)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "from IPython.display import Video\n", - "from tqdm import tqdm\n", - "\n", - "\n", - "def plot_video_2D(plot_params: dict, video_name=\"video.mp4\", margin=0.2, fps=15):\n", - " from matplotlib import pyplot as plt\n", - " import matplotlib.animation as manimation\n", - "\n", - " t = np.array(plot_params[\"time\"])\n", - " positions_over_time = np.array(plot_params[\"position\"])\n", - " total_time = int(np.around(t[..., -1], 1))\n", - " total_frames = fps * total_time\n", - " step = round(len(t) / total_frames)\n", - "\n", - " print(\"creating video -- this can take a few minutes\")\n", - " FFMpegWriter = manimation.writers[\"ffmpeg\"]\n", - " metadata = dict(title=\"Movie Test\", artist=\"Matplotlib\", comment=\"Movie support!\")\n", - " writer = FFMpegWriter(fps=fps, metadata=metadata)\n", - "\n", - " fig = plt.figure()\n", - " ax = fig.add_subplot(111)\n", - " plt.axis(\"equal\")\n", - " rod_lines_2d = ax.plot(\n", - " positions_over_time[0][2], positions_over_time[0][0], linewidth=3\n", - " )[0]\n", - " ax.set_xlim([0 - margin, 3 + margin])\n", - " ax.set_ylim([-1.5 - margin, 1.5 + margin])\n", - " with writer.saving(fig, video_name, dpi=100):\n", - " for time in range(1, len(t), step):\n", - " rod_lines_2d.set_xdata([positions_over_time[time][2]])\n", - " rod_lines_2d.set_ydata([positions_over_time[time][0]])\n", - "\n", - " writer.grab_frame()\n", - " plt.close(fig)\n", - "\n", - "\n", - "filename_video = \"continuum_snake.mp4\"\n", - "plot_video_2D(pp_list, video_name=filename_video, margin=0.2, fps=125)\n", - "\n", - "Video(\"continuum_snake.mp4\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "Finally, you can also plot the position of the snake from a 3D perspective. This is most helpful is you have a simulation that consists of more than planar motion. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "from IPython.display import Video\n", - "\n", - "\n", - "def plot_video(plot_params: dict, video_name=\"video.mp4\", margin=0.2, fps=15):\n", - " from matplotlib import pyplot as plt\n", - " import matplotlib.animation as manimation\n", - " from mpl_toolkits import mplot3d\n", - "\n", - " t = np.array(plot_params[\"time\"])\n", - " positions_over_time = np.array(plot_params[\"position\"])\n", - " total_time = int(np.around(t[..., -1], 1))\n", - " total_frames = fps * total_time\n", - " step = round(len(t) / total_frames)\n", - " print(\"creating video -- this can take a few minutes\")\n", - " FFMpegWriter = manimation.writers[\"ffmpeg\"]\n", - " metadata = dict(title=\"Movie Test\", artist=\"Matplotlib\", comment=\"Movie support!\")\n", - " writer = FFMpegWriter(fps=fps, metadata=metadata)\n", - " fig = plt.figure()\n", - " ax = fig.add_subplot(111, projection=\"3d\")\n", - " ax.set_xlim(0 - margin, 3 + margin)\n", - " ax.set_ylim(-1.5 - margin, 1.5 + margin)\n", - " ax.set_zlim(0, 1)\n", - " ax.view_init(elev=20, azim=-80)\n", - " rod_lines_3d = ax.plot(\n", - " positions_over_time[0][2],\n", - " positions_over_time[0][0],\n", - " positions_over_time[0][1],\n", - " linewidth=3,\n", - " )[0]\n", - " with writer.saving(fig, video_name, dpi=100):\n", - " for time in range(1, len(t), step):\n", - " rod_lines_3d.set_xdata([positions_over_time[time][2]])\n", - " rod_lines_3d.set_ydata([positions_over_time[time][0]])\n", - " rod_lines_3d.set_3d_properties(positions_over_time[time][1])\n", - "\n", - " writer.grab_frame()\n", - " plt.close(fig)\n", - "\n", - "\n", - "filename_video = \"continuum_snake_3d.mp4\"\n", - "plot_video(pp_list, video_name=filename_video, margin=0.2, fps=60)\n", - "\n", - "Video(\"continuum_snake_3d.mp4\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3.9.12 ('base')", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.12" - }, - "vscode": { - "interpreter": { - "hash": "69b409ac0f88f35940b70a7cc6f81becc62e64d51a49713aaa6660602c575037" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/BoundaryConditionsCases/bc_cases_postprocessing.py b/examples/BoundaryConditionsCases/bc_cases_postprocessing.py index 954dd4e8e..1a7196dd4 100644 --- a/examples/BoundaryConditionsCases/bc_cases_postprocessing.py +++ b/examples/BoundaryConditionsCases/bc_cases_postprocessing.py @@ -53,7 +53,7 @@ def plot_video( ): # (time step, x/y/z, node) import matplotlib.animation as manimation - time = plot_params_rod1["time"] + time_list = plot_params_rod1["time"] position_of_rod1 = np.array(plot_params_rod1["position"]) print("plot video") @@ -62,15 +62,15 @@ def plot_video( writer = FFMpegWriter(fps=fps, metadata=metadata) fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150) with writer.saving(fig, video_name, 100): - for time in range(1, len(time)): + for time_idx in range(1, len(time_list)): fig.clf() ax = plt.axes(projection="3d") # fig.add_subplot(111) ax.grid(which="minor", color="k", linestyle="--") ax.grid(which="major", color="k", linestyle="-") ax.plot( - position_of_rod1[time, 0], - position_of_rod1[time, 1], - position_of_rod1[time, 2], + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 1], + position_of_rod1[time_idx, 2], "or", label="rod1", ) @@ -91,7 +91,7 @@ def plot_video_xy( ): # (time step, x/y/z, node) import matplotlib.animation as manimation - time = plot_params_rod1["time"] + time_list = plot_params_rod1["time"] position_of_rod1 = np.array(plot_params_rod1["position"]) print("plot video xy") @@ -101,10 +101,13 @@ def plot_video_xy( fig = plt.figure() plt.axis("equal") with writer.saving(fig, video_name, 100): - for time in range(1, len(time)): + for time_idx in range(1, len(time_list)): fig.clf() plt.plot( - position_of_rod1[time, 0], position_of_rod1[time, 1], "or", label="rod1" + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 1], + "or", + label="rod1", ) plt.xlim([-0.25, 0.25]) @@ -121,7 +124,7 @@ def plot_video_xz( ): # (time step, x/y/z, node) import matplotlib.animation as manimation - time = plot_params_rod1["time"] + time_list = plot_params_rod1["time"] position_of_rod1 = np.array(plot_params_rod1["position"]) print("plot video xz") @@ -131,10 +134,13 @@ def plot_video_xz( fig = plt.figure() plt.axis("equal") with writer.saving(fig, video_name, 100): - for time in range(1, len(time)): + for time_idx in range(1, len(time_list)): fig.clf() plt.plot( - position_of_rod1[time, 0], position_of_rod1[time, 2], "or", label="rod1" + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 2], + "or", + label="rod1", ) plt.xlim([-0.25, 0.25]) diff --git a/examples/BoundaryConditionsCases/general_constraint_allow_yaw.py b/examples/BoundaryConditionsCases/general_constraint_allow_yaw.py index 99989ce78..08d2505f5 100644 --- a/examples/BoundaryConditionsCases/general_constraint_allow_yaw.py +++ b/examples/BoundaryConditionsCases/general_constraint_allow_yaw.py @@ -1,9 +1,11 @@ -__doc__ = """Fixed joint example, for detailed explanation refer to Zhang et. al. Nature Comm. methods section.""" +__doc__ = """General constraint example allowing yaw rotation, for detailed explanation refer to Zhang et. al. Nature Comm. methods section.""" import numpy as np +from collections import defaultdict + import elastica as ea -from examples.BoundaryConditionsCases.bc_cases_postprocessing import ( +from bc_cases_postprocessing import ( plot_position, plot_orientation, plot_video, @@ -15,7 +17,6 @@ class GeneralConstraintSimulator( ea.BaseSystemCollection, ea.Constraints, - ea.Connections, ea.Forcing, ea.Damping, ea.CallBacks, @@ -31,7 +32,6 @@ class GeneralConstraintSimulator( normal = np.array([0.0, 1.0, 0.0]) base_length = 0.2 base_radius = 0.007 -base_area = np.pi * base_radius**2 density = 1750 E = 3e7 poisson_ratio = 0.5 @@ -39,14 +39,12 @@ class GeneralConstraintSimulator( # setting up timestepper and video final_time = 10 -dl = base_length / n_elem dt = 1e-5 total_steps = int(final_time / dt) fps = 100 # frames per second of the video diagnostic_step_skip = 1 / (fps * dt) start_rod_1 = np.zeros((3,)) -start_rod_2 = start_rod_1 + direction * base_length # Create rod 1 rod1 = ea.CosseratRod.straight_rod( @@ -92,7 +90,7 @@ class GeneralConstraintSimulator( ) -pp_list_rod1 = ea.defaultdict(list) +pp_list_rod1 = defaultdict(list) general_constraint_sim.collect_diagnostics(rod1).using( @@ -103,7 +101,10 @@ class GeneralConstraintSimulator( timestepper = ea.PositionVerlet() print("Total steps", total_steps) -ea.integrate(timestepper, general_constraint_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(general_constraint_sim, time, dt) plot_orientation( diff --git a/examples/ButterflyCase/.gitignore b/examples/ButterflyCase/.gitignore deleted file mode 100644 index 62b9c3468..000000000 --- a/examples/ButterflyCase/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -butterfly_data.dat -butterfly.pdf -energies.pdf -butterfly.png -energies.png diff --git a/examples/ButterflyCase/README.md b/examples/ButterflyCase/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/examples/ButterflyCase/butterfly.py b/examples/ButterflyCase/run_butterfly.py similarity index 50% rename from examples/ButterflyCase/butterfly.py rename to examples/ButterflyCase/run_butterfly.py index 529463067..87f338549 100644 --- a/examples/ButterflyCase/butterfly.py +++ b/examples/ButterflyCase/run_butterfly.py @@ -1,11 +1,30 @@ +""" +Butterfly +========= + +This case simulates the motion of a rod that is initially shaped like a +butterfly. The rod is released from rest and allowed to deform freely. +The goal of the simulation is as a sanity check: how does the timestepper +reliably preserve total energy of the system, when the system is simple Hamiltonian. +The simulation tracks the position and energy of the rod over time. + +This example case also demonstrate how to setup rod with customized +positions and directors. +""" + import numpy as np from matplotlib import pyplot as plt from matplotlib.colors import to_rgb - +from collections import defaultdict import elastica as ea from elastica.utils import MaxDimension +# %% +# Simulation Setup +# ---------------- +# We define a simulator class that inherits from the necessary mixins. + class ButterflySimulator(ea.BaseSystemCollection, ea.CallBacks): pass @@ -14,14 +33,14 @@ class ButterflySimulator(ea.BaseSystemCollection, ea.CallBacks): butterfly_sim = ButterflySimulator() final_time = 40.0 -# Options -PLOT_FIGURE = True -SAVE_FIGURE = True -SAVE_RESULTS = True -ADD_UNSHEARABLE_ROD = False +# %% +# Rod Setup +# --------- +# Next, we set up the test parameters for the simulation. # setting up test params -# FIXME : Doesn't work with elements > 10 (the inverse rotate kernel fails) +# Note: This example has a limitation where n_elem > 10 may fail due to +# inverse rotation kernel issues. For reliable results, keep n_elem <= 10. n_elem = 4 # Change based on requirements, but be careful n_elem += n_elem % 2 half_n_elem = n_elem // 2 @@ -38,12 +57,15 @@ class ButterflySimulator(ea.BaseSystemCollection, ea.CallBacks): total_length = 3.0 base_radius = 0.25 -base_area = np.pi * base_radius**2 density = 5000 youngs_modulus = 1e4 poisson_ratio = 0.5 shear_modulus = youngs_modulus / (poisson_ratio + 1.0) +# %% +# We then define the initial positions of the nodes of the rod to create the +# butterfly shape. + positions = np.empty((MaxDimension.value(), n_elem + 1)) dl = total_length / n_elem @@ -60,6 +82,9 @@ class ButterflySimulator(ea.BaseSystemCollection, ea.CallBacks): - np.sin(angle_of_inclination) * vertical_direction ) +# %% +# Now we can create the `CosseratRod` object with the specified positions. + butterfly_rod = ea.CosseratRod.straight_rod( n_elem, start=origin.reshape(3), @@ -76,14 +101,21 @@ class ButterflySimulator(ea.BaseSystemCollection, ea.CallBacks): butterfly_sim.append(butterfly_rod) +# %% +# Callback Setup +# -------------- +# A callback object is defined to record the position and energy of the rod +# during the simulation. + + # Add call backs -class VelocityCallBack(ea.CallBackBaseClass): +class ButterflyCallBack(ea.CallBackBaseClass): """ - Call back function for continuum snake + Call back function for butterfly case to track position and energy """ def __init__(self, step_skip: int, callback_params: dict) -> None: - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -104,7 +136,9 @@ def make_callback( return -recorded_history: dict[str, list] = ea.defaultdict(list) +# database +recorded_history: dict[str, list] = defaultdict(list) + # initially record history recorded_history["time"].append(0.0) recorded_history["position"].append(butterfly_rod.position_collection.copy()) @@ -114,9 +148,13 @@ def make_callback( recorded_history["be"].append(butterfly_rod.compute_bending_energy()) butterfly_sim.collect_diagnostics(butterfly_rod).using( - VelocityCallBack, step_skip=100, callback_params=recorded_history + ButterflyCallBack, step_skip=100, callback_params=recorded_history ) +# %% +# Finalize and Run +# ---------------- +# We finalize the simulator and create the time-stepper. butterfly_sim.finalize() timestepper: ea.typing.StepperProtocol @@ -126,54 +164,52 @@ def make_callback( dt = 0.01 * dl total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, butterfly_sim, final_time, total_steps) - -if PLOT_FIGURE: - # Plot the histories - fig = plt.figure(figsize=(5, 4), frameon=True, dpi=150) - ax = fig.add_subplot(111) - positions_history = recorded_history["position"] - # record first position - first_position = positions_history.pop(0) - ax.plot(first_position[2, ...], first_position[0, ...], "r--", lw=2.0) - n_positions = len(positions_history) - for i, pos in enumerate(positions_history): - alpha = np.exp(i / n_positions - 1) - ax.plot(pos[2, ...], pos[0, ...], "b", lw=0.6, alpha=alpha) - # final position is also separate - last_position = positions_history.pop() - ax.plot(last_position[2, ...], last_position[0, ...], "k--", lw=2.0) - # don't block - fig.show() - - # Plot the energies - energy_fig = plt.figure(figsize=(5, 4), frameon=True, dpi=150) - energy_ax = energy_fig.add_subplot(111) - times = np.asarray(recorded_history["time"]) - te = np.asarray(recorded_history["te"]) - re = np.asarray(recorded_history["re"]) - be = np.asarray(recorded_history["be"]) - se = np.asarray(recorded_history["se"]) - - energy_ax.plot(times, te, c=to_rgb("xkcd:reddish"), lw=2.0, label="Translations") - energy_ax.plot(times, re, c=to_rgb("xkcd:bluish"), lw=2.0, label="Rotation") - energy_ax.plot(times, be, c=to_rgb("xkcd:burple"), lw=2.0, label="Bend") - energy_ax.plot(times, se, c=to_rgb("xkcd:goldenrod"), lw=2.0, label="Shear") - energy_ax.plot(times, te + re + be + se, c="k", lw=2.0, label="Total energy") - energy_ax.legend() - # don't block - energy_fig.show() - - if SAVE_FIGURE: - fig.savefig("butterfly.png") - energy_fig.savefig("energies.png") - - plt.show() - -if SAVE_RESULTS: - import pickle - - filename = "butterfly_data.dat" - file = open(filename, "wb") - pickle.dump(butterfly_rod, file) - file.close() +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(butterfly_sim, time, dt) + +# %% +# Post-Processing +# --------------- +# The position of the rod is plotted at different time steps, +# and the energies are plotted as a function of time. + +# Plot the histories +fig = plt.figure(figsize=(5, 4), frameon=True, dpi=150) +ax = fig.add_subplot(111) +positions_history = recorded_history["position"] +# record first position +first_position = positions_history.pop(0) +ax.plot(first_position[2, ...], first_position[0, ...], "r--", lw=2.0) +n_positions = len(positions_history) +for i, pos in enumerate(positions_history): + alpha = np.exp(i / n_positions - 1) + ax.plot(pos[2, ...], pos[0, ...], "b", lw=0.6, alpha=alpha) +# final position is also separate +last_position = positions_history.pop() +ax.plot(last_position[2, ...], last_position[0, ...], "k--", lw=2.0) +# don't block +fig.show() + +# %% + +# Plot the energies +energy_fig = plt.figure(figsize=(5, 4), frameon=True, dpi=150) +energy_ax = energy_fig.add_subplot(111) +times = np.asarray(recorded_history["time"]) +te = np.asarray(recorded_history["te"]) +re = np.asarray(recorded_history["re"]) +be = np.asarray(recorded_history["be"]) +se = np.asarray(recorded_history["se"]) + +energy_ax.plot(times, te, c=to_rgb("xkcd:reddish"), lw=2.0, label="Translational") +energy_ax.plot(times, re, c=to_rgb("xkcd:bluish"), lw=2.0, label="Rotation") +energy_ax.plot(times, be, c=to_rgb("xkcd:burple"), lw=2.0, label="Bend") +energy_ax.plot(times, se, c=to_rgb("xkcd:goldenrod"), lw=2.0, label="Shear") +energy_ax.plot(times, te + re + be + se, c="k", lw=2.0, label="Total energy") +energy_ax.legend() +# don't block +energy_fig.show() + +plt.show() diff --git a/examples/CantileverDistributedLoad/cantilever_conservative_distributed_load.py b/examples/CantileverDistributedLoad/cantilever_conservative_distributed_load.py index 4dfc9cbc9..730c313d4 100644 --- a/examples/CantileverDistributedLoad/cantilever_conservative_distributed_load.py +++ b/examples/CantileverDistributedLoad/cantilever_conservative_distributed_load.py @@ -1,4 +1,5 @@ from matplotlib import pyplot as plt +from collections import defaultdict import numpy as np import elastica as ea import json @@ -29,7 +30,7 @@ class SquareRodSimulator( np.pi ** (1 / 2) ) # The Cross-sectional area is 1e-4(we assume its equivalent to a square cross-sectional surface with same area) base_area = np.pi * base_radius**2 - density = 1000 # nomilized with conservative case F=15 + density = 1000 # normalized with conservative case F=15 youngs_modulus = 1.2e7 dl = base_length / n_elem dt = 0.1 * dl / 50 @@ -79,7 +80,7 @@ class SquareRodSimulator( # Add call backs class CantileverDistributedLoadCallBack(ea.CallBackBaseClass): def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -90,9 +91,6 @@ def make_callback(self, system, time, current_step: int): self.callback_params["position"].append( system.position_collection.copy() ) - self.callback_params["com"].append( - system.compute_position_center_of_mass() - ) self.callback_params["radius"].append(system.radius.copy()) self.callback_params["velocity"].append( system.velocity_collection.copy() @@ -113,7 +111,7 @@ def make_callback(self, system, time, current_step: int): ** 0.5 ) - recorded_history = ea.defaultdict(list) + recorded_history = defaultdict(list) square_rod_sim.collect_diagnostics(square_rod).using( CantileverDistributedLoadCallBack, step_skip=200, @@ -124,13 +122,10 @@ def make_callback(self, system, time, current_step: int): timestepper = ea.PositionVerlet() total_steps = int(final_time / dt) - ea.integrate(timestepper, square_rod_sim, final_time, total_steps) - - relative_tip_position = np.zeros( - 2, - ) - relative_tip_position[0] = find_tip_position(square_rod, n_elem)[0] / base_length - relative_tip_position[1] = -find_tip_position(square_rod, n_elem)[1] / base_length + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(square_rod_sim, time, dt) if animation: plot_video_with_surface( diff --git a/examples/CantileverDistributedLoad/cantilever_distrubuted_load_postprecessing.py b/examples/CantileverDistributedLoad/cantilever_distrubuted_load_postprecessing.py index e0fb8abb8..3a5aa8077 100644 --- a/examples/CantileverDistributedLoad/cantilever_distrubuted_load_postprecessing.py +++ b/examples/CantileverDistributedLoad/cantilever_distrubuted_load_postprecessing.py @@ -8,17 +8,17 @@ from elastica.external_forces import NoForces, inplace_addition, SystemType -class NonconserativeForce(NoForces): +class NonConservativeForce(NoForces): def __init__(self, load=1): - super(NonconserativeForce, self).__init__() + super().__init__() self.load = load def apply_forces(self, system: SystemType, time=0.0): - self.compute_nonconserative_forces( + self.compute_nonconservative_forces( self.load, system.mass, system.director_collection, system.external_forces ) - def compute_nonconserative_forces(self, load, mass, direction, external_forces): + def compute_nonconservative_forces(self, load, mass, direction, external_forces): NCforce_direction = direction[0] NCforce_direction = NCforce_direction / np.linalg.norm( NCforce_direction, axis=0 @@ -158,7 +158,9 @@ def plot_video_with_surface( rods_history[rod_idx]["radius"][t_idx], ) # Rod center of mass - com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx]["com"][time_idx] + com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx][ + "center_of_mass" + ][t_idx] # Generate target sphere data sphere_flag = False diff --git a/examples/CantileverDistributedLoad/cantilever_nonconservative_distributed_load.py b/examples/CantileverDistributedLoad/cantilever_nonconservative_distributed_load.py index 7b3f91fec..d03d729e3 100644 --- a/examples/CantileverDistributedLoad/cantilever_nonconservative_distributed_load.py +++ b/examples/CantileverDistributedLoad/cantilever_nonconservative_distributed_load.py @@ -1,4 +1,5 @@ from matplotlib import pyplot as plt +from collections import defaultdict import numpy as np import elastica as ea import json @@ -7,7 +8,7 @@ plot_video_with_surface, find_tip_position, adjust_square_cross_section, - NonconserativeForce, + NonConservativeForce, ) @@ -18,15 +19,27 @@ class SquareRodSimulator( def cantilever_subjected_to_a_nonconservative_load( - n_elem, - base_length, - side_length, - base_radius, - youngs_modulus, - dimentionless_varible, + dimensionless_variable, animation=False, plot_figure_equilibrium=False, ): + # Setting up test params + final_time = 10 + n_elem = 100 + start = np.zeros((3,)) + direction = np.array([1.0, 0.0, 0.0]) + normal = np.array([0.0, 1.0, 0.0]) + base_length = 0.5 + side_length = 0.01 + base_radius = 0.01 / (np.pi ** (1 / 2)) + base_area = np.pi * base_radius**2 + density = 1000 + youngs_modulus = 1.2e7 + # For shear modulus of 1e4, nu is 99! + poisson_ratio = 0 + shear_modulus = youngs_modulus / (2 * (poisson_ratio + 1.0)) + I = (0.01**4) / 12 + square_rod_sim = SquareRodSimulator() square_rod = ea.CosseratRod.straight_rod( @@ -49,11 +62,11 @@ def cantilever_subjected_to_a_nonconservative_load( ea.OneEndFixedBC, constrained_position_idx=(0,), constrained_director_idx=(0,) ) - load = (youngs_modulus * I * dimentionless_varible) / ( + load = (youngs_modulus * I * dimensionless_variable) / ( density * base_area * (base_length**3) ) - square_rod_sim.add_forcing_to(square_rod).using(NonconserativeForce, load) + square_rod_sim.add_forcing_to(square_rod).using(NonConservativeForce, load) # add damping dl = base_length / n_elem @@ -73,7 +86,7 @@ class NonConservativeDistributedLoadCallBack(ea.CallBackBaseClass): """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -84,9 +97,6 @@ def make_callback(self, system, time, current_step: int): self.callback_params["position"].append( system.position_collection.copy() ) - self.callback_params["com"].append( - system.compute_position_center_of_mass() - ) self.callback_params["radius"].append(system.radius.copy()) self.callback_params["velocity"].append( system.velocity_collection.copy() @@ -107,7 +117,7 @@ def make_callback(self, system, time, current_step: int): ** 0.5 ) - recorded_history = ea.defaultdict(list) + recorded_history = defaultdict(list) square_rod_sim.collect_diagnostics(square_rod).using( NonConservativeDistributedLoadCallBack, @@ -120,7 +130,10 @@ def make_callback(self, system, time, current_step: int): total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, square_rod_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(square_rod_sim, time, dt) if plot_figure_equilibrium: @@ -166,27 +179,7 @@ def make_callback(self, system, time, current_step: int): if __name__ == "__main__": - final_time = 10 - # setting up test params - n_elem = 100 - start = np.zeros((3,)) - direction = np.array([1.0, 0.0, 0.0]) - normal = np.array([0.0, 1.0, 0.0]) - base_length = 0.5 - side_length = 0.01 - base_radius = 0.01 / (np.pi ** (1 / 2)) - base_area = np.pi * base_radius**2 - density = 1000 - dimentionless_varible = 15 - youngs_modulus = 1.2e7 - # For shear modulus of 1e4, nu is 99! - poisson_ratio = 0 - shear_modulus = youngs_modulus / (2 * (poisson_ratio + 1.0)) - I = (0.01**4) / 12 - - cantilever_subjected_to_a_nonconservative_load( - n_elem, base_length, side_length, base_radius, youngs_modulus, -15, True, False - ) + cantilever_subjected_to_a_nonconservative_load(-15, True, False) with open("cantilever_distributed_load_data.json", "r") as file: tip_position_paper = json.load(file) @@ -198,16 +191,11 @@ def make_callback(self, system, time, current_step: int): load_on_rod = np.arange(1, 26, 2) for i in load_on_rod: - x_tip_experiment.append( - cantilever_subjected_to_a_nonconservative_load( - n_elem, base_length, base_radius, youngs_modulus, i, False, False - )[0] - ) - y_tip_experiment.append( - -cantilever_subjected_to_a_nonconservative_load( - n_elem, base_length, base_radius, youngs_modulus, i, False, False - )[1] + relative_tip_position = cantilever_subjected_to_a_nonconservative_load( + i, False, False ) + x_tip_experiment.append(relative_tip_position[0]) + y_tip_experiment.append(-relative_tip_position[1]) plt.plot( load_on_rod, diff --git a/examples/CantileverTransversalLoadCase/cantilever_transversal_load.py b/examples/CantileverTransversalLoadCase/cantilever_transversal_load.py index c37c2279e..e78161c0f 100644 --- a/examples/CantileverTransversalLoadCase/cantilever_transversal_load.py +++ b/examples/CantileverTransversalLoadCase/cantilever_transversal_load.py @@ -2,14 +2,14 @@ from elastica.boundary_conditions import OneEndFixedBC from elastica.external_forces import EndpointForces from elastica.timestepper.symplectic_steppers import PositionVerlet -from elastica.timestepper import integrate import elastica as ea -from examples.convergence_functions import calculate_error_norm -from cantilever_transversal_load_postprocessing import adjust_square_cross_section from matplotlib import pyplot as plt from matplotlib.colors import to_rgb import json +from convergence_functions import calculate_error_norm +from setup_helper import adjust_square_cross_section + def analytical_results(index): with open("cantilever_transversal_load_data.json", "r") as file: @@ -27,7 +27,6 @@ def cantilever_subjected_to_a_transversal_load(n_elem=19): base_radius = 0.01 / ( np.pi ** (1 / 2) ) # The Cross-sectional area is 1e-4(we assume its equivalent to a square cross-sectional surface with same area) - base_area = 1e-4 density = 1000 youngs_modulus = 1e9 poisson_ratio = 0 @@ -40,7 +39,6 @@ class SquareRodSimulator( square_rod_sim = SquareRodSimulator() - density = 1000 t = np.linspace(0, 0.25 * np.pi, n_elem + 1) tmp = np.zeros((3, n_elem + 1), dtype=np.float64) tmp[0, :] = -radius * np.cos(t) + 1 @@ -77,7 +75,6 @@ class SquareRodSimulator( square_rod_sim.append(square_rod) - # square_rod_sim.finalize() square_rod.rest_kappa[...] = square_rod.kappa dl = base_length / n_elem @@ -113,9 +110,8 @@ class SquareRodSimulator( square_rod_sim.finalize() print("System finalized") - # The simulation result from Project3.3.2 with 400 elements/ Tip position Z - - # generate analytical solution array from [400] + # The simulation result from Project3.3.2 with 400 elements (tip position Z) + # Generate analytical solution array by interpolating from the 400-element reference solution analytical_results_sub = np.zeros(n_elem + 1) @@ -126,7 +122,10 @@ class SquareRodSimulator( timestepper = PositionVerlet() - integrate(timestepper, square_rod_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(square_rod_sim, time, dt) print(square_rod.position_collection[2, ...]) error, l1, l2, linf = calculate_error_norm( diff --git a/examples/CantileverTransversalLoadCase/cantilever_transversal_load_postprocessing.py b/examples/CantileverTransversalLoadCase/cantilever_transversal_load_postprocessing.py deleted file mode 100644 index fc0dce06c..000000000 --- a/examples/CantileverTransversalLoadCase/cantilever_transversal_load_postprocessing.py +++ /dev/null @@ -1,252 +0,0 @@ -import numpy as np -from matplotlib import pyplot as plt -from matplotlib import cm -from tqdm import tqdm -from typing import Dict, Sequence -import logging -from elastica.utils import MaxDimension, Tolerance - - -def find_tip_position(rod, n_elem): - x_tip = rod.position_collection[0][n_elem] - y_tip = rod.position_collection[1][n_elem] - z_tip = rod.position_collection[2][n_elem] - - return x_tip, y_tip, z_tip - - -def plot_video_with_surface( - rods_history: Sequence[Dict], - video_name="video.mp4", - fps=60, - step=1, - **kwargs, -): - plt.rcParams.update({"font.size": 22}) - - folder_name = kwargs.get("folder_name", "") - - # 2d case - import matplotlib.animation as animation - - # simulation time - sim_time = np.array(rods_history[0]["time"]) - - # Rod - n_visualized_rods = len(rods_history) # should be one for now - # Rod info - rod_history_unpacker = lambda rod_idx, t_idx: ( - rods_history[rod_idx]["position"][t_idx], - rods_history[rod_idx]["radius"][t_idx], - ) - # Rod center of mass - com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx]["com"][time_idx] - - # Generate target sphere data - sphere_flag = False - if kwargs.__contains__("sphere_history"): - sphere_flag = True - sphere_history = kwargs.get("sphere_history") - n_visualized_spheres = len(sphere_history) # should be one for now - sphere_history_unpacker = lambda sph_idx, t_idx: ( - sphere_history[sph_idx]["position"][t_idx], - sphere_history[sph_idx]["radius"][t_idx], - ) - # color mapping - sphere_cmap = cm.get_cmap("Spectral", n_visualized_spheres) - - # video pre-processing - print("plot scene visualization video") - FFMpegWriter = animation.writers["ffmpeg"] - metadata = dict(title="Movie Test", artist="Matplotlib", comment="Movie support!") - writer = FFMpegWriter(fps=fps, metadata=metadata) - dpi = kwargs.get("dpi", 100) - - xlim = kwargs.get("x_limits", (-1.0, 1.0)) - ylim = kwargs.get("y_limits", (-1.0, 1.0)) - zlim = kwargs.get("z_limits", (-0.05, 1.0)) - - difference = lambda x: x[1] - x[0] - max_axis_length = max(difference(xlim), difference(ylim)) - # The scaling factor from physical space to matplotlib space - scaling_factor = (2 * 0.1) / max_axis_length # Octopus head dimension - scaling_factor *= 2.6e3 # Along one-axis - - if kwargs.get("vis3D", True): - fig = plt.figure(1, figsize=(10, 8), frameon=True, dpi=dpi) - ax = plt.axes(projection="3d") - - ax.set_xlabel("x") - ax.set_ylabel("y") - ax.set_zlabel("z") - - ax.set_xlim(*xlim) - ax.set_ylim(*ylim) - ax.set_zlim(*zlim) - - ax.view_init(elev=20, azim=20) - - time_idx = 0 - rod_scatters = [None for _ in range(n_visualized_rods)] - - for rod_idx in range(n_visualized_rods): - inst_position, inst_radius = rod_history_unpacker(rod_idx, time_idx) - if not inst_position.shape[1] == inst_radius.shape[0]: - inst_position = 0.5 * (inst_position[..., 1:] + inst_position[..., :-1]) - - rod_scatters[rod_idx] = ax.scatter( - inst_position[0], - inst_position[1], - inst_position[2], - s=np.pi * (scaling_factor * inst_radius) ** 2, - ) - - if sphere_flag: - sphere_artists = [None for _ in range(n_visualized_spheres)] - for sphere_idx in range(n_visualized_spheres): - sphere_position, sphere_radius = sphere_history_unpacker( - sphere_idx, time_idx - ) - sphere_artists[sphere_idx] = ax.scatter( - sphere_position[0], - sphere_position[1], - sphere_position[2], - s=np.pi * (scaling_factor * sphere_radius) ** 2, - ) - # sphere_radius, - # color=sphere_cmap(sphere_idx),) - ax.add_artist(sphere_artists[sphere_idx]) - - # ax.set_aspect("equal") - video_name_3D = folder_name + "3D_" + video_name - - with writer.saving(fig, video_name_3D, dpi): - for time_idx in tqdm(range(0, sim_time.shape[0], int(step))): - - for rod_idx in range(n_visualized_rods): - inst_position, inst_radius = rod_history_unpacker(rod_idx, time_idx) - if not inst_position.shape[1] == inst_radius.shape[0]: - inst_position = 0.5 * ( - inst_position[..., 1:] + inst_position[..., :-1] - ) - - rod_scatters[rod_idx]._offsets3d = ( - inst_position[0], - inst_position[1], - inst_position[2], - ) - - rod_scatters[rod_idx].set_sizes( - np.pi * (scaling_factor * inst_radius) ** 2 - ) - - if sphere_flag: - for sphere_idx in range(n_visualized_spheres): - sphere_position, _ = sphere_history_unpacker( - sphere_idx, time_idx - ) - sphere_artists[sphere_idx]._offsets3d = ( - sphere_position[0], - sphere_position[1], - sphere_position[2], - ) - - writer.grab_frame() - - # Be a good boy and close figures - # https://stackoverflow.com/a/37451036 - # plt.close(fig) alone does not suffice - # See https://github.com/matplotlib/matplotlib/issues/8560/ - # plt.close(plt.gcf()) - - -def adjust_square_cross_section( - rod, youngs_modulus: float, length: float, ring_rod_flag: bool = False -): - n_elements = rod.n_elems - n_voronoi_elements = n_elements if ring_rod_flag else n_elements - 1 - - log = logging.getLogger() - - side_length = np.zeros(n_elements) - side_length.fill(length) - - new_area = np.pi * rod.radius * rod.radius - - new_moi_1 = (side_length**4) / 12 - new_moi_2 = (side_length**4) / 12 - new_moi_3 = new_moi_2 * 2 - - new_moi = np.array([new_moi_1, new_moi_2, new_moi_3]).transpose() - - mass_second_moment_of_inertia_temp = np.einsum( - "ij,i->ij", new_moi, rod.density * rod.rest_lengths - ) - - for i in range(n_elements): - np.fill_diagonal( - rod.mass_second_moment_of_inertia[..., i], - mass_second_moment_of_inertia_temp[i, :], - ) - # sanity check of mass second moment of inertia - if (rod.mass_second_moment_of_inertia < Tolerance.atol()).all(): - message = "Mass moment of inertia matrix smaller than tolerance, please check provided radius, density and length." - log.warning(message) - - for i in range(n_elements): - # Check rank of mass moment of inertia matrix to see if it is invertible - assert ( - np.linalg.matrix_rank(rod.mass_second_moment_of_inertia[..., i]) - == MaxDimension.value() - ) - rod.inv_mass_second_moment_of_inertia[..., i] = np.linalg.inv( - rod.mass_second_moment_of_inertia[..., i] - ) - - # Shear/Stretch matrix - shear_modulus = youngs_modulus / (2.0 * (1.0 + 0.5)) - - # Value taken based on best correlation for Poisson ratio = 0.5, from - # "On Timoshenko's correction for shear in vibrating beams" by Kaneko, 1975 - alpha_c = 27.0 / 28.0 - rod.shear_matrix *= 0.0 - for i in range(n_elements): - np.fill_diagonal( - rod.shear_matrix[..., i], - [ - alpha_c * shear_modulus * new_area[i], - alpha_c * shear_modulus * new_area[i], - youngs_modulus * new_area[i], - ], - ) - - # Bend/Twist matrix - bend_matrix = np.zeros( - (MaxDimension.value(), MaxDimension.value(), n_voronoi_elements + 1), np.float64 - ) - for i in range(n_elements): - np.fill_diagonal( - bend_matrix[..., i], - [ - youngs_modulus * new_moi_1[i], - youngs_modulus * new_moi_2[i], - shear_modulus * new_moi_3[i], - ], - ) - if ring_rod_flag: # wrap around the value in the last element - bend_matrix[..., -1] = bend_matrix[..., 0] - for i in range(0, MaxDimension.value()): - assert np.all( - bend_matrix[i, i, :] > Tolerance.atol() - ), " Bend matrix has to be greater than 0." - - # Compute bend matrix in Voronoi Domain - rest_lengths_temp_for_voronoi = ( - np.hstack((rod.rest_lengths, rod.rest_lengths[0])) - if ring_rod_flag - else rod.rest_lengths - ) - rod.bend_matrix = ( - bend_matrix[..., 1:] * rest_lengths_temp_for_voronoi[1:] - + bend_matrix[..., :-1] * rest_lengths_temp_for_voronoi[0:-1] - ) / (rest_lengths_temp_for_voronoi[1:] + rest_lengths_temp_for_voronoi[:-1]) diff --git a/examples/CantileverTransversalLoadCase/convergence_cantilever_transversal_load.py b/examples/CantileverTransversalLoadCase/convergence_cantilever_transversal_load.py index 6a47db3ac..8bee57342 100644 --- a/examples/CantileverTransversalLoadCase/convergence_cantilever_transversal_load.py +++ b/examples/CantileverTransversalLoadCase/convergence_cantilever_transversal_load.py @@ -2,14 +2,14 @@ from elastica.boundary_conditions import OneEndFixedBC from elastica.external_forces import EndpointForces from elastica.timestepper.symplectic_steppers import PositionVerlet -from elastica.timestepper import integrate import elastica as ea -from examples.convergence_functions import calculate_error_norm -from cantilever_transversal_load_postprocessing import adjust_square_cross_section from matplotlib import pyplot as plt from matplotlib.colors import to_rgb import json +from convergence_functions import calculate_error_norm, plot_convergence +from setup_helper import adjust_square_cross_section + def analytical_results(index): with open("cantilever_transversal_load_data.json", "r") as file: @@ -27,7 +27,6 @@ def cantilever_subjected_to_a_transversal_load(n_elem=19): base_radius = 0.01 / ( np.pi ** (1 / 2) ) # The Cross-sectional area is 1e-4(we assume its equivalent to a square cross-sectional surface with same area) - base_area = 1e-4 density = 1000 youngs_modulus = 1e9 poisson_ratio = 0 @@ -38,9 +37,8 @@ class SquareRodSimulator( ): pass - squarerod_sim = SquareRodSimulator() + square_rod_sim = SquareRodSimulator() - density = 1000 t = np.linspace(0, 0.25 * np.pi, n_elem + 1) tmp = np.zeros((3, n_elem + 1), dtype=np.float64) tmp[0, :] = -radius * np.cos(t) + 1 @@ -58,7 +56,7 @@ class SquareRodSimulator( director[1, :, :] = d2 director[2, :, :] = tan - rod = ea.CosseratRod.straight_rod( + square_rod = ea.CosseratRod.straight_rod( n_elem, start, direction, @@ -73,23 +71,22 @@ class SquareRodSimulator( ) # Adjust the Cross Section - adjust_square_cross_section(rod, youngs_modulus, side_length) + adjust_square_cross_section(square_rod, youngs_modulus, side_length) - squarerod_sim.append(rod) + square_rod_sim.append(square_rod) - # squarerod_sim.finalize() - rod.rest_kappa[...] = rod.kappa + square_rod.rest_kappa[...] = square_rod.kappa dl = base_length / n_elem dt = 0.01 * dl / 100 - squarerod_sim.constrain(rod).using( + square_rod_sim.constrain(square_rod).using( OneEndFixedBC, constrained_position_idx=(0,), constrained_director_idx=(0,) ) print("One end of the rod is now fixed in place") - squarerod_sim.dampen(rod).using( + square_rod_sim.dampen(square_rod).using( ea.AnalyticalLinearDamper, damping_constant=0.3, time_step=dt, @@ -100,7 +97,7 @@ class SquareRodSimulator( origin_force = np.array([0.0, 0.0, 0.0]) end_force = np.array([0.0, 0.0, 6.0]) - squarerod_sim.add_forcing_to(rod).using( + square_rod_sim.add_forcing_to(square_rod).using( EndpointForces, origin_force, end_force, ramp_up_time=ramp_up_time ) print("Forces added to the rod") @@ -110,12 +107,11 @@ class SquareRodSimulator( total_steps = int(final_time / dt) print("Total steps to take", total_steps) - squarerod_sim.finalize() + square_rod_sim.finalize() print("System finalized") - # The simulation result from Project3.3.2 with 400 elements/ Tip position Z - - # generate analytical solution array from [400] + # The simulation result from Project3.3.2 with 400 elements (tip position Z) + # Generate analytical solution array by interpolating from the 400-element reference solution analytical_results_sub = np.zeros(n_elem + 1) @@ -126,16 +122,19 @@ class SquareRodSimulator( timestepper = PositionVerlet() - integrate(timestepper, squarerod_sim, final_time, total_steps) - print(rod.position_collection[2, ...]) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(square_rod_sim, time, dt) + print(square_rod.position_collection[2, ...]) error, l1, l2, linf = calculate_error_norm( analytical_results_sub, - rod.position_collection[2, ...], + square_rod.position_collection[2, ...], n_elem, ) - return {"rod": rod, "error": error, "l1": l1, "l2": l2, "linf": linf} + return {"rod": square_rod, "error": error, "l1": l1, "l2": l2, "linf": linf} if __name__ == "__main__": @@ -162,42 +161,4 @@ class SquareRodSimulator( for i in convergence_elements: results.append(cantilever_subjected_to_a_transversal_load(i)) - l1 = [] - l2 = [] - linf = [] - - for result in results: - l1.append(result["l1"]) - l2.append(result["l2"]) - linf.append(result["linf"]) - - fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150) - ax = fig.add_subplot(111) - ax.grid(which="minor", color="k", linestyle="--") - ax.grid(which="major", color="k", linestyle="-") - ax.set_xlabel("N_element") # X-axis label - ax.set_ylabel("Error") # Y-axis label - ax.set_title("Error Convergence Analysis") - - ax.loglog( - convergence_elements, - l1, - marker="o", - ms=10, - c=to_rgb("xkcd:bluish"), - lw=2, - label="l1", - ) - ax.loglog( - convergence_elements, - l2, - marker="o", - ms=10, - c=to_rgb("xkcd:reddish"), - lw=2, - label="l2", - ) - ax.loglog(convergence_elements, linf, marker="o", ms=10, c="k", lw=2, label="linf") - fig.legend(prop={"size": 20}) - - fig.show() + plot_convergence(results, SAVE_FIGURE=False, filename="") diff --git a/examples/convergence_functions.py b/examples/CantileverTransversalLoadCase/convergence_functions.py similarity index 93% rename from examples/convergence_functions.py rename to examples/CantileverTransversalLoadCase/convergence_functions.py index 743eb0cd8..ca81417f4 100644 --- a/examples/convergence_functions.py +++ b/examples/CantileverTransversalLoadCase/convergence_functions.py @@ -51,6 +51,9 @@ def plot_convergence(results, SAVE_FIGURE, filename): label="l2", ) ax.loglog(convergence_elements, linf, marker="o", ms=10, c="k", lw=2, label="linf") + ax.set_xlabel("N_element") + ax.set_ylabel("Error") + ax.set_title("Error Convergence Analysis") fig.legend(prop={"size": 20}) if SAVE_FIGURE: assert filename != "", "provide a file name for figure" diff --git a/examples/CantileverTransversalLoadCase/setup_helper.py b/examples/CantileverTransversalLoadCase/setup_helper.py new file mode 100644 index 000000000..972b3847a --- /dev/null +++ b/examples/CantileverTransversalLoadCase/setup_helper.py @@ -0,0 +1,100 @@ +import numpy as np +from matplotlib import pyplot as plt +from matplotlib import cm +from tqdm import tqdm + +pass +import logging +from elastica.utils import MaxDimension, Tolerance + + +def adjust_square_cross_section( + rod, youngs_modulus: float, length: float, ring_rod_flag: bool = False +): + n_elements = rod.n_elems + n_voronoi_elements = n_elements if ring_rod_flag else n_elements - 1 + + log = logging.getLogger() + + side_length = np.zeros(n_elements) + side_length.fill(length) + + new_area = np.pi * rod.radius * rod.radius + + new_moi_1 = (side_length**4) / 12 + new_moi_2 = (side_length**4) / 12 + new_moi_3 = new_moi_2 * 2 + + new_moi = np.array([new_moi_1, new_moi_2, new_moi_3]).transpose() + + mass_second_moment_of_inertia_temp = np.einsum( + "ij,i->ij", new_moi, rod.density * rod.rest_lengths + ) + + for i in range(n_elements): + np.fill_diagonal( + rod.mass_second_moment_of_inertia[..., i], + mass_second_moment_of_inertia_temp[i, :], + ) + # sanity check of mass second moment of inertia + if (rod.mass_second_moment_of_inertia < Tolerance.atol()).all(): + message = "Mass moment of inertia matrix smaller than tolerance, please check provided radius, density and length." + log.warning(message) + + for i in range(n_elements): + # Check rank of mass moment of inertia matrix to see if it is invertible + assert ( + np.linalg.matrix_rank(rod.mass_second_moment_of_inertia[..., i]) + == MaxDimension.value() + ) + rod.inv_mass_second_moment_of_inertia[..., i] = np.linalg.inv( + rod.mass_second_moment_of_inertia[..., i] + ) + + # Shear/Stretch matrix + shear_modulus = youngs_modulus / (2.0 * (1.0 + 0.5)) + + # Value taken based on best correlation for Poisson ratio = 0.5, from + # "On Timoshenko's correction for shear in vibrating beams" by Kaneko, 1975 + alpha_c = 27.0 / 28.0 + rod.shear_matrix *= 0.0 + for i in range(n_elements): + np.fill_diagonal( + rod.shear_matrix[..., i], + [ + alpha_c * shear_modulus * new_area[i], + alpha_c * shear_modulus * new_area[i], + youngs_modulus * new_area[i], + ], + ) + + # Bend/Twist matrix + bend_matrix = np.zeros( + (MaxDimension.value(), MaxDimension.value(), n_voronoi_elements + 1), np.float64 + ) + for i in range(n_elements): + np.fill_diagonal( + bend_matrix[..., i], + [ + youngs_modulus * new_moi_1[i], + youngs_modulus * new_moi_2[i], + shear_modulus * new_moi_3[i], + ], + ) + if ring_rod_flag: # wrap around the value in the last element + bend_matrix[..., -1] = bend_matrix[..., 0] + for i in range(0, MaxDimension.value()): + assert np.all( + bend_matrix[i, i, :] > Tolerance.atol() + ), " Bend matrix has to be greater than 0." + + # Compute bend matrix in Voronoi Domain + rest_lengths_temp_for_voronoi = ( + np.hstack((rod.rest_lengths, rod.rest_lengths[0])) + if ring_rod_flag + else rod.rest_lengths + ) + rod.bend_matrix = ( + bend_matrix[..., 1:] * rest_lengths_temp_for_voronoi[1:] + + bend_matrix[..., :-1] * rest_lengths_temp_for_voronoi[0:-1] + ) / (rest_lengths_temp_for_voronoi[1:] + rest_lengths_temp_for_voronoi[:-1]) diff --git a/examples/CatenaryCase/README.md b/examples/CatenaryCase/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/examples/CatenaryCase/post_processing.py b/examples/CatenaryCase/post_processing.py index 69e7293bc..45da2dfdd 100644 --- a/examples/CatenaryCase/post_processing.py +++ b/examples/CatenaryCase/post_processing.py @@ -4,7 +4,7 @@ from matplotlib import pyplot as plt import matplotlib.animation as manimation from tqdm import tqdm -import scipy as sci +from scipy import optimize def plot_video( @@ -28,10 +28,10 @@ def plot_video( ax.set_ylabel("y [m]", fontsize=16) # plt.axis("equal") with writer.saving(fig, video_name, dpi=150): - rod_lines_2d = ax.plot(positions_over_time[0][2], positions_over_time[0][0])[0] - for time in tqdm(range(1, len(plot_params["time"]))): - rod_lines_2d.set_xdata([positions_over_time[time][0]]) - rod_lines_2d.set_ydata([positions_over_time[time][2]]) + rod_lines_2d = ax.plot(positions_over_time[0][0], positions_over_time[0][2])[0] + for time_idx in tqdm(range(1, len(plot_params["time"]))): + rod_lines_2d.set_xdata(positions_over_time[time_idx][0]) + rod_lines_2d.set_ydata(positions_over_time[time_idx][2]) writer.grab_frame() # Be a good boy and close figures @@ -57,7 +57,7 @@ def plot_catenary( def f_non_elastic_catenary(x: float) -> float: return x * (1 - np.cosh(1 / (2 * x))) - lowest_point - a = sci.optimize.fsolve(f_non_elastic_catenary, x0=1.0) # solve for a + a = optimize.fsolve(f_non_elastic_catenary, x0=1.0) # solve for a y_catenary = a * np.cosh((x_catenary - 0.5) / a) - a * np.cosh(1 / (2 * a)) plt.plot(position[-1][0], position[-1][2], label="Simulation", linewidth=3) plt.plot( diff --git a/examples/CatenaryCase/catenary.py b/examples/CatenaryCase/run_catenary.py similarity index 62% rename from examples/CatenaryCase/catenary.py rename to examples/CatenaryCase/run_catenary.py index 6875749e5..836a55d36 100644 --- a/examples/CatenaryCase/catenary.py +++ b/examples/CatenaryCase/run_catenary.py @@ -1,3 +1,12 @@ +""" +Catenary +======== + +This case simulates a rod hanging under its own weight, forming a catenary +curve. The rod is fixed at both ends and is allowed to settle into its +equilibrium position. +""" + from collections import defaultdict import numpy as np @@ -8,6 +17,11 @@ plot_catenary, ) +# %% +# Simulation Setup +# ---------------- +# We define a simulator class that inherits from the necessary mixins. + class CatenarySimulator( ea.BaseSystemCollection, ea.Constraints, ea.Forcing, ea.Damping, ea.CallBacks @@ -16,19 +30,22 @@ class CatenarySimulator( catenary_sim = CatenarySimulator() -final_time = 10 -damping_constant = 0.3 +final_time = 30 + +# %% +# Rod Setup +# --------- +# We set up the rod parameters. This rod is affected by a gravity force. + +n_elem = 500 time_step = 1e-4 total_steps = int(final_time / time_step) rendering_fps = 20 step_skip = int(1.0 / (rendering_fps * time_step)) -n_elem = 500 - start = np.zeros((3,)) direction = np.array([1.0, 0.0, 0.0]) normal = np.array([0.0, 0.0, 1.0]) -binormal = np.cross(direction, normal) # catenary parameters base_length = 1.0 @@ -55,17 +72,26 @@ class CatenarySimulator( catenary_sim.append(base_rod) +# Add gravity +catenary_sim.add_forcing_to(base_rod).using( + ea.GravityForces, acc_gravity=-9.80665 * normal +) + +# %% +# Damping is added to the system to help it reach a steady state. + # add damping +damping_constant = 0.3 catenary_sim.dampen(base_rod).using( ea.AnalyticalLinearDamper, damping_constant=damping_constant, time_step=time_step, ) -# Add gravity -catenary_sim.add_forcing_to(base_rod).using( - ea.GravityForces, acc_gravity=-9.80665 * normal -) +# %% +# Boundary Conditions +# ------------------- +# We fix both ends of the rod using the `FixedConstraint`. # fix catenary ends catenary_sim.constrain(base_rod).using( @@ -74,11 +100,16 @@ class CatenarySimulator( constrained_director_idx=(0, -1), ) +# %% +# Callback +# -------- +# We define a callback class to record the rod state during the simulation. + # Add call backs class CatenaryCallBack(ea.CallBackBaseClass): """ - Call back function for continuum snake + Call back function for catenary case """ def __init__(self, step_skip: int, callback_params: dict) -> None: @@ -106,32 +137,48 @@ def make_callback( CatenaryCallBack, step_skip=step_skip, callback_params=recorded_history ) +# %% +# Finalize and Run +# ---------------- +# We finalize the simulator, create the time-stepper, and run. catenary_sim.finalize() - - timestepper: ea.typing.StepperProtocol = ea.PositionVerlet() -ea.integrate(timestepper, catenary_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(catenary_sim, time, dt) position = np.array(recorded_history["position"]) b = np.min(position[-1][2]) -SAVE_VIDEO = True -if SAVE_VIDEO: - # plotting the videos - filename_video = "catenary.mp4" - plot_video( - recorded_history, - video_name=filename_video, - fps=rendering_fps, - xlim=[0, base_length], - ylim=[-0.5 * base_length, 0.5 * base_length], - ) - -PLOT_RESULTS = True -if PLOT_RESULTS: - plot_catenary( - recorded_history, - xlim=(0, base_length), - ylim=(b, 0.0), - ) +# %% +# Post-processing +# --------------- +# Finally, we can save a video of the simulation and plot the final +# shape of the catenary. + +# plotting the videos +filename_video = "catenary.mp4" +plot_video( + recorded_history, + video_name=filename_video, + fps=rendering_fps, + xlim=[0, base_length], + ylim=[-0.5 * base_length, 0.5 * base_length], +) + +# %% +# .. video:: ../../../examples/CatenaryCase/catenary.mp4 +# :width: 720 +# :autoplay: +# :muted: +# :loop: + +# %% +# plotting the catenary positions after simulation. +plot_catenary( + recorded_history, + xlim=(0, base_length), + ylim=(b, 0.0), +) diff --git a/examples/ContinuumFlagellaCase/continuum_flagella.py b/examples/ContinuumFlagellaCase/continuum_flagella.py index e4314349d..ecb6f13a7 100644 --- a/examples/ContinuumFlagellaCase/continuum_flagella.py +++ b/examples/ContinuumFlagellaCase/continuum_flagella.py @@ -1,10 +1,11 @@ -__doc__ = """Continuum flagella example, for detailed explanation refer to Gazzola et. al. R. Soc. 2018 +__doc__ = """Continuum flagella example, for detailed explanation refer to Gazzola et al. R. Soc. 2018 section 5.2.1 """ import numpy as np import os +from collections import defaultdict import elastica as ea -from examples.ContinuumFlagellaCase.continuum_flagella_postprocessing import ( +from continuum_flagella_postprocessing import ( plot_velocity, plot_video, compute_projected_velocity, @@ -88,11 +89,11 @@ def run_flagella( # Add call backs class ContinuumFlagellaCallBack(ea.CallBackBaseClass): """ - Call back function for continuum snake + Call back function for continuum flagella """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -117,7 +118,7 @@ def make_callback(self, system, time, current_step: int): return - pp_list = ea.defaultdict(list) + pp_list = defaultdict(list) flagella_sim.collect_diagnostics(shearable_rod).using( ContinuumFlagellaCallBack, step_skip=200, callback_params=pp_list ) @@ -129,7 +130,10 @@ def make_callback(self, system, time, current_step: int): final_time = (10.0 + 0.01) * period total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, flagella_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(flagella_sim, time, dt) if PLOT_FIGURE: filename_plot = "continuum_flagella_velocity.png" @@ -167,7 +171,7 @@ def make_callback(self, system, time, current_step: int): SAVE_OPTIMIZED_COEFFICIENTS = False - def optimize_snake(spline_coefficient): + def optimize_flagella(spline_coefficient): [avg_forward, _, _] = run_flagella( spline_coefficient, PLOT_FIGURE=False, @@ -177,10 +181,10 @@ def optimize_snake(spline_coefficient): ) return -avg_forward - # Optimize snake for forward velocity. In cma.fmin first input is function + # Optimize flagella for forward velocity. In cma.fmin first input is function # to be optimized, second input is initial guess for coefficients you are optimizing # for and third input is standard deviation you initially set. - optimized_spline_coefficients = cma.fmin(optimize_snake, 5 * [0], 0.5) + optimized_spline_coefficients = cma.fmin(optimize_flagella, 5 * [0], 0.5) # Save the optimized coefficients to a file filename_data = "optimized_coefficients.txt" diff --git a/examples/ContinuumFlagellaCase/continuum_flagella_postprocessing.py b/examples/ContinuumFlagellaCase/continuum_flagella_postprocessing.py index fcbd44ee7..a0945986e 100644 --- a/examples/ContinuumFlagellaCase/continuum_flagella_postprocessing.py +++ b/examples/ContinuumFlagellaCase/continuum_flagella_postprocessing.py @@ -72,11 +72,11 @@ def compute_projected_velocity(plot_params: dict, period): center_of_mass = np.array(plot_params["center_of_mass"]) # Compute rod velocity in rod direction. We need to compute that because, - # after snake starts to move it chooses an arbitrary direction, which does not + # after flagella starts to move it chooses an arbitrary direction, which does not # have to be initial tangent direction of the rod. Thus we need to project the - # snake velocity with respect to its new tangent and roll direction, after that + # flagella velocity with respect to its new tangent and roll direction, after that # we will get the correct forward and lateral speed. After this projection - # lateral velocity of the snake has to be oscillating between + and - values with + # lateral velocity of the flagella has to be oscillating between + and - values with # zero mean. # Number of steps in one period. @@ -108,7 +108,7 @@ def compute_projected_velocity(plot_params: dict, period): # velocity in the direction of rod velocity_in_rod_roll_dir = avg_velocity - velocity_in_direction_of_rod - # Compute the average velocity over the simulation, this can be used for optimizing snake + # Compute the average velocity over the simulation, this can be used for optimizing flagella # for fastest forward velocity. We start after first period, because of the ramping up happens # in first period. average_velocity_over_simulation = np.mean( diff --git a/examples/ContinuumSnakeCase/README.md b/examples/ContinuumSnakeCase/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/examples/ContinuumSnakeCase/continuum_snake.py b/examples/ContinuumSnakeCase/run-continuum-snake.py similarity index 62% rename from examples/ContinuumSnakeCase/continuum_snake.py rename to examples/ContinuumSnakeCase/run-continuum-snake.py index f998754a7..7b339831c 100644 --- a/examples/ContinuumSnakeCase/continuum_snake.py +++ b/examples/ContinuumSnakeCase/run-continuum-snake.py @@ -1,12 +1,37 @@ -__doc__ = """Snake friction case from X. Zhang et. al. Nat. Comm. 2021""" +""" +Continuum Snake +=============== + +Snake friction case from X. Zhang et al. Nat. Comm. 2021 + +This Elastica tutorial explains how to setup a Cosserat rod simulation to simulate a slithering snake. It covers many of the basics of setting up and running simulations with Elastica. + +This slithering snake example includes gravitational forces, friction forces, and internal muscle torques. It also introduces the use of call back functions to allow logging of simulations data for post-processing after the simulation is over. + +.. video:: ../../../assets/continuum_snake.mp4 + :width: 720 + :autoplay: + :muted: + :loop: + +Getting Started +--------------- +To set up the simulation, the first thing you need to do is import the necessary classes. As with the Timoshenko beam, we need to import modules which allow us to more easily construct different simulation systems. We also need to import a rod class, all the necessary forces to be applied, timestepping functions, and callback classes. +""" import os +from collections import defaultdict import numpy as np import elastica as ea from numpy.typing import NDArray from elastica.typing import RodType -from examples.ContinuumSnakeCase.continuum_snake_postprocessing import ( +# %% +# Initialize System and Add Rod +# ----------------------------- +# The first thing to do is initialize the simulator class by combining all the imported modules. After initializing, we will generate a rod and add it to the simulation. + +from continuum_snake_postprocessing import ( plot_snake_velocity, plot_video, compute_projected_velocity, @@ -71,6 +96,13 @@ def run_snake( ea.GravityForces, acc_gravity=np.array([0.0, gravitational_acc, 0.0]) ) + # %% + # Muscle Torques + # -------------- + # A snake generates torque throughout its body through muscle activations. While these muscle activations are generated internally by the snake, it is simpler to treat them as applied external forces, allowing us to apply them to the rod in the same manner as the other external forces. + # + # You may notice that the muscle torque parameters appear to have special values. These are optimized coefficients for a snake gait. + # Add muscle torques wave_length = b_coeff[-1] snake_sim.add_forcing_to(shearable_rod).using( @@ -86,6 +118,10 @@ def run_snake( with_spline=True, ) + # Anisotropic Friction Forces + # --------------------------- + # The last force that needs to be added is the friction force between the snake and the ground. Snakes exhibits anisotropic friction where the friction coefficient is different in different directions. You can also define both static and kinematic friction coefficients. This is accomplished by defining some small velocity threshold `slip_velocity_tol` that defines the transitions between static and kinematic friction. + # Add friction forces ground_plane = ea.Plane( plane_origin=np.array([0.0, -base_radius, 0.0]), plane_normal=normal @@ -116,9 +152,11 @@ def run_snake( time_step=time_step, ) - total_steps = int(final_time / time_step) - rendering_fps = 60 - step_skip = int(1.0 / (rendering_fps * time_step)) + # Add Callback Function + # --------------------- + # The simulation is now setup, but before it is run, we want to define a callback function. A callback function allows us to record time-series data throughout the simulation. If you do not define a callback function, you will only have access to the final configuration of the system. If you want to be able to analyze how the system evolves over time, it is critical that you record the appropriate quantities. + # + # To create a callback function, begin with the `CallBackBaseClass`. You can then define which state quantities you wish to record by having them appended to the `self.callback_params` dictionary as well as how often you wish to save the data by defining `skip_step`. # Add call backs class ContinuumSnakeCallBack(ea.CallBackBaseClass): @@ -127,7 +165,7 @@ class ContinuumSnakeCallBack(ea.CallBackBaseClass): """ def __init__(self, step_skip: int, callback_params: dict) -> None: - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -184,15 +222,26 @@ def get_slip_velocity(self, system: RodType) -> NDArray[np.float64]: ) return slip_function_along_axial_direction - pp_list: dict[str, list] = ea.defaultdict(list) + total_steps = int(final_time / time_step) + rendering_fps = 60 + step_skip = int(1.0 / (rendering_fps * time_step)) + + pp_list: dict[str, list] = defaultdict(list) snake_sim.collect_diagnostics(shearable_rod).using( ContinuumSnakeCallBack, step_skip=step_skip, callback_params=pp_list ) + # With the callback function added, we can now finalize the system and also define the time stepping parameters of the simulation such as the time step, final time, and time stepping algorithm to use. + snake_sim.finalize() + # Now all that is left is to run the simulation. Using the default parameters the simulation takes about 2-3 minutes to complete. + timestepper = ea.PositionVerlet() - ea.integrate(timestepper, snake_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(snake_sim, time, dt) if PLOT_FIGURE: filename_plot = "continuum_snake_velocity.png" @@ -223,6 +272,29 @@ def get_slip_velocity(self, system: RodType) -> NDArray[np.float64]: return avg_forward, avg_lateral, pp_list +# %% +# Post-Process Data +# ----------------- +# With the simulation complete, we want to analyze the simulation. Because we added a callback function, we can analyze how the snake evolves over time. All of the data from the callback function is located in the `pp_list` dictionary. Here we will use this information to compute and plot the velocity of the snake in the forward, lateral, and normal directions. We do this by using a pre-written analysis function `compute_projected_velocity`. +# +# In the plotted graph, you can see that it takes about one period for the snake to begin moving before rapidly reaching a steady gait over just 2-3 periods. We also see that the normal velocity is zero since we are only actuating the snake in a 2D plane. + + +# %% +# Gait Optimization with CMA +# -------------------------- +# The following block of code in the main script demonstrates how to use the +# Covariance Matrix Adaptation Evolution Strategy (CMA-ES) to optimize the +# snake's gait. CMA-ES is a stochastic, derivative-free method for numerical +# optimization of non-linear or non-convex continuous optimization problems. +# Here, we use it to find the optimal set of muscle torque coefficients +# (`b_coeff`) that maximize the snake's average forward velocity. The +# `optimize_snake` function serves as the objective function for the +# optimization, which takes the spline coefficients as input and returns the +# negative of the average forward velocity, as CMA-ES is a minimization +# algorithm. + + if __name__ == "__main__": # Options diff --git a/examples/ContinuumSnakeWithLiftingWaveCase/continuum_snake_with_lifting_wave.py b/examples/ContinuumSnakeWithLiftingWaveCase/continuum_snake_with_lifting_wave.py index fec1fc909..05312be0b 100755 --- a/examples/ContinuumSnakeWithLiftingWaveCase/continuum_snake_with_lifting_wave.py +++ b/examples/ContinuumSnakeWithLiftingWaveCase/continuum_snake_with_lifting_wave.py @@ -1,8 +1,9 @@ -__doc__ = """Snake friction case from X. Zhang et. al. Nat. Comm. 2021""" +__doc__ = """Snake friction case from X. Zhang et al. Nat. Comm. 2021""" import os import numpy as np import pickle +from collections import defaultdict from elastica import * @@ -188,7 +189,10 @@ def make_callback(self, system, time, current_step: int): snake_sim.finalize() timestepper = PositionVerlet() - integrate(timestepper, snake_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(snake_sim, time, dt) if PLOT_FIGURE: filename_plot = "continuum_snake_velocity.png" diff --git a/examples/ContinuumSnakeWithLiftingWaveCase/snake_contact.py b/examples/ContinuumSnakeWithLiftingWaveCase/snake_contact.py index 36fcf3133..44a3394dc 100755 --- a/examples/ContinuumSnakeWithLiftingWaveCase/snake_contact.py +++ b/examples/ContinuumSnakeWithLiftingWaveCase/snake_contact.py @@ -1,4 +1,4 @@ -__doc__ = """Rod plane contact with anistropic friction (no static friction)""" +__doc__ = """Rod plane contact with anisotropic friction (no static friction)""" from typing import Type import numpy as np @@ -25,7 +25,6 @@ from numba import njit from elastica.rod.rod_base import RodBase from elastica.surface import Plane -from elastica.surface.surface_base import SurfaceBase from elastica.contact_forces import NoContact from elastica.typing import RodType, SystemType @@ -46,7 +45,7 @@ def apply_normal_force_numba( ): """ This function computes the plane force response on the element, in the - case of contact. Contact model given in Eqn 4.8 Gazzola et. al. RSoS 2018 paper + case of contact. Contact model given in Eqn 4.8 Gazzola et al. RSoS 2018 paper is used. Parameters @@ -291,7 +290,7 @@ def __init__( @property def _allowed_system_two(self) -> list[Type]: # Modify this list to include the allowed system types for contact - return [SurfaceBase] + return [Plane] def apply_contact( self, system_one: RodType, system_two: SystemType, time: float diff --git a/examples/ContinuumSnakeWithLiftingWaveCase/snake_forcing.py b/examples/ContinuumSnakeWithLiftingWaveCase/snake_forcing.py index 3b471acad..7fedcce92 100755 --- a/examples/ContinuumSnakeWithLiftingWaveCase/snake_forcing.py +++ b/examples/ContinuumSnakeWithLiftingWaveCase/snake_forcing.py @@ -12,7 +12,7 @@ from elastica.external_forces import NoForces from elastica.external_forces import ( inplace_addition, - inplace_substraction, + inplace_subtraction, ) @@ -21,7 +21,7 @@ class MuscleTorquesLifting(NoForces): This class applies muscle torques along the body. The applied muscle torques are treated as applied external forces. This class can apply lifting muscle torques as a traveling wave with a beta spline or only - as a traveling wave. For implementation details refer to X. Zhang et. al. Nat. Comm. 2021 + as a traveling wave. For implementation details refer to X. Zhang et al. Nat. Comm. 2021 Attributes ---------- @@ -57,7 +57,7 @@ def __init__( Parameters ---------- - b_coeff: nump.ndarray + b_coeff: numpy.ndarray 1D array containing data with 'float' type. Beta coefficients for beta-spline. period: float @@ -174,7 +174,7 @@ def compute_muscle_torques( external_torques[..., 1:], _batch_matvec(director_collection[..., 1:], torque), ) - inplace_substraction( + inplace_subtraction( external_torques[..., :-1], _batch_matvec(director_collection[..., :-1], torque), ) diff --git a/examples/DynamicCantileverCase/analytical_dynamic_cantilever.py b/examples/DynamicCantileverCase/analytical_dynamic_cantilever.py index 3ce2d04c9..011f1c41b 100644 --- a/examples/DynamicCantileverCase/analytical_dynamic_cantilever.py +++ b/examples/DynamicCantileverCase/analytical_dynamic_cantilever.py @@ -41,7 +41,7 @@ class AnalyticalDynamicCantilever: Cross-sectional area of the rod moment_of_inertia: float Second moment of area of the rod's cross-section - young's_modulus: float + youngs_modulus: float Young's modulus of the rod density: float Density of the rod diff --git a/examples/DynamicCantileverCase/dynamic_cantilever.py b/examples/DynamicCantileverCase/dynamic_cantilever.py index 36f95c168..ee0374995 100644 --- a/examples/DynamicCantileverCase/dynamic_cantilever.py +++ b/examples/DynamicCantileverCase/dynamic_cantilever.py @@ -1,3 +1,5 @@ +from collections import defaultdict + import numpy as np from scipy.fft import fft, fftfreq from scipy.signal import find_peaks @@ -5,6 +7,10 @@ from analytical_dynamic_cantilever import AnalyticalDynamicCantilever +class DynamicCantileverSimulator(ea.BaseSystemCollection, ea.Constraints, ea.CallBacks): + pass + + def simulate_dynamic_cantilever_with( density=2000.0, n_elem=100, @@ -37,11 +43,6 @@ def simulate_dynamic_cantilever_with( """ - class DynamicCantileverSimulator( - ea.BaseSystemCollection, ea.Constraints, ea.CallBacks - ): - pass - cantilever_sim = DynamicCantileverSimulator() # Add test parameters @@ -57,6 +58,7 @@ class DynamicCantileverSimulator( dl = base_length / n_elem dt = dl * 0.05 + total_steps = int(final_time / dt) step_skips = int(1.0 / (rendering_fps * dt)) # Add Cosserat rod @@ -96,7 +98,7 @@ class DynamicCantileverSimulator( # Add call backs class CantileverCallBack(ea.CallBackBaseClass): def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -113,23 +115,18 @@ def make_callback(self, system, time, current_step: int): ) return - recorded_history = ea.defaultdict(list) + recorded_history = defaultdict(list) cantilever_sim.collect_diagnostics(cantilever_rod).using( CantileverCallBack, step_skip=step_skips, callback_params=recorded_history ) cantilever_sim.finalize() - total_steps = int(final_time / dt) print(f"Total steps: {total_steps}") timestepper = ea.PositionVerlet() - - ea.integrate( - timestepper, - cantilever_sim, - final_time, - total_steps, - ) + time = 0.0 + for i in range(total_steps): + time = timestepper.step(cantilever_sim, time, dt) # FFT amplitudes = np.abs(fft(recorded_history["deflection"])) diff --git a/examples/DynamicCantileverCase/dynamic_cantilever_phase_space.py b/examples/DynamicCantileverCase/dynamic_cantilever_phase_space.py index bdbec084d..52b5aed16 100644 --- a/examples/DynamicCantileverCase/dynamic_cantilever_phase_space.py +++ b/examples/DynamicCantileverCase/dynamic_cantilever_phase_space.py @@ -1,4 +1,4 @@ -__doc__ = """ Validating phase space of dynamic cantilever beam analytical_cantilever_soln with respect to varying densities. +__doc__ = """Validating phase space of dynamic cantilever beam analytical solution with respect to varying densities. The theoretical dynamic response is obtained via Euler-Bernoulli beam theory.""" from dynamic_cantilever_post_processing import plot_phase_space_with diff --git a/examples/DynamicCantileverCase/dynamic_cantilever_post_processing.py b/examples/DynamicCantileverCase/dynamic_cantilever_post_processing.py index d6387fd54..4923dfdda 100644 --- a/examples/DynamicCantileverCase/dynamic_cantilever_post_processing.py +++ b/examples/DynamicCantileverCase/dynamic_cantilever_post_processing.py @@ -1,5 +1,7 @@ +import matplotlib.animation as manimation import matplotlib.pyplot as plt import numpy as np +from tqdm import tqdm # Plotting frequency and amplitudes against densities @@ -124,9 +126,6 @@ def plot_dynamic_cantilever_video_with( print("Plotting video ...") video_name = f"Dynamic_cantilever_mode_{mode + 1}.mp4" - import matplotlib.animation as manimation - from tqdm import tqdm - positions_over_time = np.array(recorded_history["position"]) FFMpegWriter = manimation.writers["ffmpeg"] diff --git a/examples/DynamicCantileverCase/dynamic_cantilever_visualization.py b/examples/DynamicCantileverCase/dynamic_cantilever_visualization.py index bcb182cb8..f2e04a897 100644 --- a/examples/DynamicCantileverCase/dynamic_cantilever_visualization.py +++ b/examples/DynamicCantileverCase/dynamic_cantilever_visualization.py @@ -1,19 +1,11 @@ __doc__ = """Visualization of simulated dynamic cantilever beam""" -import elastica as ea from dynamic_cantilever import simulate_dynamic_cantilever_with from dynamic_cantilever_post_processing import ( plot_end_position_with, plot_dynamic_cantilever_video_with, ) - -class DynamicCantileverSimulator(ea.BaseSystemCollection, ea.Constraints, ea.CallBacks): - pass - - -cantilever_sim = DynamicCantileverSimulator() - # Options PLOT_FIGURE = True SAVE_FIGURE = True @@ -33,7 +25,6 @@ class DynamicCantileverSimulator(ea.BaseSystemCollection, ea.Constraints, ea.Cal rendering_fps=rendering_fps, ) -cantilever = sim_result["rod"] recorded_history = sim_result["recorded_history"] omegas = sim_result["fft_frequencies"] amplitudes = sim_result["fft_amplitudes"] diff --git a/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_fixed_joint.py b/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_fixed_joint.py index f0729130f..bd3e38fc5 100644 --- a/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_fixed_joint.py +++ b/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_fixed_joint.py @@ -1,6 +1,8 @@ __doc__ = """Fixed joint example, for detailed explanation refer to Zhang et. al. Nature Comm. methods section.""" import numpy as np +from collections import defaultdict + import elastica as ea from elastica.experimental.connection_contact_joint.generic_system_type_connection import ( GenericSystemTypeFixedJoint, @@ -30,10 +32,8 @@ class FixedJointSimulator( n_elem = 10 direction = np.array([0.0, 0.0, 1.0]) normal = np.array([0.0, 1.0, 0.0]) -roll_direction = np.cross(direction, normal) base_length = 0.2 base_radius = 0.007 -base_area = np.pi * base_radius**2 density = 1750 E = 3e7 poisson_ratio = 0.5 @@ -135,9 +135,9 @@ class FixedJointSimulator( time_step=dt, ) -pp_list_rod1 = ea.defaultdict(list) -pp_list_rod2 = ea.defaultdict(list) -pp_list_cylinder = ea.defaultdict(list) +pp_list_rod1 = defaultdict(list) +pp_list_rod2 = defaultdict(list) +pp_list_cylinder = defaultdict(list) fixed_joint_sim.collect_diagnostics(rod1).using( ea.MyCallBack, step_skip=step_skip, callback_params=pp_list_rod1 @@ -153,10 +153,12 @@ class FixedJointSimulator( timestepper = ea.PositionVerlet() # timestepper = PEFRL() -dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, fixed_joint_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(fixed_joint_sim, time, dt) PLOT_FIGURE = True SAVE_FIGURE = True diff --git a/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_spherical_joint.py b/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_spherical_joint.py index 065ddbd82..09ec4c68d 100644 --- a/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_spherical_joint.py +++ b/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_spherical_joint.py @@ -2,6 +2,8 @@ methods section.""" import numpy as np +from collections import defaultdict + import elastica as ea from elastica.experimental.connection_contact_joint.generic_system_type_connection import ( GenericSystemTypeFreeJoint, @@ -31,10 +33,8 @@ class SphericalJointSimulator( n_elem = 10 direction = np.array([0.0, 0.0, 1.0]) normal = np.array([0.0, 1.0, 0.0]) -roll_direction = np.cross(direction, normal) base_length = 0.2 base_radius = 0.007 -base_area = np.pi * base_radius**2 density = 1750 E = 3e7 poisson_ratio = 0.5 @@ -131,9 +131,9 @@ class SphericalJointSimulator( time_step=dt, ) -pp_list_rod1 = ea.defaultdict(list) -pp_list_rod2 = ea.defaultdict(list) -pp_list_cylinder = ea.defaultdict(list) +pp_list_rod1 = defaultdict(list) +pp_list_rod2 = defaultdict(list) +pp_list_cylinder = defaultdict(list) spherical_joint_sim.collect_diagnostics(rod1).using( ea.MyCallBack, step_skip=step_skip, callback_params=pp_list_rod1 @@ -149,10 +149,12 @@ class SphericalJointSimulator( timestepper = ea.PositionVerlet() # timestepper = PEFRL() -dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, spherical_joint_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(spherical_joint_sim, time, dt) PLOT_FIGURE = True SAVE_FIGURE = True diff --git a/examples/ExperimentalCases/GenericSystemConnectionCases/joint_cases_postprocessing.py b/examples/ExperimentalCases/GenericSystemConnectionCases/joint_cases_postprocessing.py index 5b49be8c6..71c68c936 100644 --- a/examples/ExperimentalCases/GenericSystemConnectionCases/joint_cases_postprocessing.py +++ b/examples/ExperimentalCases/GenericSystemConnectionCases/joint_cases_postprocessing.py @@ -84,7 +84,7 @@ def plot_video( ): # (time step, x/y/z, node) import matplotlib.animation as manimation - time = plot_params_rod1["time"] + time_list = plot_params_rod1["time"] position_of_rod1 = np.array(plot_params_rod1["position"]) position_of_rod2 = np.array(plot_params_rod2["position"]) position_of_cylinder = ( @@ -104,31 +104,31 @@ def plot_video( writer = FFMpegWriter(fps=fps, metadata=metadata) fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150) with writer.saving(fig, video_name, 100): - for time in range(1, len(time)): + for time_idx in range(1, len(time_list)): fig.clf() ax = plt.axes(projection="3d") # fig.add_subplot(111) ax.grid(which="minor", color="k", linestyle="--") ax.grid(which="major", color="k", linestyle="-") ax.plot( - position_of_rod1[time, 0], - position_of_rod1[time, 1], - position_of_rod1[time, 2], + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 1], + position_of_rod1[time_idx, 2], "or", label="rod1", ) ax.plot( - position_of_rod2[time, 0], - position_of_rod2[time, 1], - position_of_rod2[time, 2], + position_of_rod2[time_idx, 0], + position_of_rod2[time_idx, 1], + position_of_rod2[time_idx, 2], "o", c=to_rgb("xkcd:bluish"), label="rod2", ) if position_of_cylinder is not None: ax.plot( - position_of_cylinder[time, 0], - position_of_cylinder[time, 1], - position_of_cylinder[time, 2], + position_of_cylinder[time_idx, 0], + position_of_cylinder[time_idx, 1], + position_of_cylinder[time_idx, 2], "o", c=to_rgb("xkcd:greenish"), label="Cylinder CoM", @@ -140,7 +140,7 @@ def plot_video( stop=cylinder.length.squeeze() / 2, num=cylinder_axis_points.shape[1], ) - cylinder_director = director_of_cylinder[time, ...] + cylinder_director = director_of_cylinder[time_idx, ...] cylinder_director_batched = np.repeat( cylinder_director, repeats=cylinder_axis_points.shape[1], axis=2 ) @@ -150,7 +150,7 @@ def plot_video( cylinder_axis_points, ) # add offset position of CoM - cylinder_axis_points += position_of_cylinder[time, ...] + cylinder_axis_points += position_of_cylinder[time_idx, ...] ax.plot( cylinder_axis_points[0, :], cylinder_axis_points[1, :], @@ -178,7 +178,7 @@ def plot_video_xy( ): # (time step, x/y/z, node) import matplotlib.animation as manimation - time = plot_params_rod1["time"] + time_list = plot_params_rod1["time"] position_of_rod1 = np.array(plot_params_rod1["position"]) position_of_rod2 = np.array(plot_params_rod2["position"]) position_of_cylinder = ( @@ -199,22 +199,25 @@ def plot_video_xy( fig = plt.figure() plt.axis("equal") with writer.saving(fig, video_name, 100): - for time in range(1, len(time)): + for time_idx in range(1, len(time_list)): fig.clf() plt.plot( - position_of_rod1[time, 0], position_of_rod1[time, 1], "or", label="rod1" + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 1], + "or", + label="rod1", ) plt.plot( - position_of_rod2[time, 0], - position_of_rod2[time, 1], + position_of_rod2[time_idx, 0], + position_of_rod2[time_idx, 1], "o", c=to_rgb("xkcd:bluish"), label="rod2", ) if position_of_cylinder is not None: plt.plot( - position_of_cylinder[time, 0], - position_of_cylinder[time, 1], + position_of_cylinder[time_idx, 0], + position_of_cylinder[time_idx, 1], "o", c=to_rgb("xkcd:greenish"), label="cylinder", @@ -226,7 +229,7 @@ def plot_video_xy( stop=cylinder.length.squeeze() / 2, num=cylinder_axis_points.shape[1], ) - cylinder_director = director_of_cylinder[time, ...] + cylinder_director = director_of_cylinder[time_idx, ...] cylinder_director_batched = np.repeat( cylinder_director, repeats=cylinder_axis_points.shape[1], axis=2 ) @@ -236,7 +239,7 @@ def plot_video_xy( cylinder_axis_points, ) # add offset position of CoM - cylinder_axis_points += position_of_cylinder[time, ...] + cylinder_axis_points += position_of_cylinder[time_idx, ...] plt.plot( cylinder_axis_points[0, :], cylinder_axis_points[1, :], @@ -261,7 +264,7 @@ def plot_video_xz( ): # (time step, x/y/z, node) import matplotlib.animation as manimation - time = plot_params_rod1["time"] + time_list = plot_params_rod1["time"] position_of_rod1 = np.array(plot_params_rod1["position"]) position_of_rod2 = np.array(plot_params_rod2["position"]) position_of_cylinder = ( @@ -282,22 +285,25 @@ def plot_video_xz( fig = plt.figure() plt.axis("equal") with writer.saving(fig, video_name, 100): - for time in range(1, len(time)): + for time_idx in range(1, len(time_list)): fig.clf() plt.plot( - position_of_rod1[time, 0], position_of_rod1[time, 2], "or", label="rod1" + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 2], + "or", + label="rod1", ) plt.plot( - position_of_rod2[time, 0], - position_of_rod2[time, 2], + position_of_rod2[time_idx, 0], + position_of_rod2[time_idx, 2], "o", c=to_rgb("xkcd:bluish"), label="rod2", ) if position_of_cylinder is not None: plt.plot( - position_of_cylinder[time, 0], - position_of_cylinder[time, 2], + position_of_cylinder[time_idx, 0], + position_of_cylinder[time_idx, 2], "o", c=to_rgb("xkcd:greenish"), label="cylinder", @@ -309,7 +315,7 @@ def plot_video_xz( stop=cylinder.length.squeeze() / 2, num=cylinder_axis_points.shape[1], ) - cylinder_director = director_of_cylinder[time, ...] + cylinder_director = director_of_cylinder[time_idx, ...] cylinder_director_batched = np.repeat( cylinder_director, repeats=cylinder_axis_points.shape[1], axis=2 ) @@ -319,7 +325,7 @@ def plot_video_xz( cylinder_axis_points, ) # add offset position of CoM - cylinder_axis_points += position_of_cylinder[time, ...] + cylinder_axis_points += position_of_cylinder[time_idx, ...] plt.plot( cylinder_axis_points[0, :], cylinder_axis_points[2, :], diff --git a/examples/ExperimentalCases/ParallelConnectionExample/joint_cases_postprocessing.py b/examples/ExperimentalCases/ParallelConnectionExample/joint_cases_postprocessing.py new file mode 100644 index 000000000..71c68c936 --- /dev/null +++ b/examples/ExperimentalCases/ParallelConnectionExample/joint_cases_postprocessing.py @@ -0,0 +1,340 @@ +import numpy as np +from matplotlib import pyplot as plt +from matplotlib.colors import to_rgb +from scipy.spatial.transform import Rotation + +from elastica.rigidbody import Cylinder +from elastica._linalg import _batch_matvec + + +def plot_position( + plot_params_rod1: dict, + plot_params_rod2: dict, + plot_params_cylinder: dict = None, + filename="joint_cases_last_node_pos_xy.png", + SAVE_FIGURE=False, +): + position_of_rod1 = np.array(plot_params_rod1["position"]) + position_of_rod2 = np.array(plot_params_rod2["position"]) + position_of_cylinder = ( + np.array(plot_params_cylinder["position"]) + if plot_params_cylinder is not None + else None + ) + + fig = plt.figure(figsize=(10, 10), frameon=True, dpi=150) + ax = fig.add_subplot(111) + + ax.grid(which="minor", color="k", linestyle="--") + ax.grid(which="major", color="k", linestyle="-") + ax.plot(position_of_rod1[:, 0, -1], position_of_rod1[:, 1, -1], "r-", label="rod1") + ax.plot( + position_of_rod2[:, 0, -1], + position_of_rod2[:, 1, -1], + c=to_rgb("xkcd:bluish"), + label="rod2", + ) + if position_of_cylinder is not None: + ax.plot( + position_of_cylinder[:, 0, -1], + position_of_cylinder[:, 1, -1], + c=to_rgb("xkcd:greenish"), + label="cylinder", + ) + + fig.legend(prop={"size": 20}) + plt.xlabel("x") + plt.ylabel("y") + + plt.show() + + if SAVE_FIGURE: + fig.savefig(filename) + + +def plot_orientation(title, time, directors): + """ + Plot the orientation of one node + """ + quat = [] + for t in range(len(time)): + quat_t = Rotation.from_matrix(directors[t].T).as_quat() + quat.append(quat_t) + quat = np.array(quat) + + plt.figure(num=title) + plt.plot(time, quat[:, 0], label="x") + plt.plot(time, quat[:, 1], label="y") + plt.plot(time, quat[:, 2], label="z") + plt.plot(time, quat[:, 3], label="w") + plt.title(title) + plt.legend() + plt.xlabel("Time [s]") + plt.ylabel("Quaternion") + plt.show() + + +def plot_video( + plot_params_rod1: dict, + plot_params_rod2: dict, + plot_params_cylinder: dict = None, + video_name="joint_cases_video.mp4", + fps=100, + cylinder: Cylinder = None, +): # (time step, x/y/z, node) + import matplotlib.animation as manimation + + time_list = plot_params_rod1["time"] + position_of_rod1 = np.array(plot_params_rod1["position"]) + position_of_rod2 = np.array(plot_params_rod2["position"]) + position_of_cylinder = ( + np.array(plot_params_cylinder["position"]) + if plot_params_cylinder is not None + else None + ) + director_of_cylinder = ( + np.array(plot_params_cylinder["directors"]) + if plot_params_cylinder is not None + else None + ) + + print("plot video") + FFMpegWriter = manimation.writers["ffmpeg"] + metadata = dict(title="Movie Test", artist="Matplotlib", comment="Movie support!") + writer = FFMpegWriter(fps=fps, metadata=metadata) + fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150) + with writer.saving(fig, video_name, 100): + for time_idx in range(1, len(time_list)): + fig.clf() + ax = plt.axes(projection="3d") # fig.add_subplot(111) + ax.grid(which="minor", color="k", linestyle="--") + ax.grid(which="major", color="k", linestyle="-") + ax.plot( + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 1], + position_of_rod1[time_idx, 2], + "or", + label="rod1", + ) + ax.plot( + position_of_rod2[time_idx, 0], + position_of_rod2[time_idx, 1], + position_of_rod2[time_idx, 2], + "o", + c=to_rgb("xkcd:bluish"), + label="rod2", + ) + if position_of_cylinder is not None: + ax.plot( + position_of_cylinder[time_idx, 0], + position_of_cylinder[time_idx, 1], + position_of_cylinder[time_idx, 2], + "o", + c=to_rgb("xkcd:greenish"), + label="Cylinder CoM", + ) + if cylinder is not None: + cylinder_axis_points = np.zeros((3, 10)) + cylinder_axis_points[2, :] = np.linspace( + start=-cylinder.length.squeeze() / 2, + stop=cylinder.length.squeeze() / 2, + num=cylinder_axis_points.shape[1], + ) + cylinder_director = director_of_cylinder[time_idx, ...] + cylinder_director_batched = np.repeat( + cylinder_director, repeats=cylinder_axis_points.shape[1], axis=2 + ) + # rotate points into inertial frame + cylinder_axis_points = _batch_matvec( + cylinder_director_batched.transpose((1, 0, 2)), + cylinder_axis_points, + ) + # add offset position of CoM + cylinder_axis_points += position_of_cylinder[time_idx, ...] + ax.plot( + cylinder_axis_points[0, :], + cylinder_axis_points[1, :], + cylinder_axis_points[2, :], + c=to_rgb("xkcd:greenish"), + label="Cylinder axis", + ) + + ax.set_xlim(-0.25, 0.25) + ax.set_ylim(-0.25, 0.25) + ax.set_zlim(0, 0.61) + ax.set_xlabel("x [m]") + ax.set_ylabel("y [m]") + ax.set_zlabel("z [m]") + writer.grab_frame() + + +def plot_video_xy( + plot_params_rod1: dict, + plot_params_rod2: dict, + plot_params_cylinder: dict = None, + video_name="joint_cases_video_xy.mp4", + fps=100, + cylinder: Cylinder = None, +): # (time step, x/y/z, node) + import matplotlib.animation as manimation + + time_list = plot_params_rod1["time"] + position_of_rod1 = np.array(plot_params_rod1["position"]) + position_of_rod2 = np.array(plot_params_rod2["position"]) + position_of_cylinder = ( + np.array(plot_params_cylinder["position"]) + if plot_params_cylinder is not None + else None + ) + director_of_cylinder = ( + np.array(plot_params_cylinder["directors"]) + if plot_params_cylinder is not None + else None + ) + + print("plot video xy") + FFMpegWriter = manimation.writers["ffmpeg"] + metadata = dict(title="Movie Test", artist="Matplotlib", comment="Movie support!") + writer = FFMpegWriter(fps=fps, metadata=metadata) + fig = plt.figure() + plt.axis("equal") + with writer.saving(fig, video_name, 100): + for time_idx in range(1, len(time_list)): + fig.clf() + plt.plot( + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 1], + "or", + label="rod1", + ) + plt.plot( + position_of_rod2[time_idx, 0], + position_of_rod2[time_idx, 1], + "o", + c=to_rgb("xkcd:bluish"), + label="rod2", + ) + if position_of_cylinder is not None: + plt.plot( + position_of_cylinder[time_idx, 0], + position_of_cylinder[time_idx, 1], + "o", + c=to_rgb("xkcd:greenish"), + label="cylinder", + ) + if cylinder is not None: + cylinder_axis_points = np.zeros((3, 10)) + cylinder_axis_points[2, :] = np.linspace( + start=-cylinder.length.squeeze() / 2, + stop=cylinder.length.squeeze() / 2, + num=cylinder_axis_points.shape[1], + ) + cylinder_director = director_of_cylinder[time_idx, ...] + cylinder_director_batched = np.repeat( + cylinder_director, repeats=cylinder_axis_points.shape[1], axis=2 + ) + # rotate points into inertial frame + cylinder_axis_points = _batch_matvec( + cylinder_director_batched.transpose((1, 0, 2)), + cylinder_axis_points, + ) + # add offset position of CoM + cylinder_axis_points += position_of_cylinder[time_idx, ...] + plt.plot( + cylinder_axis_points[0, :], + cylinder_axis_points[1, :], + c=to_rgb("xkcd:greenish"), + label="Cylinder axis", + ) + + plt.xlim([-0.25, 0.25]) + plt.ylim([-0.25, 0.25]) + plt.xlabel("x [m]") + plt.ylabel("y [m]") + writer.grab_frame() + + +def plot_video_xz( + plot_params_rod1: dict, + plot_params_rod2: dict, + plot_params_cylinder: dict = None, + video_name="joint_cases_video_xz.mp4", + fps=100, + cylinder: Cylinder = None, +): # (time step, x/y/z, node) + import matplotlib.animation as manimation + + time_list = plot_params_rod1["time"] + position_of_rod1 = np.array(plot_params_rod1["position"]) + position_of_rod2 = np.array(plot_params_rod2["position"]) + position_of_cylinder = ( + np.array(plot_params_cylinder["position"]) + if plot_params_cylinder is not None + else None + ) + director_of_cylinder = ( + np.array(plot_params_cylinder["directors"]) + if plot_params_cylinder is not None + else None + ) + + print("plot video xz") + FFMpegWriter = manimation.writers["ffmpeg"] + metadata = dict(title="Movie Test", artist="Matplotlib", comment="Movie support!") + writer = FFMpegWriter(fps=fps, metadata=metadata) + fig = plt.figure() + plt.axis("equal") + with writer.saving(fig, video_name, 100): + for time_idx in range(1, len(time_list)): + fig.clf() + plt.plot( + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 2], + "or", + label="rod1", + ) + plt.plot( + position_of_rod2[time_idx, 0], + position_of_rod2[time_idx, 2], + "o", + c=to_rgb("xkcd:bluish"), + label="rod2", + ) + if position_of_cylinder is not None: + plt.plot( + position_of_cylinder[time_idx, 0], + position_of_cylinder[time_idx, 2], + "o", + c=to_rgb("xkcd:greenish"), + label="cylinder", + ) + if cylinder is not None: + cylinder_axis_points = np.zeros((3, 10)) + cylinder_axis_points[2, :] = np.linspace( + start=-cylinder.length.squeeze() / 2, + stop=cylinder.length.squeeze() / 2, + num=cylinder_axis_points.shape[1], + ) + cylinder_director = director_of_cylinder[time_idx, ...] + cylinder_director_batched = np.repeat( + cylinder_director, repeats=cylinder_axis_points.shape[1], axis=2 + ) + # rotate points into inertial frame + cylinder_axis_points = _batch_matvec( + cylinder_director_batched.transpose((1, 0, 2)), + cylinder_axis_points, + ) + # add offset position of CoM + cylinder_axis_points += position_of_cylinder[time_idx, ...] + plt.plot( + cylinder_axis_points[0, :], + cylinder_axis_points[2, :], + c=to_rgb("xkcd:greenish"), + label="Cylinder axis", + ) + + plt.xlim([-0.25, 0.25]) + plt.ylim([0, 0.61]) + plt.xlabel("x [m]") + plt.ylabel("z [m]") + writer.grab_frame() diff --git a/examples/ExperimentalCases/ParallelConnectionExample/parallel_connection_example.py b/examples/ExperimentalCases/ParallelConnectionExample/parallel_connection_example.py index aac78b943..1e45b77d2 100644 --- a/examples/ExperimentalCases/ParallelConnectionExample/parallel_connection_example.py +++ b/examples/ExperimentalCases/ParallelConnectionExample/parallel_connection_example.py @@ -1,19 +1,16 @@ __doc__ = """Parallel connection example""" import numpy as np +from collections import defaultdict + import elastica as ea from elastica.experimental.connection_contact_joint.parallel_connection import ( get_connection_vector_straight_straight_rod, SurfaceJointSideBySide, ) from elastica._calculus import difference_kernel -import sys - -sys.path.append("../") -sys.path.append("../../") -sys.path.append("../../../") -from examples.JointCases.joint_cases_postprocessing import ( +from joint_cases_postprocessing import ( plot_position, plot_video, plot_video_xy, @@ -41,7 +38,6 @@ class ParallelConnection( binormal = np.cross(direction, normal) base_length = 0.2 base_radius = 0.007 -base_area = np.pi * base_radius**2 density = 1750 E = 3e4 poisson_ratio = 0.5 @@ -152,13 +148,13 @@ def apply_forces(self, system, time: np.float64 = 0.0): ) -class ParallelConnecitonCallback(ea.CallBackBaseClass): +class ParallelConnectionCallback(ea.CallBackBaseClass): """ Call back function for parallel connection """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -171,15 +167,15 @@ def make_callback(self, system, time, current_step: int): return -pp_list_rod1 = ea.defaultdict(list) -pp_list_rod2 = ea.defaultdict(list) +pp_list_rod1 = defaultdict(list) +pp_list_rod2 = defaultdict(list) parallel_connection_sim.collect_diagnostics(rod_one).using( - ParallelConnecitonCallback, step_skip=40, callback_params=pp_list_rod1 + ParallelConnectionCallback, step_skip=40, callback_params=pp_list_rod1 ) parallel_connection_sim.collect_diagnostics(rod_two).using( - ParallelConnecitonCallback, step_skip=40, callback_params=pp_list_rod2 + ParallelConnectionCallback, step_skip=40, callback_params=pp_list_rod2 ) @@ -187,10 +183,12 @@ def make_callback(self, system, time, current_step: int): timestepper = ea.PositionVerlet() final_time = 20.0 -dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, parallel_connection_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(parallel_connection_sim, time, dt) PLOT_FIGURE = True SAVE_FIGURE = False diff --git a/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum.py b/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum.py index aff47e1a1..d2b4b59b6 100644 --- a/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum.py +++ b/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum.py @@ -2,6 +2,9 @@ isort:skip_file """ +import pickle +from collections import defaultdict + import numpy as np from matplotlib import pyplot as plt @@ -20,7 +23,7 @@ class SwingingFlexiblePendulumSimulator( SAVE_FIGURE = False SAVE_RESULTS = True -# For 10 elements, the prefac is 0.0007 +# For 10 elements, the prefactor is 0.0007 pendulum_sim = SwingingFlexiblePendulumSimulator() final_time = 1.0 if SAVE_RESULTS else 5.0 @@ -31,7 +34,6 @@ class SwingingFlexiblePendulumSimulator( normal = np.array([1.0, 0.0, 0.0]) base_length = 1.0 base_radius = 0.005 -base_area = np.pi * base_radius**2 density = 1100.0 youngs_modulus = 5e6 # For shear modulus of 1e4, nu is 99! @@ -52,10 +54,9 @@ class SwingingFlexiblePendulumSimulator( pendulum_sim.append(pendulum_rod) -# Bad name : whats a FreeRod anyway? class HingeBC(ea.ConstraintBase): """ - the end of the rod fixed x[0] + Hinge boundary condition that fixes the position of the first node """ def __init__(self, fixed_position, fixed_directors, **kwargs): @@ -81,14 +82,14 @@ def constrain_rates(self, system, time): ) -# Add call backs +# Add callbacks class PendulumCallBack(ea.CallBackBaseClass): """ - Call back function for continuum snake + Callback function for flexible swinging pendulum """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -112,7 +113,7 @@ def make_callback(self, system, time, current_step: int): total_steps = int(final_time / dt) print("Total steps", total_steps) -recorded_history = ea.defaultdict(list) +recorded_history = defaultdict(list) step_skip = ( 60 if PLOT_VIDEO @@ -126,7 +127,9 @@ def make_callback(self, system, time, current_step: int): timestepper = ea.PositionVerlet() # timestepper = PEFRL() -ea.integrate(timestepper, pendulum_sim, final_time, total_steps) +time = 0.0 +for i in range(total_steps): + time = timestepper.step(pendulum_sim, time, dt) if PLOT_VIDEO: @@ -185,8 +188,6 @@ def plot_video( plt.show() if SAVE_RESULTS: - import pickle as pickle - filename = "flexible_swinging_pendulum.dat" with open(filename, "wb") as file: pickle.dump(recorded_history, file) diff --git a/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum_visualize_data.py b/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum_visualize_data.py index 287058160..4ec6d5fc2 100644 --- a/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum_visualize_data.py +++ b/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum_visualize_data.py @@ -1,67 +1,30 @@ import numpy as np from matplotlib import pyplot as plt import os +import pickle -# def main(): data_file_name = "flexible_swinging_pendulum.dat" if os.path.exists(data_file_name): - import pickle - with open(data_file_name, "rb") as file_handle: recorded_history = pickle.load(file_handle) -# Generate data in six separate figures and not in one subplot -NODAL_SELECTION = np.arange(0, 10 + 2, 2) -ELEMENT_SELECTION = list(range(0, 8 + 2, 2)) + [9] -VORONOI_SELECTION = range(0, 9) +# Generate data in separate figures FORCE_SELECTION = range(0, 10, 3) -# 1. Centroid positions in vertical plane +# Extract time and positions time = np.array(recorded_history["time"]) -positions = np.array(recorded_history["position"]) - -if False: - fig = plt.figure(1, figsize=(8, 5)) - ax = fig.add_subplot(111) - for node in NODAL_SELECTION: - ax.plot(time, positions[:, 2, node]) - - fig = plt.figure(2, figsize=(8, 5)) - ax = fig.add_subplot(111) - for node in NODAL_SELECTION: - ax.plot(time, positions[:, 0, node]) - - fig = plt.figure(3, figsize=(8, 5)) - ax = fig.add_subplot(111) - # (time, 3, 3, n_elem) array - directors = np.array(recorded_history["directors"]) - # Plot d1 . e1 - projected_director = np.einsum( - "ijk,j->ik", directors[:, 0, :, :], np.array([1.0, 0.0, 0.0]) - ) - for elem in ELEMENT_SELECTION: - ax.plot(time, projected_director[:, elem]) - - fig = plt.figure(4, figsize=(8, 5)) - ax = fig.add_subplot(111) - # (n_time, 3, n_elem) - internal_couple = np.array(recorded_history["internal_couple"]) - for voronoi in VORONOI_SELECTION: - ax.plot(time[1:], internal_couple[:, 1, voronoi]) - +internal_stress = np.array(recorded_history["internal_stress"]) +# Plot internal stress in x-direction fig = plt.figure(5, figsize=(8, 5)) ax = fig.add_subplot(111) -internal_stress = np.array(recorded_history["internal_stress"]) for elem in FORCE_SELECTION: ax.plot(time[1:], internal_stress[:, 0, elem]) - +# Plot internal stress in z-direction fig = plt.figure(6, figsize=(8, 5)) ax = fig.add_subplot(111) for elem in FORCE_SELECTION: ax.plot(time[1:], internal_stress[:, 2, elem]) plt.show() -# if __name__ == "__main__": -# main() diff --git a/examples/FrictionValidationCases/axial_friction.py b/examples/FrictionValidationCases/axial_friction.py index 428c346a6..6ab98d47f 100644 --- a/examples/FrictionValidationCases/axial_friction.py +++ b/examples/FrictionValidationCases/axial_friction.py @@ -1,8 +1,9 @@ -__doc__ = """Axial friction validation, for detailed explanation refer to Gazzola et. al. R. Soc. 2018 +__doc__ = """Axial friction validation, for detailed explanation refer to Gazzola et al. R. Soc. 2018 section 4.1.4 and Appendix G """ import numpy as np import elastica as ea -from examples.FrictionValidationCases.friction_validation_postprocessing import ( + +from friction_validation_postprocessing import ( plot_axial_friction_validation, ) @@ -54,7 +55,7 @@ def simulate_axial_friction_with(force=0.0): ) # TODO: CosseratRod has to be able to take shear matrix as input, we should change it as done below - shearable_rod.shear_matrix = shear_matrix + shearable_rod.shear_matrix[:] = shear_matrix axial_friction_sim.append(shearable_rod) axial_friction_sim.constrain(shearable_rod).using(ea.FreeBC) @@ -95,7 +96,9 @@ def simulate_axial_friction_with(force=0.0): dt = 1e-5 total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, axial_friction_sim, final_time, total_steps) + time = 0.0 + for i in range(total_steps): + time = timestepper.step(axial_friction_sim, time, dt) # compute translational and rotational energy translational_energy = shearable_rod.compute_translational_energy() diff --git a/examples/FrictionValidationCases/rolling_friction_initial_velocity.py b/examples/FrictionValidationCases/rolling_friction_initial_velocity.py index 80efc3e61..19ca51653 100644 --- a/examples/FrictionValidationCases/rolling_friction_initial_velocity.py +++ b/examples/FrictionValidationCases/rolling_friction_initial_velocity.py @@ -1,9 +1,10 @@ -__doc__ = """Rolling friction validation, for detailed explanation refer to Gazzola et. al. R. Soc. 2018 +__doc__ = """Rolling friction validation, for detailed explanation refer to Gazzola et al. R. Soc. 2018 section 4.1.4 and Appendix G """ import numpy as np import elastica as ea -from examples.FrictionValidationCases.friction_validation_postprocessing import ( + +from friction_validation_postprocessing import ( plot_friction_validation, ) @@ -56,7 +57,7 @@ def simulate_rolling_friction_initial_velocity_with(IFactor=0.0): ) # TODO: CosseratRod has to be able to take shear matrix as input, we should change it as done below - shearable_rod.shear_matrix = shear_matrix + shearable_rod.shear_matrix[:] = shear_matrix # change the mass moment of inertia matrix and its inverse shearable_rod.mass_second_moment_of_inertia *= IFactor shearable_rod.inv_mass_second_moment_of_inertia /= IFactor @@ -108,9 +109,9 @@ def simulate_rolling_friction_initial_velocity_with(IFactor=0.0): final_time = 2.0 total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate( - timestepper, rolling_friction_initial_velocity_sim, final_time, total_steps - ) + time = 0.0 + for i in range(total_steps): + time = timestepper.step(rolling_friction_initial_velocity_sim, time, dt) # compute translational and rotational energy translational_energy = shearable_rod.compute_translational_energy() diff --git a/examples/FrictionValidationCases/rolling_friction_on_inclined_plane.py b/examples/FrictionValidationCases/rolling_friction_on_inclined_plane.py index 6b892d772..4cca61a84 100644 --- a/examples/FrictionValidationCases/rolling_friction_on_inclined_plane.py +++ b/examples/FrictionValidationCases/rolling_friction_on_inclined_plane.py @@ -1,9 +1,9 @@ -__doc__ = """Rolling friction validation, for detailed explanation refer to Gazzola et. al. R. Soc. 2018 +__doc__ = """Rolling friction validation, for detailed explanation refer to Gazzola et al. R. Soc. 2018 section 4.1.4 and Appendix G """ import numpy as np import elastica as ea -from examples.FrictionValidationCases.friction_validation_postprocessing import ( +from friction_validation_postprocessing import ( plot_friction_validation, ) @@ -55,7 +55,7 @@ def simulate_rolling_friction_on_inclined_plane_with(alpha_s=0.0): ) # TODO: CosseratRod has to be able to take shear matrix as input, we should change it as done below - shearable_rod.shear_matrix = shear_matrix + shearable_rod.shear_matrix[:] = shear_matrix rolling_friction_on_inclined_plane_sim.append(shearable_rod) rolling_friction_on_inclined_plane_sim.constrain(shearable_rod).using(ea.FreeBC) @@ -95,9 +95,9 @@ def simulate_rolling_friction_on_inclined_plane_with(alpha_s=0.0): dt = 1e-6 total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate( - timestepper, rolling_friction_on_inclined_plane_sim, final_time, total_steps - ) + time = 0.0 + for i in range(total_steps): + time = timestepper.step(rolling_friction_on_inclined_plane_sim, time, dt) # compute translational and rotational energy translational_energy = shearable_rod.compute_translational_energy() diff --git a/examples/FrictionValidationCases/rolling_friction_torque.py b/examples/FrictionValidationCases/rolling_friction_torque.py index 6258b0968..b25d88dd5 100644 --- a/examples/FrictionValidationCases/rolling_friction_torque.py +++ b/examples/FrictionValidationCases/rolling_friction_torque.py @@ -1,9 +1,9 @@ -__doc__ = """Rolling friction validation, for detailed explanation refer to Gazzola et. al. R. Soc. 2018 +__doc__ = """Rolling friction validation, for detailed explanation refer to Gazzola et al. R. Soc. 2018 section 4.1.4 and Appendix G """ import numpy as np import elastica as ea -from examples.FrictionValidationCases.friction_validation_postprocessing import ( +from friction_validation_postprocessing import ( plot_friction_validation, ) @@ -55,7 +55,7 @@ def simulate_rolling_friction_torque_with(C_s=0.0): ) # TODO: CosseratRod has to be able to take shear matrix as input, we should change it as done below - shearable_rod.shear_matrix = shear_matrix + shearable_rod.shear_matrix[:] = shear_matrix rolling_friction_torque_sim.append(shearable_rod) rolling_friction_torque_sim.constrain(shearable_rod).using(ea.FreeBC) @@ -99,7 +99,9 @@ def simulate_rolling_friction_torque_with(C_s=0.0): dt = 1e-6 total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, rolling_friction_torque_sim, final_time, total_steps) + time = 0.0 + for i in range(total_steps): + time = timestepper.step(rolling_friction_torque_sim, time, dt) # compute translational and rotational energy translational_energy = shearable_rod.compute_translational_energy() diff --git a/examples/HelicalBucklingCase/convergence_functions.py b/examples/HelicalBucklingCase/convergence_functions.py new file mode 100644 index 000000000..ca81417f4 --- /dev/null +++ b/examples/HelicalBucklingCase/convergence_functions.py @@ -0,0 +1,61 @@ +import numpy as np +from matplotlib import pyplot as plt +from matplotlib.colors import to_rgb +from scipy.linalg import norm + + +def calculate_error_norm(true_solution, computed_solution, n_elem): + assert ( + true_solution.shape == computed_solution.shape + ), "Shape of computed and true solution does not match" + error = true_solution - computed_solution + l1 = norm(error, 1) / n_elem + l2 = norm(error, 2) / n_elem + linf = norm(error, np.inf) + + return error, l1, l2, linf + + +def plot_convergence(results, SAVE_FIGURE, filename): + convergence_elements = [] + l1 = [] + l2 = [] + linf = [] + + for result in results: + convergence_elements.append(result["rod"].n_elems) + l1.append(result["l1"]) + l2.append(result["l2"]) + linf.append(result["linf"]) + + fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150) + ax = fig.add_subplot(111) + ax.grid(which="minor", color="k", linestyle="--") + ax.grid(which="major", color="k", linestyle="-") + ax.loglog( + convergence_elements, + l1, + marker="o", + ms=10, + c=to_rgb("xkcd:bluish"), + lw=2, + label="l1", + ) + ax.loglog( + convergence_elements, + l2, + marker="o", + ms=10, + c=to_rgb("xkcd:reddish"), + lw=2, + label="l2", + ) + ax.loglog(convergence_elements, linf, marker="o", ms=10, c="k", lw=2, label="linf") + ax.set_xlabel("N_element") + ax.set_ylabel("Error") + ax.set_title("Error Convergence Analysis") + fig.legend(prop={"size": 20}) + if SAVE_FIGURE: + assert filename != "", "provide a file name for figure" + fig.savefig(filename) + fig.show() diff --git a/examples/HelicalBucklingCase/convergence_helicalbuckling.py b/examples/HelicalBucklingCase/convergence_helicalbuckling.py index c69b22599..a551e3bad 100644 --- a/examples/HelicalBucklingCase/convergence_helicalbuckling.py +++ b/examples/HelicalBucklingCase/convergence_helicalbuckling.py @@ -1,14 +1,15 @@ -__doc__ = """Helical buckling convergence study, for detailed explanation refer to Gazzola et. al. R. Soc. 2018 +__doc__ = """Helical buckling convergence study, for detailed explanation refer to Gazzola et al. R. Soc. 2018 section 3.4.1 """ import numpy as np import elastica as ea -from examples.HelicalBucklingCase.helicalbuckling_postprocessing import ( + +from helicalbuckling_postprocessing import ( analytical_solution, envelope, plot_helicalbuckling, ) -from examples.convergence_functions import plot_convergence, calculate_error_norm +from convergence_functions import calculate_error_norm, plot_convergence class HelicalBucklingSimulator( @@ -23,7 +24,7 @@ class HelicalBucklingSimulator( SAVE_RESULTS = False -def simulate_helicalbucklin_beam_with( +def simulate_helicalbuckling_beam_with( elements=10, SAVE_FIGURE=False, PLOT_FIGURE=False ): helicalbuckling_sim = HelicalBucklingSimulator() @@ -41,9 +42,12 @@ def simulate_helicalbucklin_beam_with( E = 1e6 slack = 3 number_of_rotations = 27 - # For shear modulus of 1e4, nu is 99! - poisson_ratio = 99 - shear_matrix = np.repeat(1e5 * np.identity((3))[:, :, np.newaxis], n_elem, axis=2) + # For shear modulus of 1e5, poisson_ratio should be 9 + poisson_ratio = 9 + shear_modulus = E / (poisson_ratio + 1.0) + shear_matrix = np.repeat( + shear_modulus * np.identity((3))[:, :, np.newaxis], n_elem, axis=2 + ) temp_bend_matrix = np.zeros((3, 3)) np.fill_diagonal(temp_bend_matrix, [1.345, 1.345, 0.789]) bend_matrix = np.repeat(temp_bend_matrix[:, :, np.newaxis], n_elem - 1, axis=2) @@ -57,6 +61,7 @@ def simulate_helicalbucklin_beam_with( base_radius, density, youngs_modulus=E, + shear_modulus=shear_modulus, ) # TODO: CosseratRod has to be able to take shear matrix as input, we should change it as done below @@ -92,7 +97,9 @@ def simulate_helicalbucklin_beam_with( final_time = 10500 total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, helicalbuckling_sim, final_time, total_steps) + time = 0.0 + for i in range(total_steps): + time = timestepper.step(helicalbuckling_sim, time, dt) # calculate errors and norms # Since we need to evaluate analytical solution only on nodes, n_nodes = n_elems+1 @@ -117,7 +124,7 @@ def simulate_helicalbucklin_beam_with( # Convergence study # for n_elem in [5, 6, 7, 8, 9, 10] with mp.Pool(mp.cpu_count()) as pool: - results = pool.map(simulate_helicalbucklin_beam_with, convergence_elements) + results = pool.map(simulate_helicalbuckling_beam_with, convergence_elements) if PLOT_FIGURE: filename = "HelicalBuckling_convergence_test.png" diff --git a/examples/HelicalBucklingCase/helicalbuckling.py b/examples/HelicalBucklingCase/helicalbuckling.py index 39e087ea7..2da6b0e67 100644 --- a/examples/HelicalBucklingCase/helicalbuckling.py +++ b/examples/HelicalBucklingCase/helicalbuckling.py @@ -1,9 +1,9 @@ __doc__ = """Helical buckling validation case, for detailed explanation refer to -Gazzola et. al. R. Soc. 2018 section 3.4.1 """ +Gazzola et al. R. Soc. 2018 section 3.4.1 """ import numpy as np import elastica as ea -from examples.HelicalBucklingCase.helicalbuckling_postprocessing import ( +from helicalbuckling_postprocessing import ( plot_helicalbuckling, ) @@ -34,7 +34,7 @@ class HelicalBucklingSimulator( E = 1e6 slack = 3 number_of_rotations = 27 -# For shear modulus of 1e5, nu is 99! +# For shear modulus of 1e5, poisson_ratio should be 9 poisson_ratio = 9 shear_modulus = E / (poisson_ratio + 1.0) shear_matrix = np.repeat( @@ -57,8 +57,8 @@ class HelicalBucklingSimulator( ) # TODO: CosseratRod has to be able to take shear matrix as input, we should change it as done below -shearable_rod.shear_matrix = shear_matrix -shearable_rod.bend_matrix = bend_matrix +shearable_rod.shear_matrix[:] = shear_matrix +shearable_rod.bend_matrix[:] = bend_matrix helicalbuckling_sim.append(shearable_rod) @@ -88,7 +88,9 @@ class HelicalBucklingSimulator( final_time = 10500.0 total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, helicalbuckling_sim, final_time, total_steps) +time = 0.0 +for i in range(total_steps): + time = timestepper.step(helicalbuckling_sim, time, dt) if PLOT_FIGURE: plot_helicalbuckling(shearable_rod, SAVE_FIGURE) diff --git a/examples/HelicalBucklingCase/helicalbuckling_postprocessing.py b/examples/HelicalBucklingCase/helicalbuckling_postprocessing.py index c0645135e..39eab4475 100644 --- a/examples/HelicalBucklingCase/helicalbuckling_postprocessing.py +++ b/examples/HelicalBucklingCase/helicalbuckling_postprocessing.py @@ -45,7 +45,7 @@ def analytical_solution(L, n_elem=10000): # nu = 1.0 / gamma - 1.0 # These are magic constants, but you can obtain them by solving - # this equation (accoring to matlab syntax) + # this equation (according to matlab syntax) # syms x y # S = vpasolve([d == sqrt(16/y*(1-x*x/(4*y))), R == x/gamma+4*acos(x/(2*sqrt(y)))], [x, y]); # moment = double(S.x); # dimensionless end moment diff --git a/examples/JointCases/fixed_joint.py b/examples/JointCases/fixed_joint.py index 6c2aaff20..013db64c4 100644 --- a/examples/JointCases/fixed_joint.py +++ b/examples/JointCases/fixed_joint.py @@ -1,8 +1,9 @@ -__doc__ = """Fixed joint example, for detailed explanation refer to Zhang et. al. Nature Comm. methods section.""" +__doc__ = """Fixed joint example, for detailed explanation refer to Zhang et al. Nature Comm. methods section.""" import numpy as np +from collections import defaultdict import elastica as ea -from examples.JointCases.joint_cases_postprocessing import ( +from joint_cases_postprocessing import ( plot_position, plot_video, plot_video_xy, @@ -27,10 +28,9 @@ class FixedJointSimulator( n_elem = 10 direction = np.array([0.0, 0.0, 1.0]) normal = np.array([0.0, 1.0, 0.0]) -roll_direction = np.cross(direction, normal) + base_length = 0.2 base_radius = 0.007 -base_area = np.pi * base_radius**2 density = 1750 E = 3e7 poisson_ratio = 0.5 @@ -100,14 +100,33 @@ class FixedJointSimulator( time_step=dt, ) -pp_list_rod1 = ea.defaultdict(list) -pp_list_rod2 = ea.defaultdict(list) + +class JointCasesCallBack(ea.CallBackBaseClass): + """ + Callback function for joint cases. + """ + + def __init__(self, step_skip: int, callback_params: dict): + super().__init__() + self.every = step_skip + self.callback_params = callback_params + + def make_callback(self, system, time, current_step: int): + if current_step % self.every == 0: + self.callback_params["time"].append(time) + self.callback_params["position"].append(system.position_collection.copy()) + self.callback_params["directors"].append(system.director_collection.copy()) + return + + +pp_list_rod1 = defaultdict(list) +pp_list_rod2 = defaultdict(list) fixed_joint_sim.collect_diagnostics(rod1).using( - ea.MyCallBack, step_skip=1000, callback_params=pp_list_rod1 + JointCasesCallBack, step_skip=1000, callback_params=pp_list_rod1 ) fixed_joint_sim.collect_diagnostics(rod2).using( - ea.MyCallBack, step_skip=1000, callback_params=pp_list_rod2 + JointCasesCallBack, step_skip=1000, callback_params=pp_list_rod2 ) fixed_joint_sim.finalize() @@ -115,10 +134,11 @@ class FixedJointSimulator( # timestepper = PEFRL() final_time = 10 -dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, fixed_joint_sim, final_time, total_steps) +time = 0.0 +for i in range(total_steps): + time = timestepper.step(fixed_joint_sim, time, dt) PLOT_FIGURE = True SAVE_FIGURE = False diff --git a/examples/JointCases/fixed_joint_torsion.py b/examples/JointCases/fixed_joint_torsion.py index 4aa734d76..4181cea2e 100644 --- a/examples/JointCases/fixed_joint_torsion.py +++ b/examples/JointCases/fixed_joint_torsion.py @@ -1,9 +1,10 @@ -__doc__ = """Fixed joint example, for detailed explanation refer to Zhang et. al. Nature Comm. methods section.""" +__doc__ = """Fixed joint example, for detailed explanation refer to Zhang et al. Nature Comm. methods section.""" import numpy as np +from collections import defaultdict import elastica as ea -from elastica.joint import get_relative_rotation_two_systems -from examples.JointCases.joint_cases_postprocessing import ( +from elastica._rotations import get_relative_rotation_two_systems +from joint_cases_postprocessing import ( plot_position, plot_orientation, plot_video, @@ -33,7 +34,6 @@ class FixedJointSimulator( normal_rod2 = np.array([0.0, 0.0, 1.0]) base_length = 0.2 base_radius = 0.007 -base_area = np.pi * base_radius**2 density = 1750 E = 3e7 poisson_ratio = 0.5 @@ -106,25 +106,44 @@ class FixedJointSimulator( ) -pp_list_rod1 = ea.defaultdict(list) -pp_list_rod2 = ea.defaultdict(list) +class JointCasesCallBack(ea.CallBackBaseClass): + """ + Callback function for joint cases. + """ + + def __init__(self, step_skip: int, callback_params: dict): + super().__init__() + self.every = step_skip + self.callback_params = callback_params + + def make_callback(self, system, time, current_step: int): + if current_step % self.every == 0: + self.callback_params["time"].append(time) + self.callback_params["position"].append(system.position_collection.copy()) + self.callback_params["directors"].append(system.director_collection.copy()) + return + + +pp_list_rod1 = defaultdict(list) +pp_list_rod2 = defaultdict(list) fixed_joint_sim.collect_diagnostics(rod1).using( - ea.MyCallBack, step_skip=1000, callback_params=pp_list_rod1 + JointCasesCallBack, step_skip=1000, callback_params=pp_list_rod1 ) fixed_joint_sim.collect_diagnostics(rod2).using( - ea.MyCallBack, step_skip=1000, callback_params=pp_list_rod2 + JointCasesCallBack, step_skip=1000, callback_params=pp_list_rod2 ) fixed_joint_sim.finalize() timestepper = ea.PositionVerlet() final_time = 10 -dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, fixed_joint_sim, final_time, total_steps) +time = 0.0 +for i in range(total_steps): + time = timestepper.step(fixed_joint_sim, time, dt) plot_orientation( diff --git a/examples/JointCases/hinge_joint.py b/examples/JointCases/hinge_joint.py index 4aaa95195..50835b617 100644 --- a/examples/JointCases/hinge_joint.py +++ b/examples/JointCases/hinge_joint.py @@ -1,8 +1,9 @@ -__doc__ = """Hinge joint example, for detailed explanation refer to Zhang et. al. Nature Comm. methods section.""" +__doc__ = """Hinge joint example, for detailed explanation refer to Zhang et al. Nature Comm. methods section.""" import numpy as np +from collections import defaultdict import elastica as ea -from examples.JointCases.joint_cases_postprocessing import ( +from joint_cases_postprocessing import ( plot_position, plot_video, plot_video_xy, @@ -30,7 +31,6 @@ class HingeJointSimulator( roll_direction = np.cross(direction, normal) base_length = 0.2 base_radius = 0.007 -base_area = np.pi * base_radius**2 density = 1750 E = 3e7 poisson_ratio = 0.5 @@ -102,14 +102,33 @@ class HingeJointSimulator( time_step=dt, ) -pp_list_rod1 = ea.defaultdict(list) -pp_list_rod2 = ea.defaultdict(list) + +class JointCasesCallBack(ea.CallBackBaseClass): + """ + Callback function for joint cases. + """ + + def __init__(self, step_skip: int, callback_params: dict): + super().__init__() + self.every = step_skip + self.callback_params = callback_params + + def make_callback(self, system, time, current_step: int): + if current_step % self.every == 0: + self.callback_params["time"].append(time) + self.callback_params["position"].append(system.position_collection.copy()) + self.callback_params["directors"].append(system.director_collection.copy()) + return + + +pp_list_rod1 = defaultdict(list) +pp_list_rod2 = defaultdict(list) hinge_joint_sim.collect_diagnostics(rod1).using( - ea.MyCallBack, step_skip=1000, callback_params=pp_list_rod1 + JointCasesCallBack, step_skip=1000, callback_params=pp_list_rod1 ) hinge_joint_sim.collect_diagnostics(rod2).using( - ea.MyCallBack, step_skip=1000, callback_params=pp_list_rod2 + JointCasesCallBack, step_skip=1000, callback_params=pp_list_rod2 ) hinge_joint_sim.finalize() @@ -117,10 +136,11 @@ class HingeJointSimulator( # timestepper = PEFRL() final_time = 10 -dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, hinge_joint_sim, final_time, total_steps) +time = 0.0 +for i in range(total_steps): + time = timestepper.step(hinge_joint_sim, time, dt) PLOT_FIGURE = True SAVE_FIGURE = False diff --git a/examples/JointCases/joint_cases_postprocessing.py b/examples/JointCases/joint_cases_postprocessing.py index 8cf15bf07..9005fbf13 100644 --- a/examples/JointCases/joint_cases_postprocessing.py +++ b/examples/JointCases/joint_cases_postprocessing.py @@ -73,22 +73,22 @@ def plot_video( writer = FFMpegWriter(fps=fps, metadata=metadata) fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150) with writer.saving(fig, video_name, 100): - for time in range(1, len(time)): + for time_idx in range(1, len(time)): fig.clf() ax = plt.axes(projection="3d") # fig.add_subplot(111) ax.grid(which="minor", color="k", linestyle="--") ax.grid(which="major", color="k", linestyle="-") ax.plot( - position_of_rod1[time, 0], - position_of_rod1[time, 1], - position_of_rod1[time, 2], + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 1], + position_of_rod1[time_idx, 2], "or", label="rod1", ) ax.plot( - position_of_rod2[time, 0], - position_of_rod2[time, 1], - position_of_rod2[time, 2], + position_of_rod2[time_idx, 0], + position_of_rod2[time_idx, 1], + position_of_rod2[time_idx, 2], "o", c=to_rgb("xkcd:bluish"), label="rod2", @@ -120,14 +120,17 @@ def plot_video_xy( fig = plt.figure() plt.axis("equal") with writer.saving(fig, video_name, 100): - for time in range(1, len(time)): + for time_idx in range(1, len(time)): fig.clf() plt.plot( - position_of_rod1[time, 0], position_of_rod1[time, 1], "or", label="rod1" + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 1], + "or", + label="rod1", ) plt.plot( - position_of_rod2[time, 0], - position_of_rod2[time, 1], + position_of_rod2[time_idx, 0], + position_of_rod2[time_idx, 1], "o", c=to_rgb("xkcd:bluish"), label="rod2", @@ -158,14 +161,17 @@ def plot_video_xz( fig = plt.figure() plt.axis("equal") with writer.saving(fig, video_name, 100): - for time in range(1, len(time)): + for time_idx in range(1, len(time)): fig.clf() plt.plot( - position_of_rod1[time, 0], position_of_rod1[time, 2], "or", label="rod1" + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 2], + "or", + label="rod1", ) plt.plot( - position_of_rod2[time, 0], - position_of_rod2[time, 2], + position_of_rod2[time_idx, 0], + position_of_rod2[time_idx, 2], "o", c=to_rgb("xkcd:bluish"), label="rod2", diff --git a/examples/JointCases/spherical_joint.py b/examples/JointCases/spherical_joint.py index 6d2492dfb..af914f2e6 100644 --- a/examples/JointCases/spherical_joint.py +++ b/examples/JointCases/spherical_joint.py @@ -1,9 +1,10 @@ -__doc__ = """Spherical(Free) joint example, for detailed explanation refer to Zhang et. al. Nature Comm. +__doc__ = """Spherical(Free) joint example, for detailed explanation refer to Zhang et al. Nature Comm. methods section.""" import numpy as np +from collections import defaultdict import elastica as ea -from examples.JointCases.joint_cases_postprocessing import ( +from joint_cases_postprocessing import ( plot_position, plot_video, plot_video_xy, @@ -28,10 +29,9 @@ class SphericalJointSimulator( n_elem = 10 direction = np.array([0.0, 0.0, 1.0]) normal = np.array([0.0, 1.0, 0.0]) -roll_direction = np.cross(direction, normal) + base_length = 0.2 base_radius = 0.007 -base_area = np.pi * base_radius**2 density = 1750 E = 3e7 poisson_ratio = 0.5 @@ -103,14 +103,33 @@ class SphericalJointSimulator( time_step=dt, ) -pp_list_rod1 = ea.defaultdict(list) -pp_list_rod2 = ea.defaultdict(list) + +class JointCasesCallBack(ea.CallBackBaseClass): + """ + Callback function for joint cases. + """ + + def __init__(self, step_skip: int, callback_params: dict): + super().__init__() + self.every = step_skip + self.callback_params = callback_params + + def make_callback(self, system, time, current_step: int): + if current_step % self.every == 0: + self.callback_params["time"].append(time) + self.callback_params["position"].append(system.position_collection.copy()) + self.callback_params["directors"].append(system.director_collection.copy()) + return + + +pp_list_rod1 = defaultdict(list) +pp_list_rod2 = defaultdict(list) spherical_joint_sim.collect_diagnostics(rod1).using( - ea.MyCallBack, step_skip=1000, callback_params=pp_list_rod1 + JointCasesCallBack, step_skip=1000, callback_params=pp_list_rod1 ) spherical_joint_sim.collect_diagnostics(rod2).using( - ea.MyCallBack, step_skip=1000, callback_params=pp_list_rod2 + JointCasesCallBack, step_skip=1000, callback_params=pp_list_rod2 ) spherical_joint_sim.finalize() @@ -118,10 +137,11 @@ class SphericalJointSimulator( # timestepper = PEFRL() final_time = 10 -dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, spherical_joint_sim, final_time, total_steps) +time = 0.0 +for i in range(total_steps): + time = timestepper.step(spherical_joint_sim, time, dt) PLOT_FIGURE = True SAVE_FIGURE = False diff --git a/examples/KnotCase/README.md b/examples/KnotCase/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/examples/KnotCase/knot_simulation.py b/examples/KnotCase/knot_simulation.py deleted file mode 100644 index 664c5e5d1..000000000 --- a/examples/KnotCase/knot_simulation.py +++ /dev/null @@ -1,205 +0,0 @@ -__doc__ = """Simulating overhand-knot, a degenerated case of Trefoil knot. -A demonstration includes how to create an arbitrary controller for a node in a rod, -resembling a proportional-controller of SO3 Pose. The same class can be used further -to mimic the MPC control or trajectory-tracing.""" - -from typing import Any, TypeAlias -from numpy.typing import NDArray -from elastica.typing import RodType - -import numpy as np -import matplotlib.pyplot as plt -from collections import defaultdict -import elastica as ea - -from knot_forcing import TargetPoseProportionalControl -from knot_visualization import plot_video3D - -Position: TypeAlias = NDArray[np.float64] # vector (3) -Orientation: TypeAlias = NDArray[np.float64] # SO3 matrix (3, 3) -Pose: TypeAlias = tuple[Position, Orientation] - - -class SoftRodSimulator( - ea.BaseSystemCollection, - ea.Constraints, - ea.Forcing, - ea.Damping, - ea.CallBacks, - ea.Contact, -): - pass - - -class AxialStretchingCallBack(ea.CallBackBaseClass): - """ - Records the position of the rod - """ - - def __init__(self, callback_params: dict) -> None: - ea.CallBackBaseClass.__init__(self) - self.every = 200 - self.callback_params = callback_params - - def make_callback(self, system: RodType, time: float, current_step: int) -> None: - if current_step % self.every == 0: - - self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) - self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["position"].append(system.position_collection.copy()) - self.callback_params["orientation"].append( - system.director_collection.copy() - ) - return - - -if __name__ == "__main__": - # Options - GENERATE_2D_VIDEO = False - GENERATE_3D_VIDEO = True - - simulator = SoftRodSimulator() - recorded_history: dict[str, list[Any]] = defaultdict(list) - final_time = 5 - dt = 0.0002 - - # setting up test params - n_elem = 50 - start = np.zeros((3,)) - direction = np.array([1.0, 0.0, 0.0]) - normal = np.array([0.0, 1.0, 0.0]) - base_length = 1.2 - base_radius = 0.025 - density = 2000 - youngs_modulus = 1e6 - poisson_ratio = 0.5 - shear_modulus = youngs_modulus / (2 * (poisson_ratio + 1.0)) - - stretchable_rod = ea.CosseratRod.straight_rod( - n_elem, - start, - direction, - normal, - base_length, - base_radius, - density, - youngs_modulus=youngs_modulus, - shear_modulus=shear_modulus, - ) - simulator.append(stretchable_rod) - - run_time = 4 - - def base_target(t: float, rod: RodType) -> Pose: - target_position = direction * base_length - 5 * base_radius * normal - if t <= run_time / 2: - ratio = min(2 * t / run_time, 1.0) - angular_ratio = ratio * np.pi * 2 - position = target_position * ratio - orientation_twist = np.array( - [ - [0, np.cos(angular_ratio), np.sin(angular_ratio)], - [0, -np.sin(angular_ratio), np.cos(angular_ratio)], - [1, 0, 0], - ], - dtype=float, - ) - else: - ratio = min(2 * (t - run_time / 2) / run_time, 1.0) - R = 8 - position = np.array( - [ - target_position[0] * (1 - ratio), - -R * base_radius * np.cos(2 * ratio * 12) * (1 - ratio), - -R * base_radius * np.sin(2 * ratio * 12) * (1 - ratio), - ] - ) - angular_ratio = (1 - ratio) * np.pi * 2 - orientation_twist = np.array( - [ - [0, np.cos(angular_ratio), -np.sin(angular_ratio)], - [0, np.sin(angular_ratio), np.cos(angular_ratio)], - [1, 0, 0], - ], - dtype=float, - ) - return position, orientation_twist - - # Control point - p = 3e3 - pt = 5e0 - simulator.add_forcing_to(stretchable_rod).using( - TargetPoseProportionalControl, - elem_index=0, - p_linear_value=p, - p_angular_value=pt, - target=base_target, - ramp_up_time=1e-6, - target_history=recorded_history["base_pose"], - ) - - # Boundary conditions - simulator.constrain(stretchable_rod).using( - ea.FixedConstraint, constrained_position_idx=(-1, -20) - ) - - # Self contact - simulator.detect_contact_between(stretchable_rod, stretchable_rod).using( - ea.RodSelfContact, k=1e4, nu=3 - ) - - # Gravity - simulator.add_forcing_to(stretchable_rod).using( - ea.GravityForces, acc_gravity=np.array([0.0, 0.0, -9.80665]) - ) - - # Damping - damping_constant = 5.0 - simulator.dampen(stretchable_rod).using( - ea.AnalyticalLinearDamper, - translational_damping_constant=damping_constant, - rotational_damping_constant=damping_constant * 0.01, - time_step=dt, - ) - simulator.dampen(stretchable_rod).using(ea.LaplaceDissipationFilter, filter_order=5) - - simulator.collect_diagnostics(stretchable_rod).using( - AxialStretchingCallBack, callback_params=recorded_history - ) - - # Finalize and run the simulation - simulator.finalize() - timestepper = ea.PositionVerlet() - total_steps = int(final_time / dt) - print("Total steps", total_steps) - ea.integrate(timestepper, simulator, final_time, total_steps) - - if GENERATE_3D_VIDEO: - filename_video = "knot3D.mp4" - plot_video3D(recorded_history, video_name=filename_video, margin=0.2, fps=10) - - # Plot knot topological quantities - time = np.asarray(recorded_history["time"]) - positions = np.asarray(recorded_history["position"]) - orientations = np.asarray(recorded_history["orientation"]) - radii = np.asarray(recorded_history["radius"]) - total_twist, _ = ea.compute_twist(positions, orientations[:, 0, ...]) - total_writhe = ea.compute_writhe(positions, np.float64(base_length), "next_tangent") - total_link = ea.compute_link( - positions, - orientations[:, 0, ...], - radii, - np.float64(base_length), - "next_tangent", - ) - - plt.figure() - plt.plot(time, total_twist, label="twist") - plt.plot(time, total_writhe, label="writhe") - plt.plot(time, total_link, label="link") - plt.legend() - plt.xlabel("time") - plt.ylabel("link-writhe-twist quantity") - plt.savefig("LWT.png", dpi=300) - plt.close("all") diff --git a/examples/KnotCase/knot_visualization.py b/examples/KnotCase/knot_visualization.py index 05c6c42fd..17a92f615 100644 --- a/examples/KnotCase/knot_visualization.py +++ b/examples/KnotCase/knot_visualization.py @@ -61,39 +61,39 @@ def plot_video3D( length=quiver_length * 0.8, ) with writer.saving(fig, video_name, dpi=100): - for time in range(1, len(t) - 1): - rod_lines_3d.set_xdata(positions_over_time[time][0]) - rod_lines_3d.set_ydata(positions_over_time[time][1]) - rod_lines_3d.set_3d_properties(positions_over_time[time][2]) # type: ignore + for time_idx in range(1, len(t) - 1): + rod_lines_3d.set_xdata(positions_over_time[time_idx][0]) + rod_lines_3d.set_ydata(positions_over_time[time_idx][1]) + rod_lines_3d.set_3d_properties(positions_over_time[time_idx][2]) # type: ignore targets_orientation_normal.remove() targets_orientation_normal = ax.quiver( - *base_position[time], - *base_orientation[time][0], + *base_position[time_idx], + *base_orientation[time_idx][0], color="r", length=quiver_length, ) targets_orientation_binormal.remove() targets_orientation_binormal = ax.quiver( - *base_position[time], - *base_orientation[time][1], + *base_position[time_idx], + *base_orientation[time_idx][1], color="g", length=quiver_length, ) targets_orientation_tangent.remove() targets_orientation_tangent = ax.quiver( - *base_position[time], - *base_orientation[time][2], + *base_position[time_idx], + *base_orientation[time_idx][2], color="b", length=quiver_length, ) normal.remove() normal = ax.quiver( - *elem_positions[time], - *directors_over_time[time][1], + *elem_positions[time_idx], + *directors_over_time[time_idx][1], color="k", alpha=0.5, length=quiver_length * 0.8, diff --git a/examples/KnotCase/run_knot_simulation.py b/examples/KnotCase/run_knot_simulation.py new file mode 100644 index 000000000..01ede4c0a --- /dev/null +++ b/examples/KnotCase/run_knot_simulation.py @@ -0,0 +1,280 @@ +""" +Knot Simulation +=============== + +This script simulates the formation of an overhand knot in a soft rod. +It demonstrates how to create a controller to manipulate a node on the rod, +which can be used for tasks like trajectory tracing or proportional control. +""" + +from typing import Any, TypeAlias +from numpy.typing import NDArray +from elastica.typing import RodType + +import numpy as np +import matplotlib.pyplot as plt +from collections import defaultdict +import elastica as ea + +from knot_forcing import TargetPoseProportionalControl +from knot_visualization import plot_video3D + +Position: TypeAlias = NDArray[np.float64] # vector (3) +Orientation: TypeAlias = NDArray[np.float64] # SO3 matrix (3, 3) +Pose: TypeAlias = tuple[Position, Orientation] + +# %% +# Simulation Setup +# ---------------- +# We define a simulator class that inherits from the necessary mixins. + + +class SoftRodSimulator( + ea.BaseSystemCollection, + ea.Constraints, + ea.Forcing, + ea.Damping, + ea.CallBacks, + ea.Contact, +): + pass + + +simulator = SoftRodSimulator() +final_time = 5 +dt = 0.0002 + + +# %% +# Callback Setup +# -------------- +# We also define a callback class to record the position of the rod during the +# simulation. + + +class Callback(ea.CallBackBaseClass): + """ + Records the position of the rod + """ + + def __init__(self, callback_params: dict) -> None: + super().__init__() + self.every = 200 + self.callback_params = callback_params + + def make_callback(self, system: RodType, time: float, current_step: int) -> None: + if current_step % self.every == 0: + + self.callback_params["time"].append(time) + self.callback_params["step"].append(current_step) + self.callback_params["radius"].append(system.radius.copy()) + self.callback_params["position"].append(system.position_collection.copy()) + self.callback_params["orientation"].append( + system.director_collection.copy() + ) + return + + +recorded_history: dict[str, list[Any]] = defaultdict(list) + +# %% +# Rod Setup +# --------- +# Next, we set up the parameters for the rod. + +# setting up test params +n_elem = 50 +start = np.zeros((3,)) +direction = np.array([1.0, 0.0, 0.0]) +normal = np.array([0.0, 1.0, 0.0]) +base_length = 1.2 +base_radius = 0.025 +density = 2000 +youngs_modulus = 1e6 +poisson_ratio = 0.5 +shear_modulus = youngs_modulus / (2 * (poisson_ratio + 1.0)) + +# We create the `CosseratRod` object and add it to the simulator. +stretchable_rod = ea.CosseratRod.straight_rod( + n_elem, + start, + direction, + normal, + base_length, + base_radius, + density, + youngs_modulus=youngs_modulus, + shear_modulus=shear_modulus, +) +simulator.append(stretchable_rod) + +simulator.collect_diagnostics(stretchable_rod).using( + Callback, callback_params=recorded_history +) + +# %% +# Controller Setup +# ---------------- +# We define a function that returns the target pose (position and +# orientation) for the controller at a given time. This function creates +# the trajectory for the end of the rod to follow to tie the knot. + +activation_time = 4 + + +def base_target(t: float, rod: RodType) -> Pose: + target_position = direction * base_length - 5 * base_radius * normal + if t <= activation_time / 2: + ratio = min(2 * t / activation_time, 1.0) + angular_ratio = ratio * np.pi * 2 + position = target_position * ratio + orientation_twist = np.array( + [ + [0, np.cos(angular_ratio), np.sin(angular_ratio)], + [0, -np.sin(angular_ratio), np.cos(angular_ratio)], + [1, 0, 0], + ], + dtype=float, + ) + else: + ratio = min(2 * (t - activation_time / 2) / activation_time, 1.0) + R = 8 + position = np.array( + [ + target_position[0] * (1 - ratio), + -R * base_radius * np.cos(2 * ratio * 12) * (1 - ratio), + -R * base_radius * np.sin(2 * ratio * 12) * (1 - ratio), + ] + ) + angular_ratio = (1 - ratio) * np.pi * 2 + orientation_twist = np.array( + [ + [0, np.cos(angular_ratio), -np.sin(angular_ratio)], + [0, np.sin(angular_ratio), np.cos(angular_ratio)], + [1, 0, 0], + ], + dtype=float, + ) + return position, orientation_twist + + +# %% +# We add a `TargetPoseProportionalControl` forcing to the rod. This +# controller applies forces and torques to drive a specific node of the +# rod to the target pose. The class is defined in `knot_forcing.py`. + +# Control point +p = 3e3 +pt = 5e0 +simulator.add_forcing_to(stretchable_rod).using( + TargetPoseProportionalControl, + elem_index=0, + p_linear_value=p, + p_angular_value=pt, + target=base_target, + ramp_up_time=1e-6, + target_history=recorded_history["base_pose"], +) + +# %% +# Boundary Conditions +# ------------------- +# We apply boundary conditions to fix the other end of the rod. + +# Boundary conditions +simulator.constrain(stretchable_rod).using( + ea.FixedConstraint, constrained_position_idx=(-1, -20) +) + +# %% +# Contact Setup +# ------------- +# We enable self-contact detection for the rod to prevent it from passing +# through itself. + +# Self contact +simulator.detect_contact_between(stretchable_rod, stretchable_rod).using( + ea.RodSelfContact, k=1e4, nu=3 +) + +# %% +# Environmental Forcing and Damping +# --------------------------------- +# We add gravity and damping to the system. + +# Gravity +simulator.add_forcing_to(stretchable_rod).using( + ea.GravityForces, acc_gravity=np.array([0.0, 0.0, -9.80665]) +) + +# Damping +damping_constant = 5.0 +simulator.dampen(stretchable_rod).using( + ea.AnalyticalLinearDamper, + translational_damping_constant=damping_constant, + rotational_damping_constant=damping_constant * 0.01, + time_step=dt, +) +simulator.dampen(stretchable_rod).using(ea.LaplaceDissipationFilter, filter_order=5) + + +# %% +# Finalize and Run +# ---------------- +# We finalize the simulator and create the time-stepper. + +# Finalize and run the simulation +simulator.finalize() +timestepper = ea.PositionVerlet() + +total_steps = int(final_time / dt) +print("Total steps", total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(simulator, time, dt) + +# %% +# Post-Processing +# --------------- +# After the simulation, we can generate a 3D video of the knot tying +# process. + +filename_video = "knot3D.mp4" +plot_video3D(recorded_history, video_name=filename_video, margin=0.2, fps=10) + +# %% +# .. video:: ../../../examples/KnotCase/knot3D.mp4 +# :width: 720 +# :autoplay: +# :muted: +# :loop: + + +# %% +# We can also plot the topological quantities of the knot, such as twist, +# writhe, and link, as a function of time. + +# Plot knot topological quantities +timestep = np.asarray(recorded_history["time"]) +positions = np.asarray(recorded_history["position"]) +orientations = np.asarray(recorded_history["orientation"]) +radii = np.asarray(recorded_history["radius"]) +total_twist, _ = ea.compute_twist(positions, orientations[:, 0, ...]) +total_writhe = ea.compute_writhe(positions, np.float64(base_length), "next_tangent") +total_link = ea.compute_link( + positions, + orientations[:, 0, ...], + radii, + np.float64(base_length), + "next_tangent", +) + +plt.figure() +plt.plot(timestep, total_twist, label="twist") +plt.plot(timestep, total_writhe, label="writhe") +plt.plot(timestep, total_link, label="link") +plt.legend() +plt.xlabel("time") +plt.ylabel("link-writhe-twist quantity") +plt.show() diff --git a/examples/MuscularFlagella/connection_flagella.py b/examples/MuscularFlagella/connection_flagella.py index 52ec94155..c059f55fc 100644 --- a/examples/MuscularFlagella/connection_flagella.py +++ b/examples/MuscularFlagella/connection_flagella.py @@ -2,11 +2,11 @@ __all__ = ["MuscularFlagellaConnection"] import numpy as np from numba import njit -from elastica.joint import FreeJoint +from elastica.joint import ConnectionBase from elastica._linalg import _batch_matvec -class MuscularFlagellaConnection(FreeJoint): +class MuscularFlagellaConnection(ConnectionBase): """ This connection class is for Muscular Flagella and it is not generalizable. Since our goal is to replicate the experimental data. We assume muscular flagella is not moving out of plane. @@ -27,8 +27,7 @@ def __init__( normal : np.ndarray 1D array of floats. Normal direction of the rods. """ - super().__init__(k, nu=0) - + self.k = np.float64(k) self.normal = normal def apply_forces(self, system_one, index_one, system_two, index_two, time): @@ -56,7 +55,7 @@ def _apply_forces( system_two_external_forces, ): # This connection routine is not generalizable. Our goal here is to replicate the experiment data. - # Thus below code is hard codded. Torques are computed along the centerline of the muscle + # Thus below code is hard coded. Torques are computed along the centerline of the muscle # and transfered to the body. start_idx = index_one[0] end_idx = index_one[-1] diff --git a/examples/MuscularFlagella/muscle_forces_flagella.py b/examples/MuscularFlagella/muscle_forces_flagella.py index 1fcd23fba..ecd342448 100644 --- a/examples/MuscularFlagella/muscle_forces_flagella.py +++ b/examples/MuscularFlagella/muscle_forces_flagella.py @@ -31,10 +31,6 @@ def __init__(self, amplitude, frequency): self.wave_number = 2 * np.pi * frequency def apply_forces(self, system, time: np.float64 = 0.0): - # muscle_force = ( - # system.tangents * self.amplitude * np.abs(np.sin(self.wave_number * time)) - # ) - # system.external_forces += difference_kernel(muscle_force) self._apply_forces( self.amplitude, diff --git a/examples/MuscularFlagella/muscular_flagella.py b/examples/MuscularFlagella/muscular_flagella.py index 8b8e13635..94174e432 100644 --- a/examples/MuscularFlagella/muscular_flagella.py +++ b/examples/MuscularFlagella/muscular_flagella.py @@ -1,17 +1,20 @@ __doc__ = """Muscular flagella example from Zhang et. al. Nature Comm 2019 paper.""" +import os +from collections import defaultdict + import numpy as np import elastica as ea -from examples.MuscularFlagella.post_processing import ( +from post_processing import ( plot_video_2D, plot_video, plot_com_position_vs_time, plot_position_vs_time_comparison_cpp, ) -from examples.MuscularFlagella.connection_flagella import ( +from connection_flagella import ( MuscularFlagellaConnection, ) -from examples.MuscularFlagella.muscle_forces_flagella import MuscleForces +from muscle_forces_flagella import MuscleForces class MuscularFlagellaSimulator( @@ -56,7 +59,7 @@ class MuscularFlagellaSimulator( start[2] = 0.1 direction = np.array([1.0, 0.0, 0.0]) normal = np.array([0.0, 0.0, 1.0]) -binormal = np.cross(direction, normal) + nu_body = 0 flagella_body = ea.CosseratRod.straight_rod( @@ -200,32 +203,38 @@ class MuscularFlagellaSimulator( ) -# Add call backs +# Add callbacks class MuscularFlagellaCallBack(ea.CallBackBaseClass): + """ + Callback function for collecting data from Muscular Flagella simulation. + Records time, position, center of mass, radius, velocity, and tangents. + """ + def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) self.callback_params["position"].append(system.position_collection.copy()) - self.callback_params["com"].append(system.compute_position_center_of_mass()) + self.callback_params["center_of_mass"].append( + system.compute_position_center_of_mass() + ) self.callback_params["radius"].append(system.radius.copy()) self.callback_params["velocity"].append(system.velocity_collection.copy()) self.callback_params["tangents"].append(system.tangents.copy()) -post_processing_dict_body = ea.defaultdict(list) +post_processing_dict_body = defaultdict(list) muscular_flagella_sim.collect_diagnostics(flagella_body).using( MuscularFlagellaCallBack, step_skip=step_skip, callback_params=post_processing_dict_body, ) -post_processing_dict_muscle = ea.defaultdict(list) +post_processing_dict_muscle = defaultdict(list) muscular_flagella_sim.collect_diagnostics(flagella_muscle).using( MuscularFlagellaCallBack, step_skip=step_skip, @@ -237,7 +246,9 @@ def make_callback(self, system, time, current_step: int): timestepper = ea.PositionVerlet() print("Total steps", total_steps) -ea.integrate(timestepper, muscular_flagella_sim, final_time, total_steps) +time = 0.0 +for i in range(total_steps): + time = timestepper.step(muscular_flagella_sim, time, time_step) # Plot the videos @@ -275,7 +286,6 @@ def make_callback(self, system, time, current_step: int): ) # Store the data for later use and plotting -import os save_folder = os.path.join(os.getcwd(), "data") os.makedirs(save_folder, exist_ok=True) @@ -283,8 +293,8 @@ def make_callback(self, system, time, current_step: int): position_history_body = np.array(post_processing_dict_body["position"]) position_history_muscle = np.array(post_processing_dict_muscle["position"]) -com_history_body = np.array(post_processing_dict_body["com"]) -com_history_muscle = np.array(post_processing_dict_muscle["com"]) +com_history_body = np.array(post_processing_dict_body["center_of_mass"]) +com_history_muscle = np.array(post_processing_dict_muscle["center_of_mass"]) radius_history_body = np.array(post_processing_dict_body["radius"]) radius_history_muscle = np.array(post_processing_dict_muscle["radius"]) diff --git a/examples/MuscularFlagella/post_processing.py b/examples/MuscularFlagella/post_processing.py index 8602c8bd4..b71eeaf38 100644 --- a/examples/MuscularFlagella/post_processing.py +++ b/examples/MuscularFlagella/post_processing.py @@ -28,7 +28,9 @@ def plot_video( rods_history[rod_idx]["radius"][t_idx], ) # Rod center of mass - com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx]["com"][time_idx] + com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx][ + "center_of_mass" + ][t_idx] # video pre-processing print("plot scene visualization video") @@ -135,7 +137,9 @@ def plot_video_2D( rods_history[rod_idx]["radius"][t_idx], ) # Rod center of mass - com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx]["com"][time_idx] + com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx][ + "center_of_mass" + ][t_idx] # video pre-processing print("plot scene visualization video") @@ -270,7 +274,7 @@ def plot_com_position_vs_time( ): time = rods_history["time"] - # rod_com_position = np.array(rods_history["com"]) * -1e3 + # rod_com_position = np.array(rods_history["center_of_mass"]) * -1e3 rod_com_position = np.array(rods_history["position"])[:, :, 9] * -1e3 # We are interested in dx, subtract initial position. diff --git a/examples/MuscularSnake/muscular_snake.py b/examples/MuscularSnake/muscular_snake.py index 61d68f304..bde9852f1 100644 --- a/examples/MuscularSnake/muscular_snake.py +++ b/examples/MuscularSnake/muscular_snake.py @@ -1,11 +1,12 @@ __doc__ = """Muscular snake example from Zhang et. al. Nature Comm 2019 paper.""" import numpy as np +from collections import defaultdict import elastica as ea -from examples.MuscularSnake.post_processing import ( +from post_processing import ( plot_video_with_surface, plot_snake_velocity, ) -from examples.MuscularSnake.muscle_forces import MuscleForces +from muscle_forces import MuscleForces from elastica.experimental.connection_contact_joint.parallel_connection import ( SurfaceJointSideBySide, get_connection_vector_straight_straight_rod, @@ -248,7 +249,7 @@ class MuscularSnakeSimulator( post_processing_forces_dict_list = [] for i in range(n_muscle_fibers): - post_processing_forces_dict_list.append(ea.defaultdict(list)) + post_processing_forces_dict_list.append(defaultdict(list)) muscle_rod = muscle_rod_list[i] side_of_body = 1 if i % 2 == 0 else -1 @@ -268,7 +269,7 @@ class MuscularSnakeSimulator( straight_straight_rod_connection_list = [] -straight_straight_rod_connection_post_processing_dict = ea.defaultdict(list) +straight_straight_rod_connection_post_processing_dict = defaultdict(list) for idx, rod_two in enumerate(muscle_rod_list): rod_one = snake_body ( @@ -359,8 +360,13 @@ class MuscularSnakeSimulator( class MuscularSnakeCallBack(ea.CallBackBaseClass): + """ + Callback function for collecting data from Muscular Snake simulation. + Records time, position, center of mass, radius, and average velocity. + """ + def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -368,15 +374,11 @@ def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) self.callback_params["position"].append(system.position_collection.copy()) - self.callback_params["com"].append(system.compute_position_center_of_mass()) self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["velocity"].append(system.velocity_collection.copy()) self.callback_params["avg_velocity"].append( system.compute_velocity_center_of_mass() ) - self.callback_params["center_of_mass"].append( system.compute_position_center_of_mass() ) @@ -385,7 +387,7 @@ def make_callback(self, system, time, current_step: int): post_processing_dict_list = [] for idx, rod in enumerate(rod_list): - post_processing_dict_list.append(ea.defaultdict(list)) + post_processing_dict_list.append(defaultdict(list)) muscular_snake_simulator.collect_diagnostics(rod).using( MuscularSnakeCallBack, step_skip=step_skip, @@ -394,7 +396,9 @@ def make_callback(self, system, time, current_step: int): muscular_snake_simulator.finalize() timestepper = ea.PositionVerlet() -ea.integrate(timestepper, muscular_snake_simulator, final_time, total_steps) +time = 0.0 +for i in range(total_steps): + time = timestepper.step(muscular_snake_simulator, time, time_step) plot_video_with_surface( diff --git a/examples/MuscularSnake/post_processing.py b/examples/MuscularSnake/post_processing.py index 5157c490c..56c4a9deb 100644 --- a/examples/MuscularSnake/post_processing.py +++ b/examples/MuscularSnake/post_processing.py @@ -1,10 +1,11 @@ import numpy as np import matplotlib -matplotlib.use("Agg") # Must be before importing matplotlib.pyplot or pylab! from matplotlib import pyplot as plt from matplotlib.colors import to_rgb from matplotlib import cm +from matplotlib.patches import Circle +import matplotlib.animation as manimation from tqdm import tqdm from typing import Dict, Sequence @@ -22,9 +23,6 @@ def plot_video_with_surface( folder_name = kwargs.get("folder_name", "") - # 2d case - import matplotlib.animation as animation - # simulation time sim_time = np.array(rods_history[0]["time"]) @@ -36,7 +34,9 @@ def plot_video_with_surface( rods_history[rod_idx]["radius"][t_idx], ) # Rod center of mass - com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx]["com"][time_idx] + com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx][ + "center_of_mass" + ][t_idx] # Generate target sphere data sphere_flag = False @@ -53,7 +53,7 @@ def plot_video_with_surface( # video pre-processing print("plot scene visualization video") - FFMpegWriter = animation.writers["ffmpeg"] + FFMpegWriter = manimation.writers["ffmpeg"] metadata = dict(title="Movie Test", artist="Matplotlib", comment="Movie support!") writer = FFMpegWriter(fps=fps, metadata=metadata) dpi = kwargs.get("dpi", 100) diff --git a/examples/README.md b/examples/README.md index ee70687a1..a5c0893fd 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,16 +1,9 @@ # PyElastica Examples -This directory contains number of examples of elastica. +This directory contains a number of examples of elastica. Each [example cases](#example-cases) are stored in separate subdirectories, containing case descriptions, run file, and all other data/script necessary to run. More [advanced cases](#advanced-cases) are stored in separate repository with its description. -## Installing Requirements -In order to run examples, you will need to install additional dependencies. - -```bash -make install_examples_dependencies -``` - ## Case Examples Some examples provide additional files or links to published paper for a complete description. @@ -21,7 +14,7 @@ Examples can serve as a starting template for customized usages. * __Features__: CosseratRod, OneEndFixedRod, EndpointForces * [TimoshenkoBeamCase](./TimoshenkoBeamCase) * __Purpose__: Physical convergence test of simple Timoshenko beam. - * __Features__: CosseratRod, OneEndFixedRod, EndpointForces, + * __Features__: CosseratRod, OneEndFixedRod, EndpointForces * [FlexibleSwingingPendulumCase](./FlexibleSwingingPendulumCase) * __Purpose__: Physical convergence test of simple pendulum with flexible rod. * __Features__: CosseratRod, HingeBC, GravityForces @@ -38,13 +31,13 @@ Examples can serve as a starting template for customized usages. * __Purpose__: Demonstrate simple restoration with initial strain. * __Features__: CosseratRod * [CantileverDistributedLoad](./CantileverDistributedLoad) - * __Purpose__: Demonstrate the demformation of a straight cantilever under both conservative (like water pressure) and non-conservative (like gravity) distributed load, compared with numerical solutions from Tschisgale, Silvio (2019).["Chapter 3: Numerical models of partitioned problems"](https://nbn-resolving.org/urn:nbn:de:bsz:14-qucosa2-387063) Technische Univerisitat Dresden Institution of Fluid Mechanics + * __Purpose__: Demonstrate the deformation of a straight cantilever under both conservative (like water pressure) and non-conservative (like gravity) distributed load, compared with numerical solutions from Tschisgale, Silvio (2019).["Chapter 3: Numerical models of partitioned problems"](https://nbn-resolving.org/urn:nbn:de:bsz:14-qucosa2-387063) Technische Univerisitat Dresden Institution of Fluid Mechanics * __Features__: CosseratRod * [CantileverTransversalLoadCase](./CantileverTransversalLoadCase) - * __Purpose__: Demonstrate the demformation of a curved cantilever under transversal one-end load, and also do Physical convergence test, compared with numerical solutions from Tschisgale, Silvio (2019). + * __Purpose__: Demonstrate the deformation of a curved cantilever under transversal one-end load, and also do Physical convergence test, compared with numerical solutions from Tschisgale, Silvio (2019). * __Features__: CosseratRod * [TumblingUnconstrainedRod](./TumblingUnconstrainedRod) - * __Purpose__: Demostrate the dynamics of tumbling uncontrained rod, compared with analytical solution from [Hisao, Kou Hou (1998).](https://www.sciencedirect.com/science/article/pii/S0045782598001522), Computer methods in applied mechanics and engineering. + * __Purpose__: Demonstrate the dynamics of tumbling unconstrained rod, compared with analytical solution from [Hisao, Kou Hou (1998).](https://www.sciencedirect.com/science/article/pii/S0045782598001522), Computer methods in applied mechanics and engineering. * __Features__: CosseratRod * [FrictionValidationCases](./FrictionValidationCases) * __Purpose__: Physical validation of rolling and translational friction. @@ -56,8 +49,8 @@ Examples can serve as a starting template for customized usages. * __Purpose__: Demonstrate usage of rigid body on simulation. * __Features__: Cylinder, Sphere * [RodRigidBodyContact](./RigidBodyCases/RodRigidBodyContact) - * __Purpose__: Demonstrate contact between cylinder and rod, for different intial conditions. - * __Features__: Cylinder, CosseratRods, RodCylinderContact + * __Purpose__: Demonstrate contact between cylinder and rod, for different intial conditions. + * __Features__: Cylinder, CosseratRods, RodCylinderContact * [HelicalBucklingCase](./HelicalBucklingCase) * __Purpose__: Demonstrate helical buckling with extreme twisting boundary condition. * __Features__: HelicalBucklingBC @@ -68,16 +61,16 @@ Examples can serve as a starting template for customized usages. * __Purpose__: Example of customizing [Joint module](./MuscularFlagella/connection_flagella.py) and [Force module](./MuscularFlagella/muscle_forces_flagella.py) to implement muscular flagella. * __Features__: MuscleForces(custom implemented) * [RodContactCase](./RodContactCase) - * [RodRodContact](./RodContactCase/RodRodContact) - * __Purpose__: Demonstrates contact between two rods, for different initial conditions. - * __Features__: CosseratRod, RodRodContact - * [RodSelfContact](./RodContactCase/RodSelfContact) - * [PlectonemesCase](./RodContactCase/RodSelfContact/PlectonemesCase) - * __Purpose__: Demonstrates rod self contact with Plectoneme example, and how to use link-writhe-twist after simulation completed. - * __Features__: CosseratRod, SelonoidsBC, RodSelfContact, Link-Writhe-Twist - * [SolenoidsCase](./RodContactCase/RodSelfContact/SolenoidsCase) - * __Purpose__: Demonstrates rod self contact with Solenoid example, and how to use link-writhe-twist after simulation completed. - * __Features__: CosseratRod, SelonoidsBC, RodSelfContact, Link-Writhe-Twist + * [RodRodContact](./RodContactCase/RodRodContact) + * __Purpose__: Demonstrates contact between two rods, for different initial conditions. + * __Features__: CosseratRod, RodRodContact + * [RodSelfContact](./RodContactCase/RodSelfContact) + * [PlectonemesCase](./RodContactCase/RodSelfContact/PlectonemesCase) + * __Purpose__: Demonstrates rod self contact with Plectoneme example, and how to use link-writhe-twist after simulation completed. + * __Features__: CosseratRod, SolenoidsBC, RodSelfContact, Link-Writhe-Twist + * [SolenoidsCase](./RodContactCase/RodSelfContact/SolenoidsCase) + * __Purpose__: Demonstrates rod self contact with Solenoid example, and how to use link-writhe-twist after simulation completed. + * __Features__: CosseratRod, SolenoidsBC, RodSelfContact, Link-Writhe-Twist * [BoundaryConditionsCases](./BoundaryConditionsCases) * __Purpose__: Demonstrate the usage of boundary conditions for constraining the movement of the system. * __Features__: GeneralConstraint, CosseratRod @@ -97,11 +90,8 @@ Examples can serve as a starting template for customized usages. ## Functional Examples * [RestartExample](./RestartExample) - * __Purpose__: Demonstrate the usage of restart module. - * __Features__: save_state, load_state -* [Visualization](./Visualization) - * __Purpose__: Include simple examples of raytrace rendering data. - * __Features__: POVray + * __Purpose__: Demonstrate the usage of restart module. + * __Features__: save_state, load_state ## Advanced Cases @@ -109,6 +99,10 @@ Examples can serve as a starting template for customized usages. * [Gym Softrobot](https://github.com/skim0119/gym-softrobot) - Soft-robot control environment developed in OpenAI-gym format to study slender body control with reinforcement learning. ## Experimental Cases + * [ParallelConnectionExample](./ExperimentalCases/ParallelConnectionExample) - * __Purpose__: Demonstrate the usage of parallel connection. - * __Features__: connect two parallel rods + * __Purpose__: Demonstrate the usage of parallel connection. + * __Features__: connect two parallel rods +* [GenericSystemConnectionCases](./ExperimentalCases/GenericSystemConnectionCases) + * __Purpose__: Demonstrate the usage of generic system type connections for connecting different system types (rods and rigid bodies). + * __Features__: GenericSystemTypeFixedJoint, GenericSystemTypeFreeJoint, CosseratRod, Cylinder diff --git a/examples/RestartExample/restart_example.py b/examples/RestartExample/restart_example.py index 3d85b278c..72a066cde 100644 --- a/examples/RestartExample/restart_example.py +++ b/examples/RestartExample/restart_example.py @@ -1,7 +1,8 @@ """ -This script is an example to how to use Pyelastica restart functionality. +This script is an example of how to use PyElastica restart functionality. """ +import os import numpy as np import elastica as ea @@ -22,7 +23,6 @@ class RestartExampleSimulator( normal = np.array([0.0, 1.0, 0.0]) base_length = 3.0 base_radius = 0.25 -base_area = np.pi * base_radius**2 density = 5000 E = 1e6 # For shear modulus of 1e4, nu is 99! @@ -83,17 +83,13 @@ class RestartExampleSimulator( timestepper = ea.PositionVerlet() total_steps = int(final_time / dt) - -time = ea.integrate( - timestepper, - restart_example_simulator, - final_time, - total_steps, - restart_time=restart_time, -) +time = restart_time +for i in range(total_steps): + time = timestepper.step(restart_example_simulator, time, dt) # Save all the systems appended on the simulator class. Since in this example have only one system, under the # `restart_file_location` directory there is one file called system_0.npz . For each system appended on the simulator # separate system_#.npz file will be created. if SAVE_DATA_RESTART: + os.makedirs(restart_file_location, exist_ok=True) ea.save_state(restart_example_simulator, restart_file_location, time, True) diff --git a/examples/RigidBodyCases/RodRigidBodyContact/post_processing.py b/examples/RigidBodyCases/RodRigidBodyContact/post_processing.py index 066a0c50a..a79286b10 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/post_processing.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/post_processing.py @@ -1,6 +1,7 @@ import numpy as np from matplotlib import pyplot as plt from matplotlib import cm +import matplotlib.animation as manimation from typing import Dict, Sequence from tqdm import tqdm @@ -36,16 +37,14 @@ def plot_video( cylinder_start, cylinder_radius, cylinder_height ) - import matplotlib.animation as manimation - plt.rcParams.update({"font.size": 22}) # Should give a (n_time, 3, n_elem) array positions = np.array(rod_history["position"]) # (n_time, 3) array - com = np.array(rod_history["com"]) + com = np.array(rod_history["center_of_mass"]) - cylinder_com = np.array(cylinder_history["com"]) + cylinder_com = np.array(cylinder_history["center_of_mass"]) cylinder_origin = cylinder_com - 0.5 * cylinder_height * cylinder_direction print("plot video") @@ -54,8 +53,6 @@ def plot_video( writer = FFMpegWriter(fps=fps, metadata=metadata) dpi = 50 - # min_limits = np.roll(np.array([0.0, -0.5 * cylinder_height, 0.0]), _roll_key) - fig = plt.figure(1, figsize=(10, 8), frameon=True, dpi=dpi) ax = plt.axes(projection="3d") # fig.add_subplot(111) ax.grid(which="minor", color="k", linestyle="--") @@ -267,9 +264,6 @@ def plot_video_with_surface( folder_name = kwargs.get("folder_name", "") - # 2d case - import matplotlib.animation as animation - # simulation time sim_time = np.array(rods_history[0]["time"]) @@ -281,7 +275,9 @@ def plot_video_with_surface( rods_history[rod_idx]["radius"][t_idx], ) # Rod center of mass - com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx]["com"][time_idx] + com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx][ + "center_of_mass" + ][time_idx] # Generate target sphere data sphere_flag = False @@ -298,7 +294,7 @@ def plot_video_with_surface( # video pre-processing print("plot scene visualization video") - FFMpegWriter = animation.writers["ffmpeg"] + FFMpegWriter = manimation.writers["ffmpeg"] metadata = dict(title="Movie Test", artist="Matplotlib", comment="Movie support!") writer = FFMpegWriter(fps=fps, metadata=metadata) dpi = kwargs.get("dpi", 100) diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact.py index a45ae03d2..abcc48132 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact.py @@ -1,4 +1,5 @@ import numpy as np +from collections import defaultdict import elastica as ea from post_processing import plot_velocity, plot_video_with_surface @@ -89,23 +90,22 @@ class RodCylinderParallelContact( # For rod class StraightRodCallBack(ea.CallBackBaseClass): """ - Call back function for two arm octopus + Callback function for a straight rod in contact with a cylinder. """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) self.callback_params["position"].append( system.position_collection.copy() ) self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["com"].append( + self.callback_params["center_of_mass"].append( system.compute_position_center_of_mass() ) if current_step == 0: @@ -129,13 +129,13 @@ def make_callback(self, system, time, current_step: int): class RigidCylinderCallBack(ea.CallBackBaseClass): """ - Call back function for two arm octopus + Callback function for a rigid cylinder. """ def __init__( self, step_skip: int, callback_params: dict, resize_cylinder_elems: int ): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params self.n_elem_cylinder = resize_cylinder_elems @@ -144,7 +144,6 @@ def __init__( def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) cylinder_center_position = system.position_collection cylinder_length = system.length @@ -179,7 +178,7 @@ def make_callback(self, system, time, current_step: int): cylinder_velocity_collection.copy() ) self.callback_params["radius"].append(cylinder_radius_collection.copy()) - self.callback_params["com"].append( + self.callback_params["center_of_mass"].append( system.compute_position_center_of_mass() ) @@ -198,14 +197,14 @@ def make_callback(self, system, time, current_step: int): return - post_processing_dict_list.append(ea.defaultdict(list)) + post_processing_dict_list.append(defaultdict(list)) rod_cylinder_parallel_contact_simulator.collect_diagnostics(rod).using( StraightRodCallBack, step_skip=step_skip, callback_params=post_processing_dict_list[0], ) # For rigid body - post_processing_dict_list.append(ea.defaultdict(list)) + post_processing_dict_list.append(defaultdict(list)) rod_cylinder_parallel_contact_simulator.collect_diagnostics(rigid_body).using( RigidCylinderCallBack, step_skip=step_skip, @@ -216,9 +215,10 @@ def make_callback(self, system, time, current_step: int): rod_cylinder_parallel_contact_simulator.finalize() timestepper = ea.PositionVerlet() - ea.integrate( - timestepper, rod_cylinder_parallel_contact_simulator, final_time, total_steps - ) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(rod_cylinder_parallel_contact_simulator, time, dt) # Plot the rods plot_video_with_surface( @@ -242,3 +242,7 @@ def make_callback(self, system, time, current_step: int): filename=filaname, SAVE_FIGURE=True, ) + + +if __name__ == "__main__": + rod_cylinder_contact_case() diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction.py index be53c236b..e08a6716f 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction.py @@ -1,4 +1,5 @@ import numpy as np +from collections import defaultdict import elastica as ea from post_processing import plot_velocity, plot_video_with_surface @@ -109,23 +110,22 @@ class RodCylinderParallelContact( # For rod class StraightRodCallBack(ea.CallBackBaseClass): """ - Call back function for two arm octopus + Callback function for a straight rod in contact with a cylinder. """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) self.callback_params["position"].append( system.position_collection.copy() ) self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["com"].append( + self.callback_params["center_of_mass"].append( system.compute_position_center_of_mass() ) if current_step == 0: @@ -149,13 +149,13 @@ def make_callback(self, system, time, current_step: int): class RigidCylinderCallBack(ea.CallBackBaseClass): """ - Call back function for two arm octopus + Callback function for a rigid cylinder """ def __init__( self, step_skip: int, callback_params: dict, resize_cylinder_elems: int ): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params self.n_elem_cylinder = resize_cylinder_elems @@ -164,7 +164,6 @@ def __init__( def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) cylinder_center_position = system.position_collection cylinder_length = system.length @@ -199,7 +198,7 @@ def make_callback(self, system, time, current_step: int): cylinder_velocity_collection.copy() ) self.callback_params["radius"].append(cylinder_radius_collection.copy()) - self.callback_params["com"].append( + self.callback_params["center_of_mass"].append( system.compute_position_center_of_mass() ) @@ -219,14 +218,14 @@ def make_callback(self, system, time, current_step: int): return if POST_PROCESSING: - post_processing_dict_list.append(ea.defaultdict(list)) + post_processing_dict_list.append(defaultdict(list)) rod_cylinder_parallel_contact_simulator.collect_diagnostics(rod).using( StraightRodCallBack, step_skip=step_skip, callback_params=post_processing_dict_list[0], ) # For rigid body - post_processing_dict_list.append(ea.defaultdict(list)) + post_processing_dict_list.append(defaultdict(list)) rod_cylinder_parallel_contact_simulator.collect_diagnostics(rigid_body).using( RigidCylinderCallBack, step_skip=step_skip, @@ -237,9 +236,10 @@ def make_callback(self, system, time, current_step: int): rod_cylinder_parallel_contact_simulator.finalize() timestepper = ea.PositionVerlet() - ea.integrate( - timestepper, rod_cylinder_parallel_contact_simulator, final_time, total_steps - ) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(rod_cylinder_parallel_contact_simulator, time, dt) if POST_PROCESSING: # Plot the rods diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction_case.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction_case.py index c8dfe2516..85d502fed 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction_case.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction_case.py @@ -1,5 +1,5 @@ if __name__ == "__main__": - from examples.RigidBodyCases.RodRigidBodyContact.rod_cylinder_contact_friction import ( + from rod_cylinder_contact_friction import ( rod_cylinder_contact_friction_case, ) diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction_phase_space.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction_phase_space.py index 7b7ae1d13..f6ff59718 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction_phase_space.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction_phase_space.py @@ -1,9 +1,9 @@ if __name__ == "__main__": import multiprocessing as mp - from examples.RigidBodyCases.RodRigidBodyContact.rod_cylinder_contact_friction import ( + from rod_cylinder_contact_friction import ( rod_cylinder_contact_friction_case, ) - from examples.RigidBodyCases.RodRigidBodyContact.post_processing import ( + from post_processing import ( plot_force_vs_energy, ) diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_validation.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_validation.py index f1700a772..848047924 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_validation.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_validation.py @@ -1,4 +1,5 @@ import numpy as np +from collections import defaultdict import elastica as ea from post_processing import plot_video, plot_cylinder_rod_position @@ -114,14 +115,14 @@ class SingleRodSingleCylinderInteractionSimulator( ) -# Add call backs +# Add callbacks class PositionCollector(ea.CallBackBaseClass): """ - Call back function for continuum snake + Callback function for collecting position data of a rod and cylinder. """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -130,15 +131,17 @@ def make_callback(self, system, time, current_step: int): self.callback_params["time"].append(time) # Collect only x self.callback_params["position"].append(system.position_collection.copy()) - self.callback_params["com"].append(system.compute_position_center_of_mass()) + self.callback_params["center_of_mass"].append( + system.compute_position_center_of_mass() + ) return -recorded_rod_history = ea.defaultdict(list) +recorded_rod_history = defaultdict(list) single_rod_sim.collect_diagnostics(rod1).using( PositionCollector, step_skip=200, callback_params=recorded_rod_history ) -recorded_cyl_history = ea.defaultdict(list) +recorded_cyl_history = defaultdict(list) single_rod_sim.collect_diagnostics(cylinder).using( PositionCollector, step_skip=200, callback_params=recorded_cyl_history ) @@ -159,7 +162,10 @@ def make_callback(self, system, time, current_step: int): total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, single_rod_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(single_rod_sim, time, dt) if PLOT_FIGURE: plot_video( diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_with_y_normal.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_with_y_normal.py index 11fc79194..96ef770c5 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_with_y_normal.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_with_y_normal.py @@ -1,4 +1,5 @@ import numpy as np +from collections import defaultdict import elastica as ea from post_processing import plot_video, plot_cylinder_rod_position @@ -92,14 +93,14 @@ class SingleRodSingleCylinderInteractionSimulator( ) -# Add call backs +# Add callbacks class PositionCollector(ea.CallBackBaseClass): """ - Call back function for continuum snake + Callback function for collecting position data of a rod and cylinder. """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -108,15 +109,17 @@ def make_callback(self, system, time, current_step: int): self.callback_params["time"].append(time) # Collect only x self.callback_params["position"].append(system.position_collection.copy()) - self.callback_params["com"].append(system.compute_position_center_of_mass()) + self.callback_params["center_of_mass"].append( + system.compute_position_center_of_mass() + ) return -recorded_rod_history = ea.defaultdict(list) +recorded_rod_history = defaultdict(list) single_rod_sim.collect_diagnostics(rod1).using( PositionCollector, step_skip=200, callback_params=recorded_rod_history ) -recorded_cyl_history = ea.defaultdict(list) +recorded_cyl_history = defaultdict(list) single_rod_sim.collect_diagnostics(cylinder).using( PositionCollector, step_skip=200, callback_params=recorded_cyl_history ) @@ -137,7 +140,10 @@ def make_callback(self, system, time, current_step: int): total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, single_rod_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(single_rod_sim, time, dt) if PLOT_FIGURE: plot_video( @@ -156,4 +162,5 @@ def make_callback(self, system, time, current_step: int): rod_base_radius=base_radius, TIP_COLLISION=TIP_COLLISION, TIP_CHOICE=TIP_CHOICE, + _roll_key=1, # For y-normal case, we are interested in y-direction. ) diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_sphere_contact.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_sphere_contact.py new file mode 100644 index 000000000..b28a2470e --- /dev/null +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_sphere_contact.py @@ -0,0 +1,201 @@ +import numpy as np +from collections import defaultdict +from tqdm import tqdm +import elastica as ea +from post_processing import plot_video_with_surface + +start = np.zeros((3,)) +direction = np.array([0.0, 1.0, 0.0]) +normal = np.array([0.0, 0.0, 1.0]) +base_length = 0.5 +base_radius = 0.1 + +sphere_radius = 0.10 +# overlap_perc = 1.0 # Should be no contact +# overlap_perc = 1.0 + 1e-2 # Should be no contact +overlap_perc = 1.0 - 1e-2 # Contact +sphere_center = np.array( + [(base_radius + sphere_radius) * overlap_perc, base_length / 2, 0.0] +) + + +def rotate_random_axis_and_angle(R): + """ + Randomly rotate the frame for testing purpose. + """ + from scipy.spatial.transform import Rotation + + axis = np.random.rand(3) + axis /= np.linalg.norm(axis) + angle = np.random.rand() * 2 * np.pi + return R @ Rotation.from_rotvec(angle * axis).as_matrix() + + +def main(): + class Simulator( + ea.BaseSystemCollection, + ea.Constraints, + ea.Contact, + ea.CallBacks, + ea.Forcing, + ea.Damping, + ): + pass + + simulator = Simulator() + + # time step etc + final_time = 1.0 + time_step = 5e-4 + total_steps = int(final_time / time_step) + 1 + rendering_fps = 30 # 20 * 1e1 + step_skip = 100 + + # Add rod + density = 1000 + E = 3e5 + n_elem = 50 + + rod = ea.CosseratRod.straight_rod( + n_elem, + start, + direction, + normal, + base_length, + base_radius, + density, + youngs_modulus=E, + ) + simulator.append(rod) + + simulator.constrain(rod).using( + ea.FixedConstraint, + constrained_position_idx=(0, -1), + constrained_director_idx=(0, -1), + ) + + damping_constant = 1e-1 + simulator.dampen(rod).using( + ea.AnalyticalLinearDamper, + damping_constant=damping_constant, + time_step=time_step, + ) + + # Add sphere + density = 1000 + n_sphere = 1 + for _ in range(n_sphere): + rr = rotate_random_axis_and_angle(np.eye(3)) + rigid_body = ea.Sphere(sphere_center, sphere_radius, density) + rigid_body.director_collection[0] = rr[0][:, None] + rigid_body.director_collection[1] = rr[1][:, None] + rigid_body.director_collection[2] = rr[2][:, None] + simulator.append(rigid_body) + + # Add contact between rigid body and rod + simulator.detect_contact_between(rod, rigid_body).using( + ea.RodSphereContact, k=3e4, nu=0.0 + ) + + # Add callbacks + post_processing_dict_list = [] + + # For rod + class StraightRodCallBack(ea.CallBackBaseClass): + """ + Callback function for a straight rod in contact with a sphere. + """ + + def __init__(self, step_skip: int, callback_params: dict): + super().__init__() + self.every = step_skip + self.callback_params = callback_params + + def make_callback(self, system, time, current_step: int): + if current_step % self.every == 0: + self.callback_params["time"].append(time) + self.callback_params["position"].append( + system.position_collection.copy() + ) + self.callback_params["radius"].append(system.radius.copy()) + self.callback_params["center_of_mass"].append( + system.compute_position_center_of_mass() + ) + total_energy = ( + system.compute_translational_energy() + + system.compute_rotational_energy() + + system.compute_bending_energy() + + system.compute_shear_energy() + ) + self.callback_params["total_energy"].append(total_energy) + return + + class RigidBodyCallback(ea.CallBackBaseClass): + """ + Callback function for a rigid sphere. + """ + + def __init__( + self, + step_skip: int, + callback_params: dict, + ): + super().__init__() + self.every = step_skip + self.callback_params = callback_params + + def make_callback(self, system, time, current_step: int): + if current_step % self.every == 0: + self.callback_params["time"].append(time) + self.callback_params["position"].append( + system.position_collection.copy() + ) + self.callback_params["director"].append( + system.director_collection.copy() + ) + self.callback_params["radius"].append(np.array([system.radius.copy()])) + self.callback_params["center_of_mass"].append( + system.compute_position_center_of_mass() + ) + + post_processing_dict_list.append(defaultdict(list)) + simulator.collect_diagnostics(rod).using( + StraightRodCallBack, + step_skip=step_skip, + callback_params=post_processing_dict_list[0], + ) + for _ in range(n_sphere): + # For rigid body + db = defaultdict(list) + post_processing_dict_list.append(db) + simulator.collect_diagnostics(rigid_body).using( + RigidBodyCallback, + step_skip=step_skip, + callback_params=db, + ) + simulator.finalize() + + timestepper = ea.PositionVerlet() + + time = 0.0 + for i in tqdm(range(total_steps), disable=True): + time = timestepper.step(simulator, time, time_step) + + # Plot the rods + plot_video_with_surface( + post_processing_dict_list, + video_name="rod_sphere_contact.mp4", + fps=rendering_fps, + step=1, + # The following parameters are optional + x_limits=(-base_length * 5, base_length * 5), # Set bounds on x-axis + y_limits=(-base_length * 5, base_length * 5), # Set bounds on y-axis + z_limits=(-base_length * 5, base_length * 5), # Set bounds on z-axis + dpi=100, # Set the quality of the image + vis3D=True, # Turn on 3D visualization + vis2D=True, # Turn on projected (2D) visualization + ) + + +if __name__ == "__main__": + main() diff --git a/examples/RigidBodyCases/rigid_cylinder_rotational_motion_case.py b/examples/RigidBodyCases/rigid_cylinder_rotational_motion_case.py index 642719166..22b8ef685 100644 --- a/examples/RigidBodyCases/rigid_cylinder_rotational_motion_case.py +++ b/examples/RigidBodyCases/rigid_cylinder_rotational_motion_case.py @@ -1,6 +1,7 @@ import numpy as np +from collections import defaultdict import elastica as ea -from examples.FrictionValidationCases.friction_validation_postprocessing import ( +from friction_validation_postprocessing import ( plot_friction_validation, ) @@ -20,23 +21,20 @@ class RigidCylinderSimulator( def rigid_cylinder_rotational_motion_verification(torque=0.0): """ This test case is for validating rotational motion of - rigid cylinder. Here we are applying point for on the cylinder + rigid cylinder. Here we are applying point torque on the cylinder and compare the kinetic energy of the cylinder after T=0.25s with the analytical calculation. - :param force: + :param torque: :return: """ rigid_cylinder_sim = RigidCylinderSimulator() - # setting up test params # setting up test params start = np.zeros((3,)) direction = np.array([0.0, 1.0, 0.0]) normal = np.array([0.0, 0.0, 1.0]) - binormal = np.cross(direction, normal) base_length = 1.0 base_radius = 0.05 - base_area = np.pi * base_radius**2 density = 1000 cylinder = ea.Cylinder(start, direction, normal, base_length, base_radius, density) @@ -52,7 +50,7 @@ def __init__(self, torque, direction=np.array([0.0, 0.0, 0.0])): super(PointCoupleToCenter, self).__init__() self.torque = (torque * direction).reshape(3, 1) - def apply_forces(self, system, time: np.float64 = np.float64(0.0)): + def apply_torques(self, system, time: np.float64 = np.float64(0.0)): system.external_torques += np.einsum( "ijk, jk->ik", system.director_collection, self.torque ) @@ -62,34 +60,30 @@ def apply_forces(self, system, time: np.float64 = np.float64(0.0)): PointCoupleToCenter, torque=torque, direction=direction ) - # Add call backs - class RigidSphereCallBack(ea.CallBackBaseClass): + # Add callbacks + class RigidCylinderCallBack(ea.CallBackBaseClass): """ - Call back function for continuum snake + Callback function for rigid cylinder """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) self.callback_params["position"].append( system.position_collection.copy() ) - self.callback_params["velocity"].append( - system.velocity_collection.copy() - ) return step_skip = 200 - pp_list = ea.defaultdict(list) + pp_list = defaultdict(list) rigid_cylinder_sim.collect_diagnostics(cylinder).using( - RigidSphereCallBack, step_skip=step_skip, callback_params=pp_list + RigidCylinderCallBack, step_skip=step_skip, callback_params=pp_list ) rigid_cylinder_sim.finalize() @@ -99,7 +93,9 @@ def make_callback(self, system, time, current_step: int): dt = 4.0e-5 total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, rigid_cylinder_sim, final_time, total_steps) + time = 0.0 + for i in range(total_steps): + time = timestepper.step(rigid_cylinder_sim, time, dt) # compute translational and rotational energy translational_energy = cylinder.compute_translational_energy() diff --git a/examples/RigidBodyCases/rigid_cylinder_translational_motion_case.py b/examples/RigidBodyCases/rigid_cylinder_translational_motion_case.py index ce78a84af..255ea5f1c 100644 --- a/examples/RigidBodyCases/rigid_cylinder_translational_motion_case.py +++ b/examples/RigidBodyCases/rigid_cylinder_translational_motion_case.py @@ -1,6 +1,7 @@ import numpy as np +from collections import defaultdict import elastica as ea -from examples.FrictionValidationCases.friction_validation_postprocessing import ( +from friction_validation_postprocessing import ( plot_friction_validation, ) @@ -28,15 +29,12 @@ def rigid_cylinder_translational_motion_verification(force=0.0): """ rigid_cylinder_sim = RigidCylinderSimulator() - # setting up test params # setting up test params start = np.zeros((3,)) direction = np.array([0.0, 1.0, 0.0]) normal = np.array([0.0, 0.0, 1.0]) - binormal = np.cross(direction, normal) base_length = 1.0 base_radius = 0.05 - base_area = np.pi * base_radius**2 density = 1000 cylinder = ea.Cylinder(start, direction, normal, base_length, base_radius, density) @@ -60,34 +58,30 @@ def apply_forces(self, system, time: np.float64 = np.float64(0.0)): PointForceToCenter, force=force, direction=normal.reshape(3, 1) ) - # Add call backs - class RigidSphereCallBack(ea.CallBackBaseClass): + # Add callbacks + class RigidCylinderCallBack(ea.CallBackBaseClass): """ - Call back function + Callback function """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) self.callback_params["position"].append( system.position_collection.copy() ) - self.callback_params["velocity"].append( - system.velocity_collection.copy() - ) return step_skip = 200 - pp_list = ea.defaultdict(list) + pp_list = defaultdict(list) rigid_cylinder_sim.collect_diagnostics(cylinder).using( - RigidSphereCallBack, step_skip=step_skip, callback_params=pp_list + RigidCylinderCallBack, step_skip=step_skip, callback_params=pp_list ) rigid_cylinder_sim.finalize() @@ -97,7 +91,9 @@ def make_callback(self, system, time, current_step: int): dt = 4.0e-5 total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, rigid_cylinder_sim, final_time, total_steps) + time = 0.0 + for i in range(total_steps): + time = timestepper.step(rigid_cylinder_sim, time, dt) # compute translational and rotational energy translational_energy = cylinder.compute_translational_energy() diff --git a/examples/RigidBodyCases/rigid_sphere_rotational_motion_case.py b/examples/RigidBodyCases/rigid_sphere_rotational_motion_case.py index 5f38beba2..b0c666176 100644 --- a/examples/RigidBodyCases/rigid_sphere_rotational_motion_case.py +++ b/examples/RigidBodyCases/rigid_sphere_rotational_motion_case.py @@ -1,6 +1,7 @@ import numpy as np +from collections import defaultdict import elastica as ea -from examples.FrictionValidationCases.friction_validation_postprocessing import ( +from friction_validation_postprocessing import ( plot_friction_validation, ) @@ -19,10 +20,10 @@ class RigidSphereSimulator( def rigid_sphere_rolling_verification(torque=0.0): """ - This test case is for validating friction calculation for rigid body. Here cylinder direction - and normal directions are parallel and base of the cylinder is touching the ground. We are validating - our friction model for different forces. - :param force: + This test case is for validating rotational motion of a rigid sphere. + Here, a torque is applied to the sphere, and its kinetic energy + is compared with analytical calculations after a given time. + :param torque: :return: """ rigid_sphere_sim = RigidSphereSimulator() @@ -43,7 +44,7 @@ def __init__(self, torque, direction=np.array([0.0, 0.0, 0.0])): super(PointCoupleToCenter, self).__init__() self.torque = (torque * direction).reshape(3, 1) - def apply_forces(self, system, time: np.float64 = np.float64(0.0)): + def apply_torques(self, system, time: np.float64 = np.float64(0.0)): system.external_torques += np.einsum( "ijk, jk->ik", system.director_collection, self.torque ) @@ -53,32 +54,28 @@ def apply_forces(self, system, time: np.float64 = np.float64(0.0)): PointCoupleToCenter, torque=torque, direction=np.array([0.0, -1.0, 0.0]) ) - # Add call backs + # Add callbacks class RigidSphereCallBack(ea.CallBackBaseClass): """ - Call back function + Callback function """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) self.callback_params["position"].append( system.position_collection.copy() ) - self.callback_params["velocity"].append( - system.velocity_collection.copy() - ) return step_skip = 200 - pp_list = ea.defaultdict(list) + pp_list = defaultdict(list) rigid_sphere_sim.collect_diagnostics(sphere).using( RigidSphereCallBack, step_skip=step_skip, callback_params=pp_list ) @@ -86,11 +83,13 @@ def make_callback(self, system, time, current_step: int): rigid_sphere_sim.finalize() timestepper = ea.PositionVerlet() - final_time = 0.25 # 11.0 + 0.01) + final_time = 0.25 dt = 4.0e-5 total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, rigid_sphere_sim, final_time, total_steps) + time = 0.0 + for i in range(total_steps): + time = timestepper.step(rigid_sphere_sim, time, dt) # compute translational and rotational energy translational_energy = sphere.compute_translational_energy() @@ -127,13 +126,13 @@ def make_callback(self, system, time, current_step: int): results = pool.map(rigid_sphere_rolling_verification, torque) if PLOT_FIGURE: - filename = "rotationa_energy_test_for_sphere.png" + filename = "rotational_energy_test_for_sphere.png" plot_friction_validation(results, SAVE_FIGURE, filename) if SAVE_RESULTS: import pickle - filename = "rotationa_energy_test_for_sphere.dat" + filename = "rotational_energy_test_for_sphere.dat" file = open(filename, "wb") pickle.dump([results], file) file.close() diff --git a/examples/RigidBodyCases/rigid_sphere_translational_motion_case.py b/examples/RigidBodyCases/rigid_sphere_translational_motion_case.py index d4dc17f18..30f27db2d 100644 --- a/examples/RigidBodyCases/rigid_sphere_translational_motion_case.py +++ b/examples/RigidBodyCases/rigid_sphere_translational_motion_case.py @@ -1,6 +1,7 @@ import numpy as np +from collections import defaultdict import elastica as ea -from examples.FrictionValidationCases.friction_validation_postprocessing import ( +from friction_validation_postprocessing import ( plot_friction_validation, ) @@ -54,32 +55,28 @@ def apply_forces(self, system, time: np.float64 = np.float64(0.0)): direction=np.array([0.0, -1.0, 0.0]).reshape(3, 1), ) - # Add call backs + # Add callbacks class RigidSphereCallBack(ea.CallBackBaseClass): """ - Call back function + Callback function """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) self.callback_params["position"].append( system.position_collection.copy() ) - self.callback_params["velocity"].append( - system.velocity_collection.copy() - ) return step_skip = 200 - pp_list = ea.defaultdict(list) + pp_list = defaultdict(list) rigid_sphere_sim.collect_diagnostics(sphere).using( RigidSphereCallBack, step_skip=step_skip, callback_params=pp_list ) @@ -91,7 +88,9 @@ def make_callback(self, system, time, current_step: int): dt = 4.0e-5 total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, rigid_sphere_sim, final_time, total_steps) + time = 0.0 + for i in range(total_steps): + time = timestepper.step(rigid_sphere_sim, time, dt) # compute translational and rotational energy translational_energy = sphere.compute_translational_energy() diff --git a/examples/RingRodCase/ring_rod.py b/examples/RingRodCase/ring_rod.py index eec6588e6..8579d2421 100644 --- a/examples/RingRodCase/ring_rod.py +++ b/examples/RingRodCase/ring_rod.py @@ -1,7 +1,8 @@ import numpy as np +from collections import defaultdict import elastica as ea -from examples.RingRodCase.ring_rod_post_processing import plot_video +from ring_rod_post_processing import plot_video class RingSimulator( @@ -67,14 +68,14 @@ class RingSimulator( step_skip = int(1.0 / (rendering_fps * time_step)) -# Add call backs +# Add callbacks class RingRodCallBack(ea.CallBackBaseClass): """ - Call back function for ring rod + Callback function for ring rod """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -85,20 +86,16 @@ def make_callback(self, system, time, current_step: int): self.callback_params["time"].append(time) self.callback_params["step"].append(current_step) self.callback_params["position"].append(system.position_collection.copy()) - self.callback_params["length"].append(system.rest_lengths.copy()) self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["velocity"].append(system.velocity_collection.copy()) - self.callback_params["avg_velocity"].append( - system.compute_velocity_center_of_mass() - ) - self.callback_params["com"].append(system.compute_position_center_of_mass()) - self.callback_params["curvature"].append(system.kappa.copy()) + self.callback_params["center_of_mass"].append( + system.compute_position_center_of_mass() + ) return -pp_list = ea.defaultdict(list) +pp_list = defaultdict(list) ring_sim.collect_diagnostics(ring_rod).using( RingRodCallBack, step_skip=step_skip, callback_params=pp_list ) @@ -106,7 +103,9 @@ def make_callback(self, system, time, current_step: int): ring_sim.finalize() timestepper = ea.PositionVerlet() -ea.integrate(timestepper, ring_sim, final_time, total_steps) +time = 0.0 +for i in range(total_steps): + time = timestepper.step(ring_sim, time, time_step) filename_video = "ring_rod.mp4" diff --git a/examples/RingRodCase/ring_rod_post_processing.py b/examples/RingRodCase/ring_rod_post_processing.py index 933b0787f..e16567729 100644 --- a/examples/RingRodCase/ring_rod_post_processing.py +++ b/examples/RingRodCase/ring_rod_post_processing.py @@ -32,7 +32,9 @@ def plot_video( rods_history[rod_idx]["radius"][t_idx], ) # Rod center of mass - com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx]["com"][time_idx] + com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx][ + "center_of_mass" + ][t_idx] # Generate target sphere data sphere_flag = False diff --git a/examples/RodContactCase/RodRodContact/rod_rod_contact_inclined_validation.py b/examples/RodContactCase/RodRodContact/rod_rod_contact_inclined_validation.py index d60940ced..76c4d721c 100644 --- a/examples/RodContactCase/RodRodContact/rod_rod_contact_inclined_validation.py +++ b/examples/RodContactCase/RodRodContact/rod_rod_contact_inclined_validation.py @@ -1,6 +1,8 @@ import numpy as np +from collections import defaultdict + import elastica as ea -from examples.RodContactCase.post_processing import ( +from post_processing import ( plot_video_with_surface, plot_velocity, ) @@ -30,9 +32,7 @@ class InclinedRodRodContact( # Rod parameters base_length = 0.5 base_radius = 0.01 -base_area = np.pi * base_radius**2 density = 1750 -nu = 0.0 E = 3e5 poisson_ratio = 0.5 shear_modulus = E / (poisson_ratio + 1.0) @@ -112,7 +112,7 @@ class RodCallBack(ea.CallBackBaseClass): """ """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -122,7 +122,9 @@ def make_callback(self, system, time, current_step: int): self.callback_params["step"].append(current_step) self.callback_params["position"].append(system.position_collection.copy()) self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["com"].append(system.compute_position_center_of_mass()) + self.callback_params["center_of_mass"].append( + system.compute_position_center_of_mass() + ) self.callback_params["com_velocity"].append( system.compute_velocity_center_of_mass() ) @@ -138,7 +140,7 @@ def make_callback(self, system, time, current_step: int): return -post_processing_dict_rod1 = ea.defaultdict( +post_processing_dict_rod1 = defaultdict( list ) # list which collected data will be append # set the diagnostics for rod and collect data @@ -148,7 +150,7 @@ def make_callback(self, system, time, current_step: int): callback_params=post_processing_dict_rod1, ) -post_processing_dict_rod2 = ea.defaultdict( +post_processing_dict_rod2 = defaultdict( list ) # list which collected data will be append # set the diagnostics for rod and collect data @@ -162,7 +164,10 @@ def make_callback(self, system, time, current_step: int): # Do the simulation timestepper = ea.PositionVerlet() -ea.integrate(timestepper, inclined_rod_rod_contact_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(inclined_rod_rod_contact_sim, time, dt) # plotting the videos filename_video = "inclined_rods_contact.mp4" @@ -175,10 +180,10 @@ def make_callback(self, system, time, current_step: int): vis2D=True, ) -filaname = "inclined_rods_velocity.png" +filename = "inclined_rods_velocity.png" plot_velocity( post_processing_dict_rod1, post_processing_dict_rod2, - filename=filaname, + filename=filename, SAVE_FIGURE=True, ) diff --git a/examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py b/examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py index 042efd701..359650b63 100644 --- a/examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py +++ b/examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py @@ -1,6 +1,8 @@ import numpy as np +from collections import defaultdict + import elastica as ea -from examples.RodContactCase.post_processing import ( +from post_processing import ( plot_video_with_surface, plot_velocity, ) @@ -30,9 +32,7 @@ class ParallelRodRodContact( # Rod parameters base_length = 0.5 base_radius = 0.01 -base_area = np.pi * base_radius**2 density = 1750 -nu = 0.0 E = 3e5 poisson_ratio = 0.5 shear_modulus = E / (poisson_ratio + 1.0) @@ -109,7 +109,7 @@ class RodCallBack(ea.CallBackBaseClass): """ """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -119,7 +119,9 @@ def make_callback(self, system, time, current_step: int): self.callback_params["step"].append(current_step) self.callback_params["position"].append(system.position_collection.copy()) self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["com"].append(system.compute_position_center_of_mass()) + self.callback_params["center_of_mass"].append( + system.compute_position_center_of_mass() + ) self.callback_params["com_velocity"].append( system.compute_velocity_center_of_mass() ) @@ -135,7 +137,7 @@ def make_callback(self, system, time, current_step: int): return -post_processing_dict_rod1 = ea.defaultdict( +post_processing_dict_rod1 = defaultdict( list ) # list which collected data will be append # set the diagnostics for rod and collect data @@ -145,7 +147,7 @@ def make_callback(self, system, time, current_step: int): callback_params=post_processing_dict_rod1, ) -post_processing_dict_rod2 = ea.defaultdict( +post_processing_dict_rod2 = defaultdict( list ) # list which collected data will be append # set the diagnostics for rod and collect data @@ -159,7 +161,10 @@ def make_callback(self, system, time, current_step: int): # Do the simulation timestepper = ea.PositionVerlet() -ea.integrate(timestepper, parallel_rod_rod_contact_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(parallel_rod_rod_contact_sim, time, dt) # plotting the videos filename_video = "parallel_rods_contact.mp4" @@ -172,10 +177,10 @@ def make_callback(self, system, time, current_step: int): vis2D=True, ) -filaname = "parallel_rods_velocity.png" +filename = "parallel_rods_velocity.png" plot_velocity( post_processing_dict_rod1, post_processing_dict_rod2, - filename=filaname, + filename=filename, SAVE_FIGURE=True, ) diff --git a/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py b/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py index 384b85864..e85db9184 100644 --- a/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py +++ b/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py @@ -1,6 +1,8 @@ import numpy as np +from collections import defaultdict + import elastica as ea -from examples.RodContactCase.post_processing import ( +from post_processing import ( plot_video_with_surface, plot_link_writhe_twist, ) @@ -80,7 +82,7 @@ class PlectonemesCase( from elastica._rotations import _get_rotation_matrix -class SelonoidsBC(ea.ConstraintBase): +class SolenoidsBC(ea.ConstraintBase): """ """ def __init__( @@ -100,7 +102,7 @@ def __init__( theta = 2.0 * number_of_rotations * np.pi - angel_vel_scalar = theta / self.twisting_time + angle_vel_scalar = theta / self.twisting_time direction = -(position_end - position_start) / np.linalg.norm( position_end - position_start @@ -118,45 +120,45 @@ def __init__( @ director_end ) # rotation_matrix wants vectors 3,1 - self.ang_vel = angel_vel_scalar * axis_of_rotation_in_material_frame + self.ang_vel = angle_vel_scalar * axis_of_rotation_in_material_frame self.position_start = position_start self.director_start = director_start def constrain_values(self, system, time): if time > self.twisting_time + self.time_twis_start: - rod.position_collection[..., 0] = self.position_start - rod.position_collection[0, -1] = 0.0 - rod.position_collection[2, -1] = 0.0 + system.position_collection[..., 0] = self.position_start + system.position_collection[0, -1] = 0.0 + system.position_collection[2, -1] = 0.0 - rod.director_collection[..., 0] = self.director_start - rod.director_collection[..., -1] = self.final_end_directors + system.director_collection[..., 0] = self.director_start + system.director_collection[..., -1] = self.final_end_directors def constrain_rates(self, system, time): if time > self.twisting_time + self.time_twis_start: - rod.velocity_collection[..., 0] = 0.0 - rod.omega_collection[..., 0] = 0.0 + system.velocity_collection[..., 0] = 0.0 + system.omega_collection[..., 0] = 0.0 - rod.velocity_collection[..., -1] = 0.0 - rod.omega_collection[..., -1] = 0.0 + system.velocity_collection[..., -1] = 0.0 + system.omega_collection[..., -1] = 0.0 elif time < self.time_twis_start: - rod.velocity_collection[..., 0] = 0.0 - rod.omega_collection[..., 0] = 0.0 + system.velocity_collection[..., 0] = 0.0 + system.omega_collection[..., 0] = 0.0 else: - rod.velocity_collection[..., 0] = 0.0 - rod.omega_collection[..., 0] = 0.0 + system.velocity_collection[..., 0] = 0.0 + system.omega_collection[..., 0] = 0.0 - rod.velocity_collection[0, -1] = 0.0 - rod.velocity_collection[2, -1] = 0.0 - rod.omega_collection[..., -1] = -self.ang_vel + system.velocity_collection[0, -1] = 0.0 + system.velocity_collection[2, -1] = 0.0 + system.omega_collection[..., -1] = -self.ang_vel - rod.velocity_collection[2, int(rod.n_elems / 2)] -= 1e-4 + system.velocity_collection[2, int(system.n_elems / 2)] -= 1e-4 plectonemes_sim.constrain(sherable_rod).using( - SelonoidsBC, + SolenoidsBC, constrained_position_idx=(0, -1), constrained_director_idx=(0, -1), time_twis_start=time_start_twist, @@ -175,7 +177,7 @@ class RodCallBack(ea.CallBackBaseClass): """ """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -185,7 +187,9 @@ def make_callback(self, system, time, current_step: int): self.callback_params["step"].append(current_step) self.callback_params["position"].append(system.position_collection.copy()) self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["com"].append(system.compute_position_center_of_mass()) + self.callback_params["center_of_mass"].append( + system.compute_position_center_of_mass() + ) self.callback_params["com_velocity"].append( system.compute_velocity_center_of_mass() ) @@ -202,7 +206,7 @@ def make_callback(self, system, time, current_step: int): return -post_processing_dict = ea.defaultdict(list) # list which collected data will be append +post_processing_dict = defaultdict(list) # list which collected data will be append # set the diagnostics for rod and collect data plectonemes_sim.collect_diagnostics(sherable_rod).using( RodCallBack, @@ -215,7 +219,10 @@ def make_callback(self, system, time, current_step: int): # Run the simulation time_stepper = ea.PositionVerlet() -ea.integrate(time_stepper, plectonemes_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = time_stepper.step(plectonemes_sim, time, dt) # plotting the videos filename_video = "plectonemes.mp4" @@ -239,14 +246,14 @@ def make_callback(self, system, time, current_step: int): # Compute twist density theta = 2.0 * number_of_rotations * np.pi -angel_vel_scalar = theta / time_twist +angle_vel_scalar = theta / time_twist twist_time_interval_start_idx = np.where(time > time_start_twist)[0][0] twist_time_interval_end_idx = np.where(time < (time_relax + time_twist))[0][-1] twist_density = ( (time[twist_time_interval_start_idx:twist_time_interval_end_idx] - time_start_twist) - * angel_vel_scalar + * angle_vel_scalar * base_radius ) diff --git a/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py b/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py index 661294d43..afd34defd 100644 --- a/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py +++ b/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py @@ -1,6 +1,8 @@ import numpy as np +from collections import defaultdict + import elastica as ea -from examples.RodContactCase.post_processing import ( +from post_processing import ( plot_video_with_surface, plot_link_writhe_twist, ) @@ -38,7 +40,6 @@ class SolenoidCase( # Rest of the rod parameters and construct rod base_radius = 0.025 base_area = np.pi * base_radius**2 -I = np.pi / 4 * base_radius**4 volume = base_area * base_length mass = 1.0 density = mass / volume @@ -82,7 +83,7 @@ class SolenoidCase( from elastica._rotations import _get_rotation_matrix -class SelonoidsBC(ea.ConstraintBase): +class SolenoidsBC(ea.ConstraintBase): """ """ def __init__( @@ -102,7 +103,7 @@ def __init__( theta = 2.0 * number_of_rotations * np.pi - angel_vel_scalar = theta / self.twisting_time + angle_vel_scalar = theta / self.twisting_time direction = -(position_end - position_start) / np.linalg.norm( position_end - position_start @@ -120,45 +121,45 @@ def __init__( @ director_end ) # rotation_matrix wants vectors 3,1 - self.ang_vel = angel_vel_scalar * axis_of_rotation_in_material_frame + self.ang_vel = angle_vel_scalar * axis_of_rotation_in_material_frame self.position_start = position_start self.director_start = director_start def constrain_values(self, system, time): if time > self.twisting_time + self.time_twis_start: - rod.position_collection[..., 0] = self.position_start - rod.position_collection[0, -1] = 0.0 - rod.position_collection[2, -1] = 0.0 + system.position_collection[..., 0] = self.position_start + system.position_collection[0, -1] = 0.0 + system.position_collection[2, -1] = 0.0 - rod.director_collection[..., 0] = self.director_start - rod.director_collection[..., -1] = self.final_end_directors + system.director_collection[..., 0] = self.director_start + system.director_collection[..., -1] = self.final_end_directors def constrain_rates(self, system, time): if time > self.twisting_time + self.time_twis_start: - rod.velocity_collection[..., 0] = 0.0 - rod.omega_collection[..., 0] = 0.0 + system.velocity_collection[..., 0] = 0.0 + system.omega_collection[..., 0] = 0.0 - rod.velocity_collection[..., -1] = 0.0 - rod.omega_collection[..., -1] = 0.0 + system.velocity_collection[..., -1] = 0.0 + system.omega_collection[..., -1] = 0.0 elif time < self.time_twis_start: - rod.velocity_collection[..., 0] = 0.0 - rod.omega_collection[..., 0] = 0.0 + system.velocity_collection[..., 0] = 0.0 + system.omega_collection[..., 0] = 0.0 else: - rod.velocity_collection[..., 0] = 0.0 - rod.omega_collection[..., 0] = 0.0 + system.velocity_collection[..., 0] = 0.0 + system.omega_collection[..., 0] = 0.0 - rod.velocity_collection[0, -1] = 0.0 - rod.velocity_collection[2, -1] = 0.0 - rod.omega_collection[..., -1] = -self.ang_vel + system.velocity_collection[0, -1] = 0.0 + system.velocity_collection[2, -1] = 0.0 + system.omega_collection[..., -1] = -self.ang_vel - rod.velocity_collection[2, int(rod.n_elems / 2)] -= 1e-4 + system.velocity_collection[2, int(system.n_elems / 2)] -= 1e-4 solenoid_sim.constrain(sherable_rod).using( - SelonoidsBC, + SolenoidsBC, constrained_position_idx=(0, -1), constrained_director_idx=(0, -1), time_twis_start=time_start_twist, @@ -186,7 +187,7 @@ class RodCallBack(ea.CallBackBaseClass): """ """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -196,7 +197,9 @@ def make_callback(self, system, time, current_step: int): self.callback_params["step"].append(current_step) self.callback_params["position"].append(system.position_collection.copy()) self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["com"].append(system.compute_position_center_of_mass()) + self.callback_params["center_of_mass"].append( + system.compute_position_center_of_mass() + ) self.callback_params["com_velocity"].append( system.compute_velocity_center_of_mass() ) @@ -213,7 +216,7 @@ def make_callback(self, system, time, current_step: int): return -post_processing_dict = ea.defaultdict(list) # list which collected data will be append +post_processing_dict = defaultdict(list) # list which collected data will be append # set the diagnostics for rod and collect data solenoid_sim.collect_diagnostics(sherable_rod).using( RodCallBack, @@ -226,7 +229,10 @@ def make_callback(self, system, time, current_step: int): # Run the simulation time_stepper = ea.PositionVerlet() -ea.integrate(time_stepper, solenoid_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = time_stepper.step(solenoid_sim, time, dt) # plotting the videos filename_video = "solenoid.mp4" @@ -250,14 +256,14 @@ def make_callback(self, system, time, current_step: int): # Compute twist density theta = 2.0 * number_of_rotations * np.pi -angel_vel_scalar = theta / time_twist +angle_vel_scalar = theta / time_twist twist_time_interval_start_idx = np.where(time > time_start_twist)[0][0] twist_time_interval_end_idx = np.where(time < (time_relax + time_twist))[0][-1] twist_density = ( (time[twist_time_interval_start_idx:twist_time_interval_end_idx] - time_start_twist) - * angel_vel_scalar + * angle_vel_scalar * base_radius ) diff --git a/examples/RodContactCase/post_processing.py b/examples/RodContactCase/post_processing.py index 43f0764b8..c11928804 100644 --- a/examples/RodContactCase/post_processing.py +++ b/examples/RodContactCase/post_processing.py @@ -28,7 +28,9 @@ def plot_video_with_surface( rods_history[rod_idx]["radius"][t_idx], ) # Rod center of mass - com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx]["com"][time_idx] + com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx][ + "center_of_mass" + ][t_idx] # video pre-processing print("plot scene visualization video") diff --git a/examples/TimoshenkoBeamCase/README.md b/examples/TimoshenkoBeamCase/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/examples/TimoshenkoBeamCase/convergence_functions.py b/examples/TimoshenkoBeamCase/convergence_functions.py new file mode 100644 index 000000000..ca81417f4 --- /dev/null +++ b/examples/TimoshenkoBeamCase/convergence_functions.py @@ -0,0 +1,61 @@ +import numpy as np +from matplotlib import pyplot as plt +from matplotlib.colors import to_rgb +from scipy.linalg import norm + + +def calculate_error_norm(true_solution, computed_solution, n_elem): + assert ( + true_solution.shape == computed_solution.shape + ), "Shape of computed and true solution does not match" + error = true_solution - computed_solution + l1 = norm(error, 1) / n_elem + l2 = norm(error, 2) / n_elem + linf = norm(error, np.inf) + + return error, l1, l2, linf + + +def plot_convergence(results, SAVE_FIGURE, filename): + convergence_elements = [] + l1 = [] + l2 = [] + linf = [] + + for result in results: + convergence_elements.append(result["rod"].n_elems) + l1.append(result["l1"]) + l2.append(result["l2"]) + linf.append(result["linf"]) + + fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150) + ax = fig.add_subplot(111) + ax.grid(which="minor", color="k", linestyle="--") + ax.grid(which="major", color="k", linestyle="-") + ax.loglog( + convergence_elements, + l1, + marker="o", + ms=10, + c=to_rgb("xkcd:bluish"), + lw=2, + label="l1", + ) + ax.loglog( + convergence_elements, + l2, + marker="o", + ms=10, + c=to_rgb("xkcd:reddish"), + lw=2, + label="l2", + ) + ax.loglog(convergence_elements, linf, marker="o", ms=10, c="k", lw=2, label="linf") + ax.set_xlabel("N_element") + ax.set_ylabel("Error") + ax.set_title("Error Convergence Analysis") + fig.legend(prop={"size": 20}) + if SAVE_FIGURE: + assert filename != "", "provide a file name for figure" + fig.savefig(filename) + fig.show() diff --git a/examples/TimoshenkoBeamCase/convergence_timoshenko.py b/examples/TimoshenkoBeamCase/convergence_timoshenko.py index ee3262a3c..5b06f9ec6 100644 --- a/examples/TimoshenkoBeamCase/convergence_timoshenko.py +++ b/examples/TimoshenkoBeamCase/convergence_timoshenko.py @@ -3,11 +3,12 @@ import numpy as np import elastica as ea -from examples.TimoshenkoBeamCase.timoshenko_postprocessing import ( + +from timoshenko_postprocessing import ( plot_timoshenko, analytical_shearable, ) -from examples.convergence_functions import calculate_error_norm, plot_convergence +from convergence_functions import calculate_error_norm, plot_convergence class TimoshenkoBeamSimulator( @@ -112,7 +113,10 @@ def simulate_timoshenko_beam_with( dt = 0.01 * dl total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, timoshenko_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(timoshenko_sim, time, dt) if PLOT_FIGURE: plot_timoshenko(shearable_rod, end_force, SAVE_FIGURE, ADD_UNSHEARABLE_ROD) diff --git a/examples/TimoshenkoBeamCase/run_timoshenko.py b/examples/TimoshenkoBeamCase/run_timoshenko.py new file mode 100644 index 000000000..6a9aa29f6 --- /dev/null +++ b/examples/TimoshenkoBeamCase/run_timoshenko.py @@ -0,0 +1,227 @@ +""" +Timoshenko Beam +=============== + +Timoshenko beam validation case, for detailed explanation refer to +Gazzola et. al. R. Soc. 2018 section 3.4.3 + +This Elastica tutorial explains the basics of setting up and running a simple simulation of rods in Elastica. Elastica simulates Cosserat Rods, which are thin, 1-dimensional rods that undergo all possible modes of deformation. This example considers a Timoshenko beam, which is the deformation of a beam under a constant applied force while accounting for shear deforation and rotational bending. This is a good example of the capabilities of Elastica and Cosserat Rods as it requires accounting for the effects of shear deformation, something that the classical Euler-Bernoulli beam solution does not. + +.. image:: ../../../assets/timoshenko_beam_figure.png + +Getting Started +--------------- + +To set up the simulation, the first thing you need to do is import the necessary classes. Here we will only import the classes that we need. The `elastica.modules` classes make it easy to construct different simulation systems. Along with these modules, we need to import a rod class, classes for the boundary conditions, and time-stepping functions. As a note, this method of explicitly importing all classes can be a bit cumbersome. Future releases will simplify this step. +""" + +import numpy as np +from collections import defaultdict +import elastica as ea +from elastica.version import VERSION + +from timoshenko_postprocessing import plot_timoshenko + +# %% +# Now that we have imported all the necessary classes, we want to create our beam system. We do this by combining all the modules we need to represent the physics that we to include in the simulation. In this case, that is the ``BaseSystemCollection``, ``Constraint``, ``Forcings`` and ``Damping`` because the simulation will consider a rod that is fixed in place on one end, and subject to an applied force on the other end. + + +class TimoshenkoBeamSimulator( + ea.BaseSystemCollection, ea.Constraints, ea.Forcing, ea.CallBacks, ea.Damping +): + pass + + +timoshenko_sim = TimoshenkoBeamSimulator() + +# %% +# Creating Rods +# ------------- +# With our simulator set up, we can now define the numerical, material, and geometric properties. +# +# First we define the number of elements in the rod. Next, the material properties are defined for every rod. These are the Young's modulus, the Poisson ratio, the density and the viscous damping coefficient. Finally, the geometry of the rod also needs to be defined by specifying the location of the rod and its orientation, length and radius. +# +# All of the values defined here are done in SI units, though this is not strictly necessary. You can rescale properties however you want, as long as you use consistent units throughout the simulation. See `here `_ for an example of consistent units. +# +# In order to make the difference between a shearable and unshearable rod more clear, we are using a Poisson ratio of 99. This is an unphysical value, as Poisson ratios can not exceed 0.5, however, it is used here for demonstration purposes. + +# setting up test params +simulation_time = 500 # 5000.0 # (sec) + +n_elem = 100 +start = np.zeros((3,)) +direction = np.array([0.0, 0.0, 1.0]) +normal = np.array([0.0, 1.0, 0.0]) +base_length = 3.0 +base_radius = 0.25 +base_area = np.pi * base_radius**2 +density = 5000 +nu = 0.1 / 7 / density / base_area +E = 1e6 +# For shear modulus of 1e4, nu is 99! +poisson_ratio = 99 +shear_modulus = E / (poisson_ratio + 1.0) + +# %% +# With all of the rod's parameters set, we can now create a rod with the specificed properties and add the rod to the simulator system. **Important:** Make sure that any rods you create get added to the simulator system (``timoshenko_sim``), otherwise they will not be included in your simulation. + +shearable_rod = ea.CosseratRod.straight_rod( + n_elem, + start, + direction, + normal, + base_length, + base_radius, + density, + youngs_modulus=E, + shear_modulus=shear_modulus, +) +timoshenko_sim.append(shearable_rod) + +# %% +# Adding Damping +# -------------- +# With the rod added to the simulator, we can add damping to the rod. We do this using the ``.dampen()`` option and the ``AnalyticalLinearDamper``. We are modifying ``timoshenko_sim`` simulator to ``dampen`` the ``shearable_rod`` object using ``AnalyticalLinearDamper`` type of dissipation (damping) model. +# +# We also need to define ``damping_constant`` and simulation ``time_step`` and pass in ``.using()`` method. + +dl = base_length / n_elem +dt = 0.07 * dl +timoshenko_sim.dampen(shearable_rod).using( + ea.AnalyticalLinearDamper, + damping_constant=nu, + time_step=dt, +) + +# %% +# Adding Boundary Conditions +# -------------------------- +# With the rod added to the system, we need to apply boundary conditions. The first condition we will apply is fixing the location of one end of the rod. We do this using the ``.constrain()`` option and the ``OneEndFixedRod`` boundary condition. We are modifying the ``timoshenko_sim`` simulator to ``constrain`` the ``shearable_rod`` object using the ``OneEndFixedRod`` type of constraint. +# +# We also need to define which node of the rod is being constrained. We do this by passing the index of the nodes that we want to constrain to ``constrained_position_idx``. Here we are fixing the first node in the rod. In order to keep the rod from rotating around the fixed node, we also need to constrain an element between two nodes. This fixes the orientation of the rod. We do this by passing the index of the element that we want to fix to ``constrained_director_idx``. Like with the position, we are fixing the first element of the rod. Together, this constrains the position and orientation of the rod at the origin. + +# One end of the rod is now fixed in place +timoshenko_sim.constrain(shearable_rod).using( + ea.OneEndFixedBC, constrained_position_idx=(0,), constrained_director_idx=(0,) +) + +# %% +# The next boundary condition that we want to apply is the endpoint force. Similarly to how we constrained one of the points, we want the ``timoshenko_sim`` simulator to ``add_forcing_to`` the ``shearable_rod`` object using the ``EndpointForces`` type of forcing. This ``EndpointForces`` applies forces to both ends of the rod. We want to apply a negative force in the :math:`d_1` direction, but only at the end of the rod. We do this by specifying the force vector to be applied at each end as ``origin_force`` and ``end_force``. We also want to ramp up the force over time, so we make the force take some ``ramp_up_time`` to reach its steady-state value. This helps avoid numerical errors due to discontinuities in the applied force. + +# Forces added to the rod +end_force = np.array([-15.0, 0.0, 0.0]) +timoshenko_sim.add_forcing_to(shearable_rod).using( + ea.EndpointForces, 0.0 * end_force, end_force, ramp_up_time=simulation_time / 2.0 +) + +# %% +# Add Unshearable Rod +# ------------------- +# +# Along with the shearable rod, we also want to add an unshearable rod to be able to compare the difference between the two. We do this the same way we did for the first rod, however, because this rod is unsherable, we need to change the Poisson ratio to make the rod unsherable. For a truely unsheraable rod, you would need a Poisson ratio of -1.0, however, this causes the system to be numerically unstable, so instead we make the system nearly unshearable by using a Poisson ratio of -0.85. + +# Start into the plane +unshearable_start = np.array([0.0, -1.0, 0.0]) +shear_modulus = E / (-0.7 + 1.0) +unshearable_rod = ea.CosseratRod.straight_rod( + n_elem, + unshearable_start, + direction, + normal, + base_length, + base_radius, + density, + youngs_modulus=E, + # Unshearable rod needs G -> inf, which is achievable with -ve poisson ratio + shear_modulus=shear_modulus, +) + +timoshenko_sim.append(unshearable_rod) + +# add damping +timoshenko_sim.dampen(unshearable_rod).using( + ea.AnalyticalLinearDamper, + damping_constant=nu, + time_step=dt, +) +# add boundary conditions +timoshenko_sim.constrain(unshearable_rod).using( + ea.OneEndFixedBC, constrained_position_idx=(0,), constrained_director_idx=(0,) +) +timoshenko_sim.add_forcing_to(unshearable_rod).using( + ea.EndpointForces, 0.0 * end_force, end_force, ramp_up_time=simulation_time / 2.0 +) + +# %% +# Collect Data +# ------------ + + +# Add call backs +class VelocityCallBack(ea.CallBackBaseClass): + """ + Tracks the velocity norms of the rod + """ + + def __init__(self, step_skip: int, callback_params: dict): + super().__init__() + self.every = step_skip + self.callback_params = callback_params + + def make_callback(self, system, time, current_step: int): + + if current_step % self.every == 0: + + self.callback_params["time"].append(time) + # Collect x + self.callback_params["velocity_norms"].append( + np.linalg.norm(system.velocity_collection.copy()) + ) + return + + +recorded_history = defaultdict(list) +timoshenko_sim.collect_diagnostics(shearable_rod).using( + VelocityCallBack, step_skip=500, callback_params=recorded_history +) + + +# %% +# System Finalization +# ------------------- +# We have now added all the necessary rods and boundary conditions to our system. The last thing we need to do is finalize the system. This goes through the system, rearranges things, and precomputes useful quantities to prepare the system for simulation. +# +# As a note, if you make any changes to the rod after calling finalize, you will need to re-setup the system. This requires rerunning all cells above this point. + + +timoshenko_sim.finalize() + +# %% +# Define Simulation Time +# ---------------------- +# The last thing we need to do decide how long we want the simulation to run for and what timestepping method to use. Currently, the PositionVerlet algorithim is suggested default method. +# +# In this example, we are trying to match a steady-state solution by temporally evolving our system to reach equilibrium. As such, there is a tradeoff between letting the simulation run long enough to reach the equilibrium and waiting around for the simulation to be done. Here we are running the simulation for 10 seconds, this produces reasonable agreement with the analytical solution without taking to long to finish. If you run the simulation for longer, you will get better agreement with the analytical solution. + +timestepper = ea.PositionVerlet() +# timestepper = PEFRL() + +total_steps = int(simulation_time / dt) +print("Total steps", total_steps) + +# %% +# Run Simulation +# -------------- +# +# We are now ready to perform the simulation. To run the simulation, we ``integrate`` the ``timoshenko_sim`` system using the ``timestepper`` method until ``final_time`` by taking ``total_steps``. As currently setup, the beam simulation takes about 1 minute to run. + +time = 0.0 +for i in range(total_steps): + time = timestepper.step(timoshenko_sim, time, dt) + +# %% +# Post Processing Results +# ----------------------- +# Now that we have finished the simulation, we want to post-process the results. Post processing script is separately provided in ``timoshenko_postprocessing.py``. + +plot_timoshenko(shearable_rod, end_force, False, True) diff --git a/examples/TimoshenkoBeamCase/timoshenko.py b/examples/TimoshenkoBeamCase/timoshenko.py deleted file mode 100644 index 5f04f8031..000000000 --- a/examples/TimoshenkoBeamCase/timoshenko.py +++ /dev/null @@ -1,163 +0,0 @@ -__doc__ = """Timoshenko beam validation case, for detailed explanation refer to -Gazzola et. al. R. Soc. 2018 section 3.4.3 """ - -import numpy as np -import elastica as ea -from examples.TimoshenkoBeamCase.timoshenko_postprocessing import plot_timoshenko - - -class TimoshenkoBeamSimulator( - ea.BaseSystemCollection, ea.Constraints, ea.Forcing, ea.CallBacks, ea.Damping -): - pass - - -timoshenko_sim = TimoshenkoBeamSimulator() -final_time = 5000.0 - -# Options -PLOT_FIGURE = True -SAVE_FIGURE = True -SAVE_RESULTS = False -ADD_UNSHEARABLE_ROD = False - -# setting up test params -n_elem = 100 -start = np.zeros((3,)) -direction = np.array([0.0, 0.0, 1.0]) -normal = np.array([0.0, 1.0, 0.0]) -base_length = 3.0 -base_radius = 0.25 -base_area = np.pi * base_radius**2 -density = 5000 -nu = 0.1 / 7 / density / base_area -E = 1e6 -# For shear modulus of 1e4, nu is 99! -poisson_ratio = 99 -shear_modulus = E / (poisson_ratio + 1.0) - -shearable_rod = ea.CosseratRod.straight_rod( - n_elem, - start, - direction, - normal, - base_length, - base_radius, - density, - youngs_modulus=E, - shear_modulus=shear_modulus, -) - -timoshenko_sim.append(shearable_rod) -# add damping -dl = base_length / n_elem -dt = 0.07 * dl -timoshenko_sim.dampen(shearable_rod).using( - ea.AnalyticalLinearDamper, - damping_constant=nu, - time_step=dt, -) - -timoshenko_sim.constrain(shearable_rod).using( - ea.OneEndFixedBC, constrained_position_idx=(0,), constrained_director_idx=(0,) -) - -end_force = np.array([-15.0, 0.0, 0.0]) -timoshenko_sim.add_forcing_to(shearable_rod).using( - ea.EndpointForces, 0.0 * end_force, end_force, ramp_up_time=final_time / 2.0 -) - - -if ADD_UNSHEARABLE_ROD: - # Start into the plane - unshearable_start = np.array([0.0, -1.0, 0.0]) - shear_modulus = E / (-0.7 + 1.0) - unshearable_rod = ea.CosseratRod.straight_rod( - n_elem, - unshearable_start, - direction, - normal, - base_length, - base_radius, - density, - youngs_modulus=E, - # Unshearable rod needs G -> inf, which is achievable with -ve poisson ratio - shear_modulus=shear_modulus, - ) - - timoshenko_sim.append(unshearable_rod) - - # add damping - timoshenko_sim.dampen(unshearable_rod).using( - ea.AnalyticalLinearDamper, - damping_constant=nu, - time_step=dt, - ) - timoshenko_sim.constrain(unshearable_rod).using( - ea.OneEndFixedBC, constrained_position_idx=(0,), constrained_director_idx=(0,) - ) - timoshenko_sim.add_forcing_to(unshearable_rod).using( - ea.EndpointForces, 0.0 * end_force, end_force, ramp_up_time=final_time / 2.0 - ) - - -# Add call backs -class VelocityCallBack(ea.CallBackBaseClass): - """ - Tracks the velocity norms of the rod - """ - - def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) - self.every = step_skip - self.callback_params = callback_params - - def make_callback(self, system, time, current_step: int): - - if current_step % self.every == 0: - - self.callback_params["time"].append(time) - # Collect x - self.callback_params["velocity_norms"].append( - np.linalg.norm(system.velocity_collection.copy()) - ) - return - - -recorded_history = ea.defaultdict(list) -timoshenko_sim.collect_diagnostics(shearable_rod).using( - VelocityCallBack, step_skip=500, callback_params=recorded_history -) - -timoshenko_sim.finalize() -timestepper = ea.PositionVerlet() -# timestepper = PEFRL() - -total_steps = int(final_time / dt) -print("Total steps", total_steps) -ea.integrate(timestepper, timoshenko_sim, final_time, total_steps) - -if PLOT_FIGURE: - plot_timoshenko(shearable_rod, end_force, SAVE_FIGURE, ADD_UNSHEARABLE_ROD) - -if SAVE_RESULTS: - import pickle - - filename = "Timoshenko_beam_data.dat" - file = open(filename, "wb") - pickle.dump(shearable_rod, file) - file.close() - - tv = ( - np.asarray(recorded_history["time"]), - np.asarray(recorded_history["velocity_norms"]), - ) - - def as_time_series(v): - return v.T - - np.savetxt( - "velocity_norms.csv", - as_time_series(np.stack(tv)), - delimiter=",", - ) diff --git a/examples/TimoshenkoBeamCase/timoshenko_postprocessing.py b/examples/TimoshenkoBeamCase/timoshenko_postprocessing.py index b222d5081..ebf593c09 100644 --- a/examples/TimoshenkoBeamCase/timoshenko_postprocessing.py +++ b/examples/TimoshenkoBeamCase/timoshenko_postprocessing.py @@ -51,10 +51,10 @@ def plot_timoshenko(rod, end_force, SAVE_FIGURE, ADD_UNSHEARABLE_ROD=False): ax = fig.add_subplot(111) ax.grid(which="minor", color="k", linestyle="--") ax.grid(which="major", color="k", linestyle="-") - analytical_shearable_positon = analytical_shearable(rod, end_force) + analytical_shearable_position = analytical_shearable(rod, end_force) ax.plot( - analytical_shearable_positon[0], - analytical_shearable_positon[1], + analytical_shearable_position[0], + analytical_shearable_position[1], "k--", label="Timoshenko", ) @@ -65,10 +65,10 @@ def plot_timoshenko(rod, end_force, SAVE_FIGURE, ADD_UNSHEARABLE_ROD=False): label="n=" + str(rod.n_elems), ) if ADD_UNSHEARABLE_ROD: - analytical_unshearable_positon = analytical_unshearable(rod, end_force) + analytical_unshearable_position = analytical_unshearable(rod, end_force) ax.plot( - analytical_unshearable_positon[0], - analytical_unshearable_positon[1], + analytical_unshearable_position[0], + analytical_unshearable_position[1], "r-.", label="Euler-Bernoulli", ) diff --git a/examples/TumblingUnconstrainedRod/forces.py b/examples/TumblingUnconstrainedRod/forces.py index 583301543..0155437cd 100644 --- a/examples/TumblingUnconstrainedRod/forces.py +++ b/examples/TumblingUnconstrainedRod/forces.py @@ -2,8 +2,6 @@ from elastica.external_forces import NoForces from elastica.typing import SystemType -from tqdm import tqdm - class EndpointforcesWithTimeFactor(NoForces): @@ -29,14 +27,12 @@ def __init__(self, torque, time_factor, direction=np.array([0.0, 0.0, 0.0])): self.time_factor = time_factor def apply_torques(self, system: SystemType, time: np.float64 = 0.0): - n_elems = system.n_elems - factor = self.time_factor(time) system.external_torques[..., -1] += self.torque * factor -def lamda_t_function(time): +def time_factor_function(time): if time < 2.5: factor = time * (1 / 2.5) elif time > 2.5 and time < 5.0: diff --git a/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod.py b/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod.py index f5bfae5af..b250243ca 100644 --- a/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod.py +++ b/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod.py @@ -1,17 +1,17 @@ import json import numpy as np +from collections import defaultdict from matplotlib import pyplot as plt import elastica as ea from elastica.timestepper.symplectic_steppers import PositionVerlet -from elastica.timestepper import integrate -from elastica.external_forces import UniformTorques +from tqdm import tqdm from forces import ( EndpointforcesWithTimeFactor, EndpointtorqueWithTimeFactor, - lamda_t_function, + time_factor_function, ) from tumbling_unconstrained_rod_postprocessing import ( plot_video_with_surface, @@ -20,7 +20,6 @@ n_elem = 256 start = np.array([0.0, 0.0, 8.0]) -end = np.array([6.0, 0.0, 0.0]) direction = np.array([0.6, 0.0, -0.8]) normal = np.array([0.0, 1.0, 0.0]) base_length = 10 @@ -70,14 +69,14 @@ class NonConstrainRodSimulator( end_force = np.array([20.0, 0.0, 0.0]) square_rod_sim.add_forcing_to(square_rod).using( - EndpointforcesWithTimeFactor, origin_force, end_force, lamda_t_function + EndpointforcesWithTimeFactor, origin_force, end_force, time_factor_function ) square_rod_sim.add_forcing_to(square_rod).using( EndpointtorqueWithTimeFactor, 1, - lamda_t_function, + time_factor_function, direction=np.array([0.0, 200.0, -100.0]), ) @@ -100,90 +99,86 @@ class NonConstrainRodSimulator( class TumblingUnconstrainedRodCallBack(ea.CallBackBaseClass): def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) self.callback_params["position"].append(system.position_collection.copy()) self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["velocity"].append(system.velocity_collection.copy()) - self.callback_params["avg_velocity"].append( - system.compute_velocity_center_of_mass() - ) self.callback_params["center_of_mass"].append( system.compute_position_center_of_mass() ) -if __name__ == "__main__": - - recorded_history = ea.defaultdict(list) - square_rod_sim.collect_diagnostics(square_rod).using( - TumblingUnconstrainedRodCallBack, - step_skip=step_skip, - callback_params=recorded_history, - ) - - square_rod_sim.finalize() - print("System finalized") - - timestepper = PositionVerlet() - integrate(timestepper, square_rod_sim, final_time, total_steps) - - with open("TumblingUnconstrainedRod.json", "r") as file: - analytic_data = json.load(file) - - time_analytic = analytic_data["time_analytic"] - mass_center_analytic = analytic_data["mass_center_analytic"] - - plt.plot( - time_analytic, - mass_center_analytic[0], - marker="*", - color="black", - label="x_analytic", - ) - plt.plot( - time_analytic, - mass_center_analytic[1], - marker="*", - color="black", - label="y_analytic", - ) - plt.plot( - time_analytic, - mass_center_analytic[2], - marker="*", - color="black", - label="z_analytic", - ) - - mass_center = np.array(recorded_history["center_of_mass"]) - - plt.plot(recorded_history["time"][0:240], mass_center[:, 0][0:240], label="x") - plt.plot(recorded_history["time"][0:240], mass_center[:, 1][0:240], label="y") - plt.plot(recorded_history["time"][0:240], mass_center[:, 2][0:240], label="z") - - plt.xlabel("Time/(second)") # X-axis label - plt.ylabel("Center of mass") # Y-axis label - plt.grid() - plt.legend() # Optional: Add a grid - plt.show() - - plot_video_with_surface( - [recorded_history], - video_name="Tumbling_Unconstrained_Rod.mp4", - fps=rendering_fps, - step=1, - # The following parameters are optional - x_limits=(0, 200), # Set bounds on x-axis - y_limits=(-4, 4), # Set bounds on y-axis - z_limits=(0.0, 8), # Set bounds on z-axis - dpi=100, # Set the quality of the image - vis3D=True, # Turn on 3D visualization - vis2D=False, # Turn on projected (2D) visualization - ) +recorded_history = defaultdict(list) +square_rod_sim.collect_diagnostics(square_rod).using( + TumblingUnconstrainedRodCallBack, + step_skip=step_skip, + callback_params=recorded_history, +) + +square_rod_sim.finalize() +print("System finalized") + +timestepper = PositionVerlet() + +time = 0.0 +for i in tqdm(range(total_steps)): + time = timestepper.step(square_rod_sim, time, dt) + +with open("TumblingUnconstrainedRod.json", "r") as file: + analytic_data = json.load(file) + +time_analytic = analytic_data["time_analytic"] +mass_center_analytic = analytic_data["mass_center_analytic"] + +plt.plot( + time_analytic, + mass_center_analytic[0], + marker="*", + color="black", + label="x_analytic", +) +plt.plot( + time_analytic, + mass_center_analytic[1], + marker="*", + color="black", + label="y_analytic", +) +plt.plot( + time_analytic, + mass_center_analytic[2], + marker="*", + color="black", + label="z_analytic", +) + +mass_center = np.array(recorded_history["center_of_mass"]) + +plt.plot(recorded_history["time"], mass_center[:, 0], label="x") +plt.plot(recorded_history["time"], mass_center[:, 1], label="y") +plt.plot(recorded_history["time"], mass_center[:, 2], label="z") + +plt.xlabel("Time (seconds)") +plt.ylabel("Center of mass") +plt.grid() +plt.legend() +plt.show() + +plot_video_with_surface( + [recorded_history], + video_name="Tumbling_Unconstrained_Rod.mp4", + fps=rendering_fps, + step=1, + # The following parameters are optional + x_limits=(0, 200), # Set bounds on x-axis + y_limits=(-4, 4), # Set bounds on y-axis + z_limits=(0.0, 8), # Set bounds on z-axis + dpi=100, # Set the quality of the image + vis3D=True, # Turn on 3D visualization + vis2D=False, # Turn on projected (2D) visualization +) diff --git a/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod_postprocessing.py b/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod_postprocessing.py index 5ce18bf4a..ceff240c0 100644 --- a/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod_postprocessing.py +++ b/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod_postprocessing.py @@ -36,7 +36,9 @@ def plot_video_with_surface( rods_history[rod_idx]["radius"][t_idx], ) # Rod center of mass - com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx]["com"][time_idx] + com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx][ + "center_of_mass" + ][t_idx] # Generate target sphere data sphere_flag = False @@ -83,8 +85,6 @@ def plot_video_with_surface( ax.view_init(elev=0, azim=0) time_idx = 0 - rod_lines = [None for _ in range(n_visualized_rods)] - rod_com_lines = [None for _ in range(n_visualized_rods)] rod_scatters = [None for _ in range(n_visualized_rods)] for rod_idx in range(n_visualized_rods): diff --git a/examples/Visualization/ContinuumSnakeVisualization/continuum_snake.py b/examples/Visualization/ContinuumSnakeVisualization/continuum_snake.py deleted file mode 100644 index 60e1867cf..000000000 --- a/examples/Visualization/ContinuumSnakeVisualization/continuum_snake.py +++ /dev/null @@ -1,151 +0,0 @@ -import numpy as np -from collections import defaultdict -import elastica as ea - - -class SnakeSimulator( - ea.BaseSystemCollection, ea.Constraints, ea.Forcing, ea.Damping, ea.CallBacks -): - pass - - -def run_snake(b_coeff, SAVE_RESULTS=False): - - snake_sim = SnakeSimulator() - - # setting up test params - n_elem = 20 - start = np.zeros((3,)) - direction = np.array([0.0, 0.0, 1.0]) - normal = np.array([0.0, 1.0, 0.0]) - base_length = 1.0 - base_radius = 0.025 - density = 1000 - E = 1e7 - poisson_ratio = 0.5 - shear_modulus = E / (poisson_ratio + 1.0) - - shearable_rod = ea.CosseratRod.straight_rod( - n_elem, - start, - direction, - normal, - base_length, - base_radius, - density, - youngs_modulus=E, - shear_modulus=shear_modulus, - ) - - snake_sim.append(shearable_rod) - - # Add gravitational forces - gravitational_acc = -9.80665 - snake_sim.add_forcing_to(shearable_rod).using( - ea.GravityForces, acc_gravity=np.array([0.0, gravitational_acc, 0.0]) - ) - - period = 1.0 - wave_length = b_coeff[-1] - snake_sim.add_forcing_to(shearable_rod).using( - ea.MuscleTorques, - base_length=base_length, - b_coeff=b_coeff[:-1], - period=period, - wave_number=2.0 * np.pi / (wave_length), - phase_shift=0.0, - direction=normal, - rest_lengths=shearable_rod.rest_lengths, - ramp_up_time=period, - with_spline=True, - ) - - # Add friction forces - origin_plane = np.array([0.0, -base_radius, 0.0]) - normal_plane = normal - slip_velocity_tol = 1e-8 - froude = 0.1 - mu = base_length / (period * period * np.abs(gravitational_acc) * froude) - kinetic_mu_array = np.array( - [mu, 1.5 * mu, 2.0 * mu] - ) # [forward, backward, sideways] - static_mu_array = 2 * kinetic_mu_array - snake_sim.add_forcing_to(shearable_rod).using( - ea.AnisotropicFrictionalPlane, - k=1.0, - nu=1e-6, - plane_origin=origin_plane, - plane_normal=normal_plane, - slip_velocity_tol=slip_velocity_tol, - static_mu_array=static_mu_array, - kinetic_mu_array=kinetic_mu_array, - ) - - # add damping - damping_constant = 5.0 - dt = 5.0e-5 * period - snake_sim.dampen(shearable_rod).using( - ea.AnalyticalLinearDamper, - damping_constant=damping_constant, - time_step=dt, - ) - - # Add call backs - class ContinuumSnakeCallBack(ea.CallBackBaseClass): - """ - Call back function for continuum snake - """ - - def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) - self.every = step_skip - self.callback_params = callback_params - - def make_callback(self, system, time, current_step: int): - - if current_step % self.every == 0: - - self.callback_params["time"].append(time) - self.callback_params["position"].append( - system.position_collection.copy() - ) - - return - - pp_list = defaultdict(list) - snake_sim.collect_diagnostics(shearable_rod).using( - ContinuumSnakeCallBack, step_skip=200, callback_params=pp_list - ) - - snake_sim.finalize() - timestepper = ea.PositionVerlet() - # timestepper = PEFRL() - - final_time = (11.0 + 0.01) * period - total_steps = int(final_time / dt) - print("Total steps", total_steps) - ea.integrate(timestepper, snake_sim, final_time, total_steps) - - if SAVE_RESULTS: - import pickle - - filename = "continuum_snake.dat" - file = open(filename, "wb") - pickle.dump(pp_list, file) - file.close() - - return pp_list - - -if __name__ == "__main__": - - # Options - SAVE_RESULTS = True - - # Add muscle forces on the rod - t_coeff_optimized = np.array([17.4, 48.5, 5.4, 14.7, 0.97]) - - # run the simulation - pp_list = run_snake(t_coeff_optimized, SAVE_RESULTS) - - print("Datafile Created") diff --git a/examples/Visualization/ContinuumSnakeVisualization/continuum_snake_render.py b/examples/Visualization/ContinuumSnakeVisualization/continuum_snake_render.py deleted file mode 100644 index 6c740e266..000000000 --- a/examples/Visualization/ContinuumSnakeVisualization/continuum_snake_render.py +++ /dev/null @@ -1,184 +0,0 @@ -"""Rendering Script using POVray - -This script reads simulated data file to render POVray animation movie. -The data file should contain dictionary of positions vectors and times. - -The script supports multiple camera position where a video is generated -for each camera view. - -Notes ------ - The module requires POVray installed. -""" - -import multiprocessing -import os -from functools import partial -from multiprocessing import Pool - -import numpy as np -from scipy import interpolate -from tqdm import tqdm - -from examples.Visualization._povmacros import Stages, pyelastica_rod, render - -# Setup (USER DEFINE) -DATA_PATH = "continuum_snake.dat" # Path to the simulation data -SAVE_PICKLE = True - -# Rendering Configuration (USER DEFINE) -OUTPUT_FILENAME = "pov_snake" -OUTPUT_IMAGES_DIR = "frames" -FPS = 20.0 -WIDTH = 1920 # 400 -HEIGHT = 1080 # 250 -DISPLAY_FRAMES = "Off" # Display povray images during the rendering. ['On', 'Off'] - -# Camera/Light Configuration (USER DEFINE) -stages = Stages() -stages.add_camera( - # Add diagonal viewpoint - location=[15.0, 10.5, -15.0], - angle=30, - look_at=[4.0, 2.7, 2.0], - name="diag", -) -stages.add_camera( - # Add top viewpoint - location=[0, 15, 3], - angle=30, - look_at=[0.0, 0, 3], - sky=[-1, 0, 0], - name="top", -) -stages.add_light( - # Sun light - position=[1500, 2500, -1000], - color="White", - camera_id=-1, -) -stages.add_light( - # Flash light for camera 0 - position=[15.0, 10.5, -15.0], - color=[0.09, 0.09, 0.1], - camera_id=0, -) -stages.add_light( - # Flash light for camera 1 - position=[0.0, 8.0, 5.0], - color=[0.09, 0.09, 0.1], - camera_id=1, -) -stage_scripts = stages.generate_scripts() - -# Externally Including Files (USER DEFINE) -# If user wants to include other POVray objects such as grid or coordinate axes, -# objects can be defined externally and included separately. -included = ["../default.inc"] - -# Multiprocessing Configuration (USER DEFINE) -MULTIPROCESSING = True -THREAD_PER_AGENT = 4 # Number of thread use per rendering process. -NUM_AGENT = multiprocessing.cpu_count() // 2 # number of parallel rendering. - -# Execute -if __name__ == "__main__": - # Load Data - assert os.path.exists(DATA_PATH), "File does not exists" - try: - if SAVE_PICKLE: - import pickle as pk - - with open(DATA_PATH, "rb") as fptr: - data = pk.load(fptr) - else: - # (TODO) add importing npz file format - raise NotImplementedError("Only pickled data is supported") - except OSError as err: - print("Cannot open the datafile {}".format(DATA_PATH)) - print(str(err)) - raise - - # Convert data to numpy array - times = np.array(data["time"]) # shape: (timelength) - xs = np.array(data["position"]) # shape: (timelength, 3, num_element) - - # Interpolate Data - # Interpolation step serves two purposes. If simulated frame rate is lower than - # the video frame rate, the intermediate frames are linearly interpolated to - # produce smooth video. Otherwise if simulated frame rate is higher than - # the video frame rate, interpolation reduces the number of frame to reduce - # the rendering time. - runtime = times.max() # Physical run time - total_frame = int(runtime * FPS) # Number of frames for the video - recorded_frame = times.shape[0] # Number of simulated frames - times_true = np.linspace(0, runtime, total_frame) # Adjusted timescale - - xs = interpolate.interp1d(times, xs, axis=0)(times_true) - times = interpolate.interp1d(times, times, axis=0)(times_true) - base_radius = np.ones_like(xs[:, 0, :]) * 0.050 # (TODO) radius could change - - # Rendering - # For each frame, a 'pov' script file is generated in OUTPUT_IMAGE_DIR directory. - batch = [] - for view_name in stage_scripts.keys(): # Make Directory - output_path = os.path.join(OUTPUT_IMAGES_DIR, view_name) - os.makedirs(output_path, exist_ok=True) - for frame_number in tqdm(range(total_frame), desc="Scripting"): - for view_name, stage_script in stage_scripts.items(): - output_path = os.path.join(OUTPUT_IMAGES_DIR, view_name) - - # Colect povray scripts - script = [] - script.extend(['#include "{}"'.format(s) for s in included]) - script.append(stage_script) - - # If the data contains multiple rod, this part can be modified to include - # multiple rods. - rod_object = pyelastica_rod( - x=xs[frame_number], - r=base_radius[frame_number], - color="rgb<0.45,0.39,1>", - ) - script.append(rod_object) - pov_script = "\n".join(script) - - # Write .pov script file - file_path = os.path.join(output_path, "frame_{:04d}".format(frame_number)) - with open(file_path + ".pov", "w+") as f: - f.write(pov_script) - batch.append(file_path) - - # Process POVray - # For each frames, a 'png' image file is generated in OUTPUT_IMAGE_DIR directory. - pbar = tqdm(total=len(batch), desc="Rendering") # Progress Bar - if MULTIPROCESSING: - func = partial( - render, - width=WIDTH, - height=HEIGHT, - display=DISPLAY_FRAMES, - pov_thread=THREAD_PER_AGENT, - ) - with Pool(NUM_AGENT) as p: - for message in p.imap_unordered(func, batch): - # (TODO) POVray error within child process could be an issue - pbar.update() - else: - for filename in batch: - render( - filename, - width=WIDTH, - height=HEIGHT, - display=DISPLAY_FRAMES, - pov_thread=multiprocessing.cpu_count(), - ) - pbar.update() - - # Create Video using ffmpeg - for view_name in stage_scripts.keys(): - imageset_path = os.path.join(OUTPUT_IMAGES_DIR, view_name) - - filename = OUTPUT_FILENAME + "_" + view_name + ".mp4" - - os.system(f"ffmpeg -r {FPS} -i {imageset_path}/frame_%04d.png {filename}") diff --git a/examples/Visualization/README.md b/examples/Visualization/README.md deleted file mode 100644 index a9b6f6f4f..000000000 --- a/examples/Visualization/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# POVray Visualization Example - -A simple example of rendering pyelastica using POVray. -The code [render](continuum_snake_render.py) generates POVray script (.pov) and image file (.png) to render POVray animation. - -### Bash Script to Run -``` bash -python continuum_snake.py # Creates continuum_snake.dat file -python continuum_snake_render.py # Creates pov_snake_diag.mp4 and pov_snake_top.mp4 file (3-5 minutes) -``` - -### Dependency -- povray -- ffmpeg diff --git a/examples/Visualization/_povmacros.py b/examples/Visualization/_povmacros.py deleted file mode 100644 index 102524456..000000000 --- a/examples/Visualization/_povmacros.py +++ /dev/null @@ -1,353 +0,0 @@ -"""POVray macros for pyelastica - -This module includes utility methods to support POVray rendering. - -""" - -import subprocess -from collections import defaultdict - - -def pyelastica_rod( - x, - r, - color="rgb<0.45,0.39,1>", - transmit=0.0, - interpolation="linear_spline", - deform=None, - tab=" ", -): - """pyelastica_rod POVray script generator - - Generates povray sphere_sweep object in string. - The rod is given with the element radius (r) and joint positions (x) - - Parameters - ---------- - x : numpy array - Position vector - Expected shape: [num_time_step, 3, num_element] - r : numpy array - Radius vector - Expected shape: [num_time_step, num_element] - color : str - Color of the rod (default: Purple <0.45,0.39,1>) - transmit : float - Transparency (0.0 to 1.0). - interpolation : str - Interpolation method for sphere_sweep - Supporting type: 'linear_spline', 'b_spline', 'cubic_spline' - (default: linear_spline) - deform : str - Additional object deformation - Example: "scale<4,4,4> rotate<0,90,90> translate<2,0,4>" - - Returns - ------- - cmd : string - Povray script - """ - - assert interpolation in ["linear_spline", "b_spline", "cubic_spline"] - tab = " " - - # Parameters - num_element = r.shape[0] - - lines = [] - lines.append("sphere_sweep {") - lines.append(tab + f"{interpolation} {num_element}") - for i in range(num_element): - lines.append(tab + f",<{x[0,i]},{x[1,i]},{x[2,i]}>,{r[i]}") - lines.append(tab + "texture{") - lines.append(tab + tab + "pigment{ color %s transmit %f }" % (color, transmit)) - lines.append(tab + tab + "finish{ phong 1 }") - lines.append(tab + "}") - if deform is not None: - lines.append(tab + deform) - lines.append(tab + "}\n") - - cmd = "\n".join(lines) - return cmd - - -def render( - filename, width, height, antialias="on", quality=11, display="Off", pov_thread=4 -): - """Rendering frame - - Generate the povray script file '.pov' and image file '.png' - The directory must be made before calling this method. - - Parameters - ---------- - filename : str - POV filename (without extension) - width : int - The width of the output image. - height : int - The height of the output image. - antialias : str ['on', 'off'] - Turns anti-aliasing on/off [default='on'] - quality : int - Image output quality. [default=11] - display : str - Turns display option on/off during POVray rendering. [default='off'] - pov_thread : int - Number of thread per povray process. [default=4] - Acceptable range is (4,512). - Refer 'Symmetric Multiprocessing (SMP)' for further details - https://www.povray.org/documentation/3.7.0/r3_2.html#r3_2_8_1 - - Raises - ------ - IOError - If the povray run causes unexpected error, such as parsing error, - this method will raise IOerror. - - """ - - # Define script path and image path - script_file = filename + ".pov" - image_file = filename + ".png" - - # Run Povray as subprocess - cmds = [ - "povray", - "+I" + script_file, - "+O" + image_file, - f"-H{height}", - f"-W{width}", - f"Work_Threads={pov_thread}", - f"Antialias={antialias}", - f"Quality={quality}", - f"Display={display}", - ] - process = subprocess.Popen( - cmds, stderr=subprocess.PIPE, stdin=subprocess.PIPE, stdout=subprocess.PIPE - ) - _, stderr = process.communicate() - - # Check execution error - if process.returncode: - print(type(stderr), stderr) - raise IOError( - "POVRay rendering failed with the following error: " - + stderr.decode("ascii") - ) - - -class Stages: - """Stage definition - - Collection of the camera and light sources. - Each camera added to the stage represent distinct viewpoints to render. - Lights can be assigned to multiple cameras. - The povray script can be generated for each viewpoints created using 'generate_scripts.' - - (TODO) Implement transform camera for dynamic camera moves - - Attributes - ---------- - pre_scripts : str - Prepending script for all viewpoints - post_scripts : str - Appending script for all viewpoints - cameras : list - List of camera setup - lights : list - List of lightings - _light_assign : dictionary[list] - Dictionary that pairs lighting to camera. - Example) _light_assign[2] is the list of light sources - assigned to the cameras[2] - - Methods - ------- - add_camera : Add new camera (viewpoint) to the stage. - add_light : Add new light source to the stage for a assigned camera. - generate_scripts : Generate list of povray script for each camera. - - Class Objects - ------------- - StageObject - Camera - Light - - Properties - ---------- - len : number of camera - The number of viewpoints - """ - - def __init__(self, pre_scripts="", post_scripts=""): - self.pre_scripts = pre_scripts - self.post_scripts = post_scripts - self.cameras = [] - self.lights = [] - self._light_assign = defaultdict(list) - - def add_camera(self, name, **kwargs): - """Add camera (viewpoint)""" - self.cameras.append(self.Camera(name=name, **kwargs)) - - def add_light(self, camera_id=-1, **kwargs): - """Add lighting and assign to camera - Parameters - ---------- - camera_id : int or list - Assigned camera. [default=-1] - If a list of camera_id is given, light is assigned for listed camera. - If camera_id==-1, the lighting is assigned for all camera. - """ - light_id = len(self.lights) - self.lights.append(self.Light(**kwargs)) - if isinstance(camera_id, list) or isinstance(camera_id, tuple): - camera_id = list(set(camera_id)) - for idx in camera_id: - self._light_assign[idx].append(light_id) - elif isinstance(camera_id, int): - self._light_assign[camera_id].append(light_id) - else: - raise NotImplementedError("camera_id can only be a list or int") - - def generate_scripts(self): - """Generate pov-ray script for all camera setup - Returns - ------- - scripts : list - Return list of pov-scripts (string) that includes camera and assigned lightings. - """ - scripts = {} - for idx, camera in enumerate(self.cameras): - light_ids = self._light_assign[idx] + self._light_assign[-1] - cmds = [] - cmds.append(self.pre_scripts) - cmds.append(str(camera)) # Script camera - for light_id in light_ids: # Script Lightings - cmds.append(str(self.lights[light_id])) - cmds.append(self.post_scripts) - scripts[camera.name] = "\n".join(cmds) - return scripts - - def transform_camera(self, dx, R, camera_id): - # (TODO) translate or rotate the assigned camera - raise NotImplementedError - - def __len_(self): - return len(self.cameras) - - # Stage Objects: Camera, Light - class StageObject: - """Template for stage objects - - Objects (camera and light) is defined as an object in order to - manipulate (translate or rotate) them during the rendering. - - Attributes - ---------- - str : str - String representation of object. - The placeholder exist to avoid rescripting. - - Methods - ------- - _color2str : str - Change triplet tuple (or list) of color into rgb string. - _position2str : str - Change triplet tuple (or list) of position vector into string. - """ - - def __init__(self): - self.str = "" - self.update_script() - - def update_script(self): - raise NotImplementedError - - def __str__(self): - return self.str - - def _color2str(self, color): - if isinstance(color, str): - return color - elif isinstance(color, list) and len(color) == 3: - # RGB - return "rgb<{},{},{}>".format(*color) - else: - raise NotImplementedError( - "Only string-type color or RGB input is implemented" - ) - - def _position2str(self, position): - assert len(position) == 3 - return "<{},{},{}>".format(*position) - - class Camera(StageObject): - """Camera object - - http://www.povray.org/documentation/view/3.7.0/246/ - - Attributes - ---------- - location : list or tuple - Position vector of camera location. (length=3) - angle : int - Camera angle - look_at : list or tuple - Position vector of the location where camera points to (length=3) - name : str - Name of the view-point. - sky : list or tuple - Tilt of the camera (length=3) [default=[0,1,0]] - """ - - def __init__(self, name, location, angle, look_at, sky=(0, 1, 0)): - self.name = name - self.location = location - self.angle = angle - self.look_at = look_at - self.sky = sky - super().__init__() - - def update_script(self): - location = self._position2str(self.location) - look_at = self._position2str(self.look_at) - sky = self._position2str(self.sky) - cmds = [] - cmds.append("camera{") - cmds.append(f" location {location}") - cmds.append(f" angle {self.angle}") - cmds.append(f" look_at {look_at}") - cmds.append(f" sky {sky}") - cmds.append(" right x*image_width/image_height") - cmds.append("}") - self.str = "\n".join(cmds) - - class Light(StageObject): - """Light object - - Attributes - ---------- - position : list or tuple - Position vector of light location. (length=3) - color : str or list - Color of the light. - Both string form of color or rgb (normalized) form is supported. - Example) color='White', color=[1,1,1] - """ - - def __init__(self, position, color): - self.position = position - self.color = color - super().__init__() - - def update_script(self): - position = self._position2str(self.position) - color = self._color2str(self.color) - cmds = [] - cmds.append("light_source{") - cmds.append(f" {position}") - cmds.append(f" color {color}") - cmds.append("}") - self.str = "\n".join(cmds) diff --git a/examples/Visualization/default.inc b/examples/Visualization/default.inc deleted file mode 100644 index 57f4d7216..000000000 --- a/examples/Visualization/default.inc +++ /dev/null @@ -1,94 +0,0 @@ -// POV-Ray 3.6 / 3.7 Scene File "Ribbon_Cable_1.pov" -// author: Friedrich A. Lohmueller, Sept-2009/Jan-2011 -// email: Friedrich.Lohmueller_at_t-online.de -// homepage: http://www.f-lohmueller.de -//-------------------------------------------------------------------------- -#version 3.6; // 3.7; -global_settings{ assumed_gamma 1.0 } -#default{ finish{ ambient 0.1 diffuse 0.9 }} -#include "colors.inc" -#include "textures.inc" -#include "glass.inc" -#include "metals.inc" -#include "golds.inc" -#include "stones.inc" -#include "woods.inc" -#include "shapes.inc" -#include "shapes2.inc" -#include "functions.inc" -#include "math.inc" -#include "transforms.inc" - -background{ color White } - -//------------------------------ the Axes -------------------------------- -//------------------------------------------------------------------------ -#macro Axis_( AxisLen, Dark_Texture,Light_Texture) - union{ - cylinder { <0,-AxisLen,0>,<0,AxisLen,0>,0.05 - texture{checker texture{Dark_Texture } - texture{Light_Texture} - scale 0.5 - translate<0.1,0,0.1>} - } - cone{<0,AxisLen,0>,0.2,<0,AxisLen+0.7,0>,0 - texture{Dark_Texture} - } - } // end of union -#end // of macro "Axis()" -//------------------------------------------------------------------------ -#macro AxisXYZ( AxisLenX, AxisLenY, AxisLenZ, Tex_Dark, Tex_Light) -//--------------------- drawing of 3 Axes -------------------------------- -union{ -#if (AxisLenX != 0) - object { Axis_(AxisLenX, Tex_Dark, Tex_Light) rotate< 0,0,-90>}// x-Axis -#end // of #if -#if (AxisLenY != 0) - object { Axis_(AxisLenY, Tex_Dark, Tex_Light) rotate< 0,0, 0>}// y-Axis -#end // of #if -#if (AxisLenZ != 0) - object { Axis_(AxisLenZ, Tex_Dark, Tex_Light) rotate<90,0, 0>}// z-Axis -#end // of #if -} // end of union -#end// of macro "AxisXYZ( ... )" -//------------------------------------------------------------------------ - -#declare Texture_A_Dark = texture { - pigment{ color rgb<1,0.4,0>} - finish { phong 1} - } -#declare Texture_A_Light = texture { - pigment{ color rgb<1,1,1>} - finish { phong 1} - } - -object{ AxisXYZ( 2.70, 2.70, 2.70, Texture_A_Dark, Texture_A_Light) scale 0.25 } -//-------------------------------------------------- end of coordinate axes - - -// ground ----------------------------------------------------------------- -//---------------------------------<<< settings of squared plane dimensions -#declare RasterScale = 0.50; -#declare RasterHalfLine = 0.03; -#declare RasterHalfLineZ = 0.03; -//------------------------------------------------------------------------- -#macro Raster(RScale, HLine) - pigment{ gradient x scale RScale - color_map{[0.000 color rgbt<1,1,1,0>*0.6] - [0+HLine color rgbt<1,1,1,0>*0.6] - [0+HLine color rgbt<1,1,1,1>] - [1-HLine color rgbt<1,1,1,1>] - [1-HLine color rgbt<1,1,1,0>*0.6] - [1.000 color rgbt<1,1,1,0>*0.6]} } -#end// of Raster(RScale, HLine)-macro -//------------------------------------------------------------------------- - -plane { <0,1,0>, 0 // plane with layered textures - texture { pigment{color White*1.1} - finish {ambient 0.45 diffuse 0.85} - } - texture { Raster(RasterScale,RasterHalfLine ) rotate<0,0,0> } - texture { Raster(RasterScale,RasterHalfLineZ) rotate<0,90,0>} - rotate<0,0,0> - } -//------------------------------------------------ end of squared plane XZ diff --git a/pyproject.toml b/pyproject.toml index cd6dc7d6b..a6971df9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pyelastica" -version = "0.3.3post1" +version = "1.0.0" description = "Elastica is a software to simulate the dynamics of filaments that, at every cross-section, can undergo all six possible modes of deformation, allowing the filament to bend, twist, stretch and shear, while interacting with complex environments via muscular activity, surface contact, friction and hydrodynamics." readme = "README.md" authors = [ @@ -14,7 +14,7 @@ documentation = "https://docs.cosseratrods.org/en/latest/" classifiers = [ "License :: OSI Approved :: MIT License", - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -33,10 +33,8 @@ dependencies = [ "scipy", "tqdm", "matplotlib", - "mypy", - "mypy-extensions", - "flake8", "cma", + "typing-extensions" ] [build-system] @@ -59,6 +57,8 @@ docs = [ "myst-parser>=1.0", "numpydoc>=1.3.1", "docutils>=0.18", + "sphinx-gallery>=0.19.0", + "sphinxcontrib-video>=0.4.1", ] dev = [ "black", @@ -72,6 +72,9 @@ dev = [ "codecov", "click", "autoflake", + "mypy", + "mypy-extensions", + "flake8", ] [tool.black] @@ -137,6 +140,7 @@ warn_unused_ignores = false exclude = [ "elastica/experimental/*", + "backend/*" ] [tool.coverage.report] diff --git a/tests/analytical.py b/tests/analytical.py index 05b395e5d..e356aef85 100644 --- a/tests/analytical.py +++ b/tests/analytical.py @@ -19,7 +19,7 @@ def state(self, new_state): self._state = new_state -class BaseSymplecticSystem: +class BaseSymplecticSystem(_RodSymplecticStepperMixin): def __init__(self): pass @@ -65,6 +65,9 @@ def __init__(self, state): for k in range(blocksize + 1): self.rate_collection[:, k] = state self.n_kinematic_rates = blocksize + # velocity and omega are accessed via dynamic_states by the stepper + self.velocity_collection = self.rate_collection[..., 0].reshape(3, 1) + self.omega_collection = self.rate_collection[..., 1].reshape(3, 1) # class BaseLinearStatefulSystem: @@ -113,7 +116,7 @@ def analytical_solution(self, time): ) return np.array([analytical_position, analytical_velocity]) - def __call__(self, time, *args, **kwargs): + def __call__(self): return self.A_matrix @ self._state @@ -136,13 +139,15 @@ def __init__(self, omega=2.0 * np.pi, init_val=np.array([1.0, 0.0])): self._kin_state = TestKinematicState(self._state[0:1]) # Create a view instead self._dyn_state = TestDynamicState(self._state[1:2]) # Create a view instead self.n_nodes = self._kin_state.n_nodes + self.position_collection = self._kin_state.position_collection + self.director_collection = self._kin_state.director_collection self.velocity_collection = self._dyn_state.rate_collection[..., 0].reshape(3, 1) self.omega_collection = self._dyn_state.rate_collection[..., 1].reshape(3, 1) + self.v_w_collection = self._dyn_state.rate_collection - def dynamic_rates(self, time, *args, **kwargs): - temp = super(SymplecticUndampedSimpleHarmonicOscillatorSystem, self).__call__( - *args, **kwargs - )[-1] + @property + def dvdt_dwdt_collection(self): + temp = super().__call__()[-1] # Expand rate vector in order to be consistent with time-stepper implementation blocksize = 1 # self._dyn_state.n_kinematic_rates rate = np.zeros((3, blocksize)) @@ -165,6 +170,9 @@ def energy(st): def compute_internal_forces_and_torques(self, time): pass + def update_accelerations(self, time, dt): + pass + def zeroed_out_external_forces_and_torques(self, time): pass @@ -300,22 +308,22 @@ class CollectiveSystem: """This collective system class is to test multiple memory structure blocks.""" def __init__(self): - self._memory_blocks = [] + self._final_systems = [] def systems(self): - return self._memory_blocks + return self._final_systems - def block_systems(self): - return self._memory_blocks + def final_systems(self): + return self._final_systems def __getitem__(self, idx): - return self._memory_blocks[idx] + return self._final_systems[idx] def __len__(self): - return len(self._memory_blocks) + return len(self._final_systems) def __iter__(self): - return self._memory_blocks.__iter__() + return self._final_systems.__iter__() def synchronize(self, time): pass @@ -333,12 +341,12 @@ def apply_callbacks(self, time, current_step: int): class SymplecticUndampedHarmonicOscillatorCollectiveSystem(CollectiveSystem): def __init__(self): super(SymplecticUndampedHarmonicOscillatorCollectiveSystem, self).__init__() - self._memory_blocks.append( + self._final_systems.append( SymplecticUndampedSimpleHarmonicOscillatorSystem( omega=2.0 * np.pi, init_val=np.array([1.0, 0.0]) ) ) - self._memory_blocks.append( + self._final_systems.append( SymplecticUndampedSimpleHarmonicOscillatorSystem( omega=1.0 * np.pi, init_val=np.array([0.0, 0.5]) ) @@ -350,8 +358,8 @@ def __init__(self): super( ScalarExponentialDampedHarmonicOscillatorCollectiveSystem, self ).__init__() - self._memory_blocks.append(ScalarExponentialDecaySystem()) - self._memory_blocks.append(DampedSimpleHarmonicOscillatorSystem()) + self._final_systems.append(ScalarExponentialDecaySystem()) + self._final_systems.append(DampedSimpleHarmonicOscillatorSystem()) def make_simple_system_with_positions_directors( @@ -418,7 +426,7 @@ def __init__(self, start_position, end_position, start_director): def compute_internal_forces_and_torques(self, time): pass - def update_accelerations(self, time): + def update_accelerations(self, time, dt): np.copyto(self.acceleration_collection, -np.sin(np.pi * time)) np.copyto(self.alpha_collection[2, ...], 0.1 * np.pi) diff --git a/tests/test_callback_functions.py b/tests/test_callback_functions.py index 4629ef350..9db898f98 100644 --- a/tests/test_callback_functions.py +++ b/tests/test_callback_functions.py @@ -171,22 +171,22 @@ def test_export_call_back_step_skip_param(self, step_skip): callback = ExportCallBack(step_skip, "rod", temp_dir_path, "npz") callback.make_callback(mock_rod, 1, step_skip - 1) # Check empty - callback.clear() + callback.on_close() saved_path_name = callback.get_last_saved_path() assert saved_path_name is None, "No file should be saved." # Check saved callback.make_callback(mock_rod, 1, step_skip) - callback.clear() + callback.on_close() saved_path_name = callback.get_last_saved_path() assert saved_path_name is not None, "File should be saved." assert os.path.exists(saved_path_name), "File should be saved" # Check saved file number callback.make_callback(mock_rod, 1, step_skip * 2) - callback.clear() + callback.on_close() callback.make_callback(mock_rod, 1, step_skip * 5) - callback.clear() + callback.on_close() saved_path_name = callback.get_last_saved_path() assert ( str(2) in saved_path_name @@ -222,19 +222,7 @@ def test_export_call_back_close_test(self, rng): ) for step in range(10): callback.make_callback(mock_rod, 1, step) - callback.close() - saved_path_name = callback.get_last_saved_path() - assert os.path.exists(saved_path_name), "File is not saved." - - def test_export_call_back_clear_test(self, rng): - mock_rod = MockRodWithElements(5) - with tempfile.TemporaryDirectory() as temp_dir_path: - callback = ExportCallBack( - 1, "rod", temp_dir_path, "npz", file_save_interval=50 - ) - for step in range(10): - callback.make_callback(mock_rod, 1, step) - callback.clear() + callback.on_close() saved_path_name = callback.get_last_saved_path() assert os.path.exists(saved_path_name), "File is not saved." diff --git a/tests/test_contact_classes.py b/tests/test_contact_classes.py index f99a6f3f0..29379d5fd 100644 --- a/tests/test_contact_classes.py +++ b/tests/test_contact_classes.py @@ -547,6 +547,92 @@ def test_contact_rod_sphere_with_collision_with_k_without_nu_and_friction( atol=1e-6, ) + def test_contact_rod_sphere_without_collision( + self, + ): + "Testing Rod Sphere Contact wrapper without Collision" + + mock_rod = MockRod() + mock_sphere = MockSphere() + rod_sphere_contact = RodSphereContact(k=1.0, nu=0.0) + + # Setting sphere position such that there is no collision + mock_sphere.position_collection = np.array([[100], [200], [300]]) + + mock_rod_external_forces_before_execution = mock_rod.external_forces.copy() + mock_sphere_external_forces_before_execution = ( + mock_sphere.external_forces.copy() + ) + mock_sphere_external_torques_before_execution = ( + mock_sphere.external_torques.copy() + ) + + rod_sphere_contact.apply_contact(mock_rod, mock_sphere) + + assert_allclose( + mock_rod.external_forces, mock_rod_external_forces_before_execution + ) + assert_allclose( + mock_sphere.external_forces, + mock_sphere_external_forces_before_execution, + ) + assert_allclose( + mock_sphere.external_torques, + mock_sphere_external_torques_before_execution, + ) + + def test_contact_rod_sphere_with_collision_with_nu_without_k( + self, + ): + "Testing Rod Sphere Contact wrapper with Collision with nu without k" + + mock_rod = MockRod() + mock_rod.velocity_collection = np.array([[-1, 0, 0], [-1, 0, 0], [-1, 0, 0]]) + + mock_sphere = MockSphere() + mock_sphere.velocity_collection = np.array([[1], [0], [0]]) + + rod_sphere_contact = RodSphereContact(k=0.0, nu=1.0) + rod_sphere_contact.apply_contact(mock_rod, mock_sphere) + + assert_allclose( + mock_sphere.external_forces, np.array([[-1.5], [0], [0]]), atol=1e-6 + ) + assert_allclose( + mock_sphere.external_torques, np.array([[0], [0], [0]]), atol=1e-6 + ) + assert_allclose( + mock_rod.external_forces, + np.array([[0.5, 1.0, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + + def test_contact_rod_sphere_with_collision_with_k_and_nu( + self, + ): + "Testing Rod Sphere Contact wrapper with Collision with k and nu" + + mock_rod = MockRod() + mock_rod.velocity_collection = np.array([[-1, 0, 0], [-1, 0, 0], [-1, 0, 0]]) + + mock_sphere = MockSphere() + mock_sphere.velocity_collection = np.array([[1], [0], [0]]) + + rod_sphere_contact = RodSphereContact(k=1.0, nu=1.0) + rod_sphere_contact.apply_contact(mock_rod, mock_sphere) + + assert_allclose( + mock_sphere.external_forces, np.array([[-2.0], [0], [0]]), atol=1e-6 + ) + assert_allclose( + mock_sphere.external_torques, np.array([[0], [0], [0]]), atol=1e-6 + ) + assert_allclose( + mock_rod.external_forces, + np.array([[0.666666, 1.333333, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + class TestRodPlaneContact: def initializer( @@ -600,7 +686,7 @@ def test_check_systems_validity_with_invalid_systems( # Testing Rod Plane Contact wrapper with incorrect type for second argument with pytest.raises(TypeError) as excinfo: rod_plane_contact._check_systems_validity(mock_rod, mock_list) - assert "System provided (list) must be derived from ['SurfaceBase']." == str( + assert "System provided (list) must be derived from ['Plane']." == str( excinfo.value ) @@ -873,7 +959,7 @@ def test_check_systems_validity_with_invalid_systems( # Testing Rod Plane Contact wrapper with incorrect type for second argument with pytest.raises(TypeError) as excinfo: rod_plane_contact._check_systems_validity(mock_rod, mock_list) - assert "System provided (list) must be derived from ['SurfaceBase']." == str( + assert "System provided (list) must be derived from ['Plane']." == str( excinfo.value ) @@ -1198,7 +1284,7 @@ def test_check_systems_validity_with_invalid_systems( # Testing Cylinder Plane Contact wrapper with incorrect type for second argument with pytest.raises(TypeError) as excinfo: cylinder_plane_contact._check_systems_validity(mock_cylinder, mock_list) - assert "System provided (list) must be derived from ['SurfaceBase']." == str( + assert "System provided (list) must be derived from ['Plane']." == str( excinfo.value ) diff --git a/tests/test_contact_functions.py b/tests/test_contact_functions.py index 354d57a1d..abde68b01 100644 --- a/tests/test_contact_functions.py +++ b/tests/test_contact_functions.py @@ -624,7 +624,7 @@ def test_calculate_contact_forces_rod_sphere_with_k_without_nu_and_friction( "initializing sphere parameters" sphere = MockSphere() - x_sph = sphere.position[..., 0] - sphere.radius * sphere.director[2, :, 0] + x_sph = sphere.position[..., 0] "initializing constants" """ @@ -640,16 +640,11 @@ def test_calculate_contact_forces_rod_sphere_with_k_without_nu_and_friction( _calculate_contact_forces_rod_sphere( rod_element_position, rod.lengths * rod.tangents, - sphere.position[..., 0], x_sph, - sphere.radius * sphere.director[2, :, 0], rod.radius + sphere.radius, rod.lengths + sphere.radius * 2, - rod.internal_forces, rod.external_forces, sphere.external_forces, - sphere.external_torques, - sphere.director[:, :, 0], rod.velocity_collection, sphere.velocity_collection, k, diff --git a/tests/test_contact_utils.py b/tests/test_contact_utils.py index 2be966501..d6d5f60e2 100644 --- a/tests/test_contact_utils.py +++ b/tests/test_contact_utils.py @@ -12,6 +12,7 @@ _clip, _out_of_bounds, _find_min_dist, + _find_min_dist_cylinder_sphere, _aabbs_not_intersecting, _prune_using_aabbs_rod_cylinder, _prune_using_aabbs_rod_rod, @@ -208,6 +209,51 @@ def test_find_min_dist(): assert_allclose(contact_point_of_system1, [0, 0, 0]) +def test_find_min_dist_cylinder_sphere(): + "Function to test the _find_min_dist_cylinder_sphere function" + + "testing function with analytically verified values" + + # Case 1: Closest point is on the segment (0 < t < 1). + x1 = np.array([0.0, 0.0, 0.0]) + e1 = np.array([1.0, 0.0, 0.0]) + x2 = np.array([0.5, 1.0, 0.0]) + ( + min_dist_vec, + contact_point_of_system2, + contact_point_of_system1, + ) = _find_min_dist_cylinder_sphere(x1, e1, x2) + assert_allclose(min_dist_vec, [0.0, 1.0, 0.0]) + assert_allclose(contact_point_of_system2, [0.5, 1.0, 0.0]) + assert_allclose(contact_point_of_system1, [-0.5, 0.0, 0.0]) + + # Case 2: Closest point is at the start of the segment (t < 0, clipped to t=0). + x1 = np.array([0.0, 0.0, 0.0]) + e1 = np.array([1.0, 0.0, 0.0]) + x2 = np.array([-0.5, 1.0, 0.0]) + ( + min_dist_vec, + contact_point_of_system2, + contact_point_of_system1, + ) = _find_min_dist_cylinder_sphere(x1, e1, x2) + assert_allclose(min_dist_vec, [-0.5, 1.0, 0.0]) + assert_allclose(contact_point_of_system2, [-0.5, 1.0, 0.0]) + assert_allclose(contact_point_of_system1, [0.0, 0.0, 0.0]) + + # Case 3: Closest point is at the end of the segment (t > 1, clipped to t=1). + x1 = np.array([0.0, 0.0, 0.0]) + e1 = np.array([1.0, 0.0, 0.0]) + x2 = np.array([1.5, 1.0, 0.0]) + ( + min_dist_vec, + contact_point_of_system2, + contact_point_of_system1, + ) = _find_min_dist_cylinder_sphere(x1, e1, x2) + assert_allclose(min_dist_vec, [0.5, 1.0, 0.0]) + assert_allclose(contact_point_of_system2, [1.5, 1.0, 0.0]) + assert_allclose(contact_point_of_system1, [-1.0, 0.0, 0.0]) + + def test_aabbs_not_intersecting(): "Function to test the _aabb_intersecting function" diff --git a/tests/test_external_forces.py b/tests/test_external_forces.py index a10b910b6..1d9bf72a2 100644 --- a/tests/test_external_forces.py +++ b/tests/test_external_forces.py @@ -13,7 +13,7 @@ UniformForces, MuscleTorques, inplace_addition, - inplace_substraction, + inplace_subtraction, EndpointForcesSinusoidal, ) from elastica.utils import Tolerance @@ -297,7 +297,7 @@ def test_inplace_addition(rng, n_elem): @pytest.mark.parametrize("n_elem", [33, 59, 100]) -def test_inplace_substraction(rng, n_elem): +def test_inplace_subtraction(rng, n_elem): """ This test is for inplace substraction written using Numba njit functions Parameters @@ -317,6 +317,6 @@ def test_inplace_substraction(rng, n_elem): correct_vector = first_input_vector - second_input_vector test_vector = first_input_vector.copy() - inplace_substraction(test_vector, second_input_vector) + inplace_subtraction(test_vector, second_input_vector) assert_allclose(correct_vector, test_vector, atol=Tolerance.atol()) diff --git a/tests/test_interaction.py b/tests/test_interaction.py index f6b8fe241..9bb00e62c 100644 --- a/tests/test_interaction.py +++ b/tests/test_interaction.py @@ -7,8 +7,6 @@ from elastica.interaction import ( - InteractionPlane, - AnisotropicFrictionalPlane, SlenderBodyTheory, ) from elastica.contact_utils import ( @@ -60,658 +58,6 @@ def _compute_internal_torques(self): return np.zeros((MaxDimension.value(), self.n_elem)) -class TestInteractionPlane: - def initializer( - self, - rng, - n_elem, - shift=0.0, - k_w=0.0, - nu_w=0.0, - plane_normal=np.array([0.0, 1.0, 0.0]), - ): - rod = BaseRodClass(n_elem) - plane_origin = np.array([0.0, -rod.radius[0] + shift, 0.0]) - interaction_plane = InteractionPlane(k_w, nu_w, plane_origin, plane_normal) - fnormal = -10.0 * np.sign(plane_normal[1]) * rng.random(1).item() - external_forces = np.repeat( - np.array([0.0, fnormal, 0.0]).reshape(3, 1), n_elem + 1, axis=1 - ) - external_forces[..., 0] *= 0.5 - external_forces[..., -1] *= 0.5 - rod.external_forces = external_forces.copy() - - return rod, interaction_plane, external_forces - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - def test_interaction_without_contact(self, n_elem, rng): - """ - This test case tests the forces on rod, when there is no - contact between rod and the plane. - Parameters - ---------- - n_elem - - Returns - ------- - - """ - - shift = -( - (2.0 - 1.0) * rng.random(1) + 1.0 - ).item() # we move plane away from rod - - [rod, interaction_plane, external_forces] = self.initializer(rng, n_elem, shift) - - interaction_plane.apply_forces(rod) - correct_forces = external_forces # since no contact - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - def test_interaction_plane_without_k_and_nu(self, n_elem, rng): - """ - This function tests wall response on the rod. Here - wall stiffness coefficient and damping coefficient set - to zero to check only sum of all forces on the rod. - - Parameters - ---------- - n_elem - - Returns - ------- - - """ - - [rod, interaction_plane, external_forces] = self.initializer(rng, n_elem) - - interaction_plane.apply_forces(rod) - - correct_forces = np.zeros((3, n_elem + 1)) - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("k_w", [0.1, 0.5, 1.0, 2, 10]) - def test_interaction_plane_with_k_without_nu(self, n_elem, k_w, rng): - """ - Here wall stiffness coefficient changed parametrically - and damping coefficient set to zero . - Parameters - ---------- - n_elem - k_w - - Returns - ------- - - """ - - shift = rng.random(1).item() # we move plane towards to rod - [rod, interaction_plane, external_forces] = self.initializer( - rng, n_elem, shift=shift, k_w=k_w - ) - correct_forces = k_w * np.repeat( - np.array([0.0, shift, 0.0]).reshape(3, 1), n_elem + 1, axis=1 - ) - correct_forces[..., 0] *= 0.5 - correct_forces[..., -1] *= 0.5 - - interaction_plane.apply_forces(rod) - - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("nu_w", [0.5, 1.0, 5.0, 7.0, 12.0]) - def test_interaction_plane_without_k_with_nu(self, n_elem, nu_w, rng): - """ - Here wall damping coefficient are changed parametrically and - wall response functions tested. - Parameters - ---------- - n_elem - nu_w - - Returns - ------- - - """ - - [rod, interaction_plane, external_forces] = self.initializer( - rng, n_elem, nu_w=nu_w - ) - - normal_velocity = rng.random(1).item() - rod.velocity_collection[..., :] += np.array( - [0.0, -normal_velocity, 0.0] - ).reshape(3, 1) - - correct_forces = np.repeat( - (nu_w * np.array([0.0, normal_velocity, 0.0])).reshape(3, 1), - n_elem + 1, - axis=1, - ) - - correct_forces[..., 0] *= 0.5 - correct_forces[..., -1] *= 0.5 - - interaction_plane.apply_forces(rod) - - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - def test_interaction_when_rod_is_under_plane(self, n_elem, rng): - """ - This test case tests plane response forces on the rod - in the case rod is under the plane and pushed towards - the plane. - Parameters - ---------- - n_elem - - Returns - ------- - - """ - - # we move plane on top of the rod. Note that 0.25 is radius of the rod. - offset_of_plane_with_respect_to_rod = 2.0 * 0.25 - - # plane normal changed, it is towards the negative direction, because rod - # is under the rod. - plane_normal = np.array([0.0, -1.0, 0.0]) - - [rod, interaction_plane, external_forces] = self.initializer( - rng, - n_elem, - shift=offset_of_plane_with_respect_to_rod, - plane_normal=plane_normal, - ) - - interaction_plane.apply_forces(rod) - correct_forces = np.zeros((3, n_elem + 1)) - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("k_w", [0.1, 0.5, 1.0, 2, 10]) - def test_interaction_when_rod_is_under_plane_with_k_without_nu( - self, n_elem, k_w, rng - ): - """ - In this test case we move the rod under the plane. - Here wall stiffness coefficient changed parametrically - and damping coefficient set to zero . - Parameters - ---------- - n_elem - k_w - - Returns - ------- - - """ - # we move plane on top of the rod. Note that 0.25 is radius of the rod. - offset_of_plane_with_respect_to_rod = 2.0 * 0.25 - - # we move plane towards to rod by random distance - shift = offset_of_plane_with_respect_to_rod - rng.random(1).item() - - # plane normal changed, it is towards the negative direction, because rod - # is under the rod. - plane_normal = np.array([0.0, -1.0, 0.0]) - - [rod, interaction_plane, external_forces] = self.initializer( - rng, n_elem, shift=shift, k_w=k_w, plane_normal=plane_normal - ) - - # we have to substract rod offset because top part - correct_forces = k_w * np.repeat( - np.array([0.0, shift - offset_of_plane_with_respect_to_rod, 0.0]).reshape( - 3, 1 - ), - n_elem + 1, - axis=1, - ) - correct_forces[..., 0] *= 0.5 - correct_forces[..., -1] *= 0.5 - - interaction_plane.apply_forces(rod) - - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("nu_w", [0.5, 1.0, 5.0, 7.0, 12.0]) - def test_interaction_when_rod_is_under_plane_without_k_with_nu( - self, n_elem, nu_w, rng - ): - """ - In this test case we move under the plane and test damping force. - Here wall damping coefficient are changed parametrically and - wall response functions tested. - Parameters - ---------- - n_elem - nu_w - - Returns - ------- - - """ - # we move plane on top of the rod. Note that 0.25 is radius of the rod. - offset_of_plane_with_respect_to_rod = 2.0 * 0.25 - - # plane normal changed, it is towards the negative direction, because rod - # is under the rod. - plane_normal = np.array([0.0, -1.0, 0.0]) - - [rod, interaction_plane, external_forces] = self.initializer( - rng, - n_elem, - shift=offset_of_plane_with_respect_to_rod, - nu_w=nu_w, - plane_normal=plane_normal, - ) - - normal_velocity = rng.random(1).item() - rod.velocity_collection[..., :] += np.array( - [0.0, -normal_velocity, 0.0] - ).reshape(3, 1) - - correct_forces = np.repeat( - (nu_w * np.array([0.0, normal_velocity, 0.0])).reshape(3, 1), - n_elem + 1, - axis=1, - ) - - correct_forces[..., 0] *= 0.5 - correct_forces[..., -1] *= 0.5 - - interaction_plane.apply_forces(rod) - - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - -class TestAnisotropicFriction: - def initializer( - self, - rng, - n_elem, - static_mu_array=np.array([0.0, 0.0, 0.0]), - kinetic_mu_array=np.array([0.0, 0.0, 0.0]), - force_mag_long=0.0, # forces along the rod - force_mag_side=0.0, # side forces on the rod - ): - - rod = BaseRodClass(n_elem) - - origin_plane = np.array([0.0, -rod.radius[0], 0.0]) - normal_plane = np.array([0.0, 1.0, 0.0]) - slip_velocity_tol = 1e-2 - friction_plane = AnisotropicFrictionalPlane( - 0.0, - 0.0, - origin_plane, - normal_plane, - slip_velocity_tol, - static_mu_array, # forward, backward, sideways - kinetic_mu_array, # forward, backward, sideways - ) - fnormal = (10.0 - 5.0) * rng.random( - 1 - ).item() + 5.0 # generates random numbers [5.0,10) - external_forces = np.array([force_mag_side, -fnormal, force_mag_long]) - - external_forces_collection = np.repeat( - external_forces.reshape(3, 1), n_elem + 1, axis=1 - ) - external_forces_collection[..., 0] *= 0.5 - external_forces_collection[..., -1] *= 0.5 - rod.external_forces = external_forces_collection.copy() - - # Velocities has to be set to zero - assert_allclose( - np.zeros((3, n_elem)), rod.omega_collection, atol=Tolerance.atol() - ) - assert_allclose( - np.zeros((3, n_elem + 1)), rod.velocity_collection, atol=Tolerance.atol() - ) - - # We have not changed torques also, they have to be zero as well - assert_allclose( - np.zeros((3, n_elem)), rod.external_torques, atol=Tolerance.atol() - ) - assert_allclose( - np.zeros((3, n_elem)), - rod._compute_internal_torques(), - atol=Tolerance.atol(), - ) - - return rod, friction_plane, external_forces_collection - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("velocity", [-1.0, -3.0, 1.0, 5.0, 2.0]) - def test_axial_kinetic_friction(self, n_elem, velocity, rng): - """ - This function tests kinetic friction in forward and backward direction. - All other friction coefficients set to zero. - Parameters - ---------- - n_elem - velocity - - Returns - ------- - - """ - - [rod, friction_plane, external_forces_collection] = self.initializer( - rng, n_elem, kinetic_mu_array=np.array([1.0, 1.0, 0.0]) - ) - - rod.velocity_collection += np.array([0.0, 0.0, velocity]).reshape(3, 1) - - friction_plane.apply_forces(rod) - - direction_collection = np.repeat( - np.array([0.0, 0.0, 1.0]).reshape(3, 1), n_elem + 1, axis=1 - ) - correct_forces = ( - -1.0 - * np.sign(velocity) - * np.linalg.norm(external_forces_collection, axis=0) - * direction_collection - ) - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("force_mag", [-1.0, -3.0, 1.0, 5.0, 2.0]) - def test_axial_static_friction_total_force_smaller_than_static_friction_force( - self, n_elem, force_mag, rng - ): - """ - This test is for static friction when total forces applied - on the rod is smaller than the static friction force. - Fx < F_normal*mu_s - Parameters - ---------- - n_elem - force_mag - - Returns - ------- - - """ - [rod, frictionplane, external_forces_collection] = self.initializer( - rng, - n_elem, - static_mu_array=np.array([1.0, 1.0, 0.0]), - force_mag_long=force_mag, - ) - - frictionplane.apply_forces(rod) - correct_forces = np.zeros((3, n_elem + 1)) - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("force_mag", [-20.0, -15.0, 15.0, 20.0]) - def test_axial_static_friction_total_force_larger_than_static_friction_force( - self, n_elem, force_mag, rng - ): - """ - This test is for static friction when total forces applied - on the rod is larger than the static friction force. - Fx > F_normal*mu_s - Parameters - ---------- - n_elem - force_mag - - Returns - ------- - - """ - - [rod, frictionplane, external_forces_collection] = self.initializer( - rng, - n_elem, - static_mu_array=np.array([1.0, 1.0, 0.0]), - force_mag_long=force_mag, - ) - - frictionplane.apply_forces(rod) - correct_forces = np.zeros((3, n_elem + 1)) - if np.sign(force_mag) < 0: - correct_forces[2] = ( - external_forces_collection[2] - ) - 1.0 * external_forces_collection[1] - else: - correct_forces[2] = ( - external_forces_collection[2] - ) + 1.0 * external_forces_collection[1] - - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("velocity", [-1.0, -3.0, 1.0, 2.0, 5.0]) - @pytest.mark.parametrize("omega", [-5.0, -2.0, 0.0, 4.0, 6.0]) - def test_kinetic_rolling_friction(self, n_elem, velocity, omega, rng): - """ - This test is for testing kinetic rolling friction, - for different translational and angular velocities, - we compute the final external forces and torques on the rod - using apply friction function and compare results with - analytical solutions. - Parameters - ---------- - n_elem - velocity - omega - - Returns - ------- - - """ - [rod, frictionplane, external_forces_collection] = self.initializer( - rng, n_elem, kinetic_mu_array=np.array([0.0, 0.0, 1.0]) - ) - - rod.velocity_collection += np.array([velocity, 0.0, 0.0]).reshape(3, 1) - rod.omega_collection += np.array([0.0, 0.0, omega]).reshape(3, 1) - - frictionplane.apply_forces(rod) - - correct_forces = np.zeros((3, n_elem + 1)) - correct_forces[0] = ( - -1.0 - * np.sign(velocity + omega * rod.radius[0]) - * np.fabs(external_forces_collection[1]) - ) - - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - forces_on_elements = _node_to_element_mass_or_force(external_forces_collection) - correct_torques = np.zeros((3, n_elem)) - correct_torques[2] += ( - -1.0 - * np.sign(velocity + omega * rod.radius[0]) - * np.fabs(forces_on_elements[1]) - * rod.radius - ) - - assert_allclose(correct_torques, rod.external_torques, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("force_mag", [-20.0, -15.0, 15.0, 20.0]) - def test_static_rolling_friction_total_force_smaller_than_static_friction_force( - self, n_elem, force_mag, rng - ): - """ - In this test case static rolling friction force is tested. We set external and internal torques to - zero and only changed the force in rolling direction. In this test case, total force in rolling direction - is smaller than static friction force in rolling direction. Next test case will check what happens if - total forces in rolling direction larger than static friction force. - Parameters - ---------- - n_elem - force_mag - - Returns - ------- - - """ - - [rod, frictionplane, external_forces_collection] = self.initializer( - rng, - n_elem, - static_mu_array=np.array([0.0, 0.0, 10.0]), - force_mag_side=force_mag, - ) - - frictionplane.apply_forces(rod) - - correct_forces = np.zeros((3, n_elem + 1)) - correct_forces[0] = 2.0 / 3.0 * external_forces_collection[0] - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - forces_on_elements = _node_to_element_mass_or_force(external_forces_collection) - correct_torques = np.zeros((3, n_elem)) - correct_torques[2] += ( - -1.0 - * np.sign(forces_on_elements[0]) - * np.fabs(forces_on_elements[0]) - * rod.radius - / 3.0 - ) - - assert_allclose(correct_torques, rod.external_torques, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("force_mag", [-100.0, -80.0, 65.0, 95.0]) - def test_static_rolling_friction_total_force_larger_than_static_friction_force( - self, n_elem, force_mag, rng - ): - """ - In this test case static rolling friction force is tested. We set external and internal torques to - zero and only changed the force in rolling direction. In this test case, total force in rolling direction - is larger than static friction force in rolling direction. - Parameters - ---------- - n_elem - force_mag - - Returns - ------- - - """ - - [rod, frictionplane, external_forces_collection] = self.initializer( - rng, - n_elem, - static_mu_array=np.array([0.0, 0.0, 1.0]), - force_mag_side=force_mag, - ) - - frictionplane.apply_forces(rod) - - correct_forces = np.zeros((3, n_elem + 1)) - correct_forces[0] = external_forces_collection[0] - np.sign( - external_forces_collection[0] - ) * np.fabs(external_forces_collection[1]) - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - forces_on_elements = _node_to_element_mass_or_force(external_forces_collection) - correct_torques = np.zeros((3, n_elem)) - correct_torques[2] += ( - -1.0 - * np.sign(forces_on_elements[0]) - * np.fabs(forces_on_elements[1]) - * rod.radius - ) - - assert_allclose(correct_torques, rod.external_torques, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("torque_mag", [-3.0, -1.0, 2.0, 3.5]) - def test_static_rolling_friction_total_torque_smaller_than_static_friction_force( - self, n_elem, torque_mag, rng - ): - """ - In this test case, static rolling friction force tested with zero internal and external force and - with non-zero external torque. Here torque magnitude chosen such that total rolling force is - always smaller than the static friction force. - Parameters - ---------- - n_elem - torque_mag - - Returns - ------- - - """ - - [rod, frictionplane, external_forces_collection] = self.initializer( - rng, n_elem, static_mu_array=np.array([0.0, 0.0, 10.0]) - ) - - external_torques = np.zeros((3, n_elem)) - external_torques[2] = torque_mag - rod.external_torques = external_torques.copy() - - frictionplane.apply_forces(rod) - - correct_forces = np.zeros((3, n_elem + 1)) - correct_forces[0, :-1] -= external_torques[2] / (3.0 * rod.radius) - correct_forces[0, 1:] -= external_torques[2] / (3.0 * rod.radius) - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - correct_torques = np.zeros((3, n_elem)) - correct_torques[2] += external_torques[2] - 2.0 / 3.0 * external_torques[2] - - assert_allclose(correct_torques, rod.external_torques, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("torque_mag", [-10.0, -5.0, 6.0, 7.5]) - def test_static_rolling_friction_total_torque_larger_than_static_friction_force( - self, n_elem, torque_mag, rng - ): - """ - In this test case, static rolling friction force tested with zero internal and external force and - with non-zero external torque. Here torque magnitude chosen such that total rolling force is - always larger than the static friction force. Thus, lateral friction force will be equal to static - friction force. - Parameters - ---------- - n_elem - torque_mag - - Returns - ------- - - """ - - [rod, frictionplane, external_forces_collection] = self.initializer( - rng, n_elem, static_mu_array=np.array([0.0, 0.0, 1.0]) - ) - - external_torques = np.zeros((3, n_elem)) - external_torques[2] = torque_mag - rod.external_torques = external_torques.copy() - - frictionplane.apply_forces(rod) - - correct_forces = np.zeros((3, n_elem + 1)) - correct_forces[0] = ( - -1.0 * np.sign(torque_mag) * np.fabs(external_forces_collection[1]) - ) - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - forces_on_elements = _node_to_element_mass_or_force(external_forces_collection) - correct_torques = external_torques - correct_torques[2] += -( - np.sign(torque_mag) * np.fabs(forces_on_elements[1]) * rod.radius - ) - - assert_allclose(correct_torques, rod.external_torques, atol=Tolerance.atol()) - - # Slender Body Theory Unit Tests from elastica.interaction import ( sum_over_elements, diff --git a/tests/test_math/test_governing_equations.py b/tests/test_math/test_governing_equations.py index 6488e7fa8..4af63cc26 100644 --- a/tests/test_math/test_governing_equations.py +++ b/tests/test_math/test_governing_equations.py @@ -845,7 +845,7 @@ def test_update_acceleration(self, n_elem, rng): test_rod.inv_mass_second_moment_of_inertia[:] = inv_mass_moment_of_inertia # Compute acceleration - test_rod.update_accelerations(time=0) + test_rod.update_accelerations(time=0, dt=0) correct_acceleration = external_forces / mass assert_allclose( diff --git a/tests/test_math/test_memory_block_with_symplectic_timestepper.py b/tests/test_math/test_memory_block_with_symplectic_timestepper.py index a9a402645..99e5f6f5f 100644 --- a/tests/test_math/test_memory_block_with_symplectic_timestepper.py +++ b/tests/test_math/test_memory_block_with_symplectic_timestepper.py @@ -84,50 +84,11 @@ class BlockStructureWithSymplecticStepper( ): def __init__(self, systems): MemoryBlockCosseratRod.__init__(self, systems, [i for i in range(len(systems))]) - _RodSymplecticStepperMixin.__init__(self) - def update_accelerations(self, time): + def update_accelerations(self, time, dt): pass -@pytest.mark.parametrize("n_rods", [1, 2, 5, 6]) -def test_block_structure_kinematic_state_references(n_rods, rng): - """ - This function is testing validity of kinematic state views and compare them - with the block structure vectors. - - Parameters - ---------- - n_rods - - Returns - ------- - - """ - world_rods = [MockRod(rng.randint(10, 30 + 1)) for _ in range(n_rods)] - block_structure = BlockStructureWithSymplecticStepper(world_rods) - - assert_allclose( - block_structure.position_collection, - block_structure.kinematic_states.position_collection, - atol=Tolerance.atol(), - ) - assert np.shares_memory( - block_structure.position_collection, - block_structure.kinematic_states.position_collection, - ) - - assert_allclose( - block_structure.director_collection, - block_structure.kinematic_states.director_collection, - atol=Tolerance.atol(), - ) - assert np.shares_memory( - block_structure.director_collection, - block_structure.kinematic_states.director_collection, - ) - - @pytest.mark.parametrize("n_rods", [1, 2, 5, 6]) def test_block_structure_kinematic_update(n_rods, rng): """ @@ -162,10 +123,7 @@ def test_block_structure_kinematic_update(n_rods, rng): out=correct_director, ) - # block_structure.kinematic_states += block_structure.kinematic_rates(0, prefac) - overload_operator_kinematic_numba( - block_structure.n_nodes, prefac, block_structure.position_collection, block_structure.director_collection, @@ -181,125 +139,6 @@ def test_block_structure_kinematic_update(n_rods, rng): ) -@pytest.mark.parametrize("n_rods", [1, 2, 5, 6]) -def test_block_structure_dynamic_state_references(n_rods, rng): - """ - This function is testing validity of dynamic state views and compare them - with the block structure vectors. - - Parameters - ---------- - n_rods - - Returns - ------- - - """ - world_rods = [MockRod(rng.randint(10, 30 + 1)) for _ in range(n_rods)] - block_structure = BlockStructureWithSymplecticStepper(world_rods) - - assert_allclose( - block_structure.velocity_collection, - block_structure.dynamic_states.velocity_collection, - atol=Tolerance.atol(), - ) - assert np.shares_memory( - block_structure.velocity_collection, - block_structure.dynamic_states.velocity_collection, - ) - - assert_allclose( - block_structure.omega_collection, - block_structure.dynamic_states.omega_collection, - atol=Tolerance.atol(), - ) - assert np.shares_memory( - block_structure.omega_collection, - block_structure.dynamic_states.omega_collection, - ) - - assert_allclose( - block_structure.v_w_collection, - block_structure.dynamic_states.rate_collection, - atol=Tolerance.atol(), - ) - assert np.shares_memory( - block_structure.v_w_collection, block_structure.dynamic_states.rate_collection - ) - - assert_allclose( - block_structure.dvdt_dwdt_collection, - block_structure.dynamic_states.dvdt_dwdt_collection, - atol=Tolerance.atol(), - ) - assert np.shares_memory( - block_structure.dvdt_dwdt_collection, - block_structure.dynamic_states.dvdt_dwdt_collection, - ) - - -@pytest.mark.parametrize("n_rods", [1, 2, 5, 6]) -def test_block_structure_dynamic_state_kinematic_rates(n_rods, rng): - """ - This function is testing validity of dynamic state function and compare them - with the block structure vectors. - - Parameters - ---------- - n_rods - - Returns - ------- - - """ - world_rods = [MockRod(rng.randint(10, 30 + 1)) for _ in range(n_rods)] - block_structure = BlockStructureWithSymplecticStepper(world_rods) - - prefac = 1.0 - - correct_velocity = prefac * block_structure.velocity_collection.copy() - velocity_test = block_structure.kinematic_rates(0, prefac)[0].copy() - - assert_allclose( - correct_velocity, - velocity_test, - atol=Tolerance.atol(), - ) - - correct_omega = prefac * block_structure.omega_collection.copy() - omega_test = block_structure.kinematic_rates(0, prefac)[1].copy() - - assert_allclose( - correct_omega, - omega_test, - atol=Tolerance.atol(), - ) - - -@pytest.mark.parametrize("n_rods", [1, 2, 5, 6]) -def test_block_structure_dynamic_state_dynamic_rates(n_rods, rng): - """ - This function is testing validity of dynamic rates function and compare them - with the block structure vector. - - Parameters - ---------- - n_rods - - Returns - ------- - - """ - world_rods = [MockRod(rng.randint(10, 30 + 1)) for _ in range(n_rods)] - block_structure = BlockStructureWithSymplecticStepper(world_rods) - - assert_allclose( - block_structure.dvdt_dwdt_collection, - block_structure.dynamic_rates(0, prefac=1), - atol=Tolerance.atol(), - ) - - @pytest.mark.parametrize("n_rods", [1, 2, 5, 6]) def test_block_structure_dynamic_update(n_rods, rng): """ @@ -325,7 +164,9 @@ def test_block_structure_dynamic_update(n_rods, rng): correct_v_w = v_w + prefac * dvdt_dwdt overload_operator_dynamic_numba( - block_structure.v_w_collection, block_structure.dynamic_rates(0, prefac) + prefac, + block_structure.v_w_collection, + block_structure.dvdt_dwdt_collection, ) assert_allclose(correct_v_w, block_structure.v_w_collection, atol=Tolerance.atol()) diff --git a/tests/test_math/test_rotations.py b/tests/test_math/test_rotations.py index 1acb664bf..0b603ddbe 100644 --- a/tests/test_math/test_rotations.py +++ b/tests/test_math/test_rotations.py @@ -8,11 +8,19 @@ _get_rotation_matrix, _rotate, _inv_rotate, + get_relative_rotation_two_systems, ) from elastica.utils import Tolerance +class MockRod: + """Mock rod class with only director_collection attribute for testing.""" + + def __init__(self, director_collection): + self.director_collection = director_collection + + @pytest.mark.parametrize("zcomp", [0.5, 1.0]) @pytest.mark.parametrize("dt", [0.3, 1.0]) def test_get_rotation_matrix_correct_rotation_about_z(zcomp, dt): @@ -373,3 +381,150 @@ def test_inv_rotate_correctness_on_circle_in_two_dimensions_with_different_direc assert test_axis_collection.shape == (3, blocksize - 1) assert_allclose(test_axis_collection, correct_axis_collection) assert_allclose(test_scaling, 0.0 * test_scaling + dtheta_di, atol=Tolerance.atol()) + + +def test_get_relative_rotation_two_systems_identity(): + """Test that two rods with identical orientations give identity rotation matrix.""" + # Create two mock rods with same orientation (identity rotation matrices) + n = 4 + identity_director = np.eye(3) + director_collection = np.tile(identity_director[..., np.newaxis], (1, 1, n + 1)) + + rod1 = MockRod(director_collection) + rod2 = MockRod(director_collection.copy()) + + # Test with different indices + rel_rot = get_relative_rotation_two_systems(rod1, 0, rod2, 0) + rel_rot_end = get_relative_rotation_two_systems(rod1, -1, rod2, -1) + + # Should be identity matrix (or very close to it) + identity = np.eye(3) + assert rel_rot.shape == (3, 3) + assert_allclose(rel_rot, identity, atol=Tolerance.atol()) + assert_allclose(rel_rot_end, identity, atol=Tolerance.atol()) + + +def test_get_relative_rotation_two_systems_known_rotation(): + """Test with rods rotated by a known angle.""" + n = 4 + + # First rod: identity rotation (aligned with standard axes) + director1 = np.eye(3) + director_collection1 = np.tile(director1[..., np.newaxis], (1, 1, n + 1)) + rod1 = MockRod(director_collection1) + + # Second rod: rotated 90 degrees about z-axis + # Rotation matrix for 90 degrees about z-axis + director2 = np.array( + [ + [0.0, 1.0, 0.0], + [-1.0, 0.0, 0.0], + [0.0, 0.0, 1.0], + ] + ) + director_collection2 = np.tile(director2[..., np.newaxis], (1, 1, n + 1)) + rod2 = MockRod(director_collection2) + + rel_rot = get_relative_rotation_two_systems(rod1, 0, rod2, 0) + + # Expected rotation matrix: Since rel_rot = director1 @ director2.T + # and director1 = I, we get director2.T + # director2 is a 90-degree rotation about z-axis, so director2.T is: + expected_rot = np.array( + [ + [0.0, -1.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 0.0, 1.0], + ] + ) + + assert rel_rot.shape == (3, 3) + assert_allclose(rel_rot, expected_rot, atol=Tolerance.atol()) + + +def test_get_relative_rotation_two_systems_properties(): + """Test that the relative rotation matrix has proper rotation matrix properties.""" + n = 4 + + # First rod: identity rotation + director1 = np.eye(3) + director_collection1 = np.tile(director1[..., np.newaxis], (1, 1, n + 1)) + rod1 = MockRod(director_collection1) + + # Second rod: rotated 90 degrees about z-axis + director2 = np.array( + [ + [0.0, 1.0, 0.0], + [-1.0, 0.0, 0.0], + [0.0, 0.0, 1.0], + ] + ) + director_collection2 = np.tile(director2[..., np.newaxis], (1, 1, n + 1)) + rod2 = MockRod(director_collection2) + + rel_rot = get_relative_rotation_two_systems(rod1, 0, rod2, 0) + + # Check orthogonality: R @ R.T should be identity + r_rt = rel_rot @ rel_rot.T + rt_r = rel_rot.T @ rel_rot + identity = np.eye(3) + + assert_allclose(r_rt, identity, atol=Tolerance.atol()) + assert_allclose(rt_r, identity, atol=Tolerance.atol()) + + # Check determinant is 1 (for proper rotation) + det = np.linalg.det(rel_rot) + assert_allclose(det, 1.0, atol=Tolerance.atol()) + + +@pytest.mark.parametrize("index1", [0, -1, 2]) +@pytest.mark.parametrize("index2", [0, -1, 1]) +def test_get_relative_rotation_two_systems_different_indices(index1, index2): + """Test function with different index combinations.""" + n = 4 + + # Create mock rods with identity rotations + director = np.eye(3) + director_collection = np.tile(director[..., np.newaxis], (1, 1, n + 1)) + + rod1 = MockRod(director_collection) + rod2 = MockRod(director_collection.copy()) + + rel_rot = get_relative_rotation_two_systems(rod1, index1, rod2, index2) + + # Should always return a 3x3 matrix + assert rel_rot.shape == (3, 3) + + # Should be a valid rotation matrix + r_rt = rel_rot @ rel_rot.T + assert_allclose(r_rt, np.eye(3), atol=Tolerance.atol()) + assert_allclose(np.linalg.det(rel_rot), 1.0, atol=Tolerance.atol()) + + +def test_get_relative_rotation_two_systems_inverse_relationship(): + """Test that rel_rot(system1, system2) is the inverse of rel_rot(system2, system1).""" + n = 4 + + # First rod: identity rotation + director1 = np.eye(3) + director_collection1 = np.tile(director1[..., np.newaxis], (1, 1, n + 1)) + rod1 = MockRod(director_collection1) + + # Second rod: rotated 90 degrees about z-axis + director2 = np.array( + [ + [0.0, 1.0, 0.0], + [-1.0, 0.0, 0.0], + [0.0, 0.0, 1.0], + ] + ) + director_collection2 = np.tile(director2[..., np.newaxis], (1, 1, n + 1)) + rod2 = MockRod(director_collection2) + + rel_rot_12 = get_relative_rotation_two_systems(rod1, 0, rod2, 0) + rel_rot_21 = get_relative_rotation_two_systems(rod2, 0, rod1, 0) + + # rel_rot_21 should be the transpose (inverse) of rel_rot_12 + assert_allclose(rel_rot_21, rel_rot_12.T, atol=Tolerance.atol()) + # Or equivalently, their product should be identity + assert_allclose(rel_rot_12 @ rel_rot_21, np.eye(3), atol=Tolerance.atol()) diff --git a/tests/test_math/test_timestepper.py b/tests/test_math/test_timestepper.py index ace4a22b1..3f07b9889 100644 --- a/tests/test_math/test_timestepper.py +++ b/tests/test_math/test_timestepper.py @@ -4,11 +4,6 @@ from numpy.testing import assert_allclose from elastica.timestepper import integrate, extend_stepper_interface -from elastica.experimental.timestepper.explicit_steppers import ( - RungeKutta4, - EulerForward, - ExplicitStepperMixin, -) from elastica.timestepper.symplectic_steppers import ( PositionVerlet, PEFRL, @@ -49,20 +44,6 @@ def _kinematic_step(self): def _dynamic_step(self): pass - class MockExplicitStepper(ExplicitStepperMixin): - - def get_stages(self): - return [self._stage] - - def get_updates(self): - return [self._update] - - def _stage(self): - pass - - def _update(self): - pass - # We cannot call a stepper on a system until both the stepper # and system "see" one another (for performance reasons, mostly) # So before "seeing" the system, the stepper should not have @@ -72,7 +53,6 @@ def _update(self): "stepper_module", [ MockSymplecticStepper, - MockExplicitStepper, ], ) def test_symplectic_stepper_interface_for_simple_systems(self, stepper_module): @@ -86,7 +66,7 @@ def test_symplectic_stepper_interface_for_simple_systems(self, stepper_module): @pytest.mark.parametrize( "stepper_module", - [MockSymplecticStepper, MockExplicitStepper], + [MockSymplecticStepper], ) def test_symplectic_stepper_interface_for_collective_systems(self, stepper_module): system = SymplecticUndampedHarmonicOscillatorCollectiveSystem() @@ -122,11 +102,7 @@ def test_integrate_throws_an_assert_for_negative_total_steps(rng): assert "steps is negative" in str(excinfo.value) -# Added automatic discovery of Stateful explicit integrators -# ExplicitSteppers = StatefulExplicitStepper.__subclasses__() # SymplecticSteppers = SymplecticStepper.__subclasses__() -# StatefulExplicitSteppers = [StatefulRungeKutta4, StatefulEulerForward] -ExplicitSteppers = [EulerForward, RungeKutta4] SymplecticSteppers = [PositionVerlet, PEFRL] @@ -148,59 +124,6 @@ def test_symplectic_steppers(self, symplectic_stepper): atol=Tolerance.atol(), ) - @pytest.mark.parametrize("explicit_stepper", ExplicitSteppers) - def test_explicit_steppers(self, explicit_stepper): - collective_system = ScalarExponentialDampedHarmonicOscillatorCollectiveSystem() - final_time = 1.0 - if explicit_stepper == EulerForward: - # Euler requires very small time-steps and in order not to slow down test, - # we are scaling the difference between analytical and numerical solution. - n_steps = 25000 - scale = 1e3 - else: - n_steps = 500 - scale = 1 - - stepper = explicit_stepper() - - dt = np.float64(float(final_time) / n_steps) - time = np.float64(0.0) - tol = Tolerance.atol() - - # Before stepping, let's extend the interface of the stepper - # while providing memory slots - from elastica.experimental.timestepper.memory import ( - make_memory_for_explicit_stepper, - ) - - memory_collection = make_memory_for_explicit_stepper(stepper, collective_system) - from elastica.timestepper import extend_stepper_interface - - do_step, stagets_and_updates = extend_stepper_interface( - stepper, collective_system - ) - - while np.abs(final_time - time) > 1e5 * tol: - time = do_step( - stepper, - stagets_and_updates, - collective_system, - memory_collection, - time, - dt, - ) - - for system in collective_system: - assert_allclose( - system.state, - system.analytical_solution(final_time), - rtol=Tolerance.rtol() * scale, - atol=Tolerance.atol() * scale, - ) - - # @pytest.mark.parametrize("symplectic_stepper", SymplecticSteppers) - # def test_symplectic_against_collective_system(self, symplectic_stepper): - class TestSteppersAgainstRodLikeSystems: """The rods compose specific data-structures that diff --git a/tests/test_modules/test_base_system.py b/tests/test_modules/test_base_system.py index 38a907d1e..f23d4bad7 100644 --- a/tests/test_modules/test_base_system.py +++ b/tests/test_modules/test_base_system.py @@ -1,6 +1,8 @@ __doc__ = """ Test modules for base systems """ import pytest + +pass import numpy as np from elastica.modules import ( @@ -27,7 +29,8 @@ def load_collection(self): rng = np.random.default_rng(42) # Fixed seed for test reproducibility bsc = BaseSystemCollection() - bsc.extend_allowed_types((int, float, str, np.ndarray)) + bsc.extend_allowed_types((int, float, str)) + bsc.append_allowed_types(np.ndarray) # Bypass check, but its fine for testing bsc.append(3) bsc.append(5.0) @@ -76,18 +79,14 @@ def test_extend_allowed_types(self, load_collection): from elastica.rod import RodBase from elastica.rigidbody import RigidBodyBase - from elastica.surface import SurfaceBase + from elastica.systems.protocol import StaticSystemProtocol, SystemProtocol # Types are extended in the fixture - assert bsc.allowed_sys_types == ( - RodBase, - RigidBodyBase, - SurfaceBase, - int, - float, - str, - np.ndarray, - ) + assert int in bsc.allowed_sys_types + assert float in bsc.allowed_sys_types + assert str in bsc.allowed_sys_types + assert np.ndarray in bsc.allowed_sys_types + assert StaticSystemProtocol in bsc.allowed_sys_types # Minimal requirement def test_extend_correctness(self, load_collection): """ @@ -103,7 +102,7 @@ def test_extend_correctness(self, load_collection): def test_override_allowed_types(self, load_collection, mock_rod): bsc = load_collection - bsc.override_allowed_types((int, float, str)) + bsc._override_allowed_types((int, float, str)) # First check that adding a rod object throws an # error as we have replaced rods now it @@ -123,7 +122,7 @@ def test_invalid_idx_in_get_sys_index_throws(self, load_collection): from elastica.rod import RodBase bsc = load_collection - bsc.override_allowed_types((RodBase,)) + bsc._override_allowed_types((RodBase,)) with pytest.raises(AssertionError) as excinfo: bsc.get_system_index(100) assert "exceeds number of" in str(excinfo.value) @@ -150,6 +149,111 @@ def test_delitem(self, load_collection): del load_collection[0] assert load_collection[0] == 3 + def test_requisite_modules_error(self): + """Test that RuntimeError is raised when system requires modules not present.""" + + class Collection(BaseSystemCollection): + pass + + bsc = Collection() + + # Create a mock system class that requires Constraints module + class SystemWithRequisiteModules: + REQUISITE_MODULES = [int] # Require int module + + system = SystemWithRequisiteModules() + bsc.append_allowed_types( + SystemWithRequisiteModules, + ) + + # Should raise RuntimeError because BaseSystemCollection doesn't have Constraints + # The type check passes (SystemWithRequisiteModules is in allowed_sys_types), + # but REQUISITE_MODULES check fails + with pytest.raises(RuntimeError) as excinfo: + bsc._check_type(system) + assert "requires the following modules" in str(excinfo.value) + assert "int" in str(excinfo.value) + + def test_requisite_modules_success(self): + """Test that system with REQUISITE_MODULES passes when modules are present.""" + + # Create a simulator with Constraints module + class SimulatorInt(BaseSystemCollection, int): + pass + + bsc = SimulatorInt() + + # Create a mock system class that requires Constraints module + class SystemWithRequisiteModules: + REQUISITE_MODULES = [int] + + system = SystemWithRequisiteModules() + bsc.append_allowed_types( + SystemWithRequisiteModules, + ) + + # Should pass because BaseSystemCollection has Constraints + assert bsc._check_type(system) is True + + def test_enable_block_supports_new_system_type(self): + """Test enable_block_supports when system_type is not in any block_supports (else clause).""" + from elastica.rod.cosserat_rod import CosseratRod + + class CustomBlock: + pass + + class DerivedRod(CosseratRod): + def __init__(self): + pass + + derived_rod = DerivedRod() + + bsc = BaseSystemCollection() + + # Initially, CustomRod should not be in block_supports + found = False + for block_type in bsc._block_supports.values(): + if derived_rod in block_type: + found = True + break + assert not found, "CustomRod should not be in block_supports initially" + + # Enable block support for CustomRod (else clause - creates new entry) + bsc.enable_block_supports(derived_rod, CustomBlock) + assert derived_rod in bsc._block_supports[CustomBlock] + + def test_enable_block_supports_existing_system_type(self): + """Test enable_block_supports when system_type is already in block_supports (if branch).""" + from elastica.memory_block.memory_block_rod import MemoryBlockCosseratRod + from elastica.rod.cosserat_rod import CosseratRod + + class CustomBlock: + pass + + bsc = BaseSystemCollection() + + # CosseratRod should already be in block_supports (set in __init__) + assert CosseratRod in bsc._block_supports[MemoryBlockCosseratRod] + + # Get the initial count + bsc.enable_block_supports(CosseratRod, MemoryBlockCosseratRod) + assert bsc._block_supports[MemoryBlockCosseratRod].count(CosseratRod) == 1 + + # Switch block support + bsc.enable_block_supports(CosseratRod, CustomBlock) + assert bsc._block_supports[MemoryBlockCosseratRod].count(CosseratRod) == 0 + assert bsc._block_supports[CustomBlock].count(CosseratRod) == 1 + + # Create no duplicates + bsc.enable_block_supports(CosseratRod, CustomBlock) + assert bsc._block_supports[MemoryBlockCosseratRod].count(CosseratRod) == 0 + assert bsc._block_supports[CustomBlock].count(CosseratRod) == 1 + + # Switch block support back + bsc.enable_block_supports(CosseratRod, MemoryBlockCosseratRod) + assert bsc._block_supports[MemoryBlockCosseratRod].count(CosseratRod) == 1 + assert bsc._block_supports[CustomBlock].count(CosseratRod) == 0 + class GenericSimulatorClass( BaseSystemCollection, Constraints, Forcing, Connections, CallBacks @@ -231,8 +335,11 @@ def test_forcing(self, load_collection, legal_forces): from elastica.callback_functions import CallBackBaseClass @pytest.mark.parametrize("legal_callback", [CallBackBaseClass]) - def test_callback(self, load_collection, legal_callback): + def test_callback(self, mocker, load_collection, legal_callback): simulator_class, rod = load_collection + + spy = mocker.spy(legal_callback, "make_callback") + simulator_class.collect_diagnostics(rod).using(legal_callback) simulator_class.finalize() # After finalize check if the created callback object is instance of the class we have given. @@ -243,5 +350,68 @@ def test_callback(self, load_collection, legal_callback): legal_callback, ) - # TODO: this is a dummy test for apply_callbacks find a better way to test them simulator_class.apply_callbacks(time=0, current_step=0) + + assert ( + spy.call_count == 2 + ) # Callback should be called twice: once during the finalize and once during the apply_callbacks + assert spy.call_args[1]["system"] == rod + assert spy.call_args[1]["time"] == np.float64(0.0) + assert spy.call_args[1]["current_step"] == 0 + + @pytest.mark.parametrize("legal_callback", [CallBackBaseClass]) + def test_callback_in_data_structure(self, mocker, load_collection, legal_callback): + simulator_class, rod = load_collection + + spy = mocker.spy(legal_callback, "make_callback") + + simulator_class.collect_diagnostics((rod, rod)).using(legal_callback) + simulator_class.finalize() + # After finalize check if the created callback object is instance of the class we have given. + assert isinstance( + simulator_class._feature_group_callback._operator_collection[-1][ + -1 + ].func.__self__, + legal_callback, + ) + + simulator_class.apply_callbacks(time=0, current_step=0) + + assert ( + spy.call_count == 2 + ) # Callback should be called twice: once during the finalize and once during the apply_callbacks + assert spy.call_args[1]["system"] == (rod, rod) + assert spy.call_args[1]["time"] == np.float64(0.0) + assert spy.call_args[1]["current_step"] == 0 + + @pytest.mark.parametrize("legal_callback", [CallBackBaseClass]) + def test_callback_in_ellipsis(self, mocker, load_collection, legal_callback): + simulator_class, rod = load_collection + simulator_class.extend_allowed_types((int,)) + + simulator_class.append(rod) + + spy = mocker.spy(legal_callback, "make_callback") + + simulator_class.collect_diagnostics(...).using(legal_callback) + simulator_class.finalize() + # After finalize check if the created callback object is instance of the class we have given. + assert isinstance( + simulator_class._feature_group_callback._operator_collection[-1][ + -1 + ].func.__self__, + legal_callback, + ) + + simulator_class.apply_callbacks(time=0, current_step=0) + simulator_class.apply_callbacks(time=1, current_step=1) + + assert ( + spy.call_count == 3 + ) # Callback should be called twice: once during the finalize and once during the apply_callbacks + assert spy.call_args_list[1][1]["system"][0] == rod + assert spy.call_args_list[1][1]["system"][1] == rod + assert spy.call_args_list[1][1]["time"] == 0 + assert spy.call_args_list[1][1]["current_step"] == 0 + assert spy.call_args_list[2][1]["time"] == 1 + assert spy.call_args_list[2][1]["current_step"] == 1 diff --git a/tests/test_modules/test_callbacks.py b/tests/test_modules/test_callbacks.py index 09250c173..40c014920 100644 --- a/tests/test_modules/test_callbacks.py +++ b/tests/test_modules/test_callbacks.py @@ -65,7 +65,6 @@ class TestCallBacksMixin: class SystemCollectionWithCallBacksMixedin(BaseSystemCollection, CallBacks): pass - # TODO fix link after new PR from elastica.rod import RodBase class MockRod(RodBase): @@ -112,8 +111,9 @@ def test_callback_with_unregistered_system_throws(self, load_system_with_callbac def test_callback_with_illegal_system_throws(self, load_system_with_callbacks): scwc = load_system_with_callbacks - # Not a rod, but a list! - mock_rod = [1, 2, 3, 5] + # Not a rod, but a set! + # only ordered collections or single system are allowed + mock_rod = {1, 2, 3, 5} with pytest.raises(TypeError) as excinfo: scwc.collect_diagnostics(mock_rod) @@ -159,6 +159,60 @@ def mock_init(self, *args, **kwargs): return scwc, MockCallBack + @pytest.fixture + def load_multiple_rod_in_data_structure_with_callbacks( + self, load_system_with_callbacks + ): + scwc = load_system_with_callbacks + + mock_rod1 = self.MockRod(2, 3, 4, 5) + mock_rod2 = self.MockRod(2, 3, 4, 5) + mock_rod3 = self.MockRod(2, 3, 4, 5) + + scwc.append(mock_rod1) + scwc.append(mock_rod2) + scwc.append(mock_rod3) + + def mock_init(self, *args, **kwargs): + pass + + # in place class + MockCallBack = type( + "MockCallBack", (self.CallBackBaseClass, object), {"__init__": mock_init} + ) + + # Constrain any and all systems + scwc.collect_diagnostics([mock_rod1, mock_rod2, mock_rod3]).using( + MockCallBack, 2, 3 + ) # system based constraint + + return scwc, MockCallBack + + @pytest.fixture + def load_multiple_rod_in_ellipsis_with_callbacks(self, load_system_with_callbacks): + scwc = load_system_with_callbacks + + mock_rod1 = self.MockRod(2, 3, 4, 5) + mock_rod2 = self.MockRod(2, 3, 4, 5) + + scwc.append(mock_rod1) + scwc.append(mock_rod2) + + def mock_init(self, *args, **kwargs): + pass + + # in place class + MockCallBack = type( + "MockCallBack", (self.CallBackBaseClass, object), {"__init__": mock_init} + ) + + # Constrain any and all systems + scwc.collect_diagnostics(...).using( + MockCallBack, 2, 3 + ) # system based constraint + + return scwc, MockCallBack + def test_callback_finalize_correctness(self, load_rod_with_callbacks): scwc, callback_cls = load_rod_with_callbacks callback_features = [d for d in scwc._callback_list] @@ -185,17 +239,36 @@ def test_callback_finalize_sorted(self, load_rod_with_callbacks): assert num < x num = x - def test_first_call_callback_during_finalize(self, mocker, load_rod_with_callbacks): - """ - This test is to check if the callback is called during the finalize. - If this test fails, check if `apply_callbacks` is called during the finalization step. - """ - scwc, callback_cls = load_rod_with_callbacks + def test_callback_finalize_correctness_with_data_structure_of_systems( + self, load_multiple_rod_in_data_structure_with_callbacks + ): + scwc, callback_cls = load_multiple_rod_in_data_structure_with_callbacks + callback_features = [d for d in scwc._callback_list] + + scwc._finalize_callback() + + for _callback in callback_features: + x = _callback.id() + y = _callback.instantiate() + assert isinstance(x, list) + assert isinstance(x[0], int) + assert isinstance(y, callback_cls) + + assert not hasattr(scwc, "_callback_list") + + def test_callback_finalize_correctness_with_ellipsis( + self, load_multiple_rod_in_ellipsis_with_callbacks + ): + scwc, callback_cls = load_multiple_rod_in_ellipsis_with_callbacks callback_features = [d for d in scwc._callback_list] - spy = mocker.spy(scwc, "apply_callbacks") scwc._finalize_callback() - assert spy.call_count == 1 - assert spy.call_args[1]["time"] == np.float64(0.0) - assert spy.call_args[1]["current_step"] == 0 + for _callback in callback_features: + x = _callback.id() + y = _callback.instantiate() + assert isinstance(x, tuple) + assert isinstance(x[0], int) + assert isinstance(y, callback_cls) + + assert not hasattr(scwc, "_callback_list") diff --git a/tests/test_modules/test_callbacks_close.py b/tests/test_modules/test_callbacks_close.py new file mode 100644 index 000000000..4f3fe0cba --- /dev/null +++ b/tests/test_modules/test_callbacks_close.py @@ -0,0 +1,65 @@ +__doc__ = """ Test modules for callback """ +import numpy as np +import pytest + +from elastica.callback_functions import CallBackBaseClass + + +class TestCallBacksClosing: + from elastica.modules import BaseSystemCollection + from elastica.modules import CallBacks + + class SystemCollectionWithCallBacksMixedin(BaseSystemCollection, CallBacks): + pass + + def test_callback_closing_test_default_callback_impl(self): + """ + Test if any class derived from CallBackBaseClass can be used + without any error when simulator.close() is called. + This is to check the backward compatibility, as many previous + callback classes are derived from CallBackBaseClass, + but does not have explicit implementation of on_close method. + """ + sys_coll = self.SystemCollectionWithCallBacksMixedin() + sys_coll.extend_allowed_types((int,)) + rod = 0 + + class MockCallback(CallBackBaseClass): + pass + + # build flag check for some MockCallback.on_close() function call + + sys_coll.append(rod) + sys_coll.collect_diagnostics(rod).using(MockCallback) + sys_coll.close() + + def test_callback_closing_custom(self): + """ + Check if on_close is called properly with a custom callback. + """ + sys_coll = self.SystemCollectionWithCallBacksMixedin() + sys_coll.extend_allowed_types((int,)) + rod = 0 + + CLOSE_CALLED_FLAG = [] + + class MockCallback(CallBackBaseClass): + def __init__(self, o): + self.o = o + + def on_close(self): + self.o.append(42) + + sys_coll.append(rod) + sys_coll.collect_diagnostics(rod).using(MockCallback, o=CLOSE_CALLED_FLAG) + sys_coll.close() + + # Before finalize, on_close function should not be hooked. + assert not CLOSE_CALLED_FLAG + + # After finalize, on_close function should be called. + sys_coll.finalize() + sys_coll.close() + + assert len(CLOSE_CALLED_FLAG) == 1 + assert CLOSE_CALLED_FLAG[0] == 42 diff --git a/tests/test_modules/test_connections.py b/tests/test_modules/test_connections.py index eadf98a0f..8c73eff5d 100644 --- a/tests/test_modules/test_connections.py +++ b/tests/test_modules/test_connections.py @@ -126,11 +126,19 @@ def test_using_with_illegal_connect_throws_assertion_error( ): with pytest.raises(AssertionError) as excinfo: load_connect.using(illegal_connect) - assert "not a valid joint" in str(excinfo.value) - - from elastica.joint import FreeJoint, FixedJoint, HingeJoint + assert "not a valid" in str(excinfo.value) + + from elastica.joint import ( + FreeJoint, + FixedJoint, + HingeJoint, + BallJoint, + SphericalJoint, + ) - @pytest.mark.parametrize("legal_connect", [FreeJoint, HingeJoint, FixedJoint]) + @pytest.mark.parametrize( + "legal_connect", [FreeJoint, HingeJoint, FixedJoint, BallJoint, SphericalJoint] + ) def test_using_with_legal_connect(self, load_connect, legal_connect): connect = load_connect connect.using(legal_connect, 3, 4.0, "5", k=1, l_var="2", j=3.0) @@ -174,10 +182,7 @@ def mock_init(self, *args, **kwargs): # Actual test is here, this should not throw with pytest.raises(TypeError) as excinfo: _ = connect.instantiate() - assert ( - r"Unable to construct connection class.\nDid you provide all necessary joint properties?" - == str(excinfo.value) - ) + assert r"Unable to construct connection class" in str(excinfo.value) class TestConnectionsMixin: @@ -186,7 +191,6 @@ class TestConnectionsMixin: class SystemCollectionWithConnectionsMixin(BaseSystemCollection, Connections): pass - # TODO fix link after new PR from elastica.rod import RodBase class MockRod(RodBase): diff --git a/tests/test_modules/test_constraints.py b/tests/test_modules/test_constraints.py index c998db84a..facc63068 100644 --- a/tests/test_modules/test_constraints.py +++ b/tests/test_modules/test_constraints.py @@ -213,7 +213,6 @@ class TestConstraintsMixin: class SystemCollectionWithConstraintsMixedin(BaseSystemCollection, Constraints): pass - # TODO fix link after new PR from elastica.rod import RodBase class MockRod(RodBase): diff --git a/tests/test_modules/test_contact.py b/tests/test_modules/test_contact.py index 79abb7c5e..2fe53fb2f 100644 --- a/tests/test_modules/test_contact.py +++ b/tests/test_modules/test_contact.py @@ -90,7 +90,7 @@ class SystemCollectionWithContactMixin(BaseSystemCollection, Contact): from elastica.rod import RodBase from elastica.rigidbody import RigidBodyBase - from elastica.surface import SurfaceBase + from elastica.systems.protocol import StaticSystemProtocol class MockRod(RodBase): def __init__(self, *args, **kwargs): @@ -113,7 +113,7 @@ class MockRigidBody(RigidBodyBase): def __init__(self, *args, **kwargs): self.n_elems = 1 - class MockSurface(SurfaceBase): + class MockSurface(StaticSystemProtocol): def __init__(self, *args, **kwargs): self.n_facets = 1 diff --git a/tests/test_modules/test_damping.py b/tests/test_modules/test_damping.py index 993506d86..0ee541db2 100644 --- a/tests/test_modules/test_damping.py +++ b/tests/test_modules/test_damping.py @@ -89,7 +89,6 @@ class TestDampingMixin: class SystemCollectionWithDampingMixedin(BaseSystemCollection, Damping): pass - # TODO fix link after new PR from elastica.rod import RodBase class MockRod(RodBase): diff --git a/tests/test_modules/test_forcing.py b/tests/test_modules/test_forcing.py index 4f621fc40..759bdaa5f 100644 --- a/tests/test_modules/test_forcing.py +++ b/tests/test_modules/test_forcing.py @@ -72,7 +72,6 @@ class TestForcingMixin: class SystemCollectionWithForcingMixedin(BaseSystemCollection, Forcing): pass - # TODO fix link after new PR from elastica.rod import RodBase class MockRod(RodBase): @@ -166,42 +165,6 @@ def mock_init(self, *args, **kwargs): return scwf, MockForcing - def test_friction_plane_forcing_class(self, load_system_with_forcings): - - scwf = load_system_with_forcings - - mock_rod = self.MockRod(2, 3, 4, 5) - scwf.append(mock_rod) - - from elastica.interaction import AnisotropicFrictionalPlane - - # Add friction plane - scwf.add_forcing_to(1).using( - AnisotropicFrictionalPlane, - k=0, - nu=0, - plane_origin=np.zeros((3,)), - plane_normal=np.zeros((3,)), - slip_velocity_tol=0, - static_mu_array=[0, 0, 0], - kinetic_mu_array=[0, 0, 0], - ) - # Add another forcing class - - def mock_init(self, *args, **kwargs): - pass - - MockForcing = type( - "MockForcing", (self.NoForces, object), {"__init__": mock_init} - ) - scwf.add_forcing_to(1).using(MockForcing, 2, 42) # index based forcing - - # Now check if the Anisotropic friction and the MockForcing are in the list - assert scwf._ext_forces_torques[-1]._forcing_cls == MockForcing - assert scwf._ext_forces_torques[-2]._forcing_cls == AnisotropicFrictionalPlane - scwf._finalize_forcing() - assert not hasattr(scwf, "_ext_forces_torques") - def test_constrain_finalize_correctness(self, load_rod_with_forcings): scwf, forcing_cls = load_rod_with_forcings forcing_features = [f for f in scwf._ext_forces_torques] diff --git a/tests/test_modules/test_memory_block_cosserat_rod.py b/tests/test_modules/test_memory_block_cosserat_rod.py index 0a3c025a4..62b6790b2 100644 --- a/tests/test_modules/test_memory_block_cosserat_rod.py +++ b/tests/test_modules/test_memory_block_cosserat_rod.py @@ -8,6 +8,7 @@ from elastica.rod import RodBase from elastica.modules.memory_block import construct_memory_block_structures from elastica.memory_block.memory_block_rod import MemoryBlockCosseratRod +from elastica.systems.protocol import SystemProtocol class BaseRodForTesting(RodBase): @@ -74,19 +75,38 @@ def __init__(self, n_elems: np.int64, ring_rod_flag: bool): self.bend_matrix = rng.standard_normal() * np.ones((3, 3, self.n_voronoi)) +class BaseRodForTestingSteppable(SystemProtocol): + def compute_internal_forces_and_torques(self, time: np.float64) -> None: + pass + + def update_accelerations(self, time: np.float64, dt: np.float64) -> None: + pass + + def zeroed_out_external_forces_and_torques(self, time: np.float64) -> None: + pass + + @pytest.mark.parametrize("n_rods", [1, 2, 5, 6]) -def test_construct_memory_block_structures_for_cosserat_rod(n_rods): +def test_construct_memory_block_structures_for_cosserat_rod_with_non_blocking_systems( + n_rods, +): """ - This test is only testing the validity of created block-structure class, using the + This test is only testing the validity of created block-structure class with non-blocking systems, using the construct_memory_block_structures function. + """ - Parameters - ---------- - n_rods + systems = [BaseRodForTestingSteppable() for _ in range(n_rods)] + _, non_blocking_systems_list = construct_memory_block_structures(systems, {}) + + assert len(non_blocking_systems_list) == n_rods + assert systems == non_blocking_systems_list - Returns - ------- +@pytest.mark.parametrize("n_rods", [1, 2, 5, 6]) +def test_construct_memory_block_structures_for_cosserat_rod(n_rods): + """ + This test is only testing the validity of created block-structure class, using the + construct_memory_block_structures function. """ systems = [ @@ -96,9 +116,11 @@ def test_construct_memory_block_structures_for_cosserat_rod(n_rods): for _ in range(n_rods) ] - memory_block_list = construct_memory_block_structures(systems) + block_supports = {MemoryBlockCosseratRod: [BaseRodForTesting]} + memory_block_list, _ = construct_memory_block_structures(systems, block_supports) - assert issubclass(memory_block_list[0].__class__, MemoryBlockCosseratRod) + assert isinstance(memory_block_list[0], MemoryBlockCosseratRod) + assert len(memory_block_list) == 1 @pytest.mark.parametrize("n_straight_rods", [0, 1, 2, 5]) diff --git a/tests/test_modules/test_memory_block_rigid_body.py b/tests/test_modules/test_memory_block_rigid_body.py index ef43d7807..8b3e76fbc 100644 --- a/tests/test_modules/test_memory_block_rigid_body.py +++ b/tests/test_modules/test_memory_block_rigid_body.py @@ -60,9 +60,10 @@ def test_construct_memory_block_structures_for_rigid_bodies(n_bodies): systems = [MockRigidBodyForTesting() for _ in range(n_bodies)] - memory_block_list = construct_memory_block_structures(systems) + block_supports = {MemoryBlockRigidBody: [MockRigidBodyForTesting]} + memory_block_list, _ = construct_memory_block_structures(systems, block_supports) - assert issubclass(memory_block_list[0].__class__, MemoryBlockRigidBody) + assert isinstance(memory_block_list[0], MemoryBlockRigidBody) @pytest.mark.parametrize("n_bodies", [1, 2, 5, 6]) diff --git a/tests/test_restart.py b/tests/test_restart.py index e625fb54b..e538bff8f 100644 --- a/tests/test_restart.py +++ b/tests/test_restart.py @@ -168,7 +168,7 @@ class BaseSimulatorClass( return np.concatenate(recorded_list) - @pytest.mark.parametrize("final_time", [0.2, 1.0]) + @pytest.mark.parametrize("final_time", [0.002, 0.01]) def test_save_restart_run_sim(self, tmp_path, final_time): # First half of simulation diff --git a/tests/test_rigid_body/test_cylinder.py b/tests/test_rigid_body/test_cylinder.py index 73334e7c9..494217fa8 100644 --- a/tests/test_rigid_body/test_cylinder.py +++ b/tests/test_rigid_body/test_cylinder.py @@ -111,7 +111,7 @@ def test_cylinder_update_accelerations(rng): test_cylinder.external_forces[:] = external_forces test_cylinder.external_torques[:] = external_torques - test_cylinder.update_accelerations(time=0) + test_cylinder.update_accelerations(time=0, dt=0) assert_allclose( correct_acceleration, diff --git a/tests/test_rigid_body/test_rigid_body_data_structures.py b/tests/test_rigid_body/test_rigid_body_data_structures.py index 6d1451088..4bf1426c6 100644 --- a/tests/test_rigid_body/test_rigid_body_data_structures.py +++ b/tests/test_rigid_body/test_rigid_body_data_structures.py @@ -67,7 +67,7 @@ def __init__(self, start_position, start_director): def compute_internal_forces_and_torques(self, time): pass - def update_accelerations(self, time): + def update_accelerations(self, time, dt): np.copyto(self.acceleration_collection, -np.sin(np.pi * time)) np.copyto(self.alpha_collection[2, ...], 0.1 * np.pi) diff --git a/tests/test_rigid_body/test_sphere.py b/tests/test_rigid_body/test_sphere.py index 94afd266e..4366a01da 100644 --- a/tests/test_rigid_body/test_sphere.py +++ b/tests/test_rigid_body/test_sphere.py @@ -108,7 +108,7 @@ def test_sphere_update_accelerations(rng): test_sphere.external_forces[:] = external_forces test_sphere.external_torques[:] = external_torques - test_sphere.update_accelerations(time=0) + test_sphere.update_accelerations(time=0, dt=0.0) assert_allclose( correct_acceleration, test_sphere.acceleration_collection, atol=Tolerance.atol() diff --git a/tests/test_rod/mock_rod.py b/tests/test_rod/mock_rod.py index a70bf17fa..570456b10 100644 --- a/tests/test_rod/mock_rod.py +++ b/tests/test_rod/mock_rod.py @@ -3,7 +3,7 @@ import numpy as np -from elastica.memory_block.memory_block_rod_base import ( +from elastica.memory_block.utils import ( make_block_memory_periodic_boundary_metadata, ) from elastica.utils import MaxDimension diff --git a/tests/test_rod/test_knot_theory.py b/tests/test_rod/test_knot_theory.py index dfe7466e9..b6d8e91f5 100644 --- a/tests/test_rod/test_knot_theory.py +++ b/tests/test_rod/test_knot_theory.py @@ -17,8 +17,6 @@ _compute_additional_segment, ) -from elastica.rod.protocol import CosseratRodProtocol - @pytest.fixture def knot_theory(): @@ -27,13 +25,6 @@ def knot_theory(): return knot_theory -def test_knot_theory_protocol(): - # To clear the protocol test coverage - with pytest.raises(TypeError) as e_info: - protocol = CosseratRodProtocol() - assert "cannot be instantiated" in e_info - - def test_knot_theory_mixin_methods(rng, knot_theory): class TestRodWithKnotTheory(MockTestRod, knot_theory.KnotTheory): def __init__(self): @@ -183,9 +174,14 @@ def test_knot_theory_compute_additional_segment_integrity(type_str): def test_knot_theory_compute_additional_segment_straight_case( n_elem, segment_length, type_str ): - # If straight rod give, result should be same regardless of type - center_line = np.zeros([1, 3, n_elem]) + # If straight rod give, result should be same regardless of type and time steps + center_line = np.zeros([2, 3, n_elem]) center_line[0, 2, :] = np.linspace(0, 5, n_elem) + + # adding a sine curve to second time step to ensure it does not affect straight case + center_line[1, 1, :] = np.sin(np.linspace(0, 5, n_elem)) + center_line[1, 2, :] = np.cos(np.linspace(0, 5, n_elem)) + ncl, bd, ed = _compute_additional_segment(center_line, segment_length, type_str) assert_allclose(ncl[0, :, 0], np.array([0, 0, -segment_length])) assert_allclose( diff --git a/uv.lock b/uv.lock index 611217133..c4a658950 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.11'", @@ -12,18 +13,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899 } +sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903 }, + { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, ] [[package]] name = "alabaster" version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210 } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929 }, + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] [[package]] @@ -34,18 +35,18 @@ dependencies = [ { name = "pyflakes" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2a/cb/486f912d6171bc5748c311a2984a301f4e2d054833a1da78485866c71522/autoflake-2.3.1.tar.gz", hash = "sha256:c98b75dc5b0a86459c4f01a1d32ac7eb4338ec4317a4469515ff1e687ecd909e", size = 27642 } +sdist = { url = "https://files.pythonhosted.org/packages/2a/cb/486f912d6171bc5748c311a2984a301f4e2d054833a1da78485866c71522/autoflake-2.3.1.tar.gz", hash = "sha256:c98b75dc5b0a86459c4f01a1d32ac7eb4338ec4317a4469515ff1e687ecd909e", size = 27642, upload-time = "2024-03-13T03:41:28.977Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/ee/3fd29bf416eb4f1c5579cf12bf393ae954099258abd7bde03c4f9716ef6b/autoflake-2.3.1-py3-none-any.whl", hash = "sha256:3ae7495db9084b7b32818b4140e6dc4fc280b712fb414f5b8fe57b0a8e85a840", size = 32483 }, + { url = "https://files.pythonhosted.org/packages/a2/ee/3fd29bf416eb4f1c5579cf12bf393ae954099258abd7bde03c4f9716ef6b/autoflake-2.3.1-py3-none-any.whl", hash = "sha256:3ae7495db9084b7b32818b4140e6dc4fc280b712fb414f5b8fe57b0a8e85a840", size = 32483, upload-time = "2024-03-13T03:41:26.969Z" }, ] [[package]] name = "babel" version = "2.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, ] [[package]] @@ -56,9 +57,9 @@ dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285 }, + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" }, ] [[package]] @@ -74,104 +75,104 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 } +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419 }, - { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080 }, - { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886 }, - { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404 }, - { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372 }, - { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865 }, - { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699 }, - { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028 }, - { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988 }, - { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985 }, - { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816 }, - { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 }, - { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 }, - { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 }, - { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 }, - { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 }, - { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 }, + { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419, upload-time = "2025-01-29T05:37:06.642Z" }, + { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080, upload-time = "2025-01-29T05:37:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886, upload-time = "2025-01-29T04:18:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404, upload-time = "2025-01-29T04:19:04.296Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372, upload-time = "2025-01-29T05:37:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865, upload-time = "2025-01-29T05:37:14.309Z" }, + { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699, upload-time = "2025-01-29T04:18:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028, upload-time = "2025-01-29T04:18:51.711Z" }, + { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988, upload-time = "2025-01-29T05:37:16.707Z" }, + { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985, upload-time = "2025-01-29T05:37:18.273Z" }, + { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816, upload-time = "2025-01-29T04:18:33.823Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860, upload-time = "2025-01-29T04:19:12.944Z" }, + { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" }, + { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" }, + { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, ] [[package]] name = "certifi" version = "2025.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386 } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216 }, + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, ] [[package]] name = "cfgv" version = "3.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818 }, - { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649 }, - { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045 }, - { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356 }, - { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471 }, - { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317 }, - { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368 }, - { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491 }, - { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695 }, - { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849 }, - { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091 }, - { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445 }, - { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782 }, - { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 }, - { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 }, - { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 }, - { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 }, - { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 }, - { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 }, - { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 }, - { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 }, - { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 }, - { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 }, - { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 }, - { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 }, - { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 }, - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936 }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790 }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924 }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626 }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567 }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957 }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408 }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399 }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815 }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537 }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565 }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357 }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776 }, - { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622 }, - { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435 }, - { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653 }, - { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231 }, - { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243 }, - { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442 }, - { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147 }, - { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057 }, - { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454 }, - { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174 }, - { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166 }, - { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 }, - { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] [[package]] @@ -181,9 +182,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 } +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 }, + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, ] [[package]] @@ -193,9 +194,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/fe/bd65ec131f5679900c5e874ef60d088849e299c0dba6d98cce69e56b2d98/cma-4.3.0.tar.gz", hash = "sha256:faa8933e9d55e199c052dd114d123d8d9a3ca914d932629e8b6e77200681a206", size = 284531 } +sdist = { url = "https://files.pythonhosted.org/packages/ca/fe/bd65ec131f5679900c5e874ef60d088849e299c0dba6d98cce69e56b2d98/cma-4.3.0.tar.gz", hash = "sha256:faa8933e9d55e199c052dd114d123d8d9a3ca914d932629e8b6e77200681a206", size = 284531, upload-time = "2025-07-24T11:32:18.261Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/99/351d5a95a481068d83e1f1967b9d00237e70519fc6d4d5e9e390b94e5714/cma-4.3.0-py3-none-any.whl", hash = "sha256:65ad8799e6438b8b82c82c11aad070727e5887915ea6f2c17d7ca0eea940c57a", size = 295869 }, + { url = "https://files.pythonhosted.org/packages/32/99/351d5a95a481068d83e1f1967b9d00237e70519fc6d4d5e9e390b94e5714/cma-4.3.0-py3-none-any.whl", hash = "sha256:65ad8799e6438b8b82c82c11aad070727e5887915ea6f2c17d7ca0eea940c57a", size = 295869, upload-time = "2025-07-24T11:32:13.722Z" }, ] [[package]] @@ -206,18 +207,18 @@ dependencies = [ { name = "coverage" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/bb/594b26d2c85616be6195a64289c578662678afa4910cef2d3ce8417cf73e/codecov-2.1.13.tar.gz", hash = "sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c", size = 21416 } +sdist = { url = "https://files.pythonhosted.org/packages/2c/bb/594b26d2c85616be6195a64289c578662678afa4910cef2d3ce8417cf73e/codecov-2.1.13.tar.gz", hash = "sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c", size = 21416, upload-time = "2023-04-17T23:11:39.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/02/18785edcdf6266cdd6c6dc7635f1cbeefd9a5b4c3bb8aff8bd681e9dd095/codecov-2.1.13-py2.py3-none-any.whl", hash = "sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5", size = 16512 }, + { url = "https://files.pythonhosted.org/packages/af/02/18785edcdf6266cdd6c6dc7635f1cbeefd9a5b4c3bb8aff8bd681e9dd095/codecov-2.1.13-py2.py3-none-any.whl", hash = "sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5", size = 16512, upload-time = "2023-04-17T23:11:37.344Z" }, ] [[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 } +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 }, + { 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]] @@ -230,64 +231,64 @@ resolution-markers = [ dependencies = [ { name = "numpy", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551 }, - { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399 }, - { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061 }, - { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956 }, - { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872 }, - { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027 }, - { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641 }, - { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075 }, - { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534 }, - { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188 }, - { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636 }, - { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636 }, - { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053 }, - { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985 }, - { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750 }, - { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246 }, - { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728 }, - { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762 }, - { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196 }, - { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017 }, - { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580 }, - { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530 }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688 }, - { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331 }, - { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963 }, - { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681 }, - { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674 }, - { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480 }, - { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489 }, - { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042 }, - { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630 }, - { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670 }, - { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694 }, - { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986 }, - { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060 }, - { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747 }, - { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895 }, - { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098 }, - { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535 }, - { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096 }, - { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090 }, - { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643 }, - { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443 }, - { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865 }, - { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162 }, - { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355 }, - { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935 }, - { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168 }, - { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550 }, - { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214 }, - { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681 }, - { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101 }, - { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599 }, - { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807 }, - { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729 }, - { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791 }, +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, ] [[package]] @@ -300,164 +301,164 @@ resolution-markers = [ dependencies = [ { name = "numpy", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773 }, - { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149 }, - { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222 }, - { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234 }, - { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555 }, - { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238 }, - { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218 }, - { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867 }, - { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677 }, - { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234 }, - { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123 }, - { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419 }, - { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979 }, - { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653 }, - { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536 }, - { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397 }, - { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601 }, - { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288 }, - { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386 }, - { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018 }, - { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567 }, - { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655 }, - { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257 }, - { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034 }, - { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672 }, - { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234 }, - { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169 }, - { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859 }, - { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062 }, - { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932 }, - { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024 }, - { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578 }, - { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524 }, - { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730 }, - { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897 }, - { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751 }, - { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486 }, - { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106 }, - { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548 }, - { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297 }, - { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023 }, - { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157 }, - { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570 }, - { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713 }, - { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189 }, - { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251 }, - { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810 }, - { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871 }, - { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264 }, - { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819 }, - { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650 }, - { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833 }, - { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692 }, - { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424 }, - { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300 }, - { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769 }, - { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892 }, - { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748 }, - { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554 }, - { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118 }, - { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555 }, - { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295 }, - { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027 }, - { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428 }, - { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331 }, - { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831 }, - { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809 }, - { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593 }, - { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202 }, - { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207 }, - { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315 }, +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, ] [[package]] name = "coverage" version = "7.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/76/17780846fc7aade1e66712e1e27dd28faa0a5d987a1f433610974959eaa8/coverage-7.10.2.tar.gz", hash = "sha256:5d6e6d84e6dd31a8ded64759626627247d676a23c1b892e1326f7c55c8d61055", size = 820754 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/5f/5ce748ab3f142593698aff5f8a0cf020775aa4e24b9d8748b5a56b64d3f8/coverage-7.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:79f0283ab5e6499fd5fe382ca3d62afa40fb50ff227676a3125d18af70eabf65", size = 215003 }, - { url = "https://files.pythonhosted.org/packages/f4/ed/507088561217b000109552139802fa99c33c16ad19999c687b601b3790d0/coverage-7.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4545e906f595ee8ab8e03e21be20d899bfc06647925bc5b224ad7e8c40e08b8", size = 215391 }, - { url = "https://files.pythonhosted.org/packages/79/1b/0f496259fe137c4c5e1e8eaff496fb95af88b71700f5e57725a4ddbe742b/coverage-7.10.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ae385e1d58fbc6a9b1c315e5510ac52281e271478b45f92ca9b5ad42cf39643f", size = 242367 }, - { url = "https://files.pythonhosted.org/packages/b9/8e/5a8835fb0122a2e2a108bf3527931693c4625fdc4d953950a480b9625852/coverage-7.10.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f0cbe5f7dd19f3a32bac2251b95d51c3b89621ac88a2648096ce40f9a5aa1e7", size = 243627 }, - { url = "https://files.pythonhosted.org/packages/c3/96/6a528429c2e0e8d85261764d0cd42e51a429510509bcc14676ee5d1bb212/coverage-7.10.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd17f427f041f6b116dc90b4049c6f3e1230524407d00daa2d8c7915037b5947", size = 245485 }, - { url = "https://files.pythonhosted.org/packages/bf/82/1fba935c4d02c33275aca319deabf1f22c0f95f2c0000bf7c5f276d6f7b4/coverage-7.10.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7f10ca4cde7b466405cce0a0e9971a13eb22e57a5ecc8b5f93a81090cc9c7eb9", size = 243429 }, - { url = "https://files.pythonhosted.org/packages/fc/a8/c8dc0a57a729fc93be33ab78f187a8f52d455fa8f79bfb379fe23b45868d/coverage-7.10.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3b990df23dd51dccce26d18fb09fd85a77ebe46368f387b0ffba7a74e470b31b", size = 242104 }, - { url = "https://files.pythonhosted.org/packages/b9/6f/0b7da1682e2557caeed299a00897b42afde99a241a01eba0197eb982b90f/coverage-7.10.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc3902584d25c7eef57fb38f440aa849a26a3a9f761a029a72b69acfca4e31f8", size = 242397 }, - { url = "https://files.pythonhosted.org/packages/2d/e4/54dc833dadccd519c04a28852f39a37e522bad35d70cfe038817cdb8f168/coverage-7.10.2-cp310-cp310-win32.whl", hash = "sha256:9dd37e9ac00d5eb72f38ed93e3cdf2280b1dbda3bb9b48c6941805f265ad8d87", size = 217502 }, - { url = "https://files.pythonhosted.org/packages/c3/e7/2f78159c4c127549172f427dff15b02176329327bf6a6a1fcf1f603b5456/coverage-7.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:99d16f15cb5baf0729354c5bd3080ae53847a4072b9ba1e10957522fb290417f", size = 218388 }, - { url = "https://files.pythonhosted.org/packages/6e/53/0125a6fc0af4f2687b4e08b0fb332cd0d5e60f3ca849e7456f995d022656/coverage-7.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c3b210d79925a476dfc8d74c7d53224888421edebf3a611f3adae923e212b27", size = 215119 }, - { url = "https://files.pythonhosted.org/packages/0e/2e/960d9871de9152dbc9ff950913c6a6e9cf2eb4cc80d5bc8f93029f9f2f9f/coverage-7.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf67d1787cd317c3f8b2e4c6ed1ae93497be7e30605a0d32237ac37a37a8a322", size = 215511 }, - { url = "https://files.pythonhosted.org/packages/3f/34/68509e44995b9cad806d81b76c22bc5181f3535bca7cd9c15791bfd8951e/coverage-7.10.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:069b779d03d458602bc0e27189876e7d8bdf6b24ac0f12900de22dd2154e6ad7", size = 245513 }, - { url = "https://files.pythonhosted.org/packages/ef/d4/9b12f357413248ce40804b0f58030b55a25b28a5c02db95fb0aa50c5d62c/coverage-7.10.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c2de4cb80b9990e71c62c2d3e9f3ec71b804b1f9ca4784ec7e74127e0f42468", size = 247350 }, - { url = "https://files.pythonhosted.org/packages/b6/40/257945eda1f72098e4a3c350b1d68fdc5d7d032684a0aeb6c2391153ecf4/coverage-7.10.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75bf7ab2374a7eb107602f1e07310cda164016cd60968abf817b7a0b5703e288", size = 249516 }, - { url = "https://files.pythonhosted.org/packages/ff/55/8987f852ece378cecbf39a367f3f7ec53351e39a9151b130af3a3045b83f/coverage-7.10.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3f37516458ec1550815134937f73d6d15b434059cd10f64678a2068f65c62406", size = 247241 }, - { url = "https://files.pythonhosted.org/packages/df/ae/da397de7a42a18cea6062ed9c3b72c50b39e0b9e7b2893d7172d3333a9a1/coverage-7.10.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:de3c6271c482c250d3303fb5c6bdb8ca025fff20a67245e1425df04dc990ece9", size = 245274 }, - { url = "https://files.pythonhosted.org/packages/4e/64/7baa895eb55ec0e1ec35b988687ecd5d4475ababb0d7ae5ca3874dd90ee7/coverage-7.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:98a838101321ac3089c9bb1d4bfa967e8afed58021fda72d7880dc1997f20ae1", size = 245882 }, - { url = "https://files.pythonhosted.org/packages/24/6c/1fd76a0bd09ae75220ae9775a8290416d726f0e5ba26ea72346747161240/coverage-7.10.2-cp311-cp311-win32.whl", hash = "sha256:f2a79145a531a0e42df32d37be5af069b4a914845b6f686590739b786f2f7bce", size = 217541 }, - { url = "https://files.pythonhosted.org/packages/5f/2d/8c18fb7a6e74c79fd4661e82535bc8c68aee12f46c204eabf910b097ccc9/coverage-7.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:e4f5f1320f8ee0d7cfa421ceb257bef9d39fd614dd3ddcfcacd284d4824ed2c2", size = 218426 }, - { url = "https://files.pythonhosted.org/packages/da/40/425bb35e4ff7c7af177edf5dffd4154bc2a677b27696afe6526d75c77fec/coverage-7.10.2-cp311-cp311-win_arm64.whl", hash = "sha256:d8f2d83118f25328552c728b8e91babf93217db259ca5c2cd4dd4220b8926293", size = 217116 }, - { url = "https://files.pythonhosted.org/packages/4e/1e/2c752bdbbf6f1199c59b1a10557fbb6fb3dc96b3c0077b30bd41a5922c1f/coverage-7.10.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:890ad3a26da9ec7bf69255b9371800e2a8da9bc223ae5d86daeb940b42247c83", size = 215311 }, - { url = "https://files.pythonhosted.org/packages/68/6a/84277d73a2cafb96e24be81b7169372ba7ff28768ebbf98e55c85a491b0f/coverage-7.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38fd1ccfca7838c031d7a7874d4353e2f1b98eb5d2a80a2fe5732d542ae25e9c", size = 215550 }, - { url = "https://files.pythonhosted.org/packages/b5/e7/5358b73b46ac76f56cc2de921eeabd44fabd0b7ff82ea4f6b8c159c4d5dc/coverage-7.10.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:76c1ffaaf4f6f0f6e8e9ca06f24bb6454a7a5d4ced97a1bc466f0d6baf4bd518", size = 246564 }, - { url = "https://files.pythonhosted.org/packages/7c/0e/b0c901dd411cb7fc0cfcb28ef0dc6f3049030f616bfe9fc4143aecd95901/coverage-7.10.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86da8a3a84b79ead5c7d0e960c34f580bc3b231bb546627773a3f53c532c2f21", size = 248993 }, - { url = "https://files.pythonhosted.org/packages/0e/4e/a876db272072a9e0df93f311e187ccdd5f39a190c6d1c1f0b6e255a0d08e/coverage-7.10.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99cef9731c8a39801830a604cc53c93c9e57ea8b44953d26589499eded9576e0", size = 250454 }, - { url = "https://files.pythonhosted.org/packages/64/d6/1222dc69f8dd1be208d55708a9f4a450ad582bf4fa05320617fea1eaa6d8/coverage-7.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea58b112f2966a8b91eb13f5d3b1f8bb43c180d624cd3283fb33b1cedcc2dd75", size = 248365 }, - { url = "https://files.pythonhosted.org/packages/62/e3/40fd71151064fc315c922dd9a35e15b30616f00146db1d6a0b590553a75a/coverage-7.10.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:20f405188d28da9522b7232e51154e1b884fc18d0b3a10f382d54784715bbe01", size = 246562 }, - { url = "https://files.pythonhosted.org/packages/fc/14/8aa93ddcd6623ddaef5d8966268ac9545b145bce4fe7b1738fd1c3f0d957/coverage-7.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:64586ce42bbe0da4d9f76f97235c545d1abb9b25985a8791857690f96e23dc3b", size = 247772 }, - { url = "https://files.pythonhosted.org/packages/07/4e/dcb1c01490623c61e2f2ea85cb185fa6a524265bb70eeb897d3c193efeb9/coverage-7.10.2-cp312-cp312-win32.whl", hash = "sha256:bc2e69b795d97ee6d126e7e22e78a509438b46be6ff44f4dccbb5230f550d340", size = 217710 }, - { url = "https://files.pythonhosted.org/packages/79/16/e8aab4162b5f80ad2e5e1f54b1826e2053aa2f4db508b864af647f00c239/coverage-7.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:adda2268b8cf0d11f160fad3743b4dfe9813cd6ecf02c1d6397eceaa5b45b388", size = 218499 }, - { url = "https://files.pythonhosted.org/packages/06/7f/c112ec766e8f1131ce8ce26254be028772757b2d1e63e4f6a4b0ad9a526c/coverage-7.10.2-cp312-cp312-win_arm64.whl", hash = "sha256:164429decd0d6b39a0582eaa30c67bf482612c0330572343042d0ed9e7f15c20", size = 217154 }, - { url = "https://files.pythonhosted.org/packages/8d/04/9b7a741557f93c0ed791b854d27aa8d9fe0b0ce7bb7c52ca1b0f2619cb74/coverage-7.10.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aca7b5645afa688de6d4f8e89d30c577f62956fefb1bad021490d63173874186", size = 215337 }, - { url = "https://files.pythonhosted.org/packages/02/a4/8d1088cd644750c94bc305d3cf56082b4cdf7fb854a25abb23359e74892f/coverage-7.10.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:96e5921342574a14303dfdb73de0019e1ac041c863743c8fe1aa6c2b4a257226", size = 215596 }, - { url = "https://files.pythonhosted.org/packages/01/2f/643a8d73343f70e162d8177a3972b76e306b96239026bc0c12cfde4f7c7a/coverage-7.10.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11333094c1bff621aa811b67ed794865cbcaa99984dedea4bd9cf780ad64ecba", size = 246145 }, - { url = "https://files.pythonhosted.org/packages/1f/4a/722098d1848db4072cda71b69ede1e55730d9063bf868375264d0d302bc9/coverage-7.10.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6eb586fa7d2aee8d65d5ae1dd71414020b2f447435c57ee8de8abea0a77d5074", size = 248492 }, - { url = "https://files.pythonhosted.org/packages/3f/b0/8a6d7f326f6e3e6ed398cde27f9055e860a1e858317001835c521673fb60/coverage-7.10.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d358f259d8019d4ef25d8c5b78aca4c7af25e28bd4231312911c22a0e824a57", size = 249927 }, - { url = "https://files.pythonhosted.org/packages/bb/21/1aaadd3197b54d1e61794475379ecd0f68d8fc5c2ebd352964dc6f698a3d/coverage-7.10.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5250bda76e30382e0a2dcd68d961afcab92c3a7613606e6269855c6979a1b0bb", size = 248138 }, - { url = "https://files.pythonhosted.org/packages/48/65/be75bafb2bdd22fd8bf9bf63cd5873b91bb26ec0d68f02d4b8b09c02decb/coverage-7.10.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a91e027d66eff214d88d9afbe528e21c9ef1ecdf4956c46e366c50f3094696d0", size = 246111 }, - { url = "https://files.pythonhosted.org/packages/5e/30/a4f0c5e249c3cc60e6c6f30d8368e372f2d380eda40e0434c192ac27ccf5/coverage-7.10.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:228946da741558904e2c03ce870ba5efd9cd6e48cbc004d9a27abee08100a15a", size = 247493 }, - { url = "https://files.pythonhosted.org/packages/85/99/f09b9493e44a75cf99ca834394c12f8cb70da6c1711ee296534f97b52729/coverage-7.10.2-cp313-cp313-win32.whl", hash = "sha256:95e23987b52d02e7c413bf2d6dc6288bd5721beb518052109a13bfdc62c8033b", size = 217756 }, - { url = "https://files.pythonhosted.org/packages/2d/bb/cbcb09103be330c7d26ff0ab05c4a8861dd2e254656fdbd3eb7600af4336/coverage-7.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:f35481d42c6d146d48ec92d4e239c23f97b53a3f1fbd2302e7c64336f28641fe", size = 218526 }, - { url = "https://files.pythonhosted.org/packages/37/8f/8bfb4e0bca52c00ab680767c0dd8cfd928a2a72d69897d9b2d5d8b5f63f5/coverage-7.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:65b451949cb789c346f9f9002441fc934d8ccedcc9ec09daabc2139ad13853f7", size = 217176 }, - { url = "https://files.pythonhosted.org/packages/1e/25/d458ba0bf16a8204a88d74dbb7ec5520f29937ffcbbc12371f931c11efd2/coverage-7.10.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e8415918856a3e7d57a4e0ad94651b761317de459eb74d34cc1bb51aad80f07e", size = 216058 }, - { url = "https://files.pythonhosted.org/packages/0b/1c/af4dfd2d7244dc7610fed6d59d57a23ea165681cd764445dc58d71ed01a6/coverage-7.10.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f287a25a8ca53901c613498e4a40885b19361a2fe8fbfdbb7f8ef2cad2a23f03", size = 216273 }, - { url = "https://files.pythonhosted.org/packages/8e/67/ec5095d4035c6e16368226fa9cb15f77f891194c7e3725aeefd08e7a3e5a/coverage-7.10.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:75cc1a3f8c88c69bf16a871dab1fe5a7303fdb1e9f285f204b60f1ee539b8fc0", size = 257513 }, - { url = "https://files.pythonhosted.org/packages/1c/47/be5550b57a3a8ba797de4236b0fd31031f88397b2afc84ab3c2d4cf265f6/coverage-7.10.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca07fa78cc9d26bc8c4740de1abd3489cf9c47cc06d9a8ab3d552ff5101af4c0", size = 259377 }, - { url = "https://files.pythonhosted.org/packages/37/50/b12a4da1382e672305c2d17cd3029dc16b8a0470de2191dbf26b91431378/coverage-7.10.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2e117e64c26300032755d4520cd769f2623cde1a1d1c3515b05a3b8add0ade1", size = 261516 }, - { url = "https://files.pythonhosted.org/packages/db/41/4d3296dbd33dd8da178171540ca3391af7c0184c0870fd4d4574ac290290/coverage-7.10.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:daaf98009977f577b71f8800208f4d40d4dcf5c2db53d4d822787cdc198d76e1", size = 259110 }, - { url = "https://files.pythonhosted.org/packages/ea/f1/b409959ecbc0cec0e61e65683b22bacaa4a3b11512f834e16dd8ffbc37db/coverage-7.10.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ea8d8fe546c528535c761ba424410bbeb36ba8a0f24be653e94b70c93fd8a8ca", size = 257248 }, - { url = "https://files.pythonhosted.org/packages/48/ab/7076dc1c240412e9267d36ec93e9e299d7659f6a5c1e958f87e998b0fb6d/coverage-7.10.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fe024d40ac31eb8d5aae70215b41dafa264676caa4404ae155f77d2fa95c37bb", size = 258063 }, - { url = "https://files.pythonhosted.org/packages/1e/77/f6b51a0288f8f5f7dcc7c89abdd22cf514f3bc5151284f5cd628917f8e10/coverage-7.10.2-cp313-cp313t-win32.whl", hash = "sha256:8f34b09f68bdadec122ffad312154eda965ade433559cc1eadd96cca3de5c824", size = 218433 }, - { url = "https://files.pythonhosted.org/packages/7b/6d/547a86493e25270ce8481543e77f3a0aa3aa872c1374246b7b76273d66eb/coverage-7.10.2-cp313-cp313t-win_amd64.whl", hash = "sha256:71d40b3ac0f26fa9ffa6ee16219a714fed5c6ec197cdcd2018904ab5e75bcfa3", size = 219523 }, - { url = "https://files.pythonhosted.org/packages/ff/d5/3c711e38eaf9ab587edc9bed232c0298aed84e751a9f54aaa556ceaf7da6/coverage-7.10.2-cp313-cp313t-win_arm64.whl", hash = "sha256:abb57fdd38bf6f7dcc66b38dafb7af7c5fdc31ac6029ce373a6f7f5331d6f60f", size = 217739 }, - { url = "https://files.pythonhosted.org/packages/71/53/83bafa669bb9d06d4c8c6a055d8d05677216f9480c4698fb183ba7ec5e47/coverage-7.10.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a3e853cc04987c85ec410905667eed4bf08b1d84d80dfab2684bb250ac8da4f6", size = 215328 }, - { url = "https://files.pythonhosted.org/packages/1d/6c/30827a9c5a48a813e865fbaf91e2db25cce990bd223a022650ef2293fe11/coverage-7.10.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0100b19f230df72c90fdb36db59d3f39232391e8d89616a7de30f677da4f532b", size = 215608 }, - { url = "https://files.pythonhosted.org/packages/bb/a0/c92d85948056ddc397b72a3d79d36d9579c53cb25393ed3c40db7d33b193/coverage-7.10.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9c1cd71483ea78331bdfadb8dcec4f4edfb73c7002c1206d8e0af6797853f5be", size = 246111 }, - { url = "https://files.pythonhosted.org/packages/c2/cf/d695cf86b2559aadd072c91720a7844be4fb82cb4a3b642a2c6ce075692d/coverage-7.10.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f75dbf4899e29a37d74f48342f29279391668ef625fdac6d2f67363518056a1", size = 248419 }, - { url = "https://files.pythonhosted.org/packages/ce/0a/03206aec4a05986e039418c038470d874045f6e00426b0c3879adc1f9251/coverage-7.10.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7df481e7508de1c38b9b8043da48d94931aefa3e32b47dd20277e4978ed5b95", size = 250038 }, - { url = "https://files.pythonhosted.org/packages/ab/9b/b3bd6bd52118c12bc4cf319f5baba65009c9beea84e665b6b9f03fa3f180/coverage-7.10.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:835f39e618099325e7612b3406f57af30ab0a0af350490eff6421e2e5f608e46", size = 248066 }, - { url = "https://files.pythonhosted.org/packages/80/cc/bfa92e261d3e055c851a073e87ba6a3bff12a1f7134233e48a8f7d855875/coverage-7.10.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:12e52b5aa00aa720097d6947d2eb9e404e7c1101ad775f9661ba165ed0a28303", size = 245909 }, - { url = "https://files.pythonhosted.org/packages/12/80/c8df15db4847710c72084164f615ae900af1ec380dce7f74a5678ccdf5e1/coverage-7.10.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:718044729bf1fe3e9eb9f31b52e44ddae07e434ec050c8c628bf5adc56fe4bdd", size = 247329 }, - { url = "https://files.pythonhosted.org/packages/04/6f/cb66e1f7124d5dd9ced69f889f02931419cb448125e44a89a13f4e036124/coverage-7.10.2-cp314-cp314-win32.whl", hash = "sha256:f256173b48cc68486299d510a3e729a96e62c889703807482dbf56946befb5c8", size = 218007 }, - { url = "https://files.pythonhosted.org/packages/8c/e1/3d4be307278ce32c1b9d95cc02ee60d54ddab784036101d053ec9e4fe7f5/coverage-7.10.2-cp314-cp314-win_amd64.whl", hash = "sha256:2e980e4179f33d9b65ac4acb86c9c0dde904098853f27f289766657ed16e07b3", size = 218802 }, - { url = "https://files.pythonhosted.org/packages/ec/66/1e43bbeb66c55a5a5efec70f1c153cf90cfc7f1662ab4ebe2d844de9122c/coverage-7.10.2-cp314-cp314-win_arm64.whl", hash = "sha256:14fb5b6641ab5b3c4161572579f0f2ea8834f9d3af2f7dd8fbaecd58ef9175cc", size = 217397 }, - { url = "https://files.pythonhosted.org/packages/81/01/ae29c129217f6110dc694a217475b8aecbb1b075d8073401f868c825fa99/coverage-7.10.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e96649ac34a3d0e6491e82a2af71098e43be2874b619547c3282fc11d3840a4b", size = 216068 }, - { url = "https://files.pythonhosted.org/packages/a2/50/6e9221d4139f357258f36dfa1d8cac4ec56d9d5acf5fdcc909bb016954d7/coverage-7.10.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1a2e934e9da26341d342d30bfe91422bbfdb3f1f069ec87f19b2909d10d8dcc4", size = 216285 }, - { url = "https://files.pythonhosted.org/packages/eb/ec/89d1d0c0ece0d296b4588e0ef4df185200456d42a47f1141335f482c2fc5/coverage-7.10.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:651015dcd5fd9b5a51ca79ece60d353cacc5beaf304db750407b29c89f72fe2b", size = 257603 }, - { url = "https://files.pythonhosted.org/packages/82/06/c830af66734671c778fc49d35b58339e8f0687fbd2ae285c3f96c94da092/coverage-7.10.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81bf6a32212f9f66da03d63ecb9cd9bd48e662050a937db7199dbf47d19831de", size = 259568 }, - { url = "https://files.pythonhosted.org/packages/60/57/f280dd6f1c556ecc744fbf39e835c33d3ae987d040d64d61c6f821e87829/coverage-7.10.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d800705f6951f75a905ea6feb03fff8f3ea3468b81e7563373ddc29aa3e5d1ca", size = 261691 }, - { url = "https://files.pythonhosted.org/packages/54/2b/c63a0acbd19d99ec32326164c23df3a4e18984fb86e902afdd66ff7b3d83/coverage-7.10.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:248b5394718e10d067354448dc406d651709c6765669679311170da18e0e9af8", size = 259166 }, - { url = "https://files.pythonhosted.org/packages/fd/c5/cd2997dcfcbf0683634da9df52d3967bc1f1741c1475dd0e4722012ba9ef/coverage-7.10.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5c61675a922b569137cf943770d7ad3edd0202d992ce53ac328c5ff68213ccf4", size = 257241 }, - { url = "https://files.pythonhosted.org/packages/16/26/c9e30f82fdad8d47aee90af4978b18c88fa74369ae0f0ba0dbf08cee3a80/coverage-7.10.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:52d708b5fd65589461381fa442d9905f5903d76c086c6a4108e8e9efdca7a7ed", size = 258139 }, - { url = "https://files.pythonhosted.org/packages/c9/99/bdb7bd00bebcd3dedfb895fa9af8e46b91422993e4a37ac634a5f1113790/coverage-7.10.2-cp314-cp314t-win32.whl", hash = "sha256:916369b3b914186b2c5e5ad2f7264b02cff5df96cdd7cdad65dccd39aa5fd9f0", size = 218809 }, - { url = "https://files.pythonhosted.org/packages/eb/5e/56a7852e38a04d1520dda4dfbfbf74a3d6dec932c20526968f7444763567/coverage-7.10.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5b9d538e8e04916a5df63052d698b30c74eb0174f2ca9cd942c981f274a18eaf", size = 219926 }, - { url = "https://files.pythonhosted.org/packages/e0/12/7fbe6b9c52bb9d627e9556f9f2edfdbe88b315e084cdecc9afead0c3b36a/coverage-7.10.2-cp314-cp314t-win_arm64.whl", hash = "sha256:04c74f9ef1f925456a9fd23a7eef1103126186d0500ef9a0acb0bd2514bdc7cc", size = 217925 }, - { url = "https://files.pythonhosted.org/packages/18/d8/9b768ac73a8ac2d10c080af23937212434a958c8d2a1c84e89b450237942/coverage-7.10.2-py3-none-any.whl", hash = "sha256:95db3750dd2e6e93d99fa2498f3a1580581e49c494bddccc6f85c5c21604921f", size = 206973 }, +sdist = { url = "https://files.pythonhosted.org/packages/ee/76/17780846fc7aade1e66712e1e27dd28faa0a5d987a1f433610974959eaa8/coverage-7.10.2.tar.gz", hash = "sha256:5d6e6d84e6dd31a8ded64759626627247d676a23c1b892e1326f7c55c8d61055", size = 820754, upload-time = "2025-08-04T00:35:17.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/5f/5ce748ab3f142593698aff5f8a0cf020775aa4e24b9d8748b5a56b64d3f8/coverage-7.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:79f0283ab5e6499fd5fe382ca3d62afa40fb50ff227676a3125d18af70eabf65", size = 215003, upload-time = "2025-08-04T00:33:02.977Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ed/507088561217b000109552139802fa99c33c16ad19999c687b601b3790d0/coverage-7.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4545e906f595ee8ab8e03e21be20d899bfc06647925bc5b224ad7e8c40e08b8", size = 215391, upload-time = "2025-08-04T00:33:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/79/1b/0f496259fe137c4c5e1e8eaff496fb95af88b71700f5e57725a4ddbe742b/coverage-7.10.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ae385e1d58fbc6a9b1c315e5510ac52281e271478b45f92ca9b5ad42cf39643f", size = 242367, upload-time = "2025-08-04T00:33:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8e/5a8835fb0122a2e2a108bf3527931693c4625fdc4d953950a480b9625852/coverage-7.10.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f0cbe5f7dd19f3a32bac2251b95d51c3b89621ac88a2648096ce40f9a5aa1e7", size = 243627, upload-time = "2025-08-04T00:33:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/c3/96/6a528429c2e0e8d85261764d0cd42e51a429510509bcc14676ee5d1bb212/coverage-7.10.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd17f427f041f6b116dc90b4049c6f3e1230524407d00daa2d8c7915037b5947", size = 245485, upload-time = "2025-08-04T00:33:10.29Z" }, + { url = "https://files.pythonhosted.org/packages/bf/82/1fba935c4d02c33275aca319deabf1f22c0f95f2c0000bf7c5f276d6f7b4/coverage-7.10.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7f10ca4cde7b466405cce0a0e9971a13eb22e57a5ecc8b5f93a81090cc9c7eb9", size = 243429, upload-time = "2025-08-04T00:33:11.909Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a8/c8dc0a57a729fc93be33ab78f187a8f52d455fa8f79bfb379fe23b45868d/coverage-7.10.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3b990df23dd51dccce26d18fb09fd85a77ebe46368f387b0ffba7a74e470b31b", size = 242104, upload-time = "2025-08-04T00:33:13.467Z" }, + { url = "https://files.pythonhosted.org/packages/b9/6f/0b7da1682e2557caeed299a00897b42afde99a241a01eba0197eb982b90f/coverage-7.10.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc3902584d25c7eef57fb38f440aa849a26a3a9f761a029a72b69acfca4e31f8", size = 242397, upload-time = "2025-08-04T00:33:14.682Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e4/54dc833dadccd519c04a28852f39a37e522bad35d70cfe038817cdb8f168/coverage-7.10.2-cp310-cp310-win32.whl", hash = "sha256:9dd37e9ac00d5eb72f38ed93e3cdf2280b1dbda3bb9b48c6941805f265ad8d87", size = 217502, upload-time = "2025-08-04T00:33:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e7/2f78159c4c127549172f427dff15b02176329327bf6a6a1fcf1f603b5456/coverage-7.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:99d16f15cb5baf0729354c5bd3080ae53847a4072b9ba1e10957522fb290417f", size = 218388, upload-time = "2025-08-04T00:33:17.4Z" }, + { url = "https://files.pythonhosted.org/packages/6e/53/0125a6fc0af4f2687b4e08b0fb332cd0d5e60f3ca849e7456f995d022656/coverage-7.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c3b210d79925a476dfc8d74c7d53224888421edebf3a611f3adae923e212b27", size = 215119, upload-time = "2025-08-04T00:33:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2e/960d9871de9152dbc9ff950913c6a6e9cf2eb4cc80d5bc8f93029f9f2f9f/coverage-7.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf67d1787cd317c3f8b2e4c6ed1ae93497be7e30605a0d32237ac37a37a8a322", size = 215511, upload-time = "2025-08-04T00:33:20.32Z" }, + { url = "https://files.pythonhosted.org/packages/3f/34/68509e44995b9cad806d81b76c22bc5181f3535bca7cd9c15791bfd8951e/coverage-7.10.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:069b779d03d458602bc0e27189876e7d8bdf6b24ac0f12900de22dd2154e6ad7", size = 245513, upload-time = "2025-08-04T00:33:21.896Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d4/9b12f357413248ce40804b0f58030b55a25b28a5c02db95fb0aa50c5d62c/coverage-7.10.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c2de4cb80b9990e71c62c2d3e9f3ec71b804b1f9ca4784ec7e74127e0f42468", size = 247350, upload-time = "2025-08-04T00:33:23.917Z" }, + { url = "https://files.pythonhosted.org/packages/b6/40/257945eda1f72098e4a3c350b1d68fdc5d7d032684a0aeb6c2391153ecf4/coverage-7.10.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75bf7ab2374a7eb107602f1e07310cda164016cd60968abf817b7a0b5703e288", size = 249516, upload-time = "2025-08-04T00:33:25.5Z" }, + { url = "https://files.pythonhosted.org/packages/ff/55/8987f852ece378cecbf39a367f3f7ec53351e39a9151b130af3a3045b83f/coverage-7.10.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3f37516458ec1550815134937f73d6d15b434059cd10f64678a2068f65c62406", size = 247241, upload-time = "2025-08-04T00:33:26.767Z" }, + { url = "https://files.pythonhosted.org/packages/df/ae/da397de7a42a18cea6062ed9c3b72c50b39e0b9e7b2893d7172d3333a9a1/coverage-7.10.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:de3c6271c482c250d3303fb5c6bdb8ca025fff20a67245e1425df04dc990ece9", size = 245274, upload-time = "2025-08-04T00:33:28.494Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/7baa895eb55ec0e1ec35b988687ecd5d4475ababb0d7ae5ca3874dd90ee7/coverage-7.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:98a838101321ac3089c9bb1d4bfa967e8afed58021fda72d7880dc1997f20ae1", size = 245882, upload-time = "2025-08-04T00:33:30.048Z" }, + { url = "https://files.pythonhosted.org/packages/24/6c/1fd76a0bd09ae75220ae9775a8290416d726f0e5ba26ea72346747161240/coverage-7.10.2-cp311-cp311-win32.whl", hash = "sha256:f2a79145a531a0e42df32d37be5af069b4a914845b6f686590739b786f2f7bce", size = 217541, upload-time = "2025-08-04T00:33:31.376Z" }, + { url = "https://files.pythonhosted.org/packages/5f/2d/8c18fb7a6e74c79fd4661e82535bc8c68aee12f46c204eabf910b097ccc9/coverage-7.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:e4f5f1320f8ee0d7cfa421ceb257bef9d39fd614dd3ddcfcacd284d4824ed2c2", size = 218426, upload-time = "2025-08-04T00:33:32.976Z" }, + { url = "https://files.pythonhosted.org/packages/da/40/425bb35e4ff7c7af177edf5dffd4154bc2a677b27696afe6526d75c77fec/coverage-7.10.2-cp311-cp311-win_arm64.whl", hash = "sha256:d8f2d83118f25328552c728b8e91babf93217db259ca5c2cd4dd4220b8926293", size = 217116, upload-time = "2025-08-04T00:33:34.302Z" }, + { url = "https://files.pythonhosted.org/packages/4e/1e/2c752bdbbf6f1199c59b1a10557fbb6fb3dc96b3c0077b30bd41a5922c1f/coverage-7.10.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:890ad3a26da9ec7bf69255b9371800e2a8da9bc223ae5d86daeb940b42247c83", size = 215311, upload-time = "2025-08-04T00:33:35.524Z" }, + { url = "https://files.pythonhosted.org/packages/68/6a/84277d73a2cafb96e24be81b7169372ba7ff28768ebbf98e55c85a491b0f/coverage-7.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38fd1ccfca7838c031d7a7874d4353e2f1b98eb5d2a80a2fe5732d542ae25e9c", size = 215550, upload-time = "2025-08-04T00:33:37.109Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e7/5358b73b46ac76f56cc2de921eeabd44fabd0b7ff82ea4f6b8c159c4d5dc/coverage-7.10.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:76c1ffaaf4f6f0f6e8e9ca06f24bb6454a7a5d4ced97a1bc466f0d6baf4bd518", size = 246564, upload-time = "2025-08-04T00:33:38.33Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0e/b0c901dd411cb7fc0cfcb28ef0dc6f3049030f616bfe9fc4143aecd95901/coverage-7.10.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86da8a3a84b79ead5c7d0e960c34f580bc3b231bb546627773a3f53c532c2f21", size = 248993, upload-time = "2025-08-04T00:33:39.555Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4e/a876db272072a9e0df93f311e187ccdd5f39a190c6d1c1f0b6e255a0d08e/coverage-7.10.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99cef9731c8a39801830a604cc53c93c9e57ea8b44953d26589499eded9576e0", size = 250454, upload-time = "2025-08-04T00:33:41.023Z" }, + { url = "https://files.pythonhosted.org/packages/64/d6/1222dc69f8dd1be208d55708a9f4a450ad582bf4fa05320617fea1eaa6d8/coverage-7.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea58b112f2966a8b91eb13f5d3b1f8bb43c180d624cd3283fb33b1cedcc2dd75", size = 248365, upload-time = "2025-08-04T00:33:42.376Z" }, + { url = "https://files.pythonhosted.org/packages/62/e3/40fd71151064fc315c922dd9a35e15b30616f00146db1d6a0b590553a75a/coverage-7.10.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:20f405188d28da9522b7232e51154e1b884fc18d0b3a10f382d54784715bbe01", size = 246562, upload-time = "2025-08-04T00:33:43.663Z" }, + { url = "https://files.pythonhosted.org/packages/fc/14/8aa93ddcd6623ddaef5d8966268ac9545b145bce4fe7b1738fd1c3f0d957/coverage-7.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:64586ce42bbe0da4d9f76f97235c545d1abb9b25985a8791857690f96e23dc3b", size = 247772, upload-time = "2025-08-04T00:33:45.068Z" }, + { url = "https://files.pythonhosted.org/packages/07/4e/dcb1c01490623c61e2f2ea85cb185fa6a524265bb70eeb897d3c193efeb9/coverage-7.10.2-cp312-cp312-win32.whl", hash = "sha256:bc2e69b795d97ee6d126e7e22e78a509438b46be6ff44f4dccbb5230f550d340", size = 217710, upload-time = "2025-08-04T00:33:46.378Z" }, + { url = "https://files.pythonhosted.org/packages/79/16/e8aab4162b5f80ad2e5e1f54b1826e2053aa2f4db508b864af647f00c239/coverage-7.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:adda2268b8cf0d11f160fad3743b4dfe9813cd6ecf02c1d6397eceaa5b45b388", size = 218499, upload-time = "2025-08-04T00:33:48.048Z" }, + { url = "https://files.pythonhosted.org/packages/06/7f/c112ec766e8f1131ce8ce26254be028772757b2d1e63e4f6a4b0ad9a526c/coverage-7.10.2-cp312-cp312-win_arm64.whl", hash = "sha256:164429decd0d6b39a0582eaa30c67bf482612c0330572343042d0ed9e7f15c20", size = 217154, upload-time = "2025-08-04T00:33:49.299Z" }, + { url = "https://files.pythonhosted.org/packages/8d/04/9b7a741557f93c0ed791b854d27aa8d9fe0b0ce7bb7c52ca1b0f2619cb74/coverage-7.10.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aca7b5645afa688de6d4f8e89d30c577f62956fefb1bad021490d63173874186", size = 215337, upload-time = "2025-08-04T00:33:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/02/a4/8d1088cd644750c94bc305d3cf56082b4cdf7fb854a25abb23359e74892f/coverage-7.10.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:96e5921342574a14303dfdb73de0019e1ac041c863743c8fe1aa6c2b4a257226", size = 215596, upload-time = "2025-08-04T00:33:52.33Z" }, + { url = "https://files.pythonhosted.org/packages/01/2f/643a8d73343f70e162d8177a3972b76e306b96239026bc0c12cfde4f7c7a/coverage-7.10.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11333094c1bff621aa811b67ed794865cbcaa99984dedea4bd9cf780ad64ecba", size = 246145, upload-time = "2025-08-04T00:33:53.641Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4a/722098d1848db4072cda71b69ede1e55730d9063bf868375264d0d302bc9/coverage-7.10.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6eb586fa7d2aee8d65d5ae1dd71414020b2f447435c57ee8de8abea0a77d5074", size = 248492, upload-time = "2025-08-04T00:33:55.366Z" }, + { url = "https://files.pythonhosted.org/packages/3f/b0/8a6d7f326f6e3e6ed398cde27f9055e860a1e858317001835c521673fb60/coverage-7.10.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d358f259d8019d4ef25d8c5b78aca4c7af25e28bd4231312911c22a0e824a57", size = 249927, upload-time = "2025-08-04T00:33:57.042Z" }, + { url = "https://files.pythonhosted.org/packages/bb/21/1aaadd3197b54d1e61794475379ecd0f68d8fc5c2ebd352964dc6f698a3d/coverage-7.10.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5250bda76e30382e0a2dcd68d961afcab92c3a7613606e6269855c6979a1b0bb", size = 248138, upload-time = "2025-08-04T00:33:58.329Z" }, + { url = "https://files.pythonhosted.org/packages/48/65/be75bafb2bdd22fd8bf9bf63cd5873b91bb26ec0d68f02d4b8b09c02decb/coverage-7.10.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a91e027d66eff214d88d9afbe528e21c9ef1ecdf4956c46e366c50f3094696d0", size = 246111, upload-time = "2025-08-04T00:33:59.899Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/a4f0c5e249c3cc60e6c6f30d8368e372f2d380eda40e0434c192ac27ccf5/coverage-7.10.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:228946da741558904e2c03ce870ba5efd9cd6e48cbc004d9a27abee08100a15a", size = 247493, upload-time = "2025-08-04T00:34:01.619Z" }, + { url = "https://files.pythonhosted.org/packages/85/99/f09b9493e44a75cf99ca834394c12f8cb70da6c1711ee296534f97b52729/coverage-7.10.2-cp313-cp313-win32.whl", hash = "sha256:95e23987b52d02e7c413bf2d6dc6288bd5721beb518052109a13bfdc62c8033b", size = 217756, upload-time = "2025-08-04T00:34:03.277Z" }, + { url = "https://files.pythonhosted.org/packages/2d/bb/cbcb09103be330c7d26ff0ab05c4a8861dd2e254656fdbd3eb7600af4336/coverage-7.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:f35481d42c6d146d48ec92d4e239c23f97b53a3f1fbd2302e7c64336f28641fe", size = 218526, upload-time = "2025-08-04T00:34:04.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/8f/8bfb4e0bca52c00ab680767c0dd8cfd928a2a72d69897d9b2d5d8b5f63f5/coverage-7.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:65b451949cb789c346f9f9002441fc934d8ccedcc9ec09daabc2139ad13853f7", size = 217176, upload-time = "2025-08-04T00:34:05.973Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/d458ba0bf16a8204a88d74dbb7ec5520f29937ffcbbc12371f931c11efd2/coverage-7.10.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e8415918856a3e7d57a4e0ad94651b761317de459eb74d34cc1bb51aad80f07e", size = 216058, upload-time = "2025-08-04T00:34:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1c/af4dfd2d7244dc7610fed6d59d57a23ea165681cd764445dc58d71ed01a6/coverage-7.10.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f287a25a8ca53901c613498e4a40885b19361a2fe8fbfdbb7f8ef2cad2a23f03", size = 216273, upload-time = "2025-08-04T00:34:09.073Z" }, + { url = "https://files.pythonhosted.org/packages/8e/67/ec5095d4035c6e16368226fa9cb15f77f891194c7e3725aeefd08e7a3e5a/coverage-7.10.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:75cc1a3f8c88c69bf16a871dab1fe5a7303fdb1e9f285f204b60f1ee539b8fc0", size = 257513, upload-time = "2025-08-04T00:34:10.403Z" }, + { url = "https://files.pythonhosted.org/packages/1c/47/be5550b57a3a8ba797de4236b0fd31031f88397b2afc84ab3c2d4cf265f6/coverage-7.10.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca07fa78cc9d26bc8c4740de1abd3489cf9c47cc06d9a8ab3d552ff5101af4c0", size = 259377, upload-time = "2025-08-04T00:34:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/37/50/b12a4da1382e672305c2d17cd3029dc16b8a0470de2191dbf26b91431378/coverage-7.10.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2e117e64c26300032755d4520cd769f2623cde1a1d1c3515b05a3b8add0ade1", size = 261516, upload-time = "2025-08-04T00:34:13.608Z" }, + { url = "https://files.pythonhosted.org/packages/db/41/4d3296dbd33dd8da178171540ca3391af7c0184c0870fd4d4574ac290290/coverage-7.10.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:daaf98009977f577b71f8800208f4d40d4dcf5c2db53d4d822787cdc198d76e1", size = 259110, upload-time = "2025-08-04T00:34:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/b409959ecbc0cec0e61e65683b22bacaa4a3b11512f834e16dd8ffbc37db/coverage-7.10.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ea8d8fe546c528535c761ba424410bbeb36ba8a0f24be653e94b70c93fd8a8ca", size = 257248, upload-time = "2025-08-04T00:34:16.501Z" }, + { url = "https://files.pythonhosted.org/packages/48/ab/7076dc1c240412e9267d36ec93e9e299d7659f6a5c1e958f87e998b0fb6d/coverage-7.10.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fe024d40ac31eb8d5aae70215b41dafa264676caa4404ae155f77d2fa95c37bb", size = 258063, upload-time = "2025-08-04T00:34:18.338Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/f6b51a0288f8f5f7dcc7c89abdd22cf514f3bc5151284f5cd628917f8e10/coverage-7.10.2-cp313-cp313t-win32.whl", hash = "sha256:8f34b09f68bdadec122ffad312154eda965ade433559cc1eadd96cca3de5c824", size = 218433, upload-time = "2025-08-04T00:34:19.71Z" }, + { url = "https://files.pythonhosted.org/packages/7b/6d/547a86493e25270ce8481543e77f3a0aa3aa872c1374246b7b76273d66eb/coverage-7.10.2-cp313-cp313t-win_amd64.whl", hash = "sha256:71d40b3ac0f26fa9ffa6ee16219a714fed5c6ec197cdcd2018904ab5e75bcfa3", size = 219523, upload-time = "2025-08-04T00:34:21.171Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d5/3c711e38eaf9ab587edc9bed232c0298aed84e751a9f54aaa556ceaf7da6/coverage-7.10.2-cp313-cp313t-win_arm64.whl", hash = "sha256:abb57fdd38bf6f7dcc66b38dafb7af7c5fdc31ac6029ce373a6f7f5331d6f60f", size = 217739, upload-time = "2025-08-04T00:34:22.514Z" }, + { url = "https://files.pythonhosted.org/packages/71/53/83bafa669bb9d06d4c8c6a055d8d05677216f9480c4698fb183ba7ec5e47/coverage-7.10.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a3e853cc04987c85ec410905667eed4bf08b1d84d80dfab2684bb250ac8da4f6", size = 215328, upload-time = "2025-08-04T00:34:23.991Z" }, + { url = "https://files.pythonhosted.org/packages/1d/6c/30827a9c5a48a813e865fbaf91e2db25cce990bd223a022650ef2293fe11/coverage-7.10.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0100b19f230df72c90fdb36db59d3f39232391e8d89616a7de30f677da4f532b", size = 215608, upload-time = "2025-08-04T00:34:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a0/c92d85948056ddc397b72a3d79d36d9579c53cb25393ed3c40db7d33b193/coverage-7.10.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9c1cd71483ea78331bdfadb8dcec4f4edfb73c7002c1206d8e0af6797853f5be", size = 246111, upload-time = "2025-08-04T00:34:26.857Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/d695cf86b2559aadd072c91720a7844be4fb82cb4a3b642a2c6ce075692d/coverage-7.10.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f75dbf4899e29a37d74f48342f29279391668ef625fdac6d2f67363518056a1", size = 248419, upload-time = "2025-08-04T00:34:28.726Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0a/03206aec4a05986e039418c038470d874045f6e00426b0c3879adc1f9251/coverage-7.10.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7df481e7508de1c38b9b8043da48d94931aefa3e32b47dd20277e4978ed5b95", size = 250038, upload-time = "2025-08-04T00:34:30.061Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9b/b3bd6bd52118c12bc4cf319f5baba65009c9beea84e665b6b9f03fa3f180/coverage-7.10.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:835f39e618099325e7612b3406f57af30ab0a0af350490eff6421e2e5f608e46", size = 248066, upload-time = "2025-08-04T00:34:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/bfa92e261d3e055c851a073e87ba6a3bff12a1f7134233e48a8f7d855875/coverage-7.10.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:12e52b5aa00aa720097d6947d2eb9e404e7c1101ad775f9661ba165ed0a28303", size = 245909, upload-time = "2025-08-04T00:34:32.943Z" }, + { url = "https://files.pythonhosted.org/packages/12/80/c8df15db4847710c72084164f615ae900af1ec380dce7f74a5678ccdf5e1/coverage-7.10.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:718044729bf1fe3e9eb9f31b52e44ddae07e434ec050c8c628bf5adc56fe4bdd", size = 247329, upload-time = "2025-08-04T00:34:34.388Z" }, + { url = "https://files.pythonhosted.org/packages/04/6f/cb66e1f7124d5dd9ced69f889f02931419cb448125e44a89a13f4e036124/coverage-7.10.2-cp314-cp314-win32.whl", hash = "sha256:f256173b48cc68486299d510a3e729a96e62c889703807482dbf56946befb5c8", size = 218007, upload-time = "2025-08-04T00:34:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e1/3d4be307278ce32c1b9d95cc02ee60d54ddab784036101d053ec9e4fe7f5/coverage-7.10.2-cp314-cp314-win_amd64.whl", hash = "sha256:2e980e4179f33d9b65ac4acb86c9c0dde904098853f27f289766657ed16e07b3", size = 218802, upload-time = "2025-08-04T00:34:37.35Z" }, + { url = "https://files.pythonhosted.org/packages/ec/66/1e43bbeb66c55a5a5efec70f1c153cf90cfc7f1662ab4ebe2d844de9122c/coverage-7.10.2-cp314-cp314-win_arm64.whl", hash = "sha256:14fb5b6641ab5b3c4161572579f0f2ea8834f9d3af2f7dd8fbaecd58ef9175cc", size = 217397, upload-time = "2025-08-04T00:34:39.15Z" }, + { url = "https://files.pythonhosted.org/packages/81/01/ae29c129217f6110dc694a217475b8aecbb1b075d8073401f868c825fa99/coverage-7.10.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e96649ac34a3d0e6491e82a2af71098e43be2874b619547c3282fc11d3840a4b", size = 216068, upload-time = "2025-08-04T00:34:40.648Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/6e9221d4139f357258f36dfa1d8cac4ec56d9d5acf5fdcc909bb016954d7/coverage-7.10.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1a2e934e9da26341d342d30bfe91422bbfdb3f1f069ec87f19b2909d10d8dcc4", size = 216285, upload-time = "2025-08-04T00:34:42.441Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ec/89d1d0c0ece0d296b4588e0ef4df185200456d42a47f1141335f482c2fc5/coverage-7.10.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:651015dcd5fd9b5a51ca79ece60d353cacc5beaf304db750407b29c89f72fe2b", size = 257603, upload-time = "2025-08-04T00:34:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/82/06/c830af66734671c778fc49d35b58339e8f0687fbd2ae285c3f96c94da092/coverage-7.10.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81bf6a32212f9f66da03d63ecb9cd9bd48e662050a937db7199dbf47d19831de", size = 259568, upload-time = "2025-08-04T00:34:45.519Z" }, + { url = "https://files.pythonhosted.org/packages/60/57/f280dd6f1c556ecc744fbf39e835c33d3ae987d040d64d61c6f821e87829/coverage-7.10.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d800705f6951f75a905ea6feb03fff8f3ea3468b81e7563373ddc29aa3e5d1ca", size = 261691, upload-time = "2025-08-04T00:34:47.019Z" }, + { url = "https://files.pythonhosted.org/packages/54/2b/c63a0acbd19d99ec32326164c23df3a4e18984fb86e902afdd66ff7b3d83/coverage-7.10.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:248b5394718e10d067354448dc406d651709c6765669679311170da18e0e9af8", size = 259166, upload-time = "2025-08-04T00:34:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c5/cd2997dcfcbf0683634da9df52d3967bc1f1741c1475dd0e4722012ba9ef/coverage-7.10.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5c61675a922b569137cf943770d7ad3edd0202d992ce53ac328c5ff68213ccf4", size = 257241, upload-time = "2025-08-04T00:34:51.038Z" }, + { url = "https://files.pythonhosted.org/packages/16/26/c9e30f82fdad8d47aee90af4978b18c88fa74369ae0f0ba0dbf08cee3a80/coverage-7.10.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:52d708b5fd65589461381fa442d9905f5903d76c086c6a4108e8e9efdca7a7ed", size = 258139, upload-time = "2025-08-04T00:34:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/c9/99/bdb7bd00bebcd3dedfb895fa9af8e46b91422993e4a37ac634a5f1113790/coverage-7.10.2-cp314-cp314t-win32.whl", hash = "sha256:916369b3b914186b2c5e5ad2f7264b02cff5df96cdd7cdad65dccd39aa5fd9f0", size = 218809, upload-time = "2025-08-04T00:34:54.075Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5e/56a7852e38a04d1520dda4dfbfbf74a3d6dec932c20526968f7444763567/coverage-7.10.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5b9d538e8e04916a5df63052d698b30c74eb0174f2ca9cd942c981f274a18eaf", size = 219926, upload-time = "2025-08-04T00:34:55.643Z" }, + { url = "https://files.pythonhosted.org/packages/e0/12/7fbe6b9c52bb9d627e9556f9f2edfdbe88b315e084cdecc9afead0c3b36a/coverage-7.10.2-cp314-cp314t-win_arm64.whl", hash = "sha256:04c74f9ef1f925456a9fd23a7eef1103126186d0500ef9a0acb0bd2514bdc7cc", size = 217925, upload-time = "2025-08-04T00:34:57.564Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/9b768ac73a8ac2d10c080af23937212434a958c8d2a1c84e89b450237942/coverage-7.10.2-py3-none-any.whl", hash = "sha256:95db3750dd2e6e93d99fa2498f3a1580581e49c494bddccc6f85c5c21604921f", size = 206973, upload-time = "2025-08-04T00:35:15.918Z" }, ] [package.optional-dependencies] @@ -469,27 +470,27 @@ toml = [ name = "cycler" version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] [[package]] name = "distlib" version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605 } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047 }, + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] [[package]] name = "docutils" version = "0.21.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, ] [[package]] @@ -499,18 +500,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 } +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 }, + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] [[package]] name = "filelock" version = "3.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, ] [[package]] @@ -522,86 +523,86 @@ dependencies = [ { name = "pycodestyle" }, { name = "pyflakes" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326 } +sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922 }, + { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, ] [[package]] name = "fonttools" version = "4.59.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/27/ec3c723bfdf86f34c5c82bf6305df3e0f0d8ea798d2d3a7cb0c0a866d286/fonttools-4.59.0.tar.gz", hash = "sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14", size = 3532521 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/1f/3dcae710b7c4b56e79442b03db64f6c9f10c3348f7af40339dffcefb581e/fonttools-4.59.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96", size = 2761846 }, - { url = "https://files.pythonhosted.org/packages/eb/0e/ae3a1884fa1549acac1191cc9ec039142f6ac0e9cbc139c2e6a3dab967da/fonttools-4.59.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df", size = 2332060 }, - { url = "https://files.pythonhosted.org/packages/75/46/58bff92a7216829159ac7bdb1d05a48ad1b8ab8c539555f12d29fdecfdd4/fonttools-4.59.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e93df708c69a193fc7987192f94df250f83f3851fda49413f02ba5dded639482", size = 4852354 }, - { url = "https://files.pythonhosted.org/packages/05/57/767e31e48861045d89691128bd81fd4c62b62150f9a17a666f731ce4f197/fonttools-4.59.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:62224a9bb85b4b66d1b46d45cbe43d71cbf8f527d332b177e3b96191ffbc1e64", size = 4781132 }, - { url = "https://files.pythonhosted.org/packages/d7/78/adb5e9b0af5c6ce469e8b0e112f144eaa84b30dd72a486e9c778a9b03b31/fonttools-4.59.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8974b2a266b54c96709bd5e239979cddfd2dbceed331aa567ea1d7c4a2202db", size = 4832901 }, - { url = "https://files.pythonhosted.org/packages/ac/92/bc3881097fbf3d56d112bec308c863c058e5d4c9c65f534e8ae58450ab8a/fonttools-4.59.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:209b75943d158f610b78320eacb5539aa9e920bee2c775445b2846c65d20e19d", size = 4940140 }, - { url = "https://files.pythonhosted.org/packages/4a/54/39cdb23f0eeda2e07ae9cb189f2b6f41da89aabc682d3a387b3ff4a4ed29/fonttools-4.59.0-cp310-cp310-win32.whl", hash = "sha256:4c908a7036f0f3677f8afa577bcd973e3e20ddd2f7c42a33208d18bee95cdb6f", size = 2215890 }, - { url = "https://files.pythonhosted.org/packages/d8/eb/f8388d9e19f95d8df2449febe9b1a38ddd758cfdb7d6de3a05198d785d61/fonttools-4.59.0-cp310-cp310-win_amd64.whl", hash = "sha256:8b4309a2775e4feee7356e63b163969a215d663399cce1b3d3b65e7ec2d9680e", size = 2260191 }, - { url = "https://files.pythonhosted.org/packages/06/96/520733d9602fa1bf6592e5354c6721ac6fc9ea72bc98d112d0c38b967199/fonttools-4.59.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c", size = 2782387 }, - { url = "https://files.pythonhosted.org/packages/87/6a/170fce30b9bce69077d8eec9bea2cfd9f7995e8911c71be905e2eba6368b/fonttools-4.59.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5", size = 2342194 }, - { url = "https://files.pythonhosted.org/packages/b0/b6/7c8166c0066856f1408092f7968ac744060cf72ca53aec9036106f57eeca/fonttools-4.59.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705", size = 5032333 }, - { url = "https://files.pythonhosted.org/packages/eb/0c/707c5a19598eafcafd489b73c4cb1c142102d6197e872f531512d084aa76/fonttools-4.59.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464", size = 4974422 }, - { url = "https://files.pythonhosted.org/packages/f6/e7/6d33737d9fe632a0f59289b6f9743a86d2a9d0673de2a0c38c0f54729822/fonttools-4.59.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38", size = 5010631 }, - { url = "https://files.pythonhosted.org/packages/63/e1/a4c3d089ab034a578820c8f2dff21ef60daf9668034a1e4fb38bb1cc3398/fonttools-4.59.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6", size = 5122198 }, - { url = "https://files.pythonhosted.org/packages/09/77/ca82b9c12fa4de3c520b7760ee61787640cf3fde55ef1b0bfe1de38c8153/fonttools-4.59.0-cp311-cp311-win32.whl", hash = "sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757", size = 2214216 }, - { url = "https://files.pythonhosted.org/packages/ab/25/5aa7ca24b560b2f00f260acf32c4cf29d7aaf8656e159a336111c18bc345/fonttools-4.59.0-cp311-cp311-win_amd64.whl", hash = "sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0", size = 2261879 }, - { url = "https://files.pythonhosted.org/packages/e2/77/b1c8af22f4265e951cd2e5535dbef8859efcef4fb8dee742d368c967cddb/fonttools-4.59.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b", size = 2767562 }, - { url = "https://files.pythonhosted.org/packages/ff/5a/aeb975699588176bb357e8b398dfd27e5d3a2230d92b81ab8cbb6187358d/fonttools-4.59.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2", size = 2335168 }, - { url = "https://files.pythonhosted.org/packages/54/97/c6101a7e60ae138c4ef75b22434373a0da50a707dad523dd19a4889315bf/fonttools-4.59.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b", size = 4909850 }, - { url = "https://files.pythonhosted.org/packages/bd/6c/fa4d18d641054f7bff878cbea14aa9433f292b9057cb1700d8e91a4d5f4f/fonttools-4.59.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1", size = 4955131 }, - { url = "https://files.pythonhosted.org/packages/20/5c/331947fc1377deb928a69bde49f9003364f5115e5cbe351eea99e39412a2/fonttools-4.59.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e", size = 4899667 }, - { url = "https://files.pythonhosted.org/packages/8a/46/b66469dfa26b8ff0baa7654b2cc7851206c6d57fe3abdabbaab22079a119/fonttools-4.59.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e", size = 5051349 }, - { url = "https://files.pythonhosted.org/packages/2e/05/ebfb6b1f3a4328ab69787d106a7d92ccde77ce66e98659df0f9e3f28d93d/fonttools-4.59.0-cp312-cp312-win32.whl", hash = "sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b", size = 2201315 }, - { url = "https://files.pythonhosted.org/packages/09/45/d2bdc9ea20bbadec1016fd0db45696d573d7a26d95ab5174ffcb6d74340b/fonttools-4.59.0-cp312-cp312-win_amd64.whl", hash = "sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01", size = 2249408 }, - { url = "https://files.pythonhosted.org/packages/f3/bb/390990e7c457d377b00890d9f96a3ca13ae2517efafb6609c1756e213ba4/fonttools-4.59.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2", size = 2758704 }, - { url = "https://files.pythonhosted.org/packages/df/6f/d730d9fcc9b410a11597092bd2eb9ca53e5438c6cb90e4b3047ce1b723e9/fonttools-4.59.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2", size = 2330764 }, - { url = "https://files.pythonhosted.org/packages/75/b4/b96bb66f6f8cc4669de44a158099b249c8159231d254ab6b092909388be5/fonttools-4.59.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4", size = 4890699 }, - { url = "https://files.pythonhosted.org/packages/b5/57/7969af50b26408be12baa317c6147588db5b38af2759e6df94554dbc5fdb/fonttools-4.59.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97", size = 4952934 }, - { url = "https://files.pythonhosted.org/packages/d6/e2/dd968053b6cf1f46c904f5bd409b22341477c017d8201619a265e50762d3/fonttools-4.59.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c", size = 4892319 }, - { url = "https://files.pythonhosted.org/packages/6b/95/a59810d8eda09129f83467a4e58f84205dc6994ebaeb9815406363e07250/fonttools-4.59.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c", size = 5034753 }, - { url = "https://files.pythonhosted.org/packages/a5/84/51a69ee89ff8d1fea0c6997e946657e25a3f08513de8435fe124929f3eef/fonttools-4.59.0-cp313-cp313-win32.whl", hash = "sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3", size = 2199688 }, - { url = "https://files.pythonhosted.org/packages/a0/ee/f626cd372932d828508137a79b85167fdcf3adab2e3bed433f295c596c6a/fonttools-4.59.0-cp313-cp313-win_amd64.whl", hash = "sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe", size = 2248560 }, - { url = "https://files.pythonhosted.org/packages/d0/9c/df0ef2c51845a13043e5088f7bb988ca6cd5bb82d5d4203d6a158aa58cf2/fonttools-4.59.0-py3-none-any.whl", hash = "sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d", size = 1128050 }, +sdist = { url = "https://files.pythonhosted.org/packages/8a/27/ec3c723bfdf86f34c5c82bf6305df3e0f0d8ea798d2d3a7cb0c0a866d286/fonttools-4.59.0.tar.gz", hash = "sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14", size = 3532521, upload-time = "2025-07-16T12:04:54.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/1f/3dcae710b7c4b56e79442b03db64f6c9f10c3348f7af40339dffcefb581e/fonttools-4.59.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96", size = 2761846, upload-time = "2025-07-16T12:03:33.267Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0e/ae3a1884fa1549acac1191cc9ec039142f6ac0e9cbc139c2e6a3dab967da/fonttools-4.59.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df", size = 2332060, upload-time = "2025-07-16T12:03:36.472Z" }, + { url = "https://files.pythonhosted.org/packages/75/46/58bff92a7216829159ac7bdb1d05a48ad1b8ab8c539555f12d29fdecfdd4/fonttools-4.59.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e93df708c69a193fc7987192f94df250f83f3851fda49413f02ba5dded639482", size = 4852354, upload-time = "2025-07-16T12:03:39.102Z" }, + { url = "https://files.pythonhosted.org/packages/05/57/767e31e48861045d89691128bd81fd4c62b62150f9a17a666f731ce4f197/fonttools-4.59.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:62224a9bb85b4b66d1b46d45cbe43d71cbf8f527d332b177e3b96191ffbc1e64", size = 4781132, upload-time = "2025-07-16T12:03:41.415Z" }, + { url = "https://files.pythonhosted.org/packages/d7/78/adb5e9b0af5c6ce469e8b0e112f144eaa84b30dd72a486e9c778a9b03b31/fonttools-4.59.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8974b2a266b54c96709bd5e239979cddfd2dbceed331aa567ea1d7c4a2202db", size = 4832901, upload-time = "2025-07-16T12:03:43.115Z" }, + { url = "https://files.pythonhosted.org/packages/ac/92/bc3881097fbf3d56d112bec308c863c058e5d4c9c65f534e8ae58450ab8a/fonttools-4.59.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:209b75943d158f610b78320eacb5539aa9e920bee2c775445b2846c65d20e19d", size = 4940140, upload-time = "2025-07-16T12:03:44.781Z" }, + { url = "https://files.pythonhosted.org/packages/4a/54/39cdb23f0eeda2e07ae9cb189f2b6f41da89aabc682d3a387b3ff4a4ed29/fonttools-4.59.0-cp310-cp310-win32.whl", hash = "sha256:4c908a7036f0f3677f8afa577bcd973e3e20ddd2f7c42a33208d18bee95cdb6f", size = 2215890, upload-time = "2025-07-16T12:03:46.961Z" }, + { url = "https://files.pythonhosted.org/packages/d8/eb/f8388d9e19f95d8df2449febe9b1a38ddd758cfdb7d6de3a05198d785d61/fonttools-4.59.0-cp310-cp310-win_amd64.whl", hash = "sha256:8b4309a2775e4feee7356e63b163969a215d663399cce1b3d3b65e7ec2d9680e", size = 2260191, upload-time = "2025-07-16T12:03:48.908Z" }, + { url = "https://files.pythonhosted.org/packages/06/96/520733d9602fa1bf6592e5354c6721ac6fc9ea72bc98d112d0c38b967199/fonttools-4.59.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c", size = 2782387, upload-time = "2025-07-16T12:03:51.424Z" }, + { url = "https://files.pythonhosted.org/packages/87/6a/170fce30b9bce69077d8eec9bea2cfd9f7995e8911c71be905e2eba6368b/fonttools-4.59.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5", size = 2342194, upload-time = "2025-07-16T12:03:53.295Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/7c8166c0066856f1408092f7968ac744060cf72ca53aec9036106f57eeca/fonttools-4.59.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705", size = 5032333, upload-time = "2025-07-16T12:03:55.177Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0c/707c5a19598eafcafd489b73c4cb1c142102d6197e872f531512d084aa76/fonttools-4.59.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464", size = 4974422, upload-time = "2025-07-16T12:03:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e7/6d33737d9fe632a0f59289b6f9743a86d2a9d0673de2a0c38c0f54729822/fonttools-4.59.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38", size = 5010631, upload-time = "2025-07-16T12:03:59.449Z" }, + { url = "https://files.pythonhosted.org/packages/63/e1/a4c3d089ab034a578820c8f2dff21ef60daf9668034a1e4fb38bb1cc3398/fonttools-4.59.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6", size = 5122198, upload-time = "2025-07-16T12:04:01.542Z" }, + { url = "https://files.pythonhosted.org/packages/09/77/ca82b9c12fa4de3c520b7760ee61787640cf3fde55ef1b0bfe1de38c8153/fonttools-4.59.0-cp311-cp311-win32.whl", hash = "sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757", size = 2214216, upload-time = "2025-07-16T12:04:03.515Z" }, + { url = "https://files.pythonhosted.org/packages/ab/25/5aa7ca24b560b2f00f260acf32c4cf29d7aaf8656e159a336111c18bc345/fonttools-4.59.0-cp311-cp311-win_amd64.whl", hash = "sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0", size = 2261879, upload-time = "2025-07-16T12:04:05.015Z" }, + { url = "https://files.pythonhosted.org/packages/e2/77/b1c8af22f4265e951cd2e5535dbef8859efcef4fb8dee742d368c967cddb/fonttools-4.59.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b", size = 2767562, upload-time = "2025-07-16T12:04:06.895Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5a/aeb975699588176bb357e8b398dfd27e5d3a2230d92b81ab8cbb6187358d/fonttools-4.59.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2", size = 2335168, upload-time = "2025-07-16T12:04:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/54/97/c6101a7e60ae138c4ef75b22434373a0da50a707dad523dd19a4889315bf/fonttools-4.59.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b", size = 4909850, upload-time = "2025-07-16T12:04:10.761Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6c/fa4d18d641054f7bff878cbea14aa9433f292b9057cb1700d8e91a4d5f4f/fonttools-4.59.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1", size = 4955131, upload-time = "2025-07-16T12:04:12.846Z" }, + { url = "https://files.pythonhosted.org/packages/20/5c/331947fc1377deb928a69bde49f9003364f5115e5cbe351eea99e39412a2/fonttools-4.59.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e", size = 4899667, upload-time = "2025-07-16T12:04:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/8a/46/b66469dfa26b8ff0baa7654b2cc7851206c6d57fe3abdabbaab22079a119/fonttools-4.59.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e", size = 5051349, upload-time = "2025-07-16T12:04:16.388Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ebfb6b1f3a4328ab69787d106a7d92ccde77ce66e98659df0f9e3f28d93d/fonttools-4.59.0-cp312-cp312-win32.whl", hash = "sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b", size = 2201315, upload-time = "2025-07-16T12:04:18.557Z" }, + { url = "https://files.pythonhosted.org/packages/09/45/d2bdc9ea20bbadec1016fd0db45696d573d7a26d95ab5174ffcb6d74340b/fonttools-4.59.0-cp312-cp312-win_amd64.whl", hash = "sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01", size = 2249408, upload-time = "2025-07-16T12:04:20.489Z" }, + { url = "https://files.pythonhosted.org/packages/f3/bb/390990e7c457d377b00890d9f96a3ca13ae2517efafb6609c1756e213ba4/fonttools-4.59.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2", size = 2758704, upload-time = "2025-07-16T12:04:22.217Z" }, + { url = "https://files.pythonhosted.org/packages/df/6f/d730d9fcc9b410a11597092bd2eb9ca53e5438c6cb90e4b3047ce1b723e9/fonttools-4.59.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2", size = 2330764, upload-time = "2025-07-16T12:04:23.985Z" }, + { url = "https://files.pythonhosted.org/packages/75/b4/b96bb66f6f8cc4669de44a158099b249c8159231d254ab6b092909388be5/fonttools-4.59.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4", size = 4890699, upload-time = "2025-07-16T12:04:25.664Z" }, + { url = "https://files.pythonhosted.org/packages/b5/57/7969af50b26408be12baa317c6147588db5b38af2759e6df94554dbc5fdb/fonttools-4.59.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97", size = 4952934, upload-time = "2025-07-16T12:04:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e2/dd968053b6cf1f46c904f5bd409b22341477c017d8201619a265e50762d3/fonttools-4.59.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c", size = 4892319, upload-time = "2025-07-16T12:04:30.074Z" }, + { url = "https://files.pythonhosted.org/packages/6b/95/a59810d8eda09129f83467a4e58f84205dc6994ebaeb9815406363e07250/fonttools-4.59.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c", size = 5034753, upload-time = "2025-07-16T12:04:32.292Z" }, + { url = "https://files.pythonhosted.org/packages/a5/84/51a69ee89ff8d1fea0c6997e946657e25a3f08513de8435fe124929f3eef/fonttools-4.59.0-cp313-cp313-win32.whl", hash = "sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3", size = 2199688, upload-time = "2025-07-16T12:04:34.444Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ee/f626cd372932d828508137a79b85167fdcf3adab2e3bed433f295c596c6a/fonttools-4.59.0-cp313-cp313-win_amd64.whl", hash = "sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe", size = 2248560, upload-time = "2025-07-16T12:04:36.034Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/df0ef2c51845a13043e5088f7bb988ca6cd5bb82d5d4203d6a158aa58cf2/fonttools-4.59.0-py3-none-any.whl", hash = "sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d", size = 1128050, upload-time = "2025-07-16T12:04:52.687Z" }, ] [[package]] name = "identify" version = "2.6.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254, upload-time = "2025-05-23T20:37:53.3Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145 }, + { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] [[package]] name = "imagesize" version = "1.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] [[package]] @@ -611,124 +612,124 @@ 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 } +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 }, + { 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 = "kiwisolver" version = "1.4.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623 }, - { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720 }, - { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413 }, - { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826 }, - { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231 }, - { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938 }, - { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799 }, - { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362 }, - { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695 }, - { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802 }, - { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646 }, - { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260 }, - { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633 }, - { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885 }, - { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175 }, - { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635 }, - { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717 }, - { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413 }, - { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994 }, - { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804 }, - { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690 }, - { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839 }, - { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109 }, - { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269 }, - { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468 }, - { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394 }, - { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901 }, - { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306 }, - { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966 }, - { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311 }, - { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152 }, - { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555 }, - { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067 }, - { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443 }, - { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728 }, - { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388 }, - { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849 }, - { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533 }, - { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898 }, - { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605 }, - { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801 }, - { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077 }, - { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410 }, - { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853 }, - { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424 }, - { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156 }, - { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555 }, - { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071 }, - { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053 }, - { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278 }, - { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139 }, - { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517 }, - { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952 }, - { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132 }, - { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997 }, - { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060 }, - { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471 }, - { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793 }, - { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855 }, - { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430 }, - { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294 }, - { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736 }, - { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194 }, - { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942 }, - { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341 }, - { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455 }, - { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138 }, - { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857 }, - { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129 }, - { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538 }, - { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661 }, - { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710 }, - { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213 }, - { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403 }, - { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657 }, - { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948 }, - { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186 }, - { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279 }, - { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762 }, +sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538, upload-time = "2024-12-24T18:30:51.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623, upload-time = "2024-12-24T18:28:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720, upload-time = "2024-12-24T18:28:19.158Z" }, + { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413, upload-time = "2024-12-24T18:28:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826, upload-time = "2024-12-24T18:28:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231, upload-time = "2024-12-24T18:28:23.851Z" }, + { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938, upload-time = "2024-12-24T18:28:26.687Z" }, + { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799, upload-time = "2024-12-24T18:28:30.538Z" }, + { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362, upload-time = "2024-12-24T18:28:32.943Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695, upload-time = "2024-12-24T18:28:35.641Z" }, + { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802, upload-time = "2024-12-24T18:28:38.357Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646, upload-time = "2024-12-24T18:28:40.941Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260, upload-time = "2024-12-24T18:28:42.273Z" }, + { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633, upload-time = "2024-12-24T18:28:44.87Z" }, + { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885, upload-time = "2024-12-24T18:28:47.346Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175, upload-time = "2024-12-24T18:28:49.651Z" }, + { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635, upload-time = "2024-12-24T18:28:51.826Z" }, + { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717, upload-time = "2024-12-24T18:28:54.256Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413, upload-time = "2024-12-24T18:28:55.184Z" }, + { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994, upload-time = "2024-12-24T18:28:57.493Z" }, + { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804, upload-time = "2024-12-24T18:29:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690, upload-time = "2024-12-24T18:29:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839, upload-time = "2024-12-24T18:29:02.685Z" }, + { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109, upload-time = "2024-12-24T18:29:04.113Z" }, + { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269, upload-time = "2024-12-24T18:29:05.488Z" }, + { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468, upload-time = "2024-12-24T18:29:06.79Z" }, + { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394, upload-time = "2024-12-24T18:29:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901, upload-time = "2024-12-24T18:29:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306, upload-time = "2024-12-24T18:29:12.644Z" }, + { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966, upload-time = "2024-12-24T18:29:14.089Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311, upload-time = "2024-12-24T18:29:15.892Z" }, + { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152, upload-time = "2024-12-24T18:29:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555, upload-time = "2024-12-24T18:29:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067, upload-time = "2024-12-24T18:29:20.096Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443, upload-time = "2024-12-24T18:29:22.843Z" }, + { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728, upload-time = "2024-12-24T18:29:24.463Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388, upload-time = "2024-12-24T18:29:25.776Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849, upload-time = "2024-12-24T18:29:27.202Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533, upload-time = "2024-12-24T18:29:28.638Z" }, + { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898, upload-time = "2024-12-24T18:29:30.368Z" }, + { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605, upload-time = "2024-12-24T18:29:33.151Z" }, + { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801, upload-time = "2024-12-24T18:29:34.584Z" }, + { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077, upload-time = "2024-12-24T18:29:36.138Z" }, + { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410, upload-time = "2024-12-24T18:29:39.991Z" }, + { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853, upload-time = "2024-12-24T18:29:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424, upload-time = "2024-12-24T18:29:44.38Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156, upload-time = "2024-12-24T18:29:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555, upload-time = "2024-12-24T18:29:46.37Z" }, + { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071, upload-time = "2024-12-24T18:29:47.333Z" }, + { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053, upload-time = "2024-12-24T18:29:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278, upload-time = "2024-12-24T18:29:51.164Z" }, + { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139, upload-time = "2024-12-24T18:29:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517, upload-time = "2024-12-24T18:29:53.941Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952, upload-time = "2024-12-24T18:29:56.523Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132, upload-time = "2024-12-24T18:29:57.989Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997, upload-time = "2024-12-24T18:29:59.393Z" }, + { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060, upload-time = "2024-12-24T18:30:01.338Z" }, + { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471, upload-time = "2024-12-24T18:30:04.574Z" }, + { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793, upload-time = "2024-12-24T18:30:06.25Z" }, + { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855, upload-time = "2024-12-24T18:30:07.535Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430, upload-time = "2024-12-24T18:30:08.504Z" }, + { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294, upload-time = "2024-12-24T18:30:09.508Z" }, + { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736, upload-time = "2024-12-24T18:30:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194, upload-time = "2024-12-24T18:30:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942, upload-time = "2024-12-24T18:30:18.927Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341, upload-time = "2024-12-24T18:30:22.102Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455, upload-time = "2024-12-24T18:30:24.947Z" }, + { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138, upload-time = "2024-12-24T18:30:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857, upload-time = "2024-12-24T18:30:28.86Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129, upload-time = "2024-12-24T18:30:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538, upload-time = "2024-12-24T18:30:33.334Z" }, + { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661, upload-time = "2024-12-24T18:30:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710, upload-time = "2024-12-24T18:30:37.281Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213, upload-time = "2024-12-24T18:30:40.019Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403, upload-time = "2024-12-24T18:30:41.372Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657, upload-time = "2024-12-24T18:30:42.392Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948, upload-time = "2024-12-24T18:30:44.703Z" }, + { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186, upload-time = "2024-12-24T18:30:45.654Z" }, + { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279, upload-time = "2024-12-24T18:30:47.951Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762, upload-time = "2024-12-24T18:30:48.903Z" }, ] [[package]] name = "llvmlite" version = "0.44.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/75/d4863ddfd8ab5f6e70f4504cf8cc37f4e986ec6910f4ef8502bb7d3c1c71/llvmlite-0.44.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9fbadbfba8422123bab5535b293da1cf72f9f478a65645ecd73e781f962ca614", size = 28132306 }, - { url = "https://files.pythonhosted.org/packages/37/d9/6e8943e1515d2f1003e8278819ec03e4e653e2eeb71e4d00de6cfe59424e/llvmlite-0.44.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cccf8eb28f24840f2689fb1a45f9c0f7e582dd24e088dcf96e424834af11f791", size = 26201096 }, - { url = "https://files.pythonhosted.org/packages/aa/46/8ffbc114def88cc698906bf5acab54ca9fdf9214fe04aed0e71731fb3688/llvmlite-0.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7202b678cdf904823c764ee0fe2dfe38a76981f4c1e51715b4cb5abb6cf1d9e8", size = 42361859 }, - { url = "https://files.pythonhosted.org/packages/30/1c/9366b29ab050a726af13ebaae8d0dff00c3c58562261c79c635ad4f5eb71/llvmlite-0.44.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40526fb5e313d7b96bda4cbb2c85cd5374e04d80732dd36a282d72a560bb6408", size = 41184199 }, - { url = "https://files.pythonhosted.org/packages/69/07/35e7c594b021ecb1938540f5bce543ddd8713cff97f71d81f021221edc1b/llvmlite-0.44.0-cp310-cp310-win_amd64.whl", hash = "sha256:41e3839150db4330e1b2716c0be3b5c4672525b4c9005e17c7597f835f351ce2", size = 30332381 }, - { url = "https://files.pythonhosted.org/packages/b5/e2/86b245397052386595ad726f9742e5223d7aea999b18c518a50e96c3aca4/llvmlite-0.44.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:eed7d5f29136bda63b6d7804c279e2b72e08c952b7c5df61f45db408e0ee52f3", size = 28132305 }, - { url = "https://files.pythonhosted.org/packages/ff/ec/506902dc6870249fbe2466d9cf66d531265d0f3a1157213c8f986250c033/llvmlite-0.44.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ace564d9fa44bb91eb6e6d8e7754977783c68e90a471ea7ce913bff30bd62427", size = 26201090 }, - { url = "https://files.pythonhosted.org/packages/99/fe/d030f1849ebb1f394bb3f7adad5e729b634fb100515594aca25c354ffc62/llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5d22c3bfc842668168a786af4205ec8e3ad29fb1bc03fd11fd48460d0df64c1", size = 42361858 }, - { url = "https://files.pythonhosted.org/packages/d7/7a/ce6174664b9077fc673d172e4c888cb0b128e707e306bc33fff8c2035f0d/llvmlite-0.44.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f01a394e9c9b7b1d4e63c327b096d10f6f0ed149ef53d38a09b3749dcf8c9610", size = 41184200 }, - { url = "https://files.pythonhosted.org/packages/5f/c6/258801143975a6d09a373f2641237992496e15567b907a4d401839d671b8/llvmlite-0.44.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8489634d43c20cd0ad71330dde1d5bc7b9966937a263ff1ec1cebb90dc50955", size = 30331193 }, - { url = "https://files.pythonhosted.org/packages/15/86/e3c3195b92e6e492458f16d233e58a1a812aa2bfbef9bdd0fbafcec85c60/llvmlite-0.44.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:1d671a56acf725bf1b531d5ef76b86660a5ab8ef19bb6a46064a705c6ca80aad", size = 28132297 }, - { url = "https://files.pythonhosted.org/packages/d6/53/373b6b8be67b9221d12b24125fd0ec56b1078b660eeae266ec388a6ac9a0/llvmlite-0.44.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f79a728e0435493611c9f405168682bb75ffd1fbe6fc360733b850c80a026db", size = 26201105 }, - { url = "https://files.pythonhosted.org/packages/cb/da/8341fd3056419441286c8e26bf436923021005ece0bff5f41906476ae514/llvmlite-0.44.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0143a5ef336da14deaa8ec26c5449ad5b6a2b564df82fcef4be040b9cacfea9", size = 42361901 }, - { url = "https://files.pythonhosted.org/packages/53/ad/d79349dc07b8a395a99153d7ce8b01d6fcdc9f8231355a5df55ded649b61/llvmlite-0.44.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d752f89e31b66db6f8da06df8b39f9b91e78c5feea1bf9e8c1fba1d1c24c065d", size = 41184247 }, - { url = "https://files.pythonhosted.org/packages/e2/3b/a9a17366af80127bd09decbe2a54d8974b6d8b274b39bf47fbaedeec6307/llvmlite-0.44.0-cp312-cp312-win_amd64.whl", hash = "sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1", size = 30332380 }, - { url = "https://files.pythonhosted.org/packages/89/24/4c0ca705a717514c2092b18476e7a12c74d34d875e05e4d742618ebbf449/llvmlite-0.44.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:319bddd44e5f71ae2689859b7203080716448a3cd1128fb144fe5c055219d516", size = 28132306 }, - { url = "https://files.pythonhosted.org/packages/01/cf/1dd5a60ba6aee7122ab9243fd614abcf22f36b0437cbbe1ccf1e3391461c/llvmlite-0.44.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c58867118bad04a0bb22a2e0068c693719658105e40009ffe95c7000fcde88e", size = 26201090 }, - { url = "https://files.pythonhosted.org/packages/d2/1b/656f5a357de7135a3777bd735cc7c9b8f23b4d37465505bd0eaf4be9befe/llvmlite-0.44.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46224058b13c96af1365290bdfebe9a6264ae62fb79b2b55693deed11657a8bf", size = 42361904 }, - { url = "https://files.pythonhosted.org/packages/d8/e1/12c5f20cb9168fb3464a34310411d5ad86e4163c8ff2d14a2b57e5cc6bac/llvmlite-0.44.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0097052c32bf721a4efc03bd109d335dfa57d9bffb3d4c24cc680711b8b4fc", size = 41184245 }, - { url = "https://files.pythonhosted.org/packages/d0/81/e66fc86539293282fd9cb7c9417438e897f369e79ffb62e1ae5e5154d4dd/llvmlite-0.44.0-cp313-cp313-win_amd64.whl", hash = "sha256:2fb7c4f2fb86cbae6dca3db9ab203eeea0e22d73b99bc2341cdf9de93612e930", size = 30331193 }, +sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880, upload-time = "2025-01-20T11:14:41.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/75/d4863ddfd8ab5f6e70f4504cf8cc37f4e986ec6910f4ef8502bb7d3c1c71/llvmlite-0.44.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9fbadbfba8422123bab5535b293da1cf72f9f478a65645ecd73e781f962ca614", size = 28132306, upload-time = "2025-01-20T11:12:18.634Z" }, + { url = "https://files.pythonhosted.org/packages/37/d9/6e8943e1515d2f1003e8278819ec03e4e653e2eeb71e4d00de6cfe59424e/llvmlite-0.44.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cccf8eb28f24840f2689fb1a45f9c0f7e582dd24e088dcf96e424834af11f791", size = 26201096, upload-time = "2025-01-20T11:12:24.544Z" }, + { url = "https://files.pythonhosted.org/packages/aa/46/8ffbc114def88cc698906bf5acab54ca9fdf9214fe04aed0e71731fb3688/llvmlite-0.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7202b678cdf904823c764ee0fe2dfe38a76981f4c1e51715b4cb5abb6cf1d9e8", size = 42361859, upload-time = "2025-01-20T11:12:31.839Z" }, + { url = "https://files.pythonhosted.org/packages/30/1c/9366b29ab050a726af13ebaae8d0dff00c3c58562261c79c635ad4f5eb71/llvmlite-0.44.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40526fb5e313d7b96bda4cbb2c85cd5374e04d80732dd36a282d72a560bb6408", size = 41184199, upload-time = "2025-01-20T11:12:40.049Z" }, + { url = "https://files.pythonhosted.org/packages/69/07/35e7c594b021ecb1938540f5bce543ddd8713cff97f71d81f021221edc1b/llvmlite-0.44.0-cp310-cp310-win_amd64.whl", hash = "sha256:41e3839150db4330e1b2716c0be3b5c4672525b4c9005e17c7597f835f351ce2", size = 30332381, upload-time = "2025-01-20T11:12:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e2/86b245397052386595ad726f9742e5223d7aea999b18c518a50e96c3aca4/llvmlite-0.44.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:eed7d5f29136bda63b6d7804c279e2b72e08c952b7c5df61f45db408e0ee52f3", size = 28132305, upload-time = "2025-01-20T11:12:53.936Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ec/506902dc6870249fbe2466d9cf66d531265d0f3a1157213c8f986250c033/llvmlite-0.44.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ace564d9fa44bb91eb6e6d8e7754977783c68e90a471ea7ce913bff30bd62427", size = 26201090, upload-time = "2025-01-20T11:12:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/99/fe/d030f1849ebb1f394bb3f7adad5e729b634fb100515594aca25c354ffc62/llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5d22c3bfc842668168a786af4205ec8e3ad29fb1bc03fd11fd48460d0df64c1", size = 42361858, upload-time = "2025-01-20T11:13:07.623Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7a/ce6174664b9077fc673d172e4c888cb0b128e707e306bc33fff8c2035f0d/llvmlite-0.44.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f01a394e9c9b7b1d4e63c327b096d10f6f0ed149ef53d38a09b3749dcf8c9610", size = 41184200, upload-time = "2025-01-20T11:13:20.058Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c6/258801143975a6d09a373f2641237992496e15567b907a4d401839d671b8/llvmlite-0.44.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8489634d43c20cd0ad71330dde1d5bc7b9966937a263ff1ec1cebb90dc50955", size = 30331193, upload-time = "2025-01-20T11:13:26.976Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/e3c3195b92e6e492458f16d233e58a1a812aa2bfbef9bdd0fbafcec85c60/llvmlite-0.44.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:1d671a56acf725bf1b531d5ef76b86660a5ab8ef19bb6a46064a705c6ca80aad", size = 28132297, upload-time = "2025-01-20T11:13:32.57Z" }, + { url = "https://files.pythonhosted.org/packages/d6/53/373b6b8be67b9221d12b24125fd0ec56b1078b660eeae266ec388a6ac9a0/llvmlite-0.44.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f79a728e0435493611c9f405168682bb75ffd1fbe6fc360733b850c80a026db", size = 26201105, upload-time = "2025-01-20T11:13:38.744Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/8341fd3056419441286c8e26bf436923021005ece0bff5f41906476ae514/llvmlite-0.44.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0143a5ef336da14deaa8ec26c5449ad5b6a2b564df82fcef4be040b9cacfea9", size = 42361901, upload-time = "2025-01-20T11:13:46.711Z" }, + { url = "https://files.pythonhosted.org/packages/53/ad/d79349dc07b8a395a99153d7ce8b01d6fcdc9f8231355a5df55ded649b61/llvmlite-0.44.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d752f89e31b66db6f8da06df8b39f9b91e78c5feea1bf9e8c1fba1d1c24c065d", size = 41184247, upload-time = "2025-01-20T11:13:56.159Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3b/a9a17366af80127bd09decbe2a54d8974b6d8b274b39bf47fbaedeec6307/llvmlite-0.44.0-cp312-cp312-win_amd64.whl", hash = "sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1", size = 30332380, upload-time = "2025-01-20T11:14:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/89/24/4c0ca705a717514c2092b18476e7a12c74d34d875e05e4d742618ebbf449/llvmlite-0.44.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:319bddd44e5f71ae2689859b7203080716448a3cd1128fb144fe5c055219d516", size = 28132306, upload-time = "2025-01-20T11:14:09.035Z" }, + { url = "https://files.pythonhosted.org/packages/01/cf/1dd5a60ba6aee7122ab9243fd614abcf22f36b0437cbbe1ccf1e3391461c/llvmlite-0.44.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c58867118bad04a0bb22a2e0068c693719658105e40009ffe95c7000fcde88e", size = 26201090, upload-time = "2025-01-20T11:14:15.401Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1b/656f5a357de7135a3777bd735cc7c9b8f23b4d37465505bd0eaf4be9befe/llvmlite-0.44.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46224058b13c96af1365290bdfebe9a6264ae62fb79b2b55693deed11657a8bf", size = 42361904, upload-time = "2025-01-20T11:14:22.949Z" }, + { url = "https://files.pythonhosted.org/packages/d8/e1/12c5f20cb9168fb3464a34310411d5ad86e4163c8ff2d14a2b57e5cc6bac/llvmlite-0.44.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0097052c32bf721a4efc03bd109d335dfa57d9bffb3d4c24cc680711b8b4fc", size = 41184245, upload-time = "2025-01-20T11:14:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/d0/81/e66fc86539293282fd9cb7c9417438e897f369e79ffb62e1ae5e5154d4dd/llvmlite-0.44.0-cp313-cp313-win_amd64.whl", hash = "sha256:2fb7c4f2fb86cbae6dca3db9ab203eeea0e22d73b99bc2341cdf9de93612e930", size = 30331193, upload-time = "2025-01-20T11:14:38.578Z" }, ] [[package]] @@ -738,67 +739,67 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, - { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, - { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, - { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, - { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, - { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, - { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, - { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, - { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, - { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, ] [[package]] @@ -817,71 +818,71 @@ dependencies = [ { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/91/f2939bb60b7ebf12478b030e0d7f340247390f402b3b189616aad790c366/matplotlib-3.10.5.tar.gz", hash = "sha256:352ed6ccfb7998a00881692f38b4ca083c691d3e275b4145423704c34c909076", size = 34804044 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/89/5355cdfe43242cb4d1a64a67cb6831398b665ad90e9702c16247cbd8d5ab/matplotlib-3.10.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5d4773a6d1c106ca05cb5a5515d277a6bb96ed09e5c8fab6b7741b8fcaa62c8f", size = 8229094 }, - { url = "https://files.pythonhosted.org/packages/34/bc/ba802650e1c69650faed261a9df004af4c6f21759d7a1ec67fe972f093b3/matplotlib-3.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc88af74e7ba27de6cbe6faee916024ea35d895ed3d61ef6f58c4ce97da7185a", size = 8091464 }, - { url = "https://files.pythonhosted.org/packages/ac/64/8d0c8937dee86c286625bddb1902efacc3e22f2b619f5b5a8df29fe5217b/matplotlib-3.10.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:64c4535419d5617f7363dad171a5a59963308e0f3f813c4bed6c9e6e2c131512", size = 8653163 }, - { url = "https://files.pythonhosted.org/packages/11/dc/8dfc0acfbdc2fc2336c72561b7935cfa73db9ca70b875d8d3e1b3a6f371a/matplotlib-3.10.5-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a277033048ab22d34f88a3c5243938cef776493f6201a8742ed5f8b553201343", size = 9490635 }, - { url = "https://files.pythonhosted.org/packages/54/02/e3fdfe0f2e9fb05f3a691d63876639dbf684170fdcf93231e973104153b4/matplotlib-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e4a6470a118a2e93022ecc7d3bd16b3114b2004ea2bf014fff875b3bc99b70c6", size = 9539036 }, - { url = "https://files.pythonhosted.org/packages/c1/29/82bf486ff7f4dbedfb11ccc207d0575cbe3be6ea26f75be514252bde3d70/matplotlib-3.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:7e44cada61bec8833c106547786814dd4a266c1b2964fd25daa3804f1b8d4467", size = 8093529 }, - { url = "https://files.pythonhosted.org/packages/aa/c7/1f2db90a1d43710478bb1e9b57b162852f79234d28e4f48a28cc415aa583/matplotlib-3.10.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dcfc39c452c6a9f9028d3e44d2d721484f665304857188124b505b2c95e1eecf", size = 8239216 }, - { url = "https://files.pythonhosted.org/packages/82/6d/ca6844c77a4f89b1c9e4d481c412e1d1dbabf2aae2cbc5aa2da4a1d6683e/matplotlib-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:903352681b59f3efbf4546985142a9686ea1d616bb054b09a537a06e4b892ccf", size = 8102130 }, - { url = "https://files.pythonhosted.org/packages/1d/1e/5e187a30cc673a3e384f3723e5f3c416033c1d8d5da414f82e4e731128ea/matplotlib-3.10.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:080c3676a56b8ee1c762bcf8fca3fe709daa1ee23e6ef06ad9f3fc17332f2d2a", size = 8666471 }, - { url = "https://files.pythonhosted.org/packages/03/c0/95540d584d7d645324db99a845ac194e915ef75011a0d5e19e1b5cee7e69/matplotlib-3.10.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b4984d5064a35b6f66d2c11d668565f4389b1119cc64db7a4c1725bc11adffc", size = 9500518 }, - { url = "https://files.pythonhosted.org/packages/ba/2e/e019352099ea58b4169adb9c6e1a2ad0c568c6377c2b677ee1f06de2adc7/matplotlib-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3967424121d3a46705c9fa9bdb0931de3228f13f73d7bb03c999c88343a89d89", size = 9552372 }, - { url = "https://files.pythonhosted.org/packages/b7/81/3200b792a5e8b354f31f4101ad7834743ad07b6d620259f2059317b25e4d/matplotlib-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:33775bbeb75528555a15ac29396940128ef5613cf9a2d31fb1bfd18b3c0c0903", size = 8100634 }, - { url = "https://files.pythonhosted.org/packages/52/46/a944f6f0c1f5476a0adfa501969d229ce5ae60cf9a663be0e70361381f89/matplotlib-3.10.5-cp311-cp311-win_arm64.whl", hash = "sha256:c61333a8e5e6240e73769d5826b9a31d8b22df76c0778f8480baf1b4b01c9420", size = 7978880 }, - { url = "https://files.pythonhosted.org/packages/66/1e/c6f6bcd882d589410b475ca1fc22e34e34c82adff519caf18f3e6dd9d682/matplotlib-3.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:00b6feadc28a08bd3c65b2894f56cf3c94fc8f7adcbc6ab4516ae1e8ed8f62e2", size = 8253056 }, - { url = "https://files.pythonhosted.org/packages/53/e6/d6f7d1b59413f233793dda14419776f5f443bcccb2dfc84b09f09fe05dbe/matplotlib-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee98a5c5344dc7f48dc261b6ba5d9900c008fc12beb3fa6ebda81273602cc389", size = 8110131 }, - { url = "https://files.pythonhosted.org/packages/66/2b/bed8a45e74957549197a2ac2e1259671cd80b55ed9e1fe2b5c94d88a9202/matplotlib-3.10.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a17e57e33de901d221a07af32c08870ed4528db0b6059dce7d7e65c1122d4bea", size = 8669603 }, - { url = "https://files.pythonhosted.org/packages/7e/a7/315e9435b10d057f5e52dfc603cd353167ae28bb1a4e033d41540c0067a4/matplotlib-3.10.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97b9d6443419085950ee4a5b1ee08c363e5c43d7176e55513479e53669e88468", size = 9508127 }, - { url = "https://files.pythonhosted.org/packages/7f/d9/edcbb1f02ca99165365d2768d517898c22c6040187e2ae2ce7294437c413/matplotlib-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ceefe5d40807d29a66ae916c6a3915d60ef9f028ce1927b84e727be91d884369", size = 9566926 }, - { url = "https://files.pythonhosted.org/packages/3b/d9/6dd924ad5616c97b7308e6320cf392c466237a82a2040381163b7500510a/matplotlib-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:c04cba0f93d40e45b3c187c6c52c17f24535b27d545f757a2fffebc06c12b98b", size = 8107599 }, - { url = "https://files.pythonhosted.org/packages/0e/f3/522dc319a50f7b0279fbe74f86f7a3506ce414bc23172098e8d2bdf21894/matplotlib-3.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:a41bcb6e2c8e79dc99c5511ae6f7787d2fb52efd3d805fff06d5d4f667db16b2", size = 7978173 }, - { url = "https://files.pythonhosted.org/packages/8d/05/4f3c1f396075f108515e45cb8d334aff011a922350e502a7472e24c52d77/matplotlib-3.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:354204db3f7d5caaa10e5de74549ef6a05a4550fdd1c8f831ab9bca81efd39ed", size = 8253586 }, - { url = "https://files.pythonhosted.org/packages/2f/2c/e084415775aac7016c3719fe7006cdb462582c6c99ac142f27303c56e243/matplotlib-3.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b072aac0c3ad563a2b3318124756cb6112157017f7431626600ecbe890df57a1", size = 8110715 }, - { url = "https://files.pythonhosted.org/packages/52/1b/233e3094b749df16e3e6cd5a44849fd33852e692ad009cf7de00cf58ddf6/matplotlib-3.10.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d52fd5b684d541b5a51fb276b2b97b010c75bee9aa392f96b4a07aeb491e33c7", size = 8669397 }, - { url = "https://files.pythonhosted.org/packages/e8/ec/03f9e003a798f907d9f772eed9b7c6a9775d5bd00648b643ebfb88e25414/matplotlib-3.10.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee7a09ae2f4676276f5a65bd9f2bd91b4f9fbaedf49f40267ce3f9b448de501f", size = 9508646 }, - { url = "https://files.pythonhosted.org/packages/91/e7/c051a7a386680c28487bca27d23b02d84f63e3d2a9b4d2fc478e6a42e37e/matplotlib-3.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ba6c3c9c067b83481d647af88b4e441d532acdb5ef22178a14935b0b881188f4", size = 9567424 }, - { url = "https://files.pythonhosted.org/packages/36/c2/24302e93ff431b8f4173ee1dd88976c8d80483cadbc5d3d777cef47b3a1c/matplotlib-3.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:07442d2692c9bd1cceaa4afb4bbe5b57b98a7599de4dabfcca92d3eea70f9ebe", size = 8107809 }, - { url = "https://files.pythonhosted.org/packages/0b/33/423ec6a668d375dad825197557ed8fbdb74d62b432c1ed8235465945475f/matplotlib-3.10.5-cp313-cp313-win_arm64.whl", hash = "sha256:48fe6d47380b68a37ccfcc94f009530e84d41f71f5dae7eda7c4a5a84aa0a674", size = 7978078 }, - { url = "https://files.pythonhosted.org/packages/51/17/521fc16ec766455c7bb52cc046550cf7652f6765ca8650ff120aa2d197b6/matplotlib-3.10.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b80eb8621331449fc519541a7461987f10afa4f9cfd91afcd2276ebe19bd56c", size = 8295590 }, - { url = "https://files.pythonhosted.org/packages/f8/12/23c28b2c21114c63999bae129fce7fd34515641c517ae48ce7b7dcd33458/matplotlib-3.10.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47a388908e469d6ca2a6015858fa924e0e8a2345a37125948d8e93a91c47933e", size = 8158518 }, - { url = "https://files.pythonhosted.org/packages/81/f8/aae4eb25e8e7190759f3cb91cbeaa344128159ac92bb6b409e24f8711f78/matplotlib-3.10.5-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b6b49167d208358983ce26e43aa4196073b4702858670f2eb111f9a10652b4b", size = 8691815 }, - { url = "https://files.pythonhosted.org/packages/d0/ba/450c39ebdd486bd33a359fc17365ade46c6a96bf637bbb0df7824de2886c/matplotlib-3.10.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a8da0453a7fd8e3da114234ba70c5ba9ef0e98f190309ddfde0f089accd46ea", size = 9522814 }, - { url = "https://files.pythonhosted.org/packages/89/11/9c66f6a990e27bb9aa023f7988d2d5809cb98aa39c09cbf20fba75a542ef/matplotlib-3.10.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52c6573dfcb7726a9907b482cd5b92e6b5499b284ffacb04ffbfe06b3e568124", size = 9573917 }, - { url = "https://files.pythonhosted.org/packages/b3/69/8b49394de92569419e5e05e82e83df9b749a0ff550d07631ea96ed2eb35a/matplotlib-3.10.5-cp313-cp313t-win_amd64.whl", hash = "sha256:a23193db2e9d64ece69cac0c8231849db7dd77ce59c7b89948cf9d0ce655a3ce", size = 8181034 }, - { url = "https://files.pythonhosted.org/packages/47/23/82dc435bb98a2fc5c20dffcac8f0b083935ac28286413ed8835df40d0baa/matplotlib-3.10.5-cp313-cp313t-win_arm64.whl", hash = "sha256:56da3b102cf6da2776fef3e71cd96fcf22103a13594a18ac9a9b31314e0be154", size = 8023337 }, - { url = "https://files.pythonhosted.org/packages/ac/e0/26b6cfde31f5383503ee45dcb7e691d45dadf0b3f54639332b59316a97f8/matplotlib-3.10.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:96ef8f5a3696f20f55597ffa91c28e2e73088df25c555f8d4754931515512715", size = 8253591 }, - { url = "https://files.pythonhosted.org/packages/c1/89/98488c7ef7ea20ea659af7499628c240a608b337af4be2066d644cfd0a0f/matplotlib-3.10.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:77fab633e94b9da60512d4fa0213daeb76d5a7b05156840c4fd0399b4b818837", size = 8112566 }, - { url = "https://files.pythonhosted.org/packages/52/67/42294dfedc82aea55e1a767daf3263aacfb5a125f44ba189e685bab41b6f/matplotlib-3.10.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27f52634315e96b1debbfdc5c416592edcd9c4221bc2f520fd39c33db5d9f202", size = 9513281 }, - { url = "https://files.pythonhosted.org/packages/e7/68/f258239e0cf34c2cbc816781c7ab6fca768452e6bf1119aedd2bd4a882a3/matplotlib-3.10.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:525f6e28c485c769d1f07935b660c864de41c37fd716bfa64158ea646f7084bb", size = 9780873 }, - { url = "https://files.pythonhosted.org/packages/89/64/f4881554006bd12e4558bd66778bdd15d47b00a1f6c6e8b50f6208eda4b3/matplotlib-3.10.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1f5f3ec4c191253c5f2b7c07096a142c6a1c024d9f738247bfc8e3f9643fc975", size = 9568954 }, - { url = "https://files.pythonhosted.org/packages/06/f8/42779d39c3f757e1f012f2dda3319a89fb602bd2ef98ce8faf0281f4febd/matplotlib-3.10.5-cp314-cp314-win_amd64.whl", hash = "sha256:707f9c292c4cd4716f19ab8a1f93f26598222cd931e0cd98fbbb1c5994bf7667", size = 8237465 }, - { url = "https://files.pythonhosted.org/packages/cf/f8/153fd06b5160f0cd27c8b9dd797fcc9fb56ac6a0ebf3c1f765b6b68d3c8a/matplotlib-3.10.5-cp314-cp314-win_arm64.whl", hash = "sha256:21a95b9bf408178d372814de7baacd61c712a62cae560b5e6f35d791776f6516", size = 8108898 }, - { url = "https://files.pythonhosted.org/packages/9a/ee/c4b082a382a225fe0d2a73f1f57cf6f6f132308805b493a54c8641006238/matplotlib-3.10.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a6b310f95e1102a8c7c817ef17b60ee5d1851b8c71b63d9286b66b177963039e", size = 8295636 }, - { url = "https://files.pythonhosted.org/packages/30/73/2195fa2099718b21a20da82dfc753bf2af58d596b51aefe93e359dd5915a/matplotlib-3.10.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:94986a242747a0605cb3ff1cb98691c736f28a59f8ffe5175acaeb7397c49a5a", size = 8158575 }, - { url = "https://files.pythonhosted.org/packages/f6/e9/a08cdb34618a91fa08f75e6738541da5cacde7c307cea18ff10f0d03fcff/matplotlib-3.10.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ff10ea43288f0c8bab608a305dc6c918cc729d429c31dcbbecde3b9f4d5b569", size = 9522815 }, - { url = "https://files.pythonhosted.org/packages/4e/bb/34d8b7e0d1bb6d06ef45db01dfa560d5a67b1c40c0b998ce9ccde934bb09/matplotlib-3.10.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f6adb644c9d040ffb0d3434e440490a66cf73dbfa118a6f79cd7568431f7a012", size = 9783514 }, - { url = "https://files.pythonhosted.org/packages/12/09/d330d1e55dcca2e11b4d304cc5227f52e2512e46828d6249b88e0694176e/matplotlib-3.10.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4fa40a8f98428f789a9dcacd625f59b7bc4e3ef6c8c7c80187a7a709475cf592", size = 9573932 }, - { url = "https://files.pythonhosted.org/packages/eb/3b/f70258ac729aa004aca673800a53a2b0a26d49ca1df2eaa03289a1c40f81/matplotlib-3.10.5-cp314-cp314t-win_amd64.whl", hash = "sha256:95672a5d628b44207aab91ec20bf59c26da99de12b88f7e0b1fb0a84a86ff959", size = 8322003 }, - { url = "https://files.pythonhosted.org/packages/5b/60/3601f8ce6d76a7c81c7f25a0e15fde0d6b66226dd187aa6d2838e6374161/matplotlib-3.10.5-cp314-cp314t-win_arm64.whl", hash = "sha256:2efaf97d72629e74252e0b5e3c46813e9eeaa94e011ecf8084a971a31a97f40b", size = 8153849 }, - { url = "https://files.pythonhosted.org/packages/e4/eb/7d4c5de49eb78294e1a8e2be8a6ecff8b433e921b731412a56cd1abd3567/matplotlib-3.10.5-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b5fa2e941f77eb579005fb804026f9d0a1082276118d01cc6051d0d9626eaa7f", size = 8222360 }, - { url = "https://files.pythonhosted.org/packages/16/8a/e435db90927b66b16d69f8f009498775f4469f8de4d14b87856965e58eba/matplotlib-3.10.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1fc0d2a3241cdcb9daaca279204a3351ce9df3c0e7e621c7e04ec28aaacaca30", size = 8087462 }, - { url = "https://files.pythonhosted.org/packages/0b/dd/06c0e00064362f5647f318e00b435be2ff76a1bdced97c5eaf8347311fbe/matplotlib-3.10.5-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8dee65cb1424b7dc982fe87895b5613d4e691cc57117e8af840da0148ca6c1d7", size = 8659802 }, - { url = "https://files.pythonhosted.org/packages/dc/d6/e921be4e1a5f7aca5194e1f016cb67ec294548e530013251f630713e456d/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:160e125da27a749481eaddc0627962990f6029811dbeae23881833a011a0907f", size = 8233224 }, - { url = "https://files.pythonhosted.org/packages/ec/74/a2b9b04824b9c349c8f1b2d21d5af43fa7010039427f2b133a034cb09e59/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac3d50760394d78a3c9be6b28318fe22b494c4fcf6407e8fd4794b538251899b", size = 8098539 }, - { url = "https://files.pythonhosted.org/packages/fc/66/cd29ebc7f6c0d2a15d216fb572573e8fc38bd5d6dec3bd9d7d904c0949f7/matplotlib-3.10.5-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c49465bf689c4d59d174d0c7795fb42a21d4244d11d70e52b8011987367ac61", size = 8672192 }, +sdist = { url = "https://files.pythonhosted.org/packages/43/91/f2939bb60b7ebf12478b030e0d7f340247390f402b3b189616aad790c366/matplotlib-3.10.5.tar.gz", hash = "sha256:352ed6ccfb7998a00881692f38b4ca083c691d3e275b4145423704c34c909076", size = 34804044, upload-time = "2025-07-31T18:09:33.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/89/5355cdfe43242cb4d1a64a67cb6831398b665ad90e9702c16247cbd8d5ab/matplotlib-3.10.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5d4773a6d1c106ca05cb5a5515d277a6bb96ed09e5c8fab6b7741b8fcaa62c8f", size = 8229094, upload-time = "2025-07-31T18:07:36.507Z" }, + { url = "https://files.pythonhosted.org/packages/34/bc/ba802650e1c69650faed261a9df004af4c6f21759d7a1ec67fe972f093b3/matplotlib-3.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc88af74e7ba27de6cbe6faee916024ea35d895ed3d61ef6f58c4ce97da7185a", size = 8091464, upload-time = "2025-07-31T18:07:38.864Z" }, + { url = "https://files.pythonhosted.org/packages/ac/64/8d0c8937dee86c286625bddb1902efacc3e22f2b619f5b5a8df29fe5217b/matplotlib-3.10.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:64c4535419d5617f7363dad171a5a59963308e0f3f813c4bed6c9e6e2c131512", size = 8653163, upload-time = "2025-07-31T18:07:41.141Z" }, + { url = "https://files.pythonhosted.org/packages/11/dc/8dfc0acfbdc2fc2336c72561b7935cfa73db9ca70b875d8d3e1b3a6f371a/matplotlib-3.10.5-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a277033048ab22d34f88a3c5243938cef776493f6201a8742ed5f8b553201343", size = 9490635, upload-time = "2025-07-31T18:07:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/54/02/e3fdfe0f2e9fb05f3a691d63876639dbf684170fdcf93231e973104153b4/matplotlib-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e4a6470a118a2e93022ecc7d3bd16b3114b2004ea2bf014fff875b3bc99b70c6", size = 9539036, upload-time = "2025-07-31T18:07:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/c1/29/82bf486ff7f4dbedfb11ccc207d0575cbe3be6ea26f75be514252bde3d70/matplotlib-3.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:7e44cada61bec8833c106547786814dd4a266c1b2964fd25daa3804f1b8d4467", size = 8093529, upload-time = "2025-07-31T18:07:49.553Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c7/1f2db90a1d43710478bb1e9b57b162852f79234d28e4f48a28cc415aa583/matplotlib-3.10.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dcfc39c452c6a9f9028d3e44d2d721484f665304857188124b505b2c95e1eecf", size = 8239216, upload-time = "2025-07-31T18:07:51.947Z" }, + { url = "https://files.pythonhosted.org/packages/82/6d/ca6844c77a4f89b1c9e4d481c412e1d1dbabf2aae2cbc5aa2da4a1d6683e/matplotlib-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:903352681b59f3efbf4546985142a9686ea1d616bb054b09a537a06e4b892ccf", size = 8102130, upload-time = "2025-07-31T18:07:53.65Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1e/5e187a30cc673a3e384f3723e5f3c416033c1d8d5da414f82e4e731128ea/matplotlib-3.10.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:080c3676a56b8ee1c762bcf8fca3fe709daa1ee23e6ef06ad9f3fc17332f2d2a", size = 8666471, upload-time = "2025-07-31T18:07:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/03/c0/95540d584d7d645324db99a845ac194e915ef75011a0d5e19e1b5cee7e69/matplotlib-3.10.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b4984d5064a35b6f66d2c11d668565f4389b1119cc64db7a4c1725bc11adffc", size = 9500518, upload-time = "2025-07-31T18:07:57.199Z" }, + { url = "https://files.pythonhosted.org/packages/ba/2e/e019352099ea58b4169adb9c6e1a2ad0c568c6377c2b677ee1f06de2adc7/matplotlib-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3967424121d3a46705c9fa9bdb0931de3228f13f73d7bb03c999c88343a89d89", size = 9552372, upload-time = "2025-07-31T18:07:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/b7/81/3200b792a5e8b354f31f4101ad7834743ad07b6d620259f2059317b25e4d/matplotlib-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:33775bbeb75528555a15ac29396940128ef5613cf9a2d31fb1bfd18b3c0c0903", size = 8100634, upload-time = "2025-07-31T18:08:01.801Z" }, + { url = "https://files.pythonhosted.org/packages/52/46/a944f6f0c1f5476a0adfa501969d229ce5ae60cf9a663be0e70361381f89/matplotlib-3.10.5-cp311-cp311-win_arm64.whl", hash = "sha256:c61333a8e5e6240e73769d5826b9a31d8b22df76c0778f8480baf1b4b01c9420", size = 7978880, upload-time = "2025-07-31T18:08:03.407Z" }, + { url = "https://files.pythonhosted.org/packages/66/1e/c6f6bcd882d589410b475ca1fc22e34e34c82adff519caf18f3e6dd9d682/matplotlib-3.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:00b6feadc28a08bd3c65b2894f56cf3c94fc8f7adcbc6ab4516ae1e8ed8f62e2", size = 8253056, upload-time = "2025-07-31T18:08:05.385Z" }, + { url = "https://files.pythonhosted.org/packages/53/e6/d6f7d1b59413f233793dda14419776f5f443bcccb2dfc84b09f09fe05dbe/matplotlib-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee98a5c5344dc7f48dc261b6ba5d9900c008fc12beb3fa6ebda81273602cc389", size = 8110131, upload-time = "2025-07-31T18:08:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/66/2b/bed8a45e74957549197a2ac2e1259671cd80b55ed9e1fe2b5c94d88a9202/matplotlib-3.10.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a17e57e33de901d221a07af32c08870ed4528db0b6059dce7d7e65c1122d4bea", size = 8669603, upload-time = "2025-07-31T18:08:09.064Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a7/315e9435b10d057f5e52dfc603cd353167ae28bb1a4e033d41540c0067a4/matplotlib-3.10.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97b9d6443419085950ee4a5b1ee08c363e5c43d7176e55513479e53669e88468", size = 9508127, upload-time = "2025-07-31T18:08:10.845Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d9/edcbb1f02ca99165365d2768d517898c22c6040187e2ae2ce7294437c413/matplotlib-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ceefe5d40807d29a66ae916c6a3915d60ef9f028ce1927b84e727be91d884369", size = 9566926, upload-time = "2025-07-31T18:08:13.186Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d9/6dd924ad5616c97b7308e6320cf392c466237a82a2040381163b7500510a/matplotlib-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:c04cba0f93d40e45b3c187c6c52c17f24535b27d545f757a2fffebc06c12b98b", size = 8107599, upload-time = "2025-07-31T18:08:15.116Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f3/522dc319a50f7b0279fbe74f86f7a3506ce414bc23172098e8d2bdf21894/matplotlib-3.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:a41bcb6e2c8e79dc99c5511ae6f7787d2fb52efd3d805fff06d5d4f667db16b2", size = 7978173, upload-time = "2025-07-31T18:08:21.518Z" }, + { url = "https://files.pythonhosted.org/packages/8d/05/4f3c1f396075f108515e45cb8d334aff011a922350e502a7472e24c52d77/matplotlib-3.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:354204db3f7d5caaa10e5de74549ef6a05a4550fdd1c8f831ab9bca81efd39ed", size = 8253586, upload-time = "2025-07-31T18:08:23.107Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2c/e084415775aac7016c3719fe7006cdb462582c6c99ac142f27303c56e243/matplotlib-3.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b072aac0c3ad563a2b3318124756cb6112157017f7431626600ecbe890df57a1", size = 8110715, upload-time = "2025-07-31T18:08:24.675Z" }, + { url = "https://files.pythonhosted.org/packages/52/1b/233e3094b749df16e3e6cd5a44849fd33852e692ad009cf7de00cf58ddf6/matplotlib-3.10.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d52fd5b684d541b5a51fb276b2b97b010c75bee9aa392f96b4a07aeb491e33c7", size = 8669397, upload-time = "2025-07-31T18:08:26.778Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ec/03f9e003a798f907d9f772eed9b7c6a9775d5bd00648b643ebfb88e25414/matplotlib-3.10.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee7a09ae2f4676276f5a65bd9f2bd91b4f9fbaedf49f40267ce3f9b448de501f", size = 9508646, upload-time = "2025-07-31T18:08:28.848Z" }, + { url = "https://files.pythonhosted.org/packages/91/e7/c051a7a386680c28487bca27d23b02d84f63e3d2a9b4d2fc478e6a42e37e/matplotlib-3.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ba6c3c9c067b83481d647af88b4e441d532acdb5ef22178a14935b0b881188f4", size = 9567424, upload-time = "2025-07-31T18:08:30.726Z" }, + { url = "https://files.pythonhosted.org/packages/36/c2/24302e93ff431b8f4173ee1dd88976c8d80483cadbc5d3d777cef47b3a1c/matplotlib-3.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:07442d2692c9bd1cceaa4afb4bbe5b57b98a7599de4dabfcca92d3eea70f9ebe", size = 8107809, upload-time = "2025-07-31T18:08:33.928Z" }, + { url = "https://files.pythonhosted.org/packages/0b/33/423ec6a668d375dad825197557ed8fbdb74d62b432c1ed8235465945475f/matplotlib-3.10.5-cp313-cp313-win_arm64.whl", hash = "sha256:48fe6d47380b68a37ccfcc94f009530e84d41f71f5dae7eda7c4a5a84aa0a674", size = 7978078, upload-time = "2025-07-31T18:08:36.764Z" }, + { url = "https://files.pythonhosted.org/packages/51/17/521fc16ec766455c7bb52cc046550cf7652f6765ca8650ff120aa2d197b6/matplotlib-3.10.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b80eb8621331449fc519541a7461987f10afa4f9cfd91afcd2276ebe19bd56c", size = 8295590, upload-time = "2025-07-31T18:08:38.521Z" }, + { url = "https://files.pythonhosted.org/packages/f8/12/23c28b2c21114c63999bae129fce7fd34515641c517ae48ce7b7dcd33458/matplotlib-3.10.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47a388908e469d6ca2a6015858fa924e0e8a2345a37125948d8e93a91c47933e", size = 8158518, upload-time = "2025-07-31T18:08:40.195Z" }, + { url = "https://files.pythonhosted.org/packages/81/f8/aae4eb25e8e7190759f3cb91cbeaa344128159ac92bb6b409e24f8711f78/matplotlib-3.10.5-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b6b49167d208358983ce26e43aa4196073b4702858670f2eb111f9a10652b4b", size = 8691815, upload-time = "2025-07-31T18:08:42.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ba/450c39ebdd486bd33a359fc17365ade46c6a96bf637bbb0df7824de2886c/matplotlib-3.10.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a8da0453a7fd8e3da114234ba70c5ba9ef0e98f190309ddfde0f089accd46ea", size = 9522814, upload-time = "2025-07-31T18:08:44.914Z" }, + { url = "https://files.pythonhosted.org/packages/89/11/9c66f6a990e27bb9aa023f7988d2d5809cb98aa39c09cbf20fba75a542ef/matplotlib-3.10.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52c6573dfcb7726a9907b482cd5b92e6b5499b284ffacb04ffbfe06b3e568124", size = 9573917, upload-time = "2025-07-31T18:08:47.038Z" }, + { url = "https://files.pythonhosted.org/packages/b3/69/8b49394de92569419e5e05e82e83df9b749a0ff550d07631ea96ed2eb35a/matplotlib-3.10.5-cp313-cp313t-win_amd64.whl", hash = "sha256:a23193db2e9d64ece69cac0c8231849db7dd77ce59c7b89948cf9d0ce655a3ce", size = 8181034, upload-time = "2025-07-31T18:08:48.943Z" }, + { url = "https://files.pythonhosted.org/packages/47/23/82dc435bb98a2fc5c20dffcac8f0b083935ac28286413ed8835df40d0baa/matplotlib-3.10.5-cp313-cp313t-win_arm64.whl", hash = "sha256:56da3b102cf6da2776fef3e71cd96fcf22103a13594a18ac9a9b31314e0be154", size = 8023337, upload-time = "2025-07-31T18:08:50.791Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e0/26b6cfde31f5383503ee45dcb7e691d45dadf0b3f54639332b59316a97f8/matplotlib-3.10.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:96ef8f5a3696f20f55597ffa91c28e2e73088df25c555f8d4754931515512715", size = 8253591, upload-time = "2025-07-31T18:08:53.254Z" }, + { url = "https://files.pythonhosted.org/packages/c1/89/98488c7ef7ea20ea659af7499628c240a608b337af4be2066d644cfd0a0f/matplotlib-3.10.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:77fab633e94b9da60512d4fa0213daeb76d5a7b05156840c4fd0399b4b818837", size = 8112566, upload-time = "2025-07-31T18:08:55.116Z" }, + { url = "https://files.pythonhosted.org/packages/52/67/42294dfedc82aea55e1a767daf3263aacfb5a125f44ba189e685bab41b6f/matplotlib-3.10.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27f52634315e96b1debbfdc5c416592edcd9c4221bc2f520fd39c33db5d9f202", size = 9513281, upload-time = "2025-07-31T18:08:56.885Z" }, + { url = "https://files.pythonhosted.org/packages/e7/68/f258239e0cf34c2cbc816781c7ab6fca768452e6bf1119aedd2bd4a882a3/matplotlib-3.10.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:525f6e28c485c769d1f07935b660c864de41c37fd716bfa64158ea646f7084bb", size = 9780873, upload-time = "2025-07-31T18:08:59.241Z" }, + { url = "https://files.pythonhosted.org/packages/89/64/f4881554006bd12e4558bd66778bdd15d47b00a1f6c6e8b50f6208eda4b3/matplotlib-3.10.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1f5f3ec4c191253c5f2b7c07096a142c6a1c024d9f738247bfc8e3f9643fc975", size = 9568954, upload-time = "2025-07-31T18:09:01.244Z" }, + { url = "https://files.pythonhosted.org/packages/06/f8/42779d39c3f757e1f012f2dda3319a89fb602bd2ef98ce8faf0281f4febd/matplotlib-3.10.5-cp314-cp314-win_amd64.whl", hash = "sha256:707f9c292c4cd4716f19ab8a1f93f26598222cd931e0cd98fbbb1c5994bf7667", size = 8237465, upload-time = "2025-07-31T18:09:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f8/153fd06b5160f0cd27c8b9dd797fcc9fb56ac6a0ebf3c1f765b6b68d3c8a/matplotlib-3.10.5-cp314-cp314-win_arm64.whl", hash = "sha256:21a95b9bf408178d372814de7baacd61c712a62cae560b5e6f35d791776f6516", size = 8108898, upload-time = "2025-07-31T18:09:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/c4b082a382a225fe0d2a73f1f57cf6f6f132308805b493a54c8641006238/matplotlib-3.10.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a6b310f95e1102a8c7c817ef17b60ee5d1851b8c71b63d9286b66b177963039e", size = 8295636, upload-time = "2025-07-31T18:09:07.306Z" }, + { url = "https://files.pythonhosted.org/packages/30/73/2195fa2099718b21a20da82dfc753bf2af58d596b51aefe93e359dd5915a/matplotlib-3.10.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:94986a242747a0605cb3ff1cb98691c736f28a59f8ffe5175acaeb7397c49a5a", size = 8158575, upload-time = "2025-07-31T18:09:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e9/a08cdb34618a91fa08f75e6738541da5cacde7c307cea18ff10f0d03fcff/matplotlib-3.10.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ff10ea43288f0c8bab608a305dc6c918cc729d429c31dcbbecde3b9f4d5b569", size = 9522815, upload-time = "2025-07-31T18:09:11.191Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/34d8b7e0d1bb6d06ef45db01dfa560d5a67b1c40c0b998ce9ccde934bb09/matplotlib-3.10.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f6adb644c9d040ffb0d3434e440490a66cf73dbfa118a6f79cd7568431f7a012", size = 9783514, upload-time = "2025-07-31T18:09:13.307Z" }, + { url = "https://files.pythonhosted.org/packages/12/09/d330d1e55dcca2e11b4d304cc5227f52e2512e46828d6249b88e0694176e/matplotlib-3.10.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4fa40a8f98428f789a9dcacd625f59b7bc4e3ef6c8c7c80187a7a709475cf592", size = 9573932, upload-time = "2025-07-31T18:09:15.335Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3b/f70258ac729aa004aca673800a53a2b0a26d49ca1df2eaa03289a1c40f81/matplotlib-3.10.5-cp314-cp314t-win_amd64.whl", hash = "sha256:95672a5d628b44207aab91ec20bf59c26da99de12b88f7e0b1fb0a84a86ff959", size = 8322003, upload-time = "2025-07-31T18:09:17.416Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/3601f8ce6d76a7c81c7f25a0e15fde0d6b66226dd187aa6d2838e6374161/matplotlib-3.10.5-cp314-cp314t-win_arm64.whl", hash = "sha256:2efaf97d72629e74252e0b5e3c46813e9eeaa94e011ecf8084a971a31a97f40b", size = 8153849, upload-time = "2025-07-31T18:09:19.673Z" }, + { url = "https://files.pythonhosted.org/packages/e4/eb/7d4c5de49eb78294e1a8e2be8a6ecff8b433e921b731412a56cd1abd3567/matplotlib-3.10.5-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b5fa2e941f77eb579005fb804026f9d0a1082276118d01cc6051d0d9626eaa7f", size = 8222360, upload-time = "2025-07-31T18:09:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/16/8a/e435db90927b66b16d69f8f009498775f4469f8de4d14b87856965e58eba/matplotlib-3.10.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1fc0d2a3241cdcb9daaca279204a3351ce9df3c0e7e621c7e04ec28aaacaca30", size = 8087462, upload-time = "2025-07-31T18:09:23.504Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/06c0e00064362f5647f318e00b435be2ff76a1bdced97c5eaf8347311fbe/matplotlib-3.10.5-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8dee65cb1424b7dc982fe87895b5613d4e691cc57117e8af840da0148ca6c1d7", size = 8659802, upload-time = "2025-07-31T18:09:25.256Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d6/e921be4e1a5f7aca5194e1f016cb67ec294548e530013251f630713e456d/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:160e125da27a749481eaddc0627962990f6029811dbeae23881833a011a0907f", size = 8233224, upload-time = "2025-07-31T18:09:27.512Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/a2b9b04824b9c349c8f1b2d21d5af43fa7010039427f2b133a034cb09e59/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac3d50760394d78a3c9be6b28318fe22b494c4fcf6407e8fd4794b538251899b", size = 8098539, upload-time = "2025-07-31T18:09:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/fc/66/cd29ebc7f6c0d2a15d216fb572573e8fc38bd5d6dec3bd9d7d904c0949f7/matplotlib-3.10.5-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c49465bf689c4d59d174d0c7795fb42a21d4244d11d70e52b8011987367ac61", size = 8672192, upload-time = "2025-07-31T18:09:31.407Z" }, ] [[package]] name = "mccabe" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658 } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350 }, + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, ] [[package]] @@ -891,18 +892,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542 } +sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542, upload-time = "2024-09-09T20:27:49.564Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316 }, + { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316, upload-time = "2024-09-09T20:27:48.397Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] @@ -915,48 +916,48 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/a9/3d7aa83955617cdf02f94e50aab5c830d205cfa4320cf124ff64acce3a8e/mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972", size = 11003299 }, - { url = "https://files.pythonhosted.org/packages/83/e8/72e62ff837dd5caaac2b4a5c07ce769c8e808a00a65e5d8f94ea9c6f20ab/mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7", size = 10125451 }, - { url = "https://files.pythonhosted.org/packages/7d/10/f3f3543f6448db11881776f26a0ed079865926b0c841818ee22de2c6bbab/mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df", size = 11916211 }, - { url = "https://files.pythonhosted.org/packages/06/bf/63e83ed551282d67bb3f7fea2cd5561b08d2bb6eb287c096539feb5ddbc5/mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390", size = 12652687 }, - { url = "https://files.pythonhosted.org/packages/69/66/68f2eeef11facf597143e85b694a161868b3b006a5fbad50e09ea117ef24/mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94", size = 12896322 }, - { url = "https://files.pythonhosted.org/packages/a3/87/8e3e9c2c8bd0d7e071a89c71be28ad088aaecbadf0454f46a540bda7bca6/mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b", size = 9507962 }, - { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009 }, - { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482 }, - { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883 }, - { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215 }, - { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956 }, - { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307 }, - { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295 }, - { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355 }, - { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285 }, - { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895 }, - { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025 }, - { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664 }, - { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338 }, - { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066 }, - { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473 }, - { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296 }, - { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657 }, - { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320 }, - { url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037 }, - { url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550 }, - { url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963 }, - { url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189 }, - { url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322 }, - { url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879 }, - { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411 }, +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/a9/3d7aa83955617cdf02f94e50aab5c830d205cfa4320cf124ff64acce3a8e/mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972", size = 11003299, upload-time = "2025-07-31T07:54:06.425Z" }, + { url = "https://files.pythonhosted.org/packages/83/e8/72e62ff837dd5caaac2b4a5c07ce769c8e808a00a65e5d8f94ea9c6f20ab/mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7", size = 10125451, upload-time = "2025-07-31T07:53:52.974Z" }, + { url = "https://files.pythonhosted.org/packages/7d/10/f3f3543f6448db11881776f26a0ed079865926b0c841818ee22de2c6bbab/mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df", size = 11916211, upload-time = "2025-07-31T07:53:18.879Z" }, + { url = "https://files.pythonhosted.org/packages/06/bf/63e83ed551282d67bb3f7fea2cd5561b08d2bb6eb287c096539feb5ddbc5/mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390", size = 12652687, upload-time = "2025-07-31T07:53:30.544Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/68f2eeef11facf597143e85b694a161868b3b006a5fbad50e09ea117ef24/mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94", size = 12896322, upload-time = "2025-07-31T07:53:50.74Z" }, + { url = "https://files.pythonhosted.org/packages/a3/87/8e3e9c2c8bd0d7e071a89c71be28ad088aaecbadf0454f46a540bda7bca6/mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b", size = 9507962, upload-time = "2025-07-31T07:53:08.431Z" }, + { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" }, + { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307, upload-time = "2025-07-31T07:53:59.734Z" }, + { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" }, + { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" }, + { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" }, + { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664, upload-time = "2025-07-31T07:54:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" }, + { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" }, + { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" }, + { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" }, + { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" }, + { url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037, upload-time = "2025-07-31T07:54:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550, upload-time = "2025-07-31T07:53:41.307Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963, upload-time = "2025-07-31T07:53:16.878Z" }, + { url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189, upload-time = "2025-07-31T07:54:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322, upload-time = "2025-07-31T07:53:10.551Z" }, + { url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879, upload-time = "2025-07-31T07:52:56.683Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] @@ -972,18 +973,18 @@ dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985 } +sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579 }, + { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" }, ] [[package]] name = "nodeenv" version = "1.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] [[package]] @@ -994,90 +995,90 @@ dependencies = [ { name = "llvmlite" }, { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/ca/f470be59552ccbf9531d2d383b67ae0b9b524d435fb4a0d229fef135116e/numba-0.61.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:cf9f9fc00d6eca0c23fc840817ce9f439b9f03c8f03d6246c0e7f0cb15b7162a", size = 2775663 }, - { url = "https://files.pythonhosted.org/packages/f5/13/3bdf52609c80d460a3b4acfb9fdb3817e392875c0d6270cf3fd9546f138b/numba-0.61.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ea0247617edcb5dd61f6106a56255baab031acc4257bddaeddb3a1003b4ca3fd", size = 2778344 }, - { url = "https://files.pythonhosted.org/packages/e2/7d/bfb2805bcfbd479f04f835241ecf28519f6e3609912e3a985aed45e21370/numba-0.61.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae8c7a522c26215d5f62ebec436e3d341f7f590079245a2f1008dfd498cc1642", size = 3824054 }, - { url = "https://files.pythonhosted.org/packages/e3/27/797b2004745c92955470c73c82f0e300cf033c791f45bdecb4b33b12bdea/numba-0.61.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd1e74609855aa43661edffca37346e4e8462f6903889917e9f41db40907daa2", size = 3518531 }, - { url = "https://files.pythonhosted.org/packages/b1/c6/c2fb11e50482cb310afae87a997707f6c7d8a48967b9696271347441f650/numba-0.61.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae45830b129c6137294093b269ef0a22998ccc27bf7cf096ab8dcf7bca8946f9", size = 2831612 }, - { url = "https://files.pythonhosted.org/packages/3f/97/c99d1056aed767503c228f7099dc11c402906b42a4757fec2819329abb98/numba-0.61.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:efd3db391df53aaa5cfbee189b6c910a5b471488749fd6606c3f33fc984c2ae2", size = 2775825 }, - { url = "https://files.pythonhosted.org/packages/95/9e/63c549f37136e892f006260c3e2613d09d5120672378191f2dc387ba65a2/numba-0.61.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49c980e4171948ffebf6b9a2520ea81feed113c1f4890747ba7f59e74be84b1b", size = 2778695 }, - { url = "https://files.pythonhosted.org/packages/97/c8/8740616c8436c86c1b9a62e72cb891177d2c34c2d24ddcde4c390371bf4c/numba-0.61.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3945615cd73c2c7eba2a85ccc9c1730c21cd3958bfcf5a44302abae0fb07bb60", size = 3829227 }, - { url = "https://files.pythonhosted.org/packages/fc/06/66e99ae06507c31d15ff3ecd1f108f2f59e18b6e08662cd5f8a5853fbd18/numba-0.61.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbfdf4eca202cebade0b7d43896978e146f39398909a42941c9303f82f403a18", size = 3523422 }, - { url = "https://files.pythonhosted.org/packages/0f/a4/2b309a6a9f6d4d8cfba583401c7c2f9ff887adb5d54d8e2e130274c0973f/numba-0.61.2-cp311-cp311-win_amd64.whl", hash = "sha256:76bcec9f46259cedf888041b9886e257ae101c6268261b19fda8cfbc52bec9d1", size = 2831505 }, - { url = "https://files.pythonhosted.org/packages/b4/a0/c6b7b9c615cfa3b98c4c63f4316e3f6b3bbe2387740277006551784218cd/numba-0.61.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:34fba9406078bac7ab052efbf0d13939426c753ad72946baaa5bf9ae0ebb8dd2", size = 2776626 }, - { url = "https://files.pythonhosted.org/packages/92/4a/fe4e3c2ecad72d88f5f8cd04e7f7cff49e718398a2fac02d2947480a00ca/numba-0.61.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ddce10009bc097b080fc96876d14c051cc0c7679e99de3e0af59014dab7dfe8", size = 2779287 }, - { url = "https://files.pythonhosted.org/packages/9a/2d/e518df036feab381c23a624dac47f8445ac55686ec7f11083655eb707da3/numba-0.61.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b1bb509d01f23d70325d3a5a0e237cbc9544dd50e50588bc581ba860c213546", size = 3885928 }, - { url = "https://files.pythonhosted.org/packages/10/0f/23cced68ead67b75d77cfcca3df4991d1855c897ee0ff3fe25a56ed82108/numba-0.61.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48a53a3de8f8793526cbe330f2a39fe9a6638efcbf11bd63f3d2f9757ae345cd", size = 3577115 }, - { url = "https://files.pythonhosted.org/packages/68/1d/ddb3e704c5a8fb90142bf9dc195c27db02a08a99f037395503bfbc1d14b3/numba-0.61.2-cp312-cp312-win_amd64.whl", hash = "sha256:97cf4f12c728cf77c9c1d7c23707e4d8fb4632b46275f8f3397de33e5877af18", size = 2831929 }, - { url = "https://files.pythonhosted.org/packages/0b/f3/0fe4c1b1f2569e8a18ad90c159298d862f96c3964392a20d74fc628aee44/numba-0.61.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:3a10a8fc9afac40b1eac55717cece1b8b1ac0b946f5065c89e00bde646b5b154", size = 2771785 }, - { url = "https://files.pythonhosted.org/packages/e9/71/91b277d712e46bd5059f8a5866862ed1116091a7cb03bd2704ba8ebe015f/numba-0.61.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d3bcada3c9afba3bed413fba45845f2fb9cd0d2b27dd58a1be90257e293d140", size = 2773289 }, - { url = "https://files.pythonhosted.org/packages/0d/e0/5ea04e7ad2c39288c0f0f9e8d47638ad70f28e275d092733b5817cf243c9/numba-0.61.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdbca73ad81fa196bd53dc12e3aaf1564ae036e0c125f237c7644fe64a4928ab", size = 3893918 }, - { url = "https://files.pythonhosted.org/packages/17/58/064f4dcb7d7e9412f16ecf80ed753f92297e39f399c905389688cf950b81/numba-0.61.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f154aaea625fb32cfbe3b80c5456d514d416fcdf79733dd69c0df3a11348e9e", size = 3584056 }, - { url = "https://files.pythonhosted.org/packages/af/a4/6d3a0f2d3989e62a18749e1e9913d5fa4910bbb3e3311a035baea6caf26d/numba-0.61.2-cp313-cp313-win_amd64.whl", hash = "sha256:59321215e2e0ac5fa928a8020ab00b8e57cda8a97384963ac0dfa4d4e6aa54e7", size = 2831846 }, +sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615, upload-time = "2025-04-09T02:58:07.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/ca/f470be59552ccbf9531d2d383b67ae0b9b524d435fb4a0d229fef135116e/numba-0.61.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:cf9f9fc00d6eca0c23fc840817ce9f439b9f03c8f03d6246c0e7f0cb15b7162a", size = 2775663, upload-time = "2025-04-09T02:57:34.143Z" }, + { url = "https://files.pythonhosted.org/packages/f5/13/3bdf52609c80d460a3b4acfb9fdb3817e392875c0d6270cf3fd9546f138b/numba-0.61.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ea0247617edcb5dd61f6106a56255baab031acc4257bddaeddb3a1003b4ca3fd", size = 2778344, upload-time = "2025-04-09T02:57:36.609Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/bfb2805bcfbd479f04f835241ecf28519f6e3609912e3a985aed45e21370/numba-0.61.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae8c7a522c26215d5f62ebec436e3d341f7f590079245a2f1008dfd498cc1642", size = 3824054, upload-time = "2025-04-09T02:57:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/e3/27/797b2004745c92955470c73c82f0e300cf033c791f45bdecb4b33b12bdea/numba-0.61.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd1e74609855aa43661edffca37346e4e8462f6903889917e9f41db40907daa2", size = 3518531, upload-time = "2025-04-09T02:57:39.709Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c6/c2fb11e50482cb310afae87a997707f6c7d8a48967b9696271347441f650/numba-0.61.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae45830b129c6137294093b269ef0a22998ccc27bf7cf096ab8dcf7bca8946f9", size = 2831612, upload-time = "2025-04-09T02:57:41.559Z" }, + { url = "https://files.pythonhosted.org/packages/3f/97/c99d1056aed767503c228f7099dc11c402906b42a4757fec2819329abb98/numba-0.61.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:efd3db391df53aaa5cfbee189b6c910a5b471488749fd6606c3f33fc984c2ae2", size = 2775825, upload-time = "2025-04-09T02:57:43.442Z" }, + { url = "https://files.pythonhosted.org/packages/95/9e/63c549f37136e892f006260c3e2613d09d5120672378191f2dc387ba65a2/numba-0.61.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49c980e4171948ffebf6b9a2520ea81feed113c1f4890747ba7f59e74be84b1b", size = 2778695, upload-time = "2025-04-09T02:57:44.968Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/8740616c8436c86c1b9a62e72cb891177d2c34c2d24ddcde4c390371bf4c/numba-0.61.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3945615cd73c2c7eba2a85ccc9c1730c21cd3958bfcf5a44302abae0fb07bb60", size = 3829227, upload-time = "2025-04-09T02:57:46.63Z" }, + { url = "https://files.pythonhosted.org/packages/fc/06/66e99ae06507c31d15ff3ecd1f108f2f59e18b6e08662cd5f8a5853fbd18/numba-0.61.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbfdf4eca202cebade0b7d43896978e146f39398909a42941c9303f82f403a18", size = 3523422, upload-time = "2025-04-09T02:57:48.222Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a4/2b309a6a9f6d4d8cfba583401c7c2f9ff887adb5d54d8e2e130274c0973f/numba-0.61.2-cp311-cp311-win_amd64.whl", hash = "sha256:76bcec9f46259cedf888041b9886e257ae101c6268261b19fda8cfbc52bec9d1", size = 2831505, upload-time = "2025-04-09T02:57:50.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a0/c6b7b9c615cfa3b98c4c63f4316e3f6b3bbe2387740277006551784218cd/numba-0.61.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:34fba9406078bac7ab052efbf0d13939426c753ad72946baaa5bf9ae0ebb8dd2", size = 2776626, upload-time = "2025-04-09T02:57:51.857Z" }, + { url = "https://files.pythonhosted.org/packages/92/4a/fe4e3c2ecad72d88f5f8cd04e7f7cff49e718398a2fac02d2947480a00ca/numba-0.61.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ddce10009bc097b080fc96876d14c051cc0c7679e99de3e0af59014dab7dfe8", size = 2779287, upload-time = "2025-04-09T02:57:53.658Z" }, + { url = "https://files.pythonhosted.org/packages/9a/2d/e518df036feab381c23a624dac47f8445ac55686ec7f11083655eb707da3/numba-0.61.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b1bb509d01f23d70325d3a5a0e237cbc9544dd50e50588bc581ba860c213546", size = 3885928, upload-time = "2025-04-09T02:57:55.206Z" }, + { url = "https://files.pythonhosted.org/packages/10/0f/23cced68ead67b75d77cfcca3df4991d1855c897ee0ff3fe25a56ed82108/numba-0.61.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48a53a3de8f8793526cbe330f2a39fe9a6638efcbf11bd63f3d2f9757ae345cd", size = 3577115, upload-time = "2025-04-09T02:57:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/68/1d/ddb3e704c5a8fb90142bf9dc195c27db02a08a99f037395503bfbc1d14b3/numba-0.61.2-cp312-cp312-win_amd64.whl", hash = "sha256:97cf4f12c728cf77c9c1d7c23707e4d8fb4632b46275f8f3397de33e5877af18", size = 2831929, upload-time = "2025-04-09T02:57:58.45Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f3/0fe4c1b1f2569e8a18ad90c159298d862f96c3964392a20d74fc628aee44/numba-0.61.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:3a10a8fc9afac40b1eac55717cece1b8b1ac0b946f5065c89e00bde646b5b154", size = 2771785, upload-time = "2025-04-09T02:57:59.96Z" }, + { url = "https://files.pythonhosted.org/packages/e9/71/91b277d712e46bd5059f8a5866862ed1116091a7cb03bd2704ba8ebe015f/numba-0.61.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d3bcada3c9afba3bed413fba45845f2fb9cd0d2b27dd58a1be90257e293d140", size = 2773289, upload-time = "2025-04-09T02:58:01.435Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e0/5ea04e7ad2c39288c0f0f9e8d47638ad70f28e275d092733b5817cf243c9/numba-0.61.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdbca73ad81fa196bd53dc12e3aaf1564ae036e0c125f237c7644fe64a4928ab", size = 3893918, upload-time = "2025-04-09T02:58:02.933Z" }, + { url = "https://files.pythonhosted.org/packages/17/58/064f4dcb7d7e9412f16ecf80ed753f92297e39f399c905389688cf950b81/numba-0.61.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f154aaea625fb32cfbe3b80c5456d514d416fcdf79733dd69c0df3a11348e9e", size = 3584056, upload-time = "2025-04-09T02:58:04.538Z" }, + { url = "https://files.pythonhosted.org/packages/af/a4/6d3a0f2d3989e62a18749e1e9913d5fa4910bbb3e3311a035baea6caf26d/numba-0.61.2-cp313-cp313-win_amd64.whl", hash = "sha256:59321215e2e0ac5fa928a8020ab00b8e57cda8a97384963ac0dfa4d4e6aa54e7", size = 2831846, upload-time = "2025-04-09T02:58:06.125Z" }, ] [[package]] name = "numpy" version = "2.2.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440 } -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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149 }, - { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866 }, - { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506 }, - { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532 }, - { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225 }, - { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666 }, +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]] @@ -1089,147 +1090,147 @@ dependencies = [ { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/19/7721093e25804cc82c7c1cdab0cce6b9343451828fc2ce249cee10646db5/numpydoc-1.9.0.tar.gz", hash = "sha256:5fec64908fe041acc4b3afc2a32c49aab1540cf581876f5563d68bb129e27c5b", size = 91451 } +sdist = { url = "https://files.pythonhosted.org/packages/2f/19/7721093e25804cc82c7c1cdab0cce6b9343451828fc2ce249cee10646db5/numpydoc-1.9.0.tar.gz", hash = "sha256:5fec64908fe041acc4b3afc2a32c49aab1540cf581876f5563d68bb129e27c5b", size = 91451, upload-time = "2025-06-24T12:22:55.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/62/5783d8924fca72529defb2c7dbe2070d49224d2dba03a85b20b37adb24d8/numpydoc-1.9.0-py3-none-any.whl", hash = "sha256:8a2983b2d62bfd0a8c470c7caa25e7e0c3d163875cdec12a8a1034020a9d1135", size = 64871 }, + { url = "https://files.pythonhosted.org/packages/26/62/5783d8924fca72529defb2c7dbe2070d49224d2dba03a85b20b37adb24d8/numpydoc-1.9.0-py3-none-any.whl", hash = "sha256:8a2983b2d62bfd0a8c470c7caa25e7e0c3d163875cdec12a8a1034020a9d1135", size = 64871, upload-time = "2025-06-24T12:22:53.701Z" }, ] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] name = "pathspec" version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] [[package]] name = "pillow" version = "11.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554 }, - { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548 }, - { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742 }, - { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087 }, - { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350 }, - { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840 }, - { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005 }, - { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372 }, - { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090 }, - { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988 }, - { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899 }, - { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531 }, - { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560 }, - { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978 }, - { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168 }, - { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053 }, - { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273 }, - { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043 }, - { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516 }, - { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768 }, - { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055 }, - { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079 }, - { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800 }, - { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296 }, - { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726 }, - { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652 }, - { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787 }, - { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236 }, - { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950 }, - { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358 }, - { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079 }, - { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324 }, - { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067 }, - { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328 }, - { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652 }, - { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443 }, - { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474 }, - { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038 }, - { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407 }, - { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094 }, - { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503 }, - { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574 }, - { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060 }, - { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407 }, - { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841 }, - { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450 }, - { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055 }, - { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110 }, - { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547 }, - { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554 }, - { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132 }, - { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001 }, - { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814 }, - { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124 }, - { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186 }, - { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546 }, - { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102 }, - { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803 }, - { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520 }, - { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116 }, - { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597 }, - { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246 }, - { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336 }, - { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699 }, - { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789 }, - { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386 }, - { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911 }, - { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383 }, - { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385 }, - { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129 }, - { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580 }, - { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860 }, - { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694 }, - { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888 }, - { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330 }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089 }, - { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206 }, - { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370 }, - { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500 }, - { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835 }, - { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556 }, - { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625 }, - { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207 }, - { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939 }, - { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166 }, - { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482 }, - { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596 }, - { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566 }, - { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618 }, - { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248 }, - { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963 }, - { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170 }, - { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505 }, - { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598 }, +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" }, + { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" }, + { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" }, + { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, + { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, + { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, + { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, + { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, + { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, + { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, + { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, + { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" }, + { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" }, + { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" }, + { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, ] [[package]] name = "platformdirs" version = "4.3.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, ] [[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 } +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 }, + { 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]] @@ -1243,18 +1244,18 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424 } +sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707 }, + { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, ] [[package]] name = "pycodestyle" version = "2.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472 } +sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594 }, + { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, ] [[package]] @@ -1272,26 +1273,24 @@ dependencies = [ { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/ea/3ab478cccacc2e8ef69892c42c44ae547bae089f356c4b47caf61730958d/pydata_sphinx_theme-0.15.4.tar.gz", hash = "sha256:7762ec0ac59df3acecf49fd2f889e1b4565dbce8b88b2e29ee06fdd90645a06d", size = 2400673 } +sdist = { url = "https://files.pythonhosted.org/packages/67/ea/3ab478cccacc2e8ef69892c42c44ae547bae089f356c4b47caf61730958d/pydata_sphinx_theme-0.15.4.tar.gz", hash = "sha256:7762ec0ac59df3acecf49fd2f889e1b4565dbce8b88b2e29ee06fdd90645a06d", size = 2400673, upload-time = "2024-06-25T19:28:45.041Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/d3/c622950d87a2ffd1654208733b5bd1c5645930014abed8f4c0d74863988b/pydata_sphinx_theme-0.15.4-py3-none-any.whl", hash = "sha256:2136ad0e9500d0949f96167e63f3e298620040aea8f9c74621959eda5d4cf8e6", size = 4640157 }, + { url = "https://files.pythonhosted.org/packages/e7/d3/c622950d87a2ffd1654208733b5bd1c5645930014abed8f4c0d74863988b/pydata_sphinx_theme-0.15.4-py3-none-any.whl", hash = "sha256:2136ad0e9500d0949f96167e63f3e298620040aea8f9c74621959eda5d4cf8e6", size = 4640157, upload-time = "2024-06-25T19:28:42.383Z" }, ] [[package]] name = "pyelastica" -version = "0.3.3.post1" +version = "1.0.0" source = { editable = "." } dependencies = [ { name = "cma" }, - { name = "flake8" }, { name = "matplotlib" }, - { name = "mypy" }, - { name = "mypy-extensions" }, { name = "numba" }, { name = "numpy" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "tqdm" }, + { name = "typing-extensions" }, ] [package.optional-dependencies] @@ -1301,6 +1300,9 @@ dev = [ { name = "click" }, { name = "codecov" }, { name = "coverage" }, + { name = "flake8" }, + { name = "mypy" }, + { name = "mypy-extensions" }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-cov" }, @@ -1318,7 +1320,9 @@ docs = [ { name = "sphinx-autodoc-typehints", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx-autodoc-typehints", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx-book-theme" }, + { name = "sphinx-gallery" }, { name = "sphinxcontrib-mermaid" }, + { name = "sphinxcontrib-video" }, ] [package.metadata] @@ -1330,10 +1334,10 @@ requires-dist = [ { name = "codecov", marker = "extra == 'dev'" }, { name = "coverage", marker = "extra == 'dev'" }, { name = "docutils", marker = "extra == 'docs'", specifier = ">=0.18" }, - { name = "flake8" }, + { name = "flake8", marker = "extra == 'dev'" }, { name = "matplotlib" }, - { name = "mypy" }, - { name = "mypy-extensions" }, + { name = "mypy", marker = "extra == 'dev'" }, + { name = "mypy-extensions", marker = "extra == 'dev'" }, { name = "myst-parser", marker = "extra == 'docs'", specifier = ">=1.0" }, { name = "numba" }, { name = "numpy" }, @@ -1349,35 +1353,39 @@ requires-dist = [ { name = "sphinx", marker = "extra == 'docs'", specifier = ">=6.1" }, { name = "sphinx-autodoc-typehints", marker = "extra == 'docs'", specifier = ">=1.21" }, { name = "sphinx-book-theme", marker = "extra == 'docs'", specifier = ">=1.0" }, + { name = "sphinx-gallery", marker = "extra == 'docs'", specifier = ">=0.19.0" }, { name = "sphinxcontrib-mermaid", marker = "extra == 'docs'", specifier = ">=0.9.2" }, + { name = "sphinxcontrib-video", marker = "extra == 'docs'", specifier = ">=0.4.1" }, { name = "tqdm" }, + { name = "typing-extensions" }, ] +provides-extras = ["docs", "dev"] [[package]] name = "pyflakes" version = "3.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669 } +sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551 }, + { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, ] [[package]] name = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pyparsing" version = "3.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 }, + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" }, ] [[package]] @@ -1393,9 +1401,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714 } +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474 }, + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, ] [[package]] @@ -1407,9 +1415,9 @@ dependencies = [ { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432 } +sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644 }, + { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, ] [[package]] @@ -1421,9 +1429,9 @@ dependencies = [ { name = "pytest" }, { name = "pytest-metadata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/ab/4862dcb5a8a514bd87747e06b8d55483c0c9e987e1b66972336946e49b49/pytest_html-4.1.1.tar.gz", hash = "sha256:70a01e8ae5800f4a074b56a4cb1025c8f4f9b038bba5fe31e3c98eb996686f07", size = 150773 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/ab/4862dcb5a8a514bd87747e06b8d55483c0c9e987e1b66972336946e49b49/pytest_html-4.1.1.tar.gz", hash = "sha256:70a01e8ae5800f4a074b56a4cb1025c8f4f9b038bba5fe31e3c98eb996686f07", size = 150773, upload-time = "2023-11-07T15:44:28.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/c7/c160021cbecd956cc1a6f79e5fe155f7868b2e5b848f1320dad0b3e3122f/pytest_html-4.1.1-py3-none-any.whl", hash = "sha256:c8152cea03bd4e9bee6d525573b67bbc6622967b72b9628dda0ea3e2a0b5dd71", size = 23491 }, + { url = "https://files.pythonhosted.org/packages/c8/c7/c160021cbecd956cc1a6f79e5fe155f7868b2e5b848f1320dad0b3e3122f/pytest_html-4.1.1-py3-none-any.whl", hash = "sha256:c8152cea03bd4e9bee6d525573b67bbc6622967b72b9628dda0ea3e2a0b5dd71", size = 23491, upload-time = "2023-11-07T15:44:27.149Z" }, ] [[package]] @@ -1433,9 +1441,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/85/8c969f8bec4e559f8f2b958a15229a35495f5b4ce499f6b865eac54b878d/pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8", size = 9952 } +sdist = { url = "https://files.pythonhosted.org/packages/a6/85/8c969f8bec4e559f8f2b958a15229a35495f5b4ce499f6b865eac54b878d/pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8", size = 9952, upload-time = "2024-02-12T19:38:44.887Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/43/7e7b2ec865caa92f67b8f0e9231a798d102724ca4c0e1f414316be1c1ef2/pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b", size = 11428 }, + { url = "https://files.pythonhosted.org/packages/3e/43/7e7b2ec865caa92f67b8f0e9231a798d102724ca4c0e1f414316be1c1ef2/pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b", size = 11428, upload-time = "2024-02-12T19:38:42.531Z" }, ] [[package]] @@ -1445,9 +1453,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241 } +sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923 }, + { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, ] [[package]] @@ -1458,9 +1466,9 @@ dependencies = [ { name = "numpy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/b7/11c928ffabbb79d86cc120b9b67b74c2ac0d132b3ea19f14024f7f0a0e4c/pytest-rng-1.0.0.tar.gz", hash = "sha256:9d9ee96557246756072133ff9b990588f28f12d3e80357cad959ef0b05aed9fa", size = 14789 } +sdist = { url = "https://files.pythonhosted.org/packages/e1/b7/11c928ffabbb79d86cc120b9b67b74c2ac0d132b3ea19f14024f7f0a0e4c/pytest-rng-1.0.0.tar.gz", hash = "sha256:9d9ee96557246756072133ff9b990588f28f12d3e80357cad959ef0b05aed9fa", size = 14789, upload-time = "2019-08-08T19:25:57.681Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/66/40c88e87c8731d96bb2f9a49329785d24236debb9685f6aaccc52536e697/pytest_rng-1.0.0-py3-none-any.whl", hash = "sha256:346e76a34f19c1f70e1059567460df9edf34aa6b41441c8707bf9ed40446b9c7", size = 7992 }, + { url = "https://files.pythonhosted.org/packages/ac/66/40c88e87c8731d96bb2f9a49329785d24236debb9685f6aaccc52536e697/pytest_rng-1.0.0-py3-none-any.whl", hash = "sha256:346e76a34f19c1f70e1059567460df9edf34aa6b41441c8707bf9ed40446b9c7", size = 7992, upload-time = "2019-08-08T19:25:55.363Z" }, ] [[package]] @@ -1470,62 +1478,62 @@ 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 } +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 }, + { 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.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] [[package]] name = "readthedocs-sphinx-search" version = "0.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8f/96/0c51439e3dbc634cf5328ffb173ff759b7fc9abf3276e78bf71d9fc0aa51/readthedocs-sphinx-search-0.3.2.tar.gz", hash = "sha256:277773bfa28566a86694c08e568d5a648cd80f22826545555a764d6d20c365fb", size = 21949 } +sdist = { url = "https://files.pythonhosted.org/packages/8f/96/0c51439e3dbc634cf5328ffb173ff759b7fc9abf3276e78bf71d9fc0aa51/readthedocs-sphinx-search-0.3.2.tar.gz", hash = "sha256:277773bfa28566a86694c08e568d5a648cd80f22826545555a764d6d20c365fb", size = 21949, upload-time = "2024-01-15T16:46:22.565Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/3c/41bc9d7d4d936a73e380423f23996bee1691e17598d8a03c062be6aac640/readthedocs_sphinx_search-0.3.2-py3-none-any.whl", hash = "sha256:58716fd21f01581e6e67bf3bc02e79c77e10dc58b5f8e4c7cc1977e013eda173", size = 21379 }, + { url = "https://files.pythonhosted.org/packages/04/3c/41bc9d7d4d936a73e380423f23996bee1691e17598d8a03c062be6aac640/readthedocs_sphinx_search-0.3.2-py3-none-any.whl", hash = "sha256:58716fd21f01581e6e67bf3bc02e79c77e10dc58b5f8e4c7cc1977e013eda173", size = 21379, upload-time = "2024-01-15T16:46:20.552Z" }, ] [[package]] @@ -1538,18 +1546,18 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258 } +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 }, + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] [[package]] name = "roman-numerals-py" version = "3.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017 } +sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017, upload-time = "2025-02-22T07:34:54.333Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742 }, + { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742, upload-time = "2025-02-22T07:34:52.422Z" }, ] [[package]] @@ -1562,53 +1570,53 @@ resolution-markers = [ dependencies = [ { name = "numpy", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770 }, - { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511 }, - { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151 }, - { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732 }, - { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617 }, - { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964 }, - { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749 }, - { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383 }, - { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201 }, - { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255 }, - { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035 }, - { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499 }, - { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602 }, - { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415 }, - { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622 }, - { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796 }, - { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684 }, - { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504 }, - { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735 }, - { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284 }, - { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958 }, - { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454 }, - { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199 }, - { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455 }, - { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140 }, - { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549 }, - { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184 }, - { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256 }, - { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540 }, - { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115 }, - { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884 }, - { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018 }, - { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716 }, - { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342 }, - { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869 }, - { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851 }, - { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011 }, - { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407 }, - { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030 }, - { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709 }, - { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045 }, - { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062 }, - { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132 }, - { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503 }, - { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097 }, +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, ] [[package]] @@ -1621,89 +1629,89 @@ resolution-markers = [ dependencies = [ { name = "numpy", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/4a/b927028464795439faec8eaf0b03b011005c487bb2d07409f28bf30879c4/scipy-1.16.1.tar.gz", hash = "sha256:44c76f9e8b6e8e488a586190ab38016e4ed2f8a038af7cd3defa903c0a2238b3", size = 30580861 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/91/812adc6f74409b461e3a5fa97f4f74c769016919203138a3bf6fc24ba4c5/scipy-1.16.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:c033fa32bab91dc98ca59d0cf23bb876454e2bb02cbe592d5023138778f70030", size = 36552519 }, - { url = "https://files.pythonhosted.org/packages/47/18/8e355edcf3b71418d9e9f9acd2708cc3a6c27e8f98fde0ac34b8a0b45407/scipy-1.16.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6e5c2f74e5df33479b5cd4e97a9104c511518fbd979aa9b8f6aec18b2e9ecae7", size = 28638010 }, - { url = "https://files.pythonhosted.org/packages/d9/eb/e931853058607bdfbc11b86df19ae7a08686121c203483f62f1ecae5989c/scipy-1.16.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0a55ffe0ba0f59666e90951971a884d1ff6f4ec3275a48f472cfb64175570f77", size = 20909790 }, - { url = "https://files.pythonhosted.org/packages/45/0c/be83a271d6e96750cd0be2e000f35ff18880a46f05ce8b5d3465dc0f7a2a/scipy-1.16.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f8a5d6cd147acecc2603fbd382fed6c46f474cccfcf69ea32582e033fb54dcfe", size = 23513352 }, - { url = "https://files.pythonhosted.org/packages/7c/bf/fe6eb47e74f762f933cca962db7f2c7183acfdc4483bd1c3813cfe83e538/scipy-1.16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb18899127278058bcc09e7b9966d41a5a43740b5bb8dcba401bd983f82e885b", size = 33534643 }, - { url = "https://files.pythonhosted.org/packages/bb/ba/63f402e74875486b87ec6506a4f93f6d8a0d94d10467280f3d9d7837ce3a/scipy-1.16.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adccd93a2fa937a27aae826d33e3bfa5edf9aa672376a4852d23a7cd67a2e5b7", size = 35376776 }, - { url = "https://files.pythonhosted.org/packages/c3/b4/04eb9d39ec26a1b939689102da23d505ea16cdae3dbb18ffc53d1f831044/scipy-1.16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:18aca1646a29ee9a0625a1be5637fa798d4d81fdf426481f06d69af828f16958", size = 35698906 }, - { url = "https://files.pythonhosted.org/packages/04/d6/bb5468da53321baeb001f6e4e0d9049eadd175a4a497709939128556e3ec/scipy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d85495cef541729a70cdddbbf3e6b903421bc1af3e8e3a9a72a06751f33b7c39", size = 38129275 }, - { url = "https://files.pythonhosted.org/packages/c4/94/994369978509f227cba7dfb9e623254d0d5559506fe994aef4bea3ed469c/scipy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:226652fca853008119c03a8ce71ffe1b3f6d2844cc1686e8f9806edafae68596", size = 38644572 }, - { url = "https://files.pythonhosted.org/packages/f8/d9/ec4864f5896232133f51382b54a08de91a9d1af7a76dfa372894026dfee2/scipy-1.16.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81b433bbeaf35728dad619afc002db9b189e45eebe2cd676effe1fb93fef2b9c", size = 36575194 }, - { url = "https://files.pythonhosted.org/packages/5c/6d/40e81ecfb688e9d25d34a847dca361982a6addf8e31f0957b1a54fbfa994/scipy-1.16.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:886cc81fdb4c6903a3bb0464047c25a6d1016fef77bb97949817d0c0d79f9e04", size = 28594590 }, - { url = "https://files.pythonhosted.org/packages/0e/37/9f65178edfcc629377ce9a64fc09baebea18c80a9e57ae09a52edf84880b/scipy-1.16.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:15240c3aac087a522b4eaedb09f0ad061753c5eebf1ea430859e5bf8640d5919", size = 20866458 }, - { url = "https://files.pythonhosted.org/packages/2c/7b/749a66766871ea4cb1d1ea10f27004db63023074c22abed51f22f09770e0/scipy-1.16.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f81a25805f3659b48126b5053d9e823d3215e4a63730b5e1671852a1705921", size = 23539318 }, - { url = "https://files.pythonhosted.org/packages/c4/db/8d4afec60eb833a666434d4541a3151eedbf2494ea6d4d468cbe877f00cd/scipy-1.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6c62eea7f607f122069b9bad3f99489ddca1a5173bef8a0c75555d7488b6f725", size = 33292899 }, - { url = "https://files.pythonhosted.org/packages/51/1e/79023ca3bbb13a015d7d2757ecca3b81293c663694c35d6541b4dca53e98/scipy-1.16.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f965bbf3235b01c776115ab18f092a95aa74c271a52577bcb0563e85738fd618", size = 35162637 }, - { url = "https://files.pythonhosted.org/packages/b6/49/0648665f9c29fdaca4c679182eb972935b3b4f5ace41d323c32352f29816/scipy-1.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f006e323874ffd0b0b816d8c6a8e7f9a73d55ab3b8c3f72b752b226d0e3ac83d", size = 35490507 }, - { url = "https://files.pythonhosted.org/packages/62/8f/66cbb9d6bbb18d8c658f774904f42a92078707a7c71e5347e8bf2f52bb89/scipy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8fd15fc5085ab4cca74cb91fe0a4263b1f32e4420761ddae531ad60934c2119", size = 37923998 }, - { url = "https://files.pythonhosted.org/packages/14/c3/61f273ae550fbf1667675701112e380881905e28448c080b23b5a181df7c/scipy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7b8013c6c066609577d910d1a2a077021727af07b6fab0ee22c2f901f22352a", size = 38508060 }, - { url = "https://files.pythonhosted.org/packages/93/0b/b5c99382b839854a71ca9482c684e3472badc62620287cbbdab499b75ce6/scipy-1.16.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5451606823a5e73dfa621a89948096c6528e2896e40b39248295d3a0138d594f", size = 36533717 }, - { url = "https://files.pythonhosted.org/packages/eb/e5/69ab2771062c91e23e07c12e7d5033a6b9b80b0903ee709c3c36b3eb520c/scipy-1.16.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:89728678c5ca5abd610aee148c199ac1afb16e19844401ca97d43dc548a354eb", size = 28570009 }, - { url = "https://files.pythonhosted.org/packages/f4/69/bd75dbfdd3cf524f4d753484d723594aed62cfaac510123e91a6686d520b/scipy-1.16.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e756d688cb03fd07de0fffad475649b03cb89bee696c98ce508b17c11a03f95c", size = 20841942 }, - { url = "https://files.pythonhosted.org/packages/ea/74/add181c87663f178ba7d6144b370243a87af8476664d5435e57d599e6874/scipy-1.16.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5aa2687b9935da3ed89c5dbed5234576589dd28d0bf7cd237501ccfbdf1ad608", size = 23498507 }, - { url = "https://files.pythonhosted.org/packages/1d/74/ece2e582a0d9550cee33e2e416cc96737dce423a994d12bbe59716f47ff1/scipy-1.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0851f6a1e537fe9399f35986897e395a1aa61c574b178c0d456be5b1a0f5ca1f", size = 33286040 }, - { url = "https://files.pythonhosted.org/packages/e4/82/08e4076df538fb56caa1d489588d880ec7c52d8273a606bb54d660528f7c/scipy-1.16.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fedc2cbd1baed37474b1924c331b97bdff611d762c196fac1a9b71e67b813b1b", size = 35176096 }, - { url = "https://files.pythonhosted.org/packages/fa/79/cd710aab8c921375711a8321c6be696e705a120e3011a643efbbcdeeabcc/scipy-1.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2ef500e72f9623a6735769e4b93e9dcb158d40752cdbb077f305487e3e2d1f45", size = 35490328 }, - { url = "https://files.pythonhosted.org/packages/71/73/e9cc3d35ee4526d784520d4494a3e1ca969b071fb5ae5910c036a375ceec/scipy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:978d8311674b05a8f7ff2ea6c6bce5d8b45a0cb09d4c5793e0318f448613ea65", size = 37939921 }, - { url = "https://files.pythonhosted.org/packages/21/12/c0efd2941f01940119b5305c375ae5c0fcb7ec193f806bd8f158b73a1782/scipy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:81929ed0fa7a5713fcdd8b2e6f73697d3b4c4816d090dd34ff937c20fa90e8ab", size = 38479462 }, - { url = "https://files.pythonhosted.org/packages/7a/19/c3d08b675260046a991040e1ea5d65f91f40c7df1045fffff412dcfc6765/scipy-1.16.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:bcc12db731858abda693cecdb3bdc9e6d4bd200213f49d224fe22df82687bdd6", size = 36938832 }, - { url = "https://files.pythonhosted.org/packages/81/f2/ce53db652c033a414a5b34598dba6b95f3d38153a2417c5a3883da429029/scipy-1.16.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:744d977daa4becb9fc59135e75c069f8d301a87d64f88f1e602a9ecf51e77b27", size = 29093084 }, - { url = "https://files.pythonhosted.org/packages/a9/ae/7a10ff04a7dc15f9057d05b33737ade244e4bd195caa3f7cc04d77b9e214/scipy-1.16.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:dc54f76ac18073bcecffb98d93f03ed6b81a92ef91b5d3b135dcc81d55a724c7", size = 21365098 }, - { url = "https://files.pythonhosted.org/packages/36/ac/029ff710959932ad3c2a98721b20b405f05f752f07344622fd61a47c5197/scipy-1.16.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:367d567ee9fc1e9e2047d31f39d9d6a7a04e0710c86e701e053f237d14a9b4f6", size = 23896858 }, - { url = "https://files.pythonhosted.org/packages/71/13/d1ef77b6bd7898720e1f0b6b3743cb945f6c3cafa7718eaac8841035ab60/scipy-1.16.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4cf5785e44e19dcd32a0e4807555e1e9a9b8d475c6afff3d21c3c543a6aa84f4", size = 33438311 }, - { url = "https://files.pythonhosted.org/packages/2d/e0/e64a6821ffbb00b4c5b05169f1c1fddb4800e9307efe3db3788995a82a2c/scipy-1.16.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3d0b80fb26d3e13a794c71d4b837e2a589d839fd574a6bbb4ee1288c213ad4a3", size = 35279542 }, - { url = "https://files.pythonhosted.org/packages/57/59/0dc3c8b43e118f1e4ee2b798dcc96ac21bb20014e5f1f7a8e85cc0653bdb/scipy-1.16.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8503517c44c18d1030d666cb70aaac1cc8913608816e06742498833b128488b7", size = 35667665 }, - { url = "https://files.pythonhosted.org/packages/45/5f/844ee26e34e2f3f9f8febb9343748e72daeaec64fe0c70e9bf1ff84ec955/scipy-1.16.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:30cc4bb81c41831ecfd6dc450baf48ffd80ef5aed0f5cf3ea775740e80f16ecc", size = 38045210 }, - { url = "https://files.pythonhosted.org/packages/8d/d7/210f2b45290f444f1de64bc7353aa598ece9f0e90c384b4a156f9b1a5063/scipy-1.16.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c24fa02f7ed23ae514460a22c57eca8f530dbfa50b1cfdbf4f37c05b5309cc39", size = 38593661 }, - { url = "https://files.pythonhosted.org/packages/81/ea/84d481a5237ed223bd3d32d6e82d7a6a96e34756492666c260cef16011d1/scipy-1.16.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:796a5a9ad36fa3a782375db8f4241ab02a091308eb079746bc0f874c9b998318", size = 36525921 }, - { url = "https://files.pythonhosted.org/packages/4e/9f/d9edbdeff9f3a664807ae3aea383e10afaa247e8e6255e6d2aa4515e8863/scipy-1.16.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:3ea0733a2ff73fd6fdc5fecca54ee9b459f4d74f00b99aced7d9a3adb43fb1cc", size = 28564152 }, - { url = "https://files.pythonhosted.org/packages/3b/95/8125bcb1fe04bc267d103e76516243e8d5e11229e6b306bda1024a5423d1/scipy-1.16.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:85764fb15a2ad994e708258bb4ed8290d1305c62a4e1ef07c414356a24fcfbf8", size = 20836028 }, - { url = "https://files.pythonhosted.org/packages/77/9c/bf92e215701fc70bbcd3d14d86337cf56a9b912a804b9c776a269524a9e9/scipy-1.16.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ca66d980469cb623b1759bdd6e9fd97d4e33a9fad5b33771ced24d0cb24df67e", size = 23489666 }, - { url = "https://files.pythonhosted.org/packages/5e/00/5e941d397d9adac41b02839011594620d54d99488d1be5be755c00cde9ee/scipy-1.16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e7cc1ffcc230f568549fc56670bcf3df1884c30bd652c5da8138199c8c76dae0", size = 33358318 }, - { url = "https://files.pythonhosted.org/packages/0e/87/8db3aa10dde6e3e8e7eb0133f24baa011377d543f5b19c71469cf2648026/scipy-1.16.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ddfb1e8d0b540cb4ee9c53fc3dea3186f97711248fb94b4142a1b27178d8b4b", size = 35185724 }, - { url = "https://files.pythonhosted.org/packages/89/b4/6ab9ae443216807622bcff02690262d8184078ea467efee2f8c93288a3b1/scipy-1.16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4dc0e7be79e95d8ba3435d193e0d8ce372f47f774cffd882f88ea4e1e1ddc731", size = 35554335 }, - { url = "https://files.pythonhosted.org/packages/9c/9a/d0e9dc03c5269a1afb60661118296a32ed5d2c24298af61b676c11e05e56/scipy-1.16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f23634f9e5adb51b2a77766dac217063e764337fbc816aa8ad9aaebcd4397fd3", size = 37960310 }, - { url = "https://files.pythonhosted.org/packages/5e/00/c8f3130a50521a7977874817ca89e0599b1b4ee8e938bad8ae798a0e1f0d/scipy-1.16.1-cp314-cp314-win_amd64.whl", hash = "sha256:57d75524cb1c5a374958a2eae3d84e1929bb971204cc9d52213fb8589183fc19", size = 39319239 }, - { url = "https://files.pythonhosted.org/packages/f2/f2/1ca3eda54c3a7e4c92f6acef7db7b3a057deb135540d23aa6343ef8ad333/scipy-1.16.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:d8da7c3dd67bcd93f15618938f43ed0995982eb38973023d46d4646c4283ad65", size = 36939460 }, - { url = "https://files.pythonhosted.org/packages/80/30/98c2840b293a132400c0940bb9e140171dcb8189588619048f42b2ce7b4f/scipy-1.16.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:cc1d2f2fd48ba1e0620554fe5bc44d3e8f5d4185c8c109c7fbdf5af2792cfad2", size = 29093322 }, - { url = "https://files.pythonhosted.org/packages/c1/e6/1e6e006e850622cf2a039b62d1a6ddc4497d4851e58b68008526f04a9a00/scipy-1.16.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:21a611ced9275cb861bacadbada0b8c0623bc00b05b09eb97f23b370fc2ae56d", size = 21365329 }, - { url = "https://files.pythonhosted.org/packages/8e/02/72a5aa5b820589dda9a25e329ca752842bfbbaf635e36bc7065a9b42216e/scipy-1.16.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dfbb25dffc4c3dd9371d8ab456ca81beeaf6f9e1c2119f179392f0dc1ab7695", size = 23897544 }, - { url = "https://files.pythonhosted.org/packages/2b/dc/7122d806a6f9eb8a33532982234bed91f90272e990f414f2830cfe656e0b/scipy-1.16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f0ebb7204f063fad87fc0a0e4ff4a2ff40b2a226e4ba1b7e34bf4b79bf97cd86", size = 33442112 }, - { url = "https://files.pythonhosted.org/packages/24/39/e383af23564daa1021a5b3afbe0d8d6a68ec639b943661841f44ac92de85/scipy-1.16.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f1b9e5962656f2734c2b285a8745358ecb4e4efbadd00208c80a389227ec61ff", size = 35286594 }, - { url = "https://files.pythonhosted.org/packages/95/47/1a0b0aff40c3056d955f38b0df5d178350c3d74734ec54f9c68d23910be5/scipy-1.16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e1a106f8c023d57a2a903e771228bf5c5b27b5d692088f457acacd3b54511e4", size = 35665080 }, - { url = "https://files.pythonhosted.org/packages/64/df/ce88803e9ed6e27fe9b9abefa157cf2c80e4fa527cf17ee14be41f790ad4/scipy-1.16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:709559a1db68a9abc3b2c8672c4badf1614f3b440b3ab326d86a5c0491eafae3", size = 38050306 }, - { url = "https://files.pythonhosted.org/packages/6e/6c/a76329897a7cae4937d403e623aa6aaea616a0bb5b36588f0b9d1c9a3739/scipy-1.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c0c804d60492a0aad7f5b2bb1862f4548b990049e27e828391ff2bf6f7199998", size = 39427705 }, +sdist = { url = "https://files.pythonhosted.org/packages/f5/4a/b927028464795439faec8eaf0b03b011005c487bb2d07409f28bf30879c4/scipy-1.16.1.tar.gz", hash = "sha256:44c76f9e8b6e8e488a586190ab38016e4ed2f8a038af7cd3defa903c0a2238b3", size = 30580861, upload-time = "2025-07-27T16:33:30.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/91/812adc6f74409b461e3a5fa97f4f74c769016919203138a3bf6fc24ba4c5/scipy-1.16.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:c033fa32bab91dc98ca59d0cf23bb876454e2bb02cbe592d5023138778f70030", size = 36552519, upload-time = "2025-07-27T16:26:29.658Z" }, + { url = "https://files.pythonhosted.org/packages/47/18/8e355edcf3b71418d9e9f9acd2708cc3a6c27e8f98fde0ac34b8a0b45407/scipy-1.16.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6e5c2f74e5df33479b5cd4e97a9104c511518fbd979aa9b8f6aec18b2e9ecae7", size = 28638010, upload-time = "2025-07-27T16:26:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/d9/eb/e931853058607bdfbc11b86df19ae7a08686121c203483f62f1ecae5989c/scipy-1.16.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0a55ffe0ba0f59666e90951971a884d1ff6f4ec3275a48f472cfb64175570f77", size = 20909790, upload-time = "2025-07-27T16:26:43.93Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/be83a271d6e96750cd0be2e000f35ff18880a46f05ce8b5d3465dc0f7a2a/scipy-1.16.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f8a5d6cd147acecc2603fbd382fed6c46f474cccfcf69ea32582e033fb54dcfe", size = 23513352, upload-time = "2025-07-27T16:26:50.017Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bf/fe6eb47e74f762f933cca962db7f2c7183acfdc4483bd1c3813cfe83e538/scipy-1.16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb18899127278058bcc09e7b9966d41a5a43740b5bb8dcba401bd983f82e885b", size = 33534643, upload-time = "2025-07-27T16:26:57.503Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ba/63f402e74875486b87ec6506a4f93f6d8a0d94d10467280f3d9d7837ce3a/scipy-1.16.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adccd93a2fa937a27aae826d33e3bfa5edf9aa672376a4852d23a7cd67a2e5b7", size = 35376776, upload-time = "2025-07-27T16:27:06.639Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b4/04eb9d39ec26a1b939689102da23d505ea16cdae3dbb18ffc53d1f831044/scipy-1.16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:18aca1646a29ee9a0625a1be5637fa798d4d81fdf426481f06d69af828f16958", size = 35698906, upload-time = "2025-07-27T16:27:14.943Z" }, + { url = "https://files.pythonhosted.org/packages/04/d6/bb5468da53321baeb001f6e4e0d9049eadd175a4a497709939128556e3ec/scipy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d85495cef541729a70cdddbbf3e6b903421bc1af3e8e3a9a72a06751f33b7c39", size = 38129275, upload-time = "2025-07-27T16:27:23.873Z" }, + { url = "https://files.pythonhosted.org/packages/c4/94/994369978509f227cba7dfb9e623254d0d5559506fe994aef4bea3ed469c/scipy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:226652fca853008119c03a8ce71ffe1b3f6d2844cc1686e8f9806edafae68596", size = 38644572, upload-time = "2025-07-27T16:27:32.637Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d9/ec4864f5896232133f51382b54a08de91a9d1af7a76dfa372894026dfee2/scipy-1.16.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81b433bbeaf35728dad619afc002db9b189e45eebe2cd676effe1fb93fef2b9c", size = 36575194, upload-time = "2025-07-27T16:27:41.321Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6d/40e81ecfb688e9d25d34a847dca361982a6addf8e31f0957b1a54fbfa994/scipy-1.16.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:886cc81fdb4c6903a3bb0464047c25a6d1016fef77bb97949817d0c0d79f9e04", size = 28594590, upload-time = "2025-07-27T16:27:49.204Z" }, + { url = "https://files.pythonhosted.org/packages/0e/37/9f65178edfcc629377ce9a64fc09baebea18c80a9e57ae09a52edf84880b/scipy-1.16.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:15240c3aac087a522b4eaedb09f0ad061753c5eebf1ea430859e5bf8640d5919", size = 20866458, upload-time = "2025-07-27T16:27:54.98Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7b/749a66766871ea4cb1d1ea10f27004db63023074c22abed51f22f09770e0/scipy-1.16.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f81a25805f3659b48126b5053d9e823d3215e4a63730b5e1671852a1705921", size = 23539318, upload-time = "2025-07-27T16:28:01.604Z" }, + { url = "https://files.pythonhosted.org/packages/c4/db/8d4afec60eb833a666434d4541a3151eedbf2494ea6d4d468cbe877f00cd/scipy-1.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6c62eea7f607f122069b9bad3f99489ddca1a5173bef8a0c75555d7488b6f725", size = 33292899, upload-time = "2025-07-27T16:28:09.147Z" }, + { url = "https://files.pythonhosted.org/packages/51/1e/79023ca3bbb13a015d7d2757ecca3b81293c663694c35d6541b4dca53e98/scipy-1.16.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f965bbf3235b01c776115ab18f092a95aa74c271a52577bcb0563e85738fd618", size = 35162637, upload-time = "2025-07-27T16:28:17.535Z" }, + { url = "https://files.pythonhosted.org/packages/b6/49/0648665f9c29fdaca4c679182eb972935b3b4f5ace41d323c32352f29816/scipy-1.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f006e323874ffd0b0b816d8c6a8e7f9a73d55ab3b8c3f72b752b226d0e3ac83d", size = 35490507, upload-time = "2025-07-27T16:28:25.705Z" }, + { url = "https://files.pythonhosted.org/packages/62/8f/66cbb9d6bbb18d8c658f774904f42a92078707a7c71e5347e8bf2f52bb89/scipy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8fd15fc5085ab4cca74cb91fe0a4263b1f32e4420761ddae531ad60934c2119", size = 37923998, upload-time = "2025-07-27T16:28:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/14/c3/61f273ae550fbf1667675701112e380881905e28448c080b23b5a181df7c/scipy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7b8013c6c066609577d910d1a2a077021727af07b6fab0ee22c2f901f22352a", size = 38508060, upload-time = "2025-07-27T16:28:43.242Z" }, + { url = "https://files.pythonhosted.org/packages/93/0b/b5c99382b839854a71ca9482c684e3472badc62620287cbbdab499b75ce6/scipy-1.16.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5451606823a5e73dfa621a89948096c6528e2896e40b39248295d3a0138d594f", size = 36533717, upload-time = "2025-07-27T16:28:51.706Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e5/69ab2771062c91e23e07c12e7d5033a6b9b80b0903ee709c3c36b3eb520c/scipy-1.16.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:89728678c5ca5abd610aee148c199ac1afb16e19844401ca97d43dc548a354eb", size = 28570009, upload-time = "2025-07-27T16:28:57.017Z" }, + { url = "https://files.pythonhosted.org/packages/f4/69/bd75dbfdd3cf524f4d753484d723594aed62cfaac510123e91a6686d520b/scipy-1.16.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e756d688cb03fd07de0fffad475649b03cb89bee696c98ce508b17c11a03f95c", size = 20841942, upload-time = "2025-07-27T16:29:01.152Z" }, + { url = "https://files.pythonhosted.org/packages/ea/74/add181c87663f178ba7d6144b370243a87af8476664d5435e57d599e6874/scipy-1.16.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5aa2687b9935da3ed89c5dbed5234576589dd28d0bf7cd237501ccfbdf1ad608", size = 23498507, upload-time = "2025-07-27T16:29:05.202Z" }, + { url = "https://files.pythonhosted.org/packages/1d/74/ece2e582a0d9550cee33e2e416cc96737dce423a994d12bbe59716f47ff1/scipy-1.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0851f6a1e537fe9399f35986897e395a1aa61c574b178c0d456be5b1a0f5ca1f", size = 33286040, upload-time = "2025-07-27T16:29:10.201Z" }, + { url = "https://files.pythonhosted.org/packages/e4/82/08e4076df538fb56caa1d489588d880ec7c52d8273a606bb54d660528f7c/scipy-1.16.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fedc2cbd1baed37474b1924c331b97bdff611d762c196fac1a9b71e67b813b1b", size = 35176096, upload-time = "2025-07-27T16:29:17.091Z" }, + { url = "https://files.pythonhosted.org/packages/fa/79/cd710aab8c921375711a8321c6be696e705a120e3011a643efbbcdeeabcc/scipy-1.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2ef500e72f9623a6735769e4b93e9dcb158d40752cdbb077f305487e3e2d1f45", size = 35490328, upload-time = "2025-07-27T16:29:22.928Z" }, + { url = "https://files.pythonhosted.org/packages/71/73/e9cc3d35ee4526d784520d4494a3e1ca969b071fb5ae5910c036a375ceec/scipy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:978d8311674b05a8f7ff2ea6c6bce5d8b45a0cb09d4c5793e0318f448613ea65", size = 37939921, upload-time = "2025-07-27T16:29:29.108Z" }, + { url = "https://files.pythonhosted.org/packages/21/12/c0efd2941f01940119b5305c375ae5c0fcb7ec193f806bd8f158b73a1782/scipy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:81929ed0fa7a5713fcdd8b2e6f73697d3b4c4816d090dd34ff937c20fa90e8ab", size = 38479462, upload-time = "2025-07-27T16:30:24.078Z" }, + { url = "https://files.pythonhosted.org/packages/7a/19/c3d08b675260046a991040e1ea5d65f91f40c7df1045fffff412dcfc6765/scipy-1.16.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:bcc12db731858abda693cecdb3bdc9e6d4bd200213f49d224fe22df82687bdd6", size = 36938832, upload-time = "2025-07-27T16:29:35.057Z" }, + { url = "https://files.pythonhosted.org/packages/81/f2/ce53db652c033a414a5b34598dba6b95f3d38153a2417c5a3883da429029/scipy-1.16.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:744d977daa4becb9fc59135e75c069f8d301a87d64f88f1e602a9ecf51e77b27", size = 29093084, upload-time = "2025-07-27T16:29:40.201Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ae/7a10ff04a7dc15f9057d05b33737ade244e4bd195caa3f7cc04d77b9e214/scipy-1.16.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:dc54f76ac18073bcecffb98d93f03ed6b81a92ef91b5d3b135dcc81d55a724c7", size = 21365098, upload-time = "2025-07-27T16:29:44.295Z" }, + { url = "https://files.pythonhosted.org/packages/36/ac/029ff710959932ad3c2a98721b20b405f05f752f07344622fd61a47c5197/scipy-1.16.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:367d567ee9fc1e9e2047d31f39d9d6a7a04e0710c86e701e053f237d14a9b4f6", size = 23896858, upload-time = "2025-07-27T16:29:48.784Z" }, + { url = "https://files.pythonhosted.org/packages/71/13/d1ef77b6bd7898720e1f0b6b3743cb945f6c3cafa7718eaac8841035ab60/scipy-1.16.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4cf5785e44e19dcd32a0e4807555e1e9a9b8d475c6afff3d21c3c543a6aa84f4", size = 33438311, upload-time = "2025-07-27T16:29:54.164Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e0/e64a6821ffbb00b4c5b05169f1c1fddb4800e9307efe3db3788995a82a2c/scipy-1.16.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3d0b80fb26d3e13a794c71d4b837e2a589d839fd574a6bbb4ee1288c213ad4a3", size = 35279542, upload-time = "2025-07-27T16:30:00.249Z" }, + { url = "https://files.pythonhosted.org/packages/57/59/0dc3c8b43e118f1e4ee2b798dcc96ac21bb20014e5f1f7a8e85cc0653bdb/scipy-1.16.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8503517c44c18d1030d666cb70aaac1cc8913608816e06742498833b128488b7", size = 35667665, upload-time = "2025-07-27T16:30:05.916Z" }, + { url = "https://files.pythonhosted.org/packages/45/5f/844ee26e34e2f3f9f8febb9343748e72daeaec64fe0c70e9bf1ff84ec955/scipy-1.16.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:30cc4bb81c41831ecfd6dc450baf48ffd80ef5aed0f5cf3ea775740e80f16ecc", size = 38045210, upload-time = "2025-07-27T16:30:11.655Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d7/210f2b45290f444f1de64bc7353aa598ece9f0e90c384b4a156f9b1a5063/scipy-1.16.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c24fa02f7ed23ae514460a22c57eca8f530dbfa50b1cfdbf4f37c05b5309cc39", size = 38593661, upload-time = "2025-07-27T16:30:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/81/ea/84d481a5237ed223bd3d32d6e82d7a6a96e34756492666c260cef16011d1/scipy-1.16.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:796a5a9ad36fa3a782375db8f4241ab02a091308eb079746bc0f874c9b998318", size = 36525921, upload-time = "2025-07-27T16:30:30.081Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9f/d9edbdeff9f3a664807ae3aea383e10afaa247e8e6255e6d2aa4515e8863/scipy-1.16.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:3ea0733a2ff73fd6fdc5fecca54ee9b459f4d74f00b99aced7d9a3adb43fb1cc", size = 28564152, upload-time = "2025-07-27T16:30:35.336Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/8125bcb1fe04bc267d103e76516243e8d5e11229e6b306bda1024a5423d1/scipy-1.16.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:85764fb15a2ad994e708258bb4ed8290d1305c62a4e1ef07c414356a24fcfbf8", size = 20836028, upload-time = "2025-07-27T16:30:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/77/9c/bf92e215701fc70bbcd3d14d86337cf56a9b912a804b9c776a269524a9e9/scipy-1.16.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ca66d980469cb623b1759bdd6e9fd97d4e33a9fad5b33771ced24d0cb24df67e", size = 23489666, upload-time = "2025-07-27T16:30:43.663Z" }, + { url = "https://files.pythonhosted.org/packages/5e/00/5e941d397d9adac41b02839011594620d54d99488d1be5be755c00cde9ee/scipy-1.16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e7cc1ffcc230f568549fc56670bcf3df1884c30bd652c5da8138199c8c76dae0", size = 33358318, upload-time = "2025-07-27T16:30:48.982Z" }, + { url = "https://files.pythonhosted.org/packages/0e/87/8db3aa10dde6e3e8e7eb0133f24baa011377d543f5b19c71469cf2648026/scipy-1.16.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ddfb1e8d0b540cb4ee9c53fc3dea3186f97711248fb94b4142a1b27178d8b4b", size = 35185724, upload-time = "2025-07-27T16:30:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/89/b4/6ab9ae443216807622bcff02690262d8184078ea467efee2f8c93288a3b1/scipy-1.16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4dc0e7be79e95d8ba3435d193e0d8ce372f47f774cffd882f88ea4e1e1ddc731", size = 35554335, upload-time = "2025-07-27T16:30:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9a/d0e9dc03c5269a1afb60661118296a32ed5d2c24298af61b676c11e05e56/scipy-1.16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f23634f9e5adb51b2a77766dac217063e764337fbc816aa8ad9aaebcd4397fd3", size = 37960310, upload-time = "2025-07-27T16:31:06.151Z" }, + { url = "https://files.pythonhosted.org/packages/5e/00/c8f3130a50521a7977874817ca89e0599b1b4ee8e938bad8ae798a0e1f0d/scipy-1.16.1-cp314-cp314-win_amd64.whl", hash = "sha256:57d75524cb1c5a374958a2eae3d84e1929bb971204cc9d52213fb8589183fc19", size = 39319239, upload-time = "2025-07-27T16:31:59.942Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f2/1ca3eda54c3a7e4c92f6acef7db7b3a057deb135540d23aa6343ef8ad333/scipy-1.16.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:d8da7c3dd67bcd93f15618938f43ed0995982eb38973023d46d4646c4283ad65", size = 36939460, upload-time = "2025-07-27T16:31:11.865Z" }, + { url = "https://files.pythonhosted.org/packages/80/30/98c2840b293a132400c0940bb9e140171dcb8189588619048f42b2ce7b4f/scipy-1.16.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:cc1d2f2fd48ba1e0620554fe5bc44d3e8f5d4185c8c109c7fbdf5af2792cfad2", size = 29093322, upload-time = "2025-07-27T16:31:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e6/1e6e006e850622cf2a039b62d1a6ddc4497d4851e58b68008526f04a9a00/scipy-1.16.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:21a611ced9275cb861bacadbada0b8c0623bc00b05b09eb97f23b370fc2ae56d", size = 21365329, upload-time = "2025-07-27T16:31:21.188Z" }, + { url = "https://files.pythonhosted.org/packages/8e/02/72a5aa5b820589dda9a25e329ca752842bfbbaf635e36bc7065a9b42216e/scipy-1.16.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dfbb25dffc4c3dd9371d8ab456ca81beeaf6f9e1c2119f179392f0dc1ab7695", size = 23897544, upload-time = "2025-07-27T16:31:25.408Z" }, + { url = "https://files.pythonhosted.org/packages/2b/dc/7122d806a6f9eb8a33532982234bed91f90272e990f414f2830cfe656e0b/scipy-1.16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f0ebb7204f063fad87fc0a0e4ff4a2ff40b2a226e4ba1b7e34bf4b79bf97cd86", size = 33442112, upload-time = "2025-07-27T16:31:30.62Z" }, + { url = "https://files.pythonhosted.org/packages/24/39/e383af23564daa1021a5b3afbe0d8d6a68ec639b943661841f44ac92de85/scipy-1.16.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f1b9e5962656f2734c2b285a8745358ecb4e4efbadd00208c80a389227ec61ff", size = 35286594, upload-time = "2025-07-27T16:31:36.112Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/1a0b0aff40c3056d955f38b0df5d178350c3d74734ec54f9c68d23910be5/scipy-1.16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e1a106f8c023d57a2a903e771228bf5c5b27b5d692088f457acacd3b54511e4", size = 35665080, upload-time = "2025-07-27T16:31:42.025Z" }, + { url = "https://files.pythonhosted.org/packages/64/df/ce88803e9ed6e27fe9b9abefa157cf2c80e4fa527cf17ee14be41f790ad4/scipy-1.16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:709559a1db68a9abc3b2c8672c4badf1614f3b440b3ab326d86a5c0491eafae3", size = 38050306, upload-time = "2025-07-27T16:31:48.109Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6c/a76329897a7cae4937d403e623aa6aaea616a0bb5b36588f0b9d1c9a3739/scipy-1.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c0c804d60492a0aad7f5b2bb1862f4548b990049e27e828391ff2bf6f7199998", size = 39427705, upload-time = "2025-07-27T16:31:53.96Z" }, ] [[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 } +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 }, + { 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 = "snowballstemmer" version = "3.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575 } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274 }, + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, ] [[package]] name = "soupsieve" version = "2.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418 } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677 }, + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" }, ] [[package]] @@ -1732,9 +1740,9 @@ dependencies = [ { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611 } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125 }, + { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, ] [[package]] @@ -1763,9 +1771,9 @@ dependencies = [ { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" }, { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876 } +sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741 }, + { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" }, ] [[package]] @@ -1778,9 +1786,9 @@ resolution-markers = [ dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/f0/43c6a5ff3e7b08a8c3b32f81b859f1b518ccc31e45f22e2b41ced38be7b9/sphinx_autodoc_typehints-3.0.1.tar.gz", hash = "sha256:b9b40dd15dee54f6f810c924f863f9cf1c54f9f3265c495140ea01be7f44fa55", size = 36282 } +sdist = { url = "https://files.pythonhosted.org/packages/26/f0/43c6a5ff3e7b08a8c3b32f81b859f1b518ccc31e45f22e2b41ced38be7b9/sphinx_autodoc_typehints-3.0.1.tar.gz", hash = "sha256:b9b40dd15dee54f6f810c924f863f9cf1c54f9f3265c495140ea01be7f44fa55", size = 36282, upload-time = "2025-01-16T18:25:30.958Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/dc/dc46c5c7c566b7ec5e8f860f9c89533bf03c0e6aadc96fb9b337867e4460/sphinx_autodoc_typehints-3.0.1-py3-none-any.whl", hash = "sha256:4b64b676a14b5b79cefb6628a6dc8070e320d4963e8ff640a2f3e9390ae9045a", size = 20245 }, + { url = "https://files.pythonhosted.org/packages/3c/dc/dc46c5c7c566b7ec5e8f860f9c89533bf03c0e6aadc96fb9b337867e4460/sphinx_autodoc_typehints-3.0.1-py3-none-any.whl", hash = "sha256:4b64b676a14b5b79cefb6628a6dc8070e320d4963e8ff640a2f3e9390ae9045a", size = 20245, upload-time = "2025-01-16T18:25:27.394Z" }, ] [[package]] @@ -1793,9 +1801,9 @@ resolution-markers = [ dependencies = [ { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/68/a388a9b8f066cd865d9daa65af589d097efbfab9a8c302d2cb2daa43b52e/sphinx_autodoc_typehints-3.2.0.tar.gz", hash = "sha256:107ac98bc8b4837202c88c0736d59d6da44076e65a0d7d7d543a78631f662a9b", size = 36724 } +sdist = { url = "https://files.pythonhosted.org/packages/93/68/a388a9b8f066cd865d9daa65af589d097efbfab9a8c302d2cb2daa43b52e/sphinx_autodoc_typehints-3.2.0.tar.gz", hash = "sha256:107ac98bc8b4837202c88c0736d59d6da44076e65a0d7d7d543a78631f662a9b", size = 36724, upload-time = "2025-04-25T16:53:25.872Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/c7/8aab362e86cbf887e58be749a78d20ad743e1eb2c73c2b13d4761f39a104/sphinx_autodoc_typehints-3.2.0-py3-none-any.whl", hash = "sha256:884b39be23b1d884dcc825d4680c9c6357a476936e3b381a67ae80091984eb49", size = 20563 }, + { url = "https://files.pythonhosted.org/packages/f7/c7/8aab362e86cbf887e58be749a78d20ad743e1eb2c73c2b13d4761f39a104/sphinx_autodoc_typehints-3.2.0-py3-none-any.whl", hash = "sha256:884b39be23b1d884dcc825d4680c9c6357a476936e3b381a67ae80091984eb49", size = 20563, upload-time = "2025-04-25T16:53:24.492Z" }, ] [[package]] @@ -1807,45 +1815,59 @@ dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/19/d002ed96bdc7738c15847c730e1e88282d738263deac705d5713b4d8fa94/sphinx_book_theme-1.1.4.tar.gz", hash = "sha256:73efe28af871d0a89bd05856d300e61edce0d5b2fbb7984e84454be0fedfe9ed", size = 439188 } +sdist = { url = "https://files.pythonhosted.org/packages/45/19/d002ed96bdc7738c15847c730e1e88282d738263deac705d5713b4d8fa94/sphinx_book_theme-1.1.4.tar.gz", hash = "sha256:73efe28af871d0a89bd05856d300e61edce0d5b2fbb7984e84454be0fedfe9ed", size = 439188, upload-time = "2025-02-20T16:32:32.581Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/9e/c41d68be04eef5b6202b468e0f90faf0c469f3a03353f2a218fd78279710/sphinx_book_theme-1.1.4-py3-none-any.whl", hash = "sha256:843b3f5c8684640f4a2d01abd298beb66452d1b2394cd9ef5be5ebd5640ea0e1", size = 433952 }, + { url = "https://files.pythonhosted.org/packages/51/9e/c41d68be04eef5b6202b468e0f90faf0c469f3a03353f2a218fd78279710/sphinx_book_theme-1.1.4-py3-none-any.whl", hash = "sha256:843b3f5c8684640f4a2d01abd298beb66452d1b2394cd9ef5be5ebd5640ea0e1", size = 433952, upload-time = "2025-02-20T16:32:31.009Z" }, +] + +[[package]] +name = "sphinx-gallery" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pillow" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/e5/9ccd6ecd492043123adb465cba504217b9f0a82e2cb5b1d7249c648497c6/sphinx_gallery-0.19.0.tar.gz", hash = "sha256:8400cb5240ad642e28a612fdba0667f725d0505a9be0222d0243de60e8af2eb3", size = 471479, upload-time = "2025-02-13T03:24:50.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c7/52b48aec16b26c52aba854d03a3a31e0681150301dac1bea2243645a69e7/sphinx_gallery-0.19.0-py3-none-any.whl", hash = "sha256:4c28751973f81769d5bbbf5e4ebaa0dc49dff8c48eb7f11131eb5f6e4aa25f0e", size = 455923, upload-time = "2025-02-13T03:24:47.697Z" }, ] [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053 } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300 }, + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, ] [[package]] name = "sphinxcontrib-devhelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967 } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 }, + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, ] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617 } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705 }, + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, ] [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 }, + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, ] [[package]] @@ -1857,66 +1879,79 @@ dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/69/bf039237ad260073e8c02f820b3e00dc34f3a2de20aff7861e6b19d2f8c5/sphinxcontrib_mermaid-1.0.0.tar.gz", hash = "sha256:2e8ab67d3e1e2816663f9347d026a8dee4a858acdd4ad32dd1c808893db88146", size = 15153 } +sdist = { url = "https://files.pythonhosted.org/packages/97/69/bf039237ad260073e8c02f820b3e00dc34f3a2de20aff7861e6b19d2f8c5/sphinxcontrib_mermaid-1.0.0.tar.gz", hash = "sha256:2e8ab67d3e1e2816663f9347d026a8dee4a858acdd4ad32dd1c808893db88146", size = 15153, upload-time = "2024-10-12T16:33:03.863Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/c8/784b9ac6ea08aa594c1a4becbd0dbe77186785362e31fd633b8c6ae0197a/sphinxcontrib_mermaid-1.0.0-py3-none-any.whl", hash = "sha256:60b72710ea02087f212028feb09711225fbc2e343a10d34822fe787510e1caa3", size = 9597 }, + { url = "https://files.pythonhosted.org/packages/cd/c8/784b9ac6ea08aa594c1a4becbd0dbe77186785362e31fd633b8c6ae0197a/sphinxcontrib_mermaid-1.0.0-py3-none-any.whl", hash = "sha256:60b72710ea02087f212028feb09711225fbc2e343a10d34822fe787510e1caa3", size = 9597, upload-time = "2024-10-12T16:33:02.303Z" }, ] [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165 } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743 }, + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, ] [[package]] name = "sphinxcontrib-serializinghtml" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "sphinxcontrib-video" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/48/063e167b6e692bc84bbad74df30bcb27e460a7c620af7824729db8dba606/sphinxcontrib_video-0.4.1.tar.gz", hash = "sha256:75a033e71b7de124cc5902430b7ba818a1c6c377be6401d07e9f2329a95d5ca4", size = 11362, upload-time = "2025-02-19T17:06:13.632Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, + { url = "https://files.pythonhosted.org/packages/5d/8b/a0271fe65357860ccc52168181891e9fc9d354bfdc9be273e6a77b84f905/sphinxcontrib_video-0.4.1-py3-none-any.whl", hash = "sha256:d63ec68983dac36960557973281a616b5d9e68838369763313fc80533b1ad774", size = 10066, upload-time = "2025-02-19T17:06:12.561Z" }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, - { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, - { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, - { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, - { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, - { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, - { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, - { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, - { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, - { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, - { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] [[package]] @@ -1926,27 +1961,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] [[package]] name = "typing-extensions" version = "4.14.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673 } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906 }, + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, ] [[package]] name = "urllib3" version = "2.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185 } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 }, + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] [[package]] @@ -1958,7 +1993,7 @@ dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/60/4f20960df6c7b363a18a55ab034c8f2bcd5d9770d1f94f9370ec104c1855/virtualenv-20.33.1.tar.gz", hash = "sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8", size = 6082160 } +sdist = { url = "https://files.pythonhosted.org/packages/8b/60/4f20960df6c7b363a18a55ab034c8f2bcd5d9770d1f94f9370ec104c1855/virtualenv-20.33.1.tar.gz", hash = "sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8", size = 6082160, upload-time = "2025-08-05T16:10:55.605Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/ff/ded57ac5ff40a09e6e198550bab075d780941e0b0f83cbeabd087c59383a/virtualenv-20.33.1-py3-none-any.whl", hash = "sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67", size = 6060362 }, + { url = "https://files.pythonhosted.org/packages/ca/ff/ded57ac5ff40a09e6e198550bab075d780941e0b0f83cbeabd087c59383a/virtualenv-20.33.1-py3-none-any.whl", hash = "sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67", size = 6060362, upload-time = "2025-08-05T16:10:52.81Z" }, ]