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/main.yml b/.github/workflows/main.yml index 989f99642..1317163ab 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: @@ -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..61a79d21a 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 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..50b180f00 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 @@ -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: @@ -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/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..13569b984 100644 --- a/docs/api/rigidbody.rst +++ b/docs/api/rigidbody.rst @@ -9,7 +9,7 @@ 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 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..479320b14 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 @@ -67,10 +71,6 @@ If you are interested to contribute, please read `contribution-guide`_ first. .. 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..c325f760d 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, @@ -18,36 +18,43 @@ ) import numpy as np +import warnings 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 +79,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 @@ -112,6 +129,13 @@ def _check_type(self, sys_to_be_added: Any) -> bool: f"The system {sys_to_be_added.__class__} requires the following modules:\n" f"{sys_to_be_added.REQUISITE_MODULES}\n" ) + if id(sys_to_be_added) in [id(system) for system in self.__systems]: + # Warning for duplicate system instance + warnings.warn( + f"System {sys_to_be_added.__class__} is already in the system collection.\n" + "Adding multiple instance is technically allowed, but it is not recommended.", + UserWarning, + ) return True def __len__(self) -> int: @@ -141,18 +165,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 +231,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 +281,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 +296,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 +325,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 +366,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 94% rename from elastica/rigidbody/rigid_body.py rename to elastica/rigidbody/rigid_body_base.py index 999208546..ce8902238 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 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..e035c994a 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) -> None: """ Updates the acceleration variables @@ -626,88 +621,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 +679,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 +699,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 +1047,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..be3214178 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: ... + @abstractmethod def update_accelerations(self, time: 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..98c8bb875 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) + 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) + 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) + 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..38b3e80e9 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,9 +33,6 @@ dependencies = [ "scipy", "tqdm", "matplotlib", - "mypy", - "mypy-extensions", - "flake8", "cma", ] @@ -59,6 +56,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 +71,9 @@ dev = [ "codecov", "click", "autoflake", + "mypy", + "mypy-extensions", + "flake8", ] [tool.black] diff --git a/tests/analytical.py b/tests/analytical.py index 05b395e5d..a606cba12 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): + 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( 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_memory_block_with_symplectic_timestepper.py b/tests/test_math/test_memory_block_with_symplectic_timestepper.py index a9a402645..78c81cb42 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): 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..24a444f92 100644 --- a/tests/test_modules/test_base_system.py +++ b/tests/test_modules/test_base_system.py @@ -1,6 +1,7 @@ __doc__ = """ Test modules for base systems """ import pytest +import warnings import numpy as np from elastica.modules import ( @@ -27,7 +28,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 +78,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 +101,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 +121,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) @@ -145,11 +143,157 @@ def test_unregistered_system_in_get_sys_index_throws( def test_get_sys_index_returns_correct_idx(self, load_collection): assert load_collection.get_system_index(1) == 1 + def test_duplicate_system_warning(self): + """Test that adding the same system instance twice emits a warning.""" + bsc = BaseSystemCollection() + bsc.extend_allowed_types((int,)) + + # Add a system + test_system = 42 + bsc.append(test_system) + + # Try to add the same system again - should emit warning + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + bsc._check_type(test_system) + + # Verify warning was issued + assert len(w) == 1 + assert issubclass(w[0].category, UserWarning) + assert "already in the system collection" in str(w[0].message) + assert "not recommended" in str(w[0].message) + + def test_duplicate_system_warning_with_rod(self, mock_rod): + """Test that adding the same rod instance twice emits a warning.""" + bsc = BaseSystemCollection() + from elastica.rod import RodBase + + bsc.extend_allowed_types((RodBase,)) + + # Add a rod + bsc.append(mock_rod) + + # Try to add the same rod again - should emit warning + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + bsc.append(mock_rod) + + # Verify warning was issued + assert len(w) == 1 + assert issubclass(w[0].category, UserWarning) + assert "already in the system collection" in str(w[0].message) + assert "not recommended" in str(w[0].message) + @pytest.mark.xfail 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 +375,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 +390,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..71225b9f1 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) -> 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_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..bad56e1f8 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,21 +1273,18 @@ 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'" }, @@ -1301,6 +1299,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 +1319,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 +1333,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 +1352,38 @@ 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" }, ] +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 +1399,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 +1413,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 +1427,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 +1439,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 +1451,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 +1464,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 +1476,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 +1544,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 +1568,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 +1627,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 +1738,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 +1769,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 +1784,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 +1799,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 +1813,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 +1877,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 +1959,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 +1991,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" }, ]