diff --git a/.coveragerc b/.coveragerc index fffd31429..1e03b34f0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,6 +8,8 @@ exclude_lines = def __repr__ from import + raise AssertionError + raise NotImplementedError show_missing = true [run] diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index aeca65871..d75422716 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,7 +20,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11"] os: [ubuntu-latest, macos-latest] #, windows-latest] # Run macos tests if really required, since they charge 10 times more for macos include: - os: ubuntu-latest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6e303b2e7..05a964719 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,7 +61,7 @@ make pre-commit-install ``` If you are planning to contribute to the examples, -extra dependencies can be installed using `poetry install -E examples`. +extra dependencies can be installed using `make install_examples_dependencies`. If you are planning to contribute on documentation, extra dependencies can be installed using `poetry install -E docs`. diff --git a/LICENSE b/LICENSE index f0f48f3f5..fb8f3bdd2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019-2023 GazzolaLab +Copyright (c) 2019-2024 GazzolaLab Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 9c5556985..81050240d 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,12 @@ PyElastica is the python implementation of **Elastica**: an *open-source* projec [![gallery][link-readme-gallary]][link-project-website] -Visit [cosseratrods.org][link-project-website] for more information and learn about Elastica and Cosserat rod theory. +Visit [www.cosseratrods.org][link-project-website] for more information and learn about Elastica and Cosserat rod theory. ## How to Start [![PyPI version][badge-pypi]][link-pypi] [![Documentation Status][badge-docs-status]][link-docs-status] -PyElastica is compatible with Python 3.8 - 3.10. +PyElastica is compatible with Python 3.8 - 3.11. ~~~bash $ pip install pyelastica @@ -31,6 +31,11 @@ Options can be combined e.g. $ pip install "pyelastica[examples,docs]" ``` +For plotting videos, ffmpeg has to be installed: +```bash +$ conda install -c conda-forge ffmpeg +``` + Documentation of PyElastica is available [here][link-docs-website]. If you want to simulate magnetic Cosserat rods interacting with external magnetic environments you can install the derived package using @@ -77,8 +82,11 @@ We ask that any publications which use Elastica cite as following: ## List of publications and submissions +- [Soft, slender and active structures in fluids: embedding Cosserat rods in vortex methods](https://doi.org/10.48550/arXiv.2401.09506) (UIUC 2024) +- [Neural models and algorithms for sensorimotor control of an octopus arm](https://doi.org/10.48550/arXiv.2402.01074)(UIUC 2024) +- [On the mechanical origins of waving, coiling and skewing in Arabidopsis thaliana roots](https://www.pnas.org/doi/10.1073/pnas.2312761121) (Tel Aviv University, UIUC 2024) (PNAS) - [Topology, dynamics, and control of an octopus-analog muscular hydrostat](https://arxiv.org/abs/2304.08413) (UIUC, 2023) -- [Hierarchical control and learning of a foraging CyberOctopus](https://arxiv.org/abs/2302.05811) (UIUC, 2023) +- [Hierarchical control and learning of a foraging CyberOctopus](https://onlinelibrary.wiley.com/doi/full/10.1002/aisy.202300088) (UIUC, 2023) (Advanced Intelligent Systems) - [Energy-shaping control of a muscular octopus arm moving in three dimensions](https://royalsocietypublishing.org/doi/full/10.1098/rspa.2022.0593) (UIUC, 2023) (Proceedings of the Royal Society A 2023) - [A sensory feedback control law for octopus arm movements](https://ieeexplore.ieee.org/abstract/document/9993021/) (UIUC, 2022) (IEEE CDC 2022) - [Control-oriented modeling of bend propagation in an octopus arm](https://ieeexplore.ieee.org/abstract/document/9867689/) (UIUC, 2021) (IEEE ACC 2022) @@ -93,16 +101,17 @@ We ask that any publications which use Elastica cite as following: 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)). +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) -PyElastica is developed by the [Gazzola Lab][link-lab-website] at the University of Illinois at Urbana-Champaign. +PyElastica is developed by the [Gazzola Lab][link-lab-website] at the University of Illinois Urbana-Champaign. ## Senior Developers ✨ _Names arranged alphabetically_ +- Ali Albazroun - Arman Tekinalp - Chia-Hsien Shih (Cathy) - Fan Kiat Chan diff --git a/docs/api/connections.rst b/docs/api/connections.rst index 42b8e3af4..b616b4cad 100644 --- a/docs/api/connections.rst +++ b/docs/api/connections.rst @@ -1,4 +1,4 @@ -Connections / Contact / Joints +Connections / Joints ============================== .. _connections: @@ -8,44 +8,34 @@ Connections / Contact / Joints Description ----------- -.. rubric:: Available Connection/Contact/Joints +.. rubric:: Available Connections/Joints .. autosummary:: :nosignatures: FreeJoint - ExternalContact FixedJoint HingeJoint - SelfContact Compatibility ~~~~~~~~~~~~~ -=============================== ==== =========== -Connection / Contact / Joints Rod Rigid Body -=============================== ==== =========== +=============================== ==== =========== +Connection / Joints Rod Rigid Body +=============================== ==== =========== FreeJoint ✅ ❌ -ExternalContact ✅ ❌ FixedJoint ✅ ❌ HingeJoint ✅ ❌ -SelfContact ✅ ❌ -=============================== ==== =========== +=============================== ==== =========== -Built-in Connection / Contact / Joint +Built-in Connection / Joint ------------------------------------- .. autoclass:: FreeJoint - :special-members: __init__ - -.. autoclass:: ExternalContact - :special-members: __init__ + :special-members: __init__,apply_forces,apply_torques .. autoclass:: FixedJoint - :special-members: __init__ + :special-members: __init__,apply_forces,apply_torques .. autoclass:: HingeJoint - :special-members: __init__ - -.. autoclass:: SelfContact - :special-members: __init__ + :special-members: __init__,apply_forces,apply_torques diff --git a/docs/api/contact.rst b/docs/api/contact.rst new file mode 100644 index 000000000..e3b2b0099 --- /dev/null +++ b/docs/api/contact.rst @@ -0,0 +1,51 @@ +Contact +============================== + +.. _contact: + +.. automodule:: elastica.contact_forces + +Description +----------- + +.. rubric:: Available Contact Classes + +.. autosummary:: + :nosignatures: + + NoContact + RodRodContact + RodCylinderContact + RodSelfContact + RodSphereContact + RodPlaneContact + RodPlaneContactWithAnisotropicFriction + CylinderPlaneContact + + +Built-in Contact Classes +------------------------------------- + +.. autoclass:: NoContact + :special-members: __init__,apply_contact + +.. autoclass:: RodRodContact + :special-members: __init__,apply_contact + +.. autoclass:: RodCylinderContact + :special-members: __init__,apply_contact + +.. autoclass:: RodSelfContact + :special-members: __init__,apply_contact + +.. autoclass:: RodSphereContact + :special-members: __init__,apply_contact + +.. autoclass:: RodPlaneContact + :special-members: __init__,apply_contact + +.. autoclass:: RodPlaneContactWithAnisotropicFriction + :special-members: __init__,apply_contact + +.. autoclass:: CylinderPlaneContact + :special-members: __init__,apply_contact diff --git a/docs/api/external_forces.rst b/docs/api/external_forces.rst index 8f84f66fd..f34c05352 100644 --- a/docs/api/external_forces.rst +++ b/docs/api/external_forces.rst @@ -63,25 +63,25 @@ Built-in External Forces :noindex: .. autoclass:: NoForces - :special-members: __init__ + :special-members: __init__,apply_forces,apply_torques .. autoclass:: EndpointForces - :special-members: __init__ + :special-members: __init__,apply_forces,apply_torques .. autoclass:: GravityForces - :special-members: __init__ + :special-members: __init__,apply_forces,apply_torques .. autoclass:: UniformForces - :special-members: __init__ + :special-members: __init__,apply_forces,apply_torques .. autoclass:: UniformTorques - :special-members: __init__ + :special-members: __init__,apply_forces,apply_torques .. autoclass:: MuscleTorques - :special-members: __init__ + :special-members: __init__,apply_forces,apply_torques .. autoclass:: EndpointForcesSinusoidal - :special-members: __init__ + :special-members: __init__,apply_forces,apply_torques Built-in Environment Interactions --------------------------------- diff --git a/docs/api/surface.rst b/docs/api/surface.rst new file mode 100644 index 000000000..55d4615d2 --- /dev/null +++ b/docs/api/surface.rst @@ -0,0 +1,16 @@ +Surface +========== + ++----------+----+ +| type | | ++==========+====+ +| plane | | ++----------+----+ + +.. automodule:: elastica.surface.surface_base + :members: + :exclude-members: __weakref__ + +.. automodule:: elastica.surface.plane + :members: + :exclude-members: __weakref__ diff --git a/docs/conf.py b/docs/conf.py index d468c65fc..6d2047961 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,7 @@ # -- Project information ----------------------------------------------------- project = 'PyElastica' -copyright = '2023, Gazzola Lab' +copyright = '2024, Gazzola Lab' author = 'Gazzola Lab' # The full version, including alpha/beta/rc tags diff --git a/docs/index.rst b/docs/index.rst index 1bf57f219..59e639ad9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -54,9 +54,11 @@ If you are interested to contribute, please read `contribution-guide`_ first. api/rods api/rigidbody + api/surface api/constraints api/external_forces api/connections + api/contact api/callback api/time_steppers api/damping diff --git a/docs/intro_page.rst.inc b/docs/intro_page.rst.inc index df54d1050..16f631f9e 100644 --- a/docs/intro_page.rst.inc +++ b/docs/intro_page.rst.inc @@ -48,7 +48,7 @@ Details can be found `here ` Elastica++ ========== -Elastica++ is a C++ implementation of Elastica. The expected release date for the beta version is 2023 Q4. +Elastica++ is a C++ implementation of Elastica. The expected release date for the beta version is 2024 Q4. .. _project website: https://cosseratrods.org .. _GitHub repo: https://github.com/GazzolaLab/PyElastica diff --git a/docs/overview/installation.md b/docs/overview/installation.md index 1355d9cfa..5201fa554 100644 --- a/docs/overview/installation.md +++ b/docs/overview/installation.md @@ -2,7 +2,7 @@ ## Instruction -PyElastica requires Python 3.8 - 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). +PyElastica requires Python 3.8 - 3.11, 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). :::{note} Python version above 3.8 is tested only in Ubuntu and Mac OS. For Windows 10, some of the dependencies were not yet compatible. diff --git a/elastica/__init__.py b/elastica/__init__.py index f60bc902b..e96e00570 100644 --- a/elastica/__init__.py +++ b/elastica/__init__.py @@ -1,10 +1,17 @@ from collections import defaultdict -from elastica.rod.knot_theory import KnotTheory, KnotTheoryCompatibleProtocol +from elastica.rod.knot_theory import ( + KnotTheory, + KnotTheoryCompatibleProtocol, + compute_link, + compute_twist, + compute_writhe, +) from elastica.rod.rod_base import RodBase from elastica.rod.cosserat_rod import CosseratRod from elastica.rigidbody.rigid_body import RigidBodyBase from elastica.rigidbody.cylinder import Cylinder from elastica.rigidbody.sphere import Sphere +from elastica.surface.plane import Plane from elastica.boundary_conditions import ( ConstraintBase, FreeBC, @@ -36,6 +43,16 @@ HingeJoint, SelfContact, ) +from elastica.contact_forces import ( + NoContact, + RodRodContact, + RodCylinderContact, + RodSelfContact, + RodSphereContact, + RodPlaneContact, + RodPlaneContactWithAnisotropicFriction, + CylinderPlaneContact, +) from elastica.callback_functions import CallBackBaseClass, ExportCallBack, MyCallBack from elastica.dissipation import ( DamperBase, @@ -48,6 +65,8 @@ from elastica.modules.constraints import Constraints from elastica.modules.forcing import Forcing from elastica.modules.damping import Damping +from elastica.modules.contact import Contact + from elastica.transformations import inv_skew_symmetrize from elastica.transformations import rotate from elastica._calculus import ( @@ -60,7 +79,7 @@ ) from elastica._linalg import levi_civita_tensor from elastica.utils import isqrt -from elastica.typing import RodType, SystemType +from elastica.typing import RodType, SystemType, AllowedContactType from elastica.timestepper import ( integrate, PositionVerlet, diff --git a/elastica/_contact_functions.py b/elastica/_contact_functions.py new file mode 100644 index 000000000..245d92319 --- /dev/null +++ b/elastica/_contact_functions.py @@ -0,0 +1,848 @@ +__doc__ = """ Numba implementation module containing contact force calculation functions between rods and rigid bodies and other rods, rigid bodies or surfaces.""" + +from elastica.contact_utils import ( + _dot_product, + _norm, + _find_min_dist, + _find_slipping_elements, + _node_to_element_mass_or_force, + _elements_to_nodes_inplace, + _node_to_element_position, + _node_to_element_velocity, +) +from elastica._linalg import ( + _batch_matvec, + _batch_cross, + _batch_norm, + _batch_dot, + _batch_product_i_k_to_ik, + _batch_product_i_ik_to_k, + _batch_product_k_ik_to_ik, + _batch_vector_sum, + _batch_matrix_transpose, + _batch_vec_oneD_vec_cross, +) +import numba +import numpy as np + + +@numba.njit(cache=True) +def _calculate_contact_forces_rod_cylinder( + x_collection_rod, + edge_collection_rod, + x_cylinder_center, + x_cylinder_tip, + edge_cylinder, + radii_sum, + length_sum, + internal_forces_rod, + external_forces_rod, + external_forces_cylinder, + external_torques_cylinder, + cylinder_director_collection, + velocity_rod, + velocity_cylinder, + contact_k, + contact_nu, + velocity_damping_coefficient, + friction_coefficient, +) -> None: + # We already pass in only the first n_elem x + n_points = x_collection_rod.shape[1] + cylinder_total_contact_forces = np.zeros((3)) + cylinder_total_contact_torques = np.zeros((3)) + for i in range(n_points): + # Element-wise bounding box + x_selected = x_collection_rod[..., i] + # x_cylinder is already a (,) array from outised + del_x = x_selected - x_cylinder_tip + norm_del_x = _norm(del_x) + + # If outside then don't process + if norm_del_x >= (radii_sum[i] + length_sum[i]): + continue + + # find the shortest line segment between the two centerline + # segments : differs from normal cylinder-cylinder intersection + distance_vector, x_cylinder_contact_point, _ = _find_min_dist( + x_selected, edge_collection_rod[..., i], x_cylinder_tip, edge_cylinder + ) + distance_vector_length = _norm(distance_vector) + distance_vector /= distance_vector_length + + gamma = radii_sum[i] - distance_vector_length + + # If distance is large, don't worry about it + if gamma < -1e-5: + continue + + # CHECK FOR GAMMA > 0.0, heaviside but we need to overload it in numba + # As a quick fix, use this instead + mask = (gamma > 0.0) * 1.0 + + # Compute contact spring force + contact_force = contact_k * gamma * distance_vector + interpenetration_velocity = velocity_cylinder[..., 0] - 0.5 * ( + velocity_rod[..., i] + velocity_rod[..., i + 1] + ) + # Compute contact damping + normal_interpenetration_velocity = ( + _dot_product(interpenetration_velocity, distance_vector) * distance_vector + ) + contact_damping_force = -contact_nu * normal_interpenetration_velocity + + # magnitude* direction + net_contact_force = 0.5 * mask * (contact_damping_force + contact_force) + + # Compute friction + slip_interpenetration_velocity = ( + interpenetration_velocity - normal_interpenetration_velocity + ) + slip_interpenetration_velocity_mag = np.linalg.norm( + slip_interpenetration_velocity + ) + slip_interpenetration_velocity_unitized = slip_interpenetration_velocity / ( + slip_interpenetration_velocity_mag + 1e-14 + ) + # Compute friction force in the slip direction. + damping_force_in_slip_direction = ( + velocity_damping_coefficient * slip_interpenetration_velocity_mag + ) + # Compute Coulombic friction + coulombic_friction_force = friction_coefficient * np.linalg.norm( + net_contact_force + ) + # Compare damping force in slip direction and kinetic friction and minimum is the friction force. + friction_force = ( + -min(damping_force_in_slip_direction, coulombic_friction_force) + * slip_interpenetration_velocity_unitized + ) + # Update contact force + net_contact_force += friction_force + + # Torques acting on the cylinder + moment_arm = x_cylinder_contact_point - x_cylinder_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 + cylinder_total_contact_forces += 2.0 * net_contact_force + cylinder_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 + cylinder_total_contact_forces += 2.0 * net_contact_force + cylinder_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 + cylinder_total_contact_forces += 2.0 * net_contact_force + cylinder_total_contact_torques += np.cross( + moment_arm, 2.0 * net_contact_force + ) + + # Update the cylinder external forces and torques + external_forces_cylinder[..., 0] += cylinder_total_contact_forces + external_torques_cylinder[..., 0] += ( + cylinder_director_collection @ cylinder_total_contact_torques + ) + + +@numba.njit(cache=True) +def _calculate_contact_forces_rod_rod( + x_collection_rod_one, + radius_rod_one, + length_rod_one, + tangent_rod_one, + velocity_rod_one, + internal_forces_rod_one, + external_forces_rod_one, + x_collection_rod_two, + radius_rod_two, + length_rod_two, + tangent_rod_two, + velocity_rod_two, + internal_forces_rod_two, + external_forces_rod_two, + contact_k, + contact_nu, +) -> None: + # 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] + edge_collection_rod_one = _batch_product_k_ik_to_ik(length_rod_one, tangent_rod_one) + edge_collection_rod_two = _batch_product_k_ik_to_ik(length_rod_two, tangent_rod_two) + + for i in range(n_points_rod_one): + for j in range(n_points_rod_two): + radii_sum = radius_rod_one[i] + radius_rod_two[j] + length_sum = length_rod_one[i] + length_rod_two[j] + # Element-wise bounding box + x_selected_rod_one = x_collection_rod_one[..., i] + x_selected_rod_two = x_collection_rod_two[..., j] + + del_x = x_selected_rod_one - x_selected_rod_two + norm_del_x = _norm(del_x) + + # If outside then don't process + if norm_del_x >= (radii_sum + length_sum): + continue + + # find the shortest line segment between the two centerline + # segments : differs from normal cylinder-cylinder intersection + distance_vector, _, _ = _find_min_dist( + x_selected_rod_one, + edge_collection_rod_one[..., i], + x_selected_rod_two, + edge_collection_rod_two[..., j], + ) + distance_vector_length = _norm(distance_vector) + distance_vector /= distance_vector_length + gamma = radii_sum - distance_vector_length + + # If distance is large, don't worry about it + if gamma < -1e-5: + continue + + rod_one_elemental_forces = 0.5 * ( + external_forces_rod_one[..., i] + + external_forces_rod_one[..., i + 1] + + internal_forces_rod_one[..., i] + + internal_forces_rod_one[..., i + 1] + ) + + rod_two_elemental_forces = 0.5 * ( + external_forces_rod_two[..., j] + + external_forces_rod_two[..., j + 1] + + internal_forces_rod_two[..., j] + + internal_forces_rod_two[..., j + 1] + ) + + equilibrium_forces = -rod_one_elemental_forces + rod_two_elemental_forces + + """FIX ME: Remove normal force and tune rod-rod contact example""" + normal_force = _dot_product(equilibrium_forces, distance_vector) + # Following line same as np.where(normal_force < 0.0, -normal_force, 0.0) + normal_force = abs(min(normal_force, 0.0)) + + # CHECK FOR GAMMA > 0.0, heaviside but we need to overload it in numba + # As a quick fix, use this instead + mask = (gamma > 0.0) * 1.0 + + contact_force = contact_k * gamma + interpenetration_velocity = 0.5 * ( + (velocity_rod_one[..., i] + velocity_rod_one[..., i + 1]) + - (velocity_rod_two[..., j] + velocity_rod_two[..., j + 1]) + ) + contact_damping_force = contact_nu * _dot_product( + interpenetration_velocity, distance_vector + ) + + # magnitude* direction + net_contact_force = ( + normal_force + 0.5 * mask * (contact_damping_force + contact_force) + ) * distance_vector + + # Add it to the rods at the end of the day + if i == 0: + external_forces_rod_one[..., i] -= net_contact_force * 2 / 3 + external_forces_rod_one[..., i + 1] -= net_contact_force * 4 / 3 + elif i == n_points_rod_one - 1: + external_forces_rod_one[..., i] -= net_contact_force * 4 / 3 + external_forces_rod_one[..., i + 1] -= net_contact_force * 2 / 3 + else: + external_forces_rod_one[..., i] -= net_contact_force + external_forces_rod_one[..., i + 1] -= net_contact_force + + if j == 0: + external_forces_rod_two[..., j] += net_contact_force * 2 / 3 + external_forces_rod_two[..., j + 1] += net_contact_force * 4 / 3 + elif j == n_points_rod_two - 1: + external_forces_rod_two[..., j] += net_contact_force * 4 / 3 + external_forces_rod_two[..., j + 1] += net_contact_force * 2 / 3 + else: + external_forces_rod_two[..., j] += net_contact_force + external_forces_rod_two[..., j + 1] += net_contact_force + + +@numba.njit(cache=True) +def _calculate_contact_forces_self_rod( + x_collection_rod, + radius_rod, + length_rod, + tangent_rod, + velocity_rod, + external_forces_rod, + contact_k, + contact_nu, +) -> None: + # 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) + + for i in range(n_points_rod): + skip = int(1 + np.ceil(0.8 * np.pi * radius_rod[i] / length_rod[i])) + for j in range(i - skip, -1, -1): + radii_sum = radius_rod[i] + radius_rod[j] + length_sum = length_rod[i] + length_rod[j] + # Element-wise bounding box + x_selected_rod_index_i = x_collection_rod[..., i] + x_selected_rod_index_j = x_collection_rod[..., j] + + del_x = x_selected_rod_index_i - x_selected_rod_index_j + norm_del_x = _norm(del_x) + + # If outside then don't process + if norm_del_x >= (radii_sum + length_sum): + continue + + # find the shortest line segment between the two centerline + # segments : differs from normal cylinder-cylinder intersection + distance_vector, _, _ = _find_min_dist( + x_selected_rod_index_i, + edge_collection_rod_one[..., i], + x_selected_rod_index_j, + edge_collection_rod_one[..., j], + ) + distance_vector_length = _norm(distance_vector) + distance_vector /= distance_vector_length + + gamma = radii_sum - distance_vector_length + + # If distance is large, don't worry about it + if gamma < -1e-5: + continue + + # CHECK FOR GAMMA > 0.0, heaviside but we need to overload it in numba + # As a quick fix, use this instead + mask = (gamma > 0.0) * 1.0 + + contact_force = contact_k * gamma + interpenetration_velocity = 0.5 * ( + (velocity_rod[..., i] + velocity_rod[..., i + 1]) + - (velocity_rod[..., j] + velocity_rod[..., j + 1]) + ) + contact_damping_force = contact_nu * _dot_product( + interpenetration_velocity, distance_vector + ) + + # magnitude* direction + net_contact_force = ( + 0.5 * mask * (contact_damping_force + contact_force) + ) * distance_vector + + # Add it to the rods at the end of the day + # if i == 0: + # external_forces_rod[...,i] -= net_contact_force *2/3 + # external_forces_rod[...,i+1] -= net_contact_force * 4/3 + if i == n_points_rod - 1: + external_forces_rod[..., i] -= net_contact_force * 4 / 3 + external_forces_rod[..., i + 1] -= net_contact_force * 2 / 3 + else: + external_forces_rod[..., i] -= net_contact_force + external_forces_rod[..., i + 1] -= net_contact_force + + if j == 0: + external_forces_rod[..., j] += net_contact_force * 2 / 3 + external_forces_rod[..., j + 1] += net_contact_force * 4 / 3 + # elif j == n_points_rod: + # external_forces_rod[..., j] += net_contact_force * 4/3 + # external_forces_rod[..., j+1] += net_contact_force * 2/3 + else: + external_forces_rod[..., j] += net_contact_force + external_forces_rod[..., j + 1] += net_contact_force + + +@numba.njit(cache=True) +def _calculate_contact_forces_rod_sphere( + x_collection_rod, + edge_collection_rod, + x_sphere_center, + x_sphere_tip, + edge_sphere, + radii_sum, + length_sum, + internal_forces_rod, + external_forces_rod, + external_forces_sphere, + external_torques_sphere, + sphere_director_collection, + velocity_rod, + velocity_sphere, + contact_k, + contact_nu, + velocity_damping_coefficient, + friction_coefficient, +) -> None: + # 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 + norm_del_x = _norm(del_x) + + # If outside then don't process + if norm_del_x >= (radii_sum[i] + length_sum[i]): + 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_length = _norm(distance_vector) + distance_vector /= distance_vector_length + + gamma = radii_sum[i] - distance_vector_length + + # If distance is large, don't worry about it + if gamma < -1e-5: + continue + + # CHECK FOR GAMMA > 0.0, heaviside but we need to overload it in numba + # As a quick fix, use this instead + mask = (gamma > 0.0) * 1.0 + + # Compute contact spring force + contact_force = contact_k * gamma * distance_vector + interpenetration_velocity = velocity_sphere[..., 0] - 0.5 * ( + velocity_rod[..., i] + velocity_rod[..., i + 1] + ) + # Compute contact damping + normal_interpenetration_velocity = ( + _dot_product(interpenetration_velocity, distance_vector) * distance_vector + ) + contact_damping_force = -contact_nu * normal_interpenetration_velocity + + # magnitude* direction + net_contact_force = 0.5 * mask * (contact_damping_force + contact_force) + + # Compute friction + slip_interpenetration_velocity = ( + interpenetration_velocity - normal_interpenetration_velocity + ) + slip_interpenetration_velocity_mag = np.linalg.norm( + slip_interpenetration_velocity + ) + slip_interpenetration_velocity_unitized = slip_interpenetration_velocity / ( + slip_interpenetration_velocity_mag + 1e-14 + ) + # Compute friction force in the slip direction. + damping_force_in_slip_direction = ( + velocity_damping_coefficient * slip_interpenetration_velocity_mag + ) + # Compute Coulombic friction + coulombic_friction_force = friction_coefficient * np.linalg.norm( + net_contact_force + ) + # Compare damping force in slip direction and kinetic friction and minimum is the friction force. + friction_force = ( + -min(damping_force_in_slip_direction, coulombic_friction_force) + * slip_interpenetration_velocity_unitized + ) + # 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 + ) + + +@numba.njit(cache=True) +def _calculate_contact_forces_rod_plane( + plane_origin, + plane_normal, + surface_tol, + k, + nu, + radius, + mass, + position_collection, + velocity_collection, + internal_forces, + external_forces, +): + """ + 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. + + Parameters + ---------- + system + + Returns + ------- + magnitude of the plane response + """ + + # Compute plane response force + nodal_total_forces = _batch_vector_sum(internal_forces, external_forces) + element_total_forces = _node_to_element_mass_or_force(nodal_total_forces) + + force_component_along_normal_direction = _batch_product_i_ik_to_k( + plane_normal, element_total_forces + ) + forces_along_normal_direction = _batch_product_i_k_to_ik( + plane_normal, force_component_along_normal_direction + ) + + # If the total force component along the plane normal direction is greater than zero that means, + # total force is pushing rod away from the plane not towards the plane. Thus, response force + # applied by the surface has to be zero. + forces_along_normal_direction[ + ..., np.where(force_component_along_normal_direction > 0)[0] + ] = 0.0 + # Compute response force on the element. Plane response force + # has to be away from the surface and towards the element. Thus + # multiply forces along normal direction with negative sign. + plane_response_force = -forces_along_normal_direction + + # Elastic force response due to penetration + element_position = _node_to_element_position(position_collection) + distance_from_plane = _batch_product_i_ik_to_k( + plane_normal, (element_position - plane_origin) + ) + plane_penetration = np.minimum(distance_from_plane - radius, 0.0) + elastic_force = -k * _batch_product_i_k_to_ik(plane_normal, plane_penetration) + + # Damping force response due to velocity towards the plane + element_velocity = _node_to_element_velocity( + mass=mass, node_velocity_collection=velocity_collection + ) + normal_component_of_element_velocity = _batch_product_i_ik_to_k( + plane_normal, element_velocity + ) + damping_force = -nu * _batch_product_i_k_to_ik( + plane_normal, normal_component_of_element_velocity + ) + + # Compute total plane response force + plane_response_force_total = plane_response_force + elastic_force + damping_force + + # Check if the rod elements are in contact with plane. + no_contact_point_idx = np.where((distance_from_plane - radius) > surface_tol)[0] + # If rod element does not have any contact with plane, plane cannot apply response + # force on the element. Thus lets set plane response force to 0.0 for the no contact points. + plane_response_force[..., no_contact_point_idx] = 0.0 + plane_response_force_total[..., no_contact_point_idx] = 0.0 + + # Update the external forces + _elements_to_nodes_inplace(plane_response_force_total, external_forces) + + return (_batch_norm(plane_response_force), no_contact_point_idx) + + +@numba.njit(cache=True) +def _calculate_contact_forces_rod_plane_with_anisotropic_friction( + plane_origin, + plane_normal, + surface_tol, + slip_velocity_tol, + k, + nu, + kinetic_mu_forward, + kinetic_mu_backward, + kinetic_mu_sideways, + static_mu_forward, + static_mu_backward, + static_mu_sideways, + radius, + mass, + tangents, + position_collection, + director_collection, + velocity_collection, + omega_collection, + internal_forces, + external_forces, + internal_torques, + external_torques, +): + ( + plane_response_force_mag, + no_contact_point_idx, + ) = _calculate_contact_forces_rod_plane( + plane_origin, + plane_normal, + surface_tol, + k, + nu, + radius, + mass, + position_collection, + velocity_collection, + internal_forces, + external_forces, + ) + + # First compute component of rod tangent in plane. Because friction forces acts in plane not out of plane. Thus + # axial direction has to be in plane, it cannot be out of plane. We are projecting rod element tangent vector in + # to the plane. So friction forces can only be in plane forces and not out of plane. + tangent_along_normal_direction = _batch_product_i_ik_to_k(plane_normal, tangents) + tangent_perpendicular_to_normal_direction = tangents - _batch_product_i_k_to_ik( + plane_normal, tangent_along_normal_direction + ) + tangent_perpendicular_to_normal_direction_mag = _batch_norm( + tangent_perpendicular_to_normal_direction + ) + # Normalize tangent_perpendicular_to_normal_direction. This is axial direction for plane. Here we are adding + # small tolerance (1e-10) for normalization, in order to prevent division by 0. + axial_direction = _batch_product_k_ik_to_ik( + 1 / (tangent_perpendicular_to_normal_direction_mag + 1e-14), + tangent_perpendicular_to_normal_direction, + ) + element_velocity = _node_to_element_velocity( + mass=mass, node_velocity_collection=velocity_collection + ) + # first apply axial kinetic friction + velocity_mag_along_axial_direction = _batch_dot(element_velocity, axial_direction) + velocity_along_axial_direction = _batch_product_k_ik_to_ik( + velocity_mag_along_axial_direction, axial_direction + ) + + # Friction forces depends on the direction of velocity, in other words sign + # of the velocity vector. + velocity_sign_along_axial_direction = np.sign(velocity_mag_along_axial_direction) + # Check top for sign convention + kinetic_mu = 0.5 * ( + kinetic_mu_forward * (1 + velocity_sign_along_axial_direction) + + kinetic_mu_backward * (1 - velocity_sign_along_axial_direction) + ) + # Call slip function to check if elements slipping or not + slip_function_along_axial_direction = _find_slipping_elements( + velocity_along_axial_direction, slip_velocity_tol + ) + # Now rolling kinetic friction + rolling_direction = _batch_vec_oneD_vec_cross(axial_direction, plane_normal) + torque_arm = _batch_product_i_k_to_ik(-plane_normal, radius) + velocity_along_rolling_direction = _batch_dot(element_velocity, rolling_direction) + directors_transpose = _batch_matrix_transpose(director_collection) + # w_rot = Q.T @ omega @ Q @ r + rotation_velocity = _batch_matvec( + directors_transpose, + _batch_cross(omega_collection, _batch_matvec(director_collection, torque_arm)), + ) + rotation_velocity_along_rolling_direction = _batch_dot( + rotation_velocity, rolling_direction + ) + slip_velocity_mag_along_rolling_direction = ( + velocity_along_rolling_direction + rotation_velocity_along_rolling_direction + ) + slip_velocity_along_rolling_direction = _batch_product_k_ik_to_ik( + slip_velocity_mag_along_rolling_direction, rolling_direction + ) + slip_function_along_rolling_direction = _find_slipping_elements( + slip_velocity_along_rolling_direction, slip_velocity_tol + ) + # Compute unitized total slip velocity vector. We will use this to distribute the weight of the rod in axial + # and rolling directions. + unitized_total_velocity = ( + slip_velocity_along_rolling_direction + velocity_along_axial_direction + ) + unitized_total_velocity /= _batch_norm(unitized_total_velocity + 1e-14) + # Apply kinetic friction in axial direction. + kinetic_friction_force_along_axial_direction = -( + (1.0 - slip_function_along_axial_direction) + * kinetic_mu + * plane_response_force_mag + * _batch_dot(unitized_total_velocity, axial_direction) + * axial_direction + ) + # If rod element does not have any contact with plane, plane cannot apply friction + # force on the element. Thus lets set kinetic friction force to 0.0 for the no contact points. + kinetic_friction_force_along_axial_direction[..., no_contact_point_idx] = 0.0 + _elements_to_nodes_inplace( + kinetic_friction_force_along_axial_direction, external_forces + ) + # Apply kinetic friction in rolling direction. + kinetic_friction_force_along_rolling_direction = -( + (1.0 - slip_function_along_rolling_direction) + * kinetic_mu_sideways + * plane_response_force_mag + * _batch_dot(unitized_total_velocity, rolling_direction) + * rolling_direction + ) + # If rod element does not have any contact with plane, plane cannot apply friction + # force on the element. Thus lets set kinetic friction force to 0.0 for the no contact points. + kinetic_friction_force_along_rolling_direction[..., no_contact_point_idx] = 0.0 + _elements_to_nodes_inplace( + kinetic_friction_force_along_rolling_direction, external_forces + ) + # torque = Q @ r @ Fr + external_torques += _batch_matvec( + director_collection, + _batch_cross(torque_arm, kinetic_friction_force_along_rolling_direction), + ) + + # now axial static friction + nodal_total_forces = _batch_vector_sum(internal_forces, external_forces) + element_total_forces = _node_to_element_mass_or_force(nodal_total_forces) + force_component_along_axial_direction = _batch_dot( + element_total_forces, axial_direction + ) + force_component_sign_along_axial_direction = np.sign( + force_component_along_axial_direction + ) + # check top for sign convention + static_mu = 0.5 * ( + static_mu_forward * (1 + force_component_sign_along_axial_direction) + + static_mu_backward * (1 - force_component_sign_along_axial_direction) + ) + max_friction_force = ( + slip_function_along_axial_direction * static_mu * plane_response_force_mag + ) + # friction = min(mu N, pushing force) + static_friction_force_along_axial_direction = -( + np.minimum(np.fabs(force_component_along_axial_direction), max_friction_force) + * force_component_sign_along_axial_direction + * axial_direction + ) + # If rod element does not have any contact with plane, plane cannot apply friction + # force on the element. Thus lets set static friction force to 0.0 for the no contact points. + static_friction_force_along_axial_direction[..., no_contact_point_idx] = 0.0 + _elements_to_nodes_inplace( + static_friction_force_along_axial_direction, external_forces + ) + + # now rolling static friction + # there is some normal, tangent and rolling directions inconsitency from Elastica + total_torques = _batch_matvec( + directors_transpose, (internal_torques + external_torques) + ) + # Elastica has opposite defs of tangents in interaction.h and rod.cpp + total_torques_along_axial_direction = _batch_dot(total_torques, axial_direction) + force_component_along_rolling_direction = _batch_dot( + element_total_forces, rolling_direction + ) + noslip_force = -( + ( + radius * force_component_along_rolling_direction + - 2.0 * total_torques_along_axial_direction + ) + / 3.0 + / radius + ) + max_friction_force = ( + slip_function_along_rolling_direction + * static_mu_sideways + * plane_response_force_mag + ) + noslip_force_sign = np.sign(noslip_force) + static_friction_force_along_rolling_direction = ( + np.minimum(np.fabs(noslip_force), max_friction_force) + * noslip_force_sign + * rolling_direction + ) + # If rod element does not have any contact with plane, plane cannot apply friction + # force on the element. Thus lets set plane static friction force to 0.0 for the no contact points. + static_friction_force_along_rolling_direction[..., no_contact_point_idx] = 0.0 + _elements_to_nodes_inplace( + static_friction_force_along_rolling_direction, external_forces + ) + external_torques += _batch_matvec( + director_collection, + _batch_cross(torque_arm, static_friction_force_along_rolling_direction), + ) + + +@numba.njit(cache=True) +def _calculate_contact_forces_cylinder_plane( + plane_origin, + plane_normal, + surface_tol, + k, + nu, + length, + position_collection, + velocity_collection, + external_forces, +): + + # Compute plane response force + # total_forces = system.internal_forces + system.external_forces + total_forces = external_forces + force_component_along_normal_direction = _batch_product_i_ik_to_k( + plane_normal, total_forces + ) + forces_along_normal_direction = _batch_product_i_k_to_ik( + plane_normal, force_component_along_normal_direction + ) + # If the total force component along the plane normal direction is greater than zero that means, + # total force is pushing rod away from the plane not towards the plane. Thus, response force + # applied by the surface has to be zero. + forces_along_normal_direction[ + ..., np.where(force_component_along_normal_direction > 0)[0] + ] = 0.0 + # Compute response force on the element. Plane response force + # has to be away from the surface and towards the element. Thus + # multiply forces along normal direction with negative sign. + plane_response_force = -forces_along_normal_direction + + # Elastic force response due to penetration + element_position = position_collection + distance_from_plane = _batch_product_i_ik_to_k( + plane_normal, (element_position - plane_origin) + ) + plane_penetration = np.minimum(distance_from_plane - length / 2, 0.0) + elastic_force = -k * _batch_product_i_k_to_ik(plane_normal, plane_penetration) + + # Damping force response due to velocity towards the plane + element_velocity = velocity_collection + normal_component_of_element_velocity = _batch_product_i_ik_to_k( + plane_normal, element_velocity + ) + damping_force = -nu * _batch_product_i_k_to_ik( + plane_normal, normal_component_of_element_velocity + ) + + # Compute total plane response force + plane_response_force_total = plane_response_force + elastic_force + damping_force + + # Check if the rigid body is in contact with plane. + no_contact_point_idx = np.where((distance_from_plane - length / 2) > surface_tol)[0] + # If rod element does not have any contact with plane, plane cannot apply response + # force on the element. Thus lets set plane response force to 0.0 for the no contact points. + plane_response_force[..., no_contact_point_idx] = 0.0 + plane_response_force_total[..., no_contact_point_idx] = 0.0 + + # Update the external forces + external_forces += plane_response_force_total + + return (_batch_norm(plane_response_force), no_contact_point_idx) diff --git a/elastica/contact_forces.py b/elastica/contact_forces.py new file mode 100644 index 000000000..8f9b0ab5e --- /dev/null +++ b/elastica/contact_forces.py @@ -0,0 +1,845 @@ +__doc__ = """ Numba implementation module containing contact between rods and rigid bodies and other rods rigid bodies or surfaces.""" + +from elastica.typing import RodType, SystemType, AllowedContactType +from elastica.rod import RodBase +from elastica.rigidbody import Cylinder, Sphere +from elastica.surface import Plane +from elastica.contact_utils import ( + _prune_using_aabbs_rod_cylinder, + _prune_using_aabbs_rod_rod, + _prune_using_aabbs_rod_sphere, +) +from elastica._contact_functions import ( + _calculate_contact_forces_rod_cylinder, + _calculate_contact_forces_rod_rod, + _calculate_contact_forces_self_rod, + _calculate_contact_forces_rod_sphere, + _calculate_contact_forces_rod_plane, + _calculate_contact_forces_rod_plane_with_anisotropic_friction, + _calculate_contact_forces_cylinder_plane, +) +import numpy as np + + +class NoContact: + """ + This is the base class for contact applied between rod-like objects and allowed contact objects. + + Notes + ----- + Every new contact class must be derived + from NoContact class. + + """ + + def __init__(self): + """ + NoContact class does not need any input parameters. + """ + + def _check_systems_validity( + self, + system_one: SystemType, + system_two: AllowedContactType, + ) -> None: + """ + This checks the contact order between a SystemType object and an AllowedContactType object, the order should follow: Rod, Rigid body, Surface. + In NoContact class, this just checks if system_two is a rod then system_one must be a rod. + + + Parameters + ---------- + system_one + SystemType + system_two + AllowedContactType + """ + if issubclass(system_two.__class__, RodBase): + if not issubclass(system_one.__class__, RodBase): + raise TypeError( + "Systems provided to the contact class have incorrect order. \n" + " First system is {0} and second system is {1}. \n" + " If the first system is a rod, the second system can be a rod, rigid body or surface. \n" + " If the first system is a rigid body, the second system can be a rigid body or surface.".format( + system_one.__class__, system_two.__class__ + ) + ) + + def apply_contact( + self, + system_one: SystemType, + system_two: AllowedContactType, + ) -> None: + """ + Apply contact forces and torques between SystemType object and AllowedContactType object. + + In NoContact class, this routine simply passes. + + Parameters + ---------- + system_one : SystemType + Rod or rigid-body object + system_two : AllowedContactType + Rod, rigid-body, or surface object + """ + pass + + +class RodRodContact(NoContact): + """ + This class is for applying contact forces between rod-rod. + + Examples + -------- + How to define contact between rod and rod. + + >>> simulator.detect_contact_between(first_rod, second_rod).using( + ... RodRodContact, + ... k=1e4, + ... nu=10, + ... ) + + """ + + def __init__(self, k: float, nu: float): + """ + Parameters + ---------- + k : float + Contact spring constant. + nu : float + Contact damping constant. + """ + super(RodRodContact, self).__init__() + self.k = k + self.nu = nu + + def _check_systems_validity( + self, + system_one: SystemType, + system_two: AllowedContactType, + ) -> None: + """ + This checks the contact order and type of a SystemType object and an AllowedContactType object. + For the RodRodContact class both systems must be distinct rods. + + Parameters + ---------- + system_one + SystemType + system_two + AllowedContactType + """ + if not issubclass(system_one.__class__, RodBase) or not issubclass( + system_two.__class__, RodBase + ): + raise TypeError( + "Systems provided to the contact class have incorrect order. \n" + " First system is {0} and second system is {1}. \n" + " Both systems must be distinct rods".format( + system_one.__class__, system_two.__class__ + ) + ) + if system_one == system_two: + raise TypeError( + "First rod is identical to second rod. \n" + "Rods must be distinct for RodRodConact. \n" + "If you want self contact, use RodSelfContact instead" + ) + + def apply_contact(self, system_one: RodType, system_two: RodType) -> None: + """ + Apply contact forces and torques between RodType object and RodType object. + + Parameters + ---------- + system_one: object + Rod object. + system_two: object + Rod object. + + """ + # First, check for a global AABB bounding box, and see whether that + # intersects + + if _prune_using_aabbs_rod_rod( + system_one.position_collection, + system_one.radius, + system_one.lengths, + system_two.position_collection, + system_two.radius, + system_two.lengths, + ): + return + + _calculate_contact_forces_rod_rod( + system_one.position_collection[ + ..., :-1 + ], # Discount last node, we want element start position + system_one.radius, + system_one.lengths, + system_one.tangents, + system_one.velocity_collection, + system_one.internal_forces, + system_one.external_forces, + system_two.position_collection[ + ..., :-1 + ], # Discount last node, we want element start position + system_two.radius, + system_two.lengths, + system_two.tangents, + system_two.velocity_collection, + system_two.internal_forces, + system_two.external_forces, + self.k, + self.nu, + ) + + +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 + 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]_. + + Notes + ----- + The `velocity_damping_coefficient` is set to a high value (e.g. 1e4) to minimize slip and simulate stiction + (static friction), while friction_coefficient corresponds to the Coulombic friction coefficient. + + Examples + -------- + How to define contact between rod and cylinder. + + >>> simulator.detect_contact_between(rod, cylinder).using( + ... RodCylinderContact, + ... k=1e4, + ... 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__( + self, + k: float, + nu: float, + velocity_damping_coefficient=0.0, + friction_coefficient=0.0, + ): + """ + + Parameters + ---------- + k : float + Contact spring constant. + nu : float + Contact damping constant. + velocity_damping_coefficient : float + Velocity damping coefficient between rigid-body and rod contact is used to apply friction force in the + slip direction. + friction_coefficient : float + For Coulombic friction coefficient for rigid-body and rod contact. + """ + super(RodCylinderContact, self).__init__() + self.k = k + self.nu = nu + self.velocity_damping_coefficient = velocity_damping_coefficient + self.friction_coefficient = friction_coefficient + + def _check_systems_validity( + self, + system_one: SystemType, + system_two: AllowedContactType, + ) -> None: + """ + This checks the contact order and type of a SystemType object and an AllowedContactType object. + For the RodCylinderContact class first_system should be a rod and second_system should be a cylinder. + + Parameters + ---------- + system_one + SystemType + system_two + AllowedContactType + """ + if not issubclass(system_one.__class__, RodBase) or not issubclass( + system_two.__class__, Cylinder + ): + raise TypeError( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a rod, second should be a cylinder".format( + system_one.__class__, system_two.__class__ + ) + ) + + def apply_contact(self, system_one: RodType, system_two: SystemType) -> None: + # First, check for a global AABB bounding box, and see whether that + # intersects + if _prune_using_aabbs_rod_cylinder( + system_one.position_collection, + system_one.radius, + system_one.lengths, + system_two.position_collection, + system_two.director_collection, + system_two.radius[0], + system_two.length[0], + ): + return + + x_cyl = ( + system_two.position_collection[..., 0] + - 0.5 * system_two.length * system_two.director_collection[2, :, 0] + ) + + rod_element_position = 0.5 * ( + system_one.position_collection[..., 1:] + + system_one.position_collection[..., :-1] + ) + _calculate_contact_forces_rod_cylinder( + rod_element_position, + system_one.lengths * system_one.tangents, + system_two.position_collection[..., 0], + x_cyl, + system_two.length * system_two.director_collection[2, :, 0], + system_one.radius + system_two.radius, + system_one.lengths + system_two.length, + 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, + self.nu, + self.velocity_damping_coefficient, + self.friction_coefficient, + ) + + +class RodSelfContact(NoContact): + """ + This class is modeling self contact of rod. + + Examples + -------- + How to define contact rod self contact. + + >>> simulator.detect_contact_between(rod, rod).using( + ... RodSelfContact, + ... k=1e4, + ... nu=10, + ... ) + + """ + + def __init__(self, k: float, nu: float): + """ + + Parameters + ---------- + k : float + Contact spring constant. + nu : float + Contact damping constant. + """ + super(RodSelfContact, self).__init__() + self.k = k + self.nu = nu + + def _check_systems_validity( + self, + system_one: SystemType, + system_two: AllowedContactType, + ) -> None: + """ + This checks the contact order and type of a SystemType object and an AllowedContactType object. + For the RodSelfContact class first_system and second_system should be the same rod. + + Parameters + ---------- + system_one + SystemType + system_two + AllowedContactType + """ + if ( + not issubclass(system_one.__class__, RodBase) + or not issubclass(system_two.__class__, RodBase) + or system_one != system_two + ): + raise TypeError( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system and second system should be the same rod \n" + " If you want rod rod contact, use RodRodContact instead".format( + system_one.__class__, system_two.__class__ + ) + ) + + def apply_contact(self, system_one: RodType, system_two: RodType) -> None: + """ + Apply contact forces and torques between RodType object and itself. + + Parameters + ---------- + system_one: object + Rod object. + system_two: object + Rod object. + + """ + _calculate_contact_forces_self_rod( + system_one.position_collection[ + ..., :-1 + ], # Discount last node, we want element start position + system_one.radius, + system_one.lengths, + system_one.tangents, + system_one.velocity_collection, + system_one.external_forces, + self.k, + self.nu, + ) + + +class RodSphereContact(NoContact): + """ + This class is for applying contact forces between rod-sphere. + First system is always rod and second system is always sphere. + 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]_. + + Notes + ----- + The `velocity_damping_coefficient` is set to a high value (e.g. 1e4) to minimize slip and simulate stiction + (static friction), while friction_coefficient corresponds to the Coulombic friction coefficient. + + Examples + -------- + How to define contact between rod and sphere. + + >>> simulator.detect_contact_between(rod, sphere).using( + ... RodSphereContact, + ... k=1e4, + ... 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__( + self, + k: float, + nu: float, + velocity_damping_coefficient=0.0, + friction_coefficient=0.0, + ): + """ + Parameters + ---------- + k : float + Contact spring constant. + nu : float + Contact damping constant. + velocity_damping_coefficient : float + Velocity damping coefficient between rigid-body and rod contact is used to apply friction force in the + slip direction. + friction_coefficient : float + For Coulombic friction coefficient for rigid-body and rod contact. + """ + super(RodSphereContact, self).__init__() + self.k = k + self.nu = nu + self.velocity_damping_coefficient = velocity_damping_coefficient + self.friction_coefficient = friction_coefficient + + def _check_systems_validity( + self, + system_one: SystemType, + system_two: AllowedContactType, + ) -> None: + """ + This checks the contact order and type of a SystemType object and an AllowedContactType object. + For the RodSphereContact class first_system should be a rod and second_system should be a sphere. + Parameters + ---------- + system_one + SystemType + system_two + AllowedContactType + """ + if not issubclass(system_one.__class__, RodBase) or not issubclass( + system_two.__class__, Sphere + ): + raise TypeError( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a rod, second should be a sphere".format( + system_one.__class__, system_two.__class__ + ) + ) + + def apply_contact(self, system_one: RodType, system_two: SystemType) -> None: + """ + Apply contact forces and torques between RodType object and Sphere object. + + Parameters + ---------- + system_one: object + Rod object. + system_two: object + Sphere object. + + """ + # First, check for a global AABB bounding box, and see whether that + # intersects + if _prune_using_aabbs_rod_sphere( + system_one.position_collection, + system_one.radius, + system_one.lengths, + system_two.position_collection, + system_two.director_collection, + system_two.radius[0], + ): + return + + x_sph = ( + system_two.position_collection[..., 0] + - system_two.radius * system_two.director_collection[2, :, 0] + ) + + rod_element_position = 0.5 * ( + system_one.position_collection[..., 1:] + + system_one.position_collection[..., :-1] + ) + _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], + 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, + self.nu, + self.velocity_damping_coefficient, + self.friction_coefficient, + ) + + +class RodPlaneContact(NoContact): + """ + This class is for applying contact forces between rod-plane. + First system is always rod and second system is always plane. + For more details regarding the contact module refer to + Eqn 4.8 of Gazzola et al. RSoS (2018). + + Examples + -------- + How to define contact between rod and plane. + + >>> simulator.detect_contact_between(rod, plane).using( + ... RodPlaneContact, + ... k=1e4, + ... nu=10, + ... ) + """ + + def __init__( + self, + k: float, + nu: float, + ): + """ + Parameters + ---------- + k : float + Contact spring constant. + nu : float + Contact damping constant. + """ + super(RodPlaneContact, self).__init__() + self.k = k + self.nu = nu + self.surface_tol = 1e-4 + + def _check_systems_validity( + self, + system_one: SystemType, + system_two: AllowedContactType, + ) -> None: + """ + This checks the contact order and type of a SystemType object and an AllowedContactType object. + For the RodPlaneContact class first_system should be a rod and second_system should be a plane. + Parameters + ---------- + system_one + SystemType + system_two + AllowedContactType + """ + if not issubclass(system_one.__class__, RodBase) or not issubclass( + system_two.__class__, Plane + ): + raise TypeError( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a rod, second should be a plane".format( + system_one.__class__, system_two.__class__ + ) + ) + + def apply_contact(self, system_one: RodType, system_two: SystemType) -> None: + """ + Apply contact forces and torques between RodType object and Plane object. + + Parameters + ---------- + system_one: object + Rod object. + system_two: object + Plane object. + + """ + _calculate_contact_forces_rod_plane( + system_two.origin, + system_two.normal, + self.surface_tol, + self.k, + self.nu, + system_one.radius, + system_one.mass, + system_one.position_collection, + system_one.velocity_collection, + system_one.internal_forces, + system_one.external_forces, + ) + + +class RodPlaneContactWithAnisotropicFriction(NoContact): + """ + This class is for applying contact forces between rod-plane with friction. + First system is always rod and second system is always plane. + For more details regarding the contact module refer to + Eqn 4.8 of Gazzola et al. RSoS (2018). + + Examples + -------- + How to define contact between rod and plane. + + >>> simulator.detect_contact_between(rod, plane).using( + ... RodPlaneContactWithAnisotropicFriction, + ... k=1e4, + ... nu=10, + ... slip_velocity_tol = 1e-4, + ... static_mu_array = np.array([0.0,0.0,0.0]), + ... kinetic_mu_array = np.array([1.0,2.0,3.0]), + ... ) + """ + + def __init__( + self, + k: float, + nu: float, + slip_velocity_tol: float, + static_mu_array: np.ndarray, + kinetic_mu_array: np.ndarray, + ): + """ + Parameters + ---------- + k : float + Contact spring constant. + nu : float + Contact damping constant. + 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. + """ + super(RodPlaneContactWithAnisotropicFriction, self).__init__() + self.k = k + self.nu = nu + self.surface_tol = 1e-4 + self.slip_velocity_tol = 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 + + def _check_systems_validity( + self, + system_one: SystemType, + system_two: AllowedContactType, + ) -> None: + """ + This checks the contact order and type of a SystemType object and an AllowedContactType object. + For the RodSphereContact class first_system should be a rod and second_system should be a plane. + Parameters + ---------- + system_one + SystemType + system_two + AllowedContactType + """ + if not issubclass(system_one.__class__, RodBase) or not issubclass( + system_two.__class__, Plane + ): + raise TypeError( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a rod, second should be a plane".format( + system_one.__class__, system_two.__class__ + ) + ) + + def apply_contact(self, system_one: RodType, system_two: SystemType) -> None: + """ + Apply contact forces and torques between RodType object and Plane object with anisotropic friction. + + Parameters + ---------- + system_one: object + Rod object. + system_two: object + Plane object. + + """ + + _calculate_contact_forces_rod_plane_with_anisotropic_friction( + system_two.origin, + system_two.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_one.radius, + system_one.mass, + system_one.tangents, + system_one.position_collection, + system_one.director_collection, + system_one.velocity_collection, + system_one.omega_collection, + system_one.internal_forces, + system_one.external_forces, + system_one.internal_torques, + system_one.external_torques, + ) + + +class CylinderPlaneContact(NoContact): + """ + This class is for applying contact forces between cylinder-plane. + First system is always cylinder and second system is always plane. + For more details regarding the contact module refer to + Eqn 4.8 of Gazzola et al. RSoS (2018). + + Examples + -------- + How to define contact between cylinder and plane. + + >>> simulator.detect_contact_between(cylinder, plane).using( + ... CylinderPlaneContact, + ... k=1e4, + ... nu=10, + ... ) + """ + + def __init__( + self, + k: float, + nu: float, + ): + """ + Parameters + ---------- + k : float + Contact spring constant. + nu : float + Contact damping constant. + """ + super(CylinderPlaneContact, self).__init__() + self.k = k + self.nu = nu + self.surface_tol = 1e-4 + + def _check_systems_validity( + self, + system_one: SystemType, + system_two: AllowedContactType, + ) -> None: + """ + This checks the contact order and type of a SystemType object and an AllowedContactType object. + For the RodPlaneContact class first_system should be a cylinder and second_system should be a plane. + Parameters + ---------- + system_one + SystemType + system_two + AllowedContactType + """ + if not issubclass(system_one.__class__, Cylinder) or not issubclass( + system_two.__class__, Plane + ): + raise TypeError( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a cylinder, second should be a plane".format( + system_one.__class__, system_two.__class__ + ) + ) + + def apply_contact(self, system_one: Cylinder, system_two: SystemType): + """ + 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. + + Parameters + ---------- + system_one: object + Cylinder object. + system_two: object + Plane object. + + """ + return _calculate_contact_forces_cylinder_plane( + system_two.origin, + system_two.normal, + self.surface_tol, + self.k, + self.nu, + system_one.length, + system_one.position_collection, + system_one.velocity_collection, + system_one.external_forces, + ) diff --git a/elastica/contact_utils.py b/elastica/contact_utils.py new file mode 100644 index 000000000..f174bc28f --- /dev/null +++ b/elastica/contact_utils.py @@ -0,0 +1,420 @@ +__doc__ = """ Helper functions for contact force calculation """ + +from math import sqrt +import numba +import numpy as np +from elastica._linalg import ( + _batch_norm, +) + + +@numba.njit(cache=True) +def _dot_product(a, b): + sum = 0.0 + for i in range(3): + sum += a[i] * b[i] + return sum + + +@numba.njit(cache=True) +def _norm(a): + return sqrt(_dot_product(a, a)) + + +@numba.njit(cache=True) +def _clip(x, low, high): + return max(low, min(x, high)) + + +# Can this be made more efficient than 2 comp, 1 or? +@numba.njit(cache=True) +def _out_of_bounds(x, low, high): + return (x < low) or (x > high) + + +@numba.njit(cache=True) +def _find_min_dist(x1, e1, x2, e2): + e1e1 = _dot_product(e1, e1) + e1e2 = _dot_product(e1, e2) + e2e2 = _dot_product(e2, e2) + + x1e1 = _dot_product(x1, e1) + x1e2 = _dot_product(x1, e2) + x2e1 = _dot_product(e1, x2) + x2e2 = _dot_product(x2, e2) + + s = 0.0 + t = 0.0 + + parallel = abs(1.0 - e1e2 ** 2 / (e1e1 * e2e2)) < 1e-6 + if parallel: + # Some are parallel, so do processing + t = (x2e1 - x1e1) / e1e1 # Comes from taking dot of e1 with a normal + t = _clip(t, 0.0, 1.0) + s = (x1e2 + t * e1e2 - x2e2) / e2e2 # Same as before + s = _clip(s, 0.0, 1.0) + else: + # Using the Cauchy-Binet formula on eq(7) in docstring referenc + s = (e1e1 * (x1e2 - x2e2) + e1e2 * (x2e1 - x1e1)) / (e1e1 * e2e2 - (e1e2) ** 2) + t = (e1e2 * s + x2e1 - x1e1) / e1e1 + + if _out_of_bounds(s, 0.0, 1.0) or _out_of_bounds(t, 0.0, 1.0): + # potential_s = -100.0 + # potential_t = -100.0 + # potential_d = -100.0 + # overall_minimum_distance = 1e20 + + # Fill in the possibilities + potential_t = (x2e1 - x1e1) / e1e1 + s = 0.0 + t = _clip(potential_t, 0.0, 1.0) + potential_d = _norm(x1 + e1 * t - x2) + overall_minimum_distance = potential_d + + potential_t = (x2e1 + e1e2 - x1e1) / e1e1 + potential_t = _clip(potential_t, 0.0, 1.0) + potential_d = _norm(x1 + e1 * potential_t - x2 - e2) + if potential_d < overall_minimum_distance: + s = 1.0 + t = potential_t + overall_minimum_distance = potential_d + + potential_s = (x1e2 - x2e2) / e2e2 + potential_s = _clip(potential_s, 0.0, 1.0) + potential_d = _norm(x2 + potential_s * e2 - x1) + if potential_d < overall_minimum_distance: + s = potential_s + t = 0.0 + overall_minimum_distance = potential_d + + potential_s = (x1e2 + e1e2 - x2e2) / e2e2 + potential_s = _clip(potential_s, 0.0, 1.0) + potential_d = _norm(x2 + potential_s * e2 - x1 - e1) + if potential_d < overall_minimum_distance: + s = potential_s + t = 1.0 + + # Return distance, contact point of system 2, contact point of system 1 + return x2 + s * e2 - x1 - t * e1, x2 + s * e2, x1 - t * e1 + + +@numba.njit(cache=True) +def _aabbs_not_intersecting(aabb_one, aabb_two): + """Returns true if not intersecting else false""" + 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]): + return 1 + if (aabb_one[2, 1] < aabb_two[2, 0]) | (aabb_one[2, 0] > aabb_two[2, 1]): + return 1 + + return 0 + + +@numba.njit(cache=True) +def _prune_using_aabbs_rod_cylinder( + rod_one_position_collection, + rod_one_radius_collection, + rod_one_length_collection, + cylinder_position, + cylinder_director, + cylinder_radius, + cylinder_length, +): + max_possible_dimension = np.zeros((3,)) + aabb_rod = np.empty((3, 2)) + aabb_cylinder = np.empty((3, 2)) + max_possible_dimension[...] = np.max(rod_one_radius_collection) + np.max( + rod_one_length_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] + ) + + # Is actually Q^T * d but numba complains about performance so we do + # d^T @ Q + cylinder_dimensions_in_local_FOR = np.array( + [cylinder_radius, cylinder_radius, 0.5 * cylinder_length] + ) + cylinder_dimensions_in_world_FOR = np.zeros_like(cylinder_dimensions_in_local_FOR) + for i in range(3): + for j in range(3): + cylinder_dimensions_in_world_FOR[i] += ( + cylinder_director[j, i, 0] * cylinder_dimensions_in_local_FOR[j] + ) + + max_possible_dimension = np.abs(cylinder_dimensions_in_world_FOR) + aabb_cylinder[..., 0] = cylinder_position[..., 0] - max_possible_dimension + aabb_cylinder[..., 1] = cylinder_position[..., 0] + max_possible_dimension + return _aabbs_not_intersecting(aabb_cylinder, aabb_rod) + + +@numba.njit(cache=True) +def _prune_using_aabbs_rod_rod( + rod_one_position_collection, + rod_one_radius_collection, + rod_one_length_collection, + rod_two_position_collection, + rod_two_radius_collection, + rod_two_length_collection, +): + max_possible_dimension = np.zeros((3,)) + aabb_rod_one = np.empty((3, 2)) + aabb_rod_two = np.empty((3, 2)) + max_possible_dimension[...] = np.max(rod_one_radius_collection) + np.max( + rod_one_length_collection + ) + for i in range(3): + aabb_rod_one[i, 0] = ( + np.min(rod_one_position_collection[i]) - max_possible_dimension[i] + ) + aabb_rod_one[i, 1] = ( + np.max(rod_one_position_collection[i]) + max_possible_dimension[i] + ) + + max_possible_dimension[...] = np.max(rod_two_radius_collection) + np.max( + rod_two_length_collection + ) + + for i in range(3): + aabb_rod_two[i, 0] = ( + np.min(rod_two_position_collection[i]) - max_possible_dimension[i] + ) + aabb_rod_two[i, 1] = ( + np.max(rod_two_position_collection[i]) + max_possible_dimension[i] + ) + + return _aabbs_not_intersecting(aabb_rod_two, aabb_rod_one) + + +@numba.njit(cache=True) +def _prune_using_aabbs_rod_sphere( + rod_one_position_collection, + rod_one_radius_collection, + rod_one_length_collection, + sphere_position, + sphere_director, + sphere_radius, +): + max_possible_dimension = np.zeros((3,)) + 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 + ) + 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] + ) + + 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) + 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] + ) + + 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) + + +@numba.njit(cache=True) +def _find_slipping_elements(velocity_slip, velocity_threshold): + """ + This function takes the velocity of elements and checks if they are larger than the threshold velocity. + If the velocity of elements is larger than threshold velocity, that means those elements are slipping. + In other words, kinetic friction will be acting on those elements, not static friction. + This function outputs an array called slip function, this array has a size of the number of elements. + If the velocity of the element is smaller than the threshold velocity slip function value for that element is 1, + which means static friction is acting on that element. If the velocity of the element is larger than + the threshold velocity slip function value for that element is between 0 and 1, which means kinetic friction is acting + on that element. + + Parameters + ---------- + velocity_slip : numpy.ndarray + 2D (dim, blocksize) array containing data with 'float' type. + Rod-like object element velocity. + velocity_threshold : float + Threshold velocity to determine slip. + + Returns + ------- + slip_function : numpy.ndarray + 2D (dim, blocksize) array containing data with 'float' type. + """ + """ + Developer Notes + ----- + Benchmark results, for a blocksize of 100 using timeit + python version: 18.9 µs ± 2.98 µs per loop + this version: 1.96 µs ± 58.3 ns per loop + """ + abs_velocity_slip = _batch_norm(velocity_slip) + slip_points = np.where(np.fabs(abs_velocity_slip) > velocity_threshold) + slip_function = np.ones((velocity_slip.shape[1])) + slip_function[slip_points] = np.fabs( + 1.0 - np.minimum(1.0, abs_velocity_slip[slip_points] / velocity_threshold - 1.0) + ) + return slip_function + + +@numba.njit(cache=True) +def _node_to_element_mass_or_force(input): + """ + This function converts the mass/forces on rod nodes to + elements, where special treatment is necessary at the ends. + + Parameters + ---------- + input: numpy.ndarray + 2D (dim, blocksize) array containing nodal mass/forces + with 'float' type. + + Returns + ------- + output: numpy.ndarray + 2D (dim, blocksize) array containing elemental mass/forces + with 'float' type. + """ + """ + Developer Notes + ----- + Benchmark results, for a blocksize of 100 using timeit + Python version: 18.1 µs ± 1.03 µs per loop + This version: 1.55 µs ± 13.4 ns per loop + """ + blocksize = input.shape[1] - 1 # nelem + output = np.zeros((3, blocksize)) + for i in range(3): + for k in range(0, blocksize): + output[i, k] += 0.5 * (input[i, k] + input[i, k + 1]) + + # Put extra care for the first and last element + output[..., 0] += 0.5 * input[..., 0] + output[..., -1] += 0.5 * input[..., -1] + + return output + + +@numba.njit(cache=True) +def _elements_to_nodes_inplace(vector_in_element_frame, vector_in_node_frame): + """ + Updating nodal forces using the forces computed on elements + Parameters + ---------- + vector_in_element_frame + vector_in_node_frame + + Returns + ------- + Notes + ----- + Benchmark results, for a blocksize of 100 using timeit + Python version: 23.1 µs ± 7.57 µs per loop + This version: 696 ns ± 10.2 ns per loop + """ + for i in range(3): + for k in range(vector_in_element_frame.shape[1]): + vector_in_node_frame[i, k] += 0.5 * vector_in_element_frame[i, k] + vector_in_node_frame[i, k + 1] += 0.5 * vector_in_element_frame[i, k] + + +@numba.njit(cache=True) +def _node_to_element_position(node_position_collection): + """ + This function computes the position of the elements + from the nodal values. + Here we define a separate function because benchmark results + showed that using Numba, we get more than 3 times faster calculation. + + Parameters + ---------- + node_position_collection: numpy.ndarray + 2D (dim, blocksize) array containing nodal positions with + 'float' type. + + Returns + ------- + element_position_collection: numpy.ndarray + 2D (dim, blocksize) array containing elemental positions with + 'float' type. + """ + """ + Developer Notes + ----- + Benchmark results, for a blocksize of 100, + + Python version: 3.5 µs ± 149 ns per loop + + This version: 729 ns ± 14.3 ns per loop + + """ + n_elem = node_position_collection.shape[1] - 1 + element_position_collection = np.empty((3, n_elem)) + for k in range(n_elem): + element_position_collection[0, k] = 0.5 * ( + node_position_collection[0, k + 1] + node_position_collection[0, k] + ) + element_position_collection[1, k] = 0.5 * ( + node_position_collection[1, k + 1] + node_position_collection[1, k] + ) + element_position_collection[2, k] = 0.5 * ( + node_position_collection[2, k + 1] + node_position_collection[2, k] + ) + + return element_position_collection + + +@numba.njit(cache=True) +def _node_to_element_velocity(mass, node_velocity_collection): + """ + This function computes the velocity of the elements + from the nodal values. Uses the velocity of center of mass + in order to conserve momentum during computation. + + Parameters + ---------- + mass: numpy.ndarray + 2D (dim, blocksize) array containing nodal masses with + 'float' type. + node_velocity_collection: numpy.ndarray + 2D (dim, blocksize) array containing nodal velocities with + 'float' type. + + Returns + ------- + element_velocity_collection: numpy.ndarray + 2D (dim, blocksize) array containing elemental velocities with + 'float' type. + """ + n_elem = node_velocity_collection.shape[1] - 1 + element_velocity_collection = np.empty((3, n_elem)) + for k in range(n_elem): + element_velocity_collection[0, k] = ( + mass[k + 1] * node_velocity_collection[0, k + 1] + + mass[k] * node_velocity_collection[0, k] + ) + element_velocity_collection[1, k] = ( + mass[k + 1] * node_velocity_collection[1, k + 1] + + mass[k] * node_velocity_collection[1, k] + ) + element_velocity_collection[2, k] = ( + mass[k + 1] * node_velocity_collection[2, k + 1] + + mass[k] * node_velocity_collection[2, k] + ) + element_velocity_collection[:, k] /= mass[k + 1] + mass[k] + + return element_velocity_collection diff --git a/elastica/external_forces.py b/elastica/external_forces.py index 21c2b8eb0..cb9c61e6c 100644 --- a/elastica/external_forces.py +++ b/elastica/external_forces.py @@ -40,9 +40,6 @@ def apply_forces(self, system: SystemType, time: np.float64 = 0.0): time : float The time of simulation. - Returns - ------- - """ pass @@ -58,9 +55,6 @@ def apply_torques(self, system: SystemType, time: np.float64 = 0.0): time : float The time of simulation. - Returns - ------- - """ pass @@ -109,9 +103,6 @@ def compute_gravity_forces(acc_gravity, mass, external_forces): external_forces: numpy.ndarray 2D (dim, blocksize) array containing data with 'float' type. External force vector. - Returns - ------- - """ inplace_addition(external_forces, _batch_product_i_k_to_ik(acc_gravity, mass)) @@ -182,9 +173,6 @@ def compute_end_point_forces( ramp_up_time: float Applied forces are ramped up until ramp up time. - Returns - ------- - """ factor = min(1.0, time / ramp_up_time) external_forces[..., 0] += start_force * factor @@ -345,23 +333,7 @@ def __init__( self.my_spline = my_spline(self.s) else: - - def constant_function(input): - """ - Return array of ones same as the size of the input array. This - function is called when Beta spline function is not used. - - Parameters - ---------- - input - - Returns - ------- - - """ - return np.ones(input.shape) - - self.my_spline = constant_function(self.s) + self.my_spline = np.full_like(self.s, fill_value=1.0) def apply_torques(self, rod: RodType, time: np.float64 = 0.0): self.compute_muscle_torques( @@ -430,8 +402,6 @@ def inplace_addition(external_force_or_torque, force_or_torque): force_or_torque: numpy.ndarray 2D (dim, blocksize) array containing data with 'float' type. - Returns - ------- """ blocksize = force_or_torque.shape[1] @@ -454,8 +424,6 @@ def inplace_substraction(external_force_or_torque, force_or_torque): force_or_torque: numpy.ndarray 2D (dim, blocksize) array containing data with 'float' type. - Returns - ------- """ blocksize = force_or_torque.shape[1] diff --git a/elastica/interaction.py b/elastica/interaction.py index ff0d3f0b0..95d4602e1 100644 --- a/elastica/interaction.py +++ b/elastica/interaction.py @@ -3,100 +3,32 @@ import numpy as np from elastica.external_forces import NoForces - - from numba import njit -from elastica._linalg import ( - _batch_matvec, - _batch_cross, - _batch_norm, - _batch_dot, - _batch_product_i_k_to_ik, - _batch_product_i_ik_to_k, - _batch_product_k_ik_to_ik, - _batch_vector_sum, - _batch_matrix_transpose, - _batch_vec_oneD_vec_cross, +from elastica.contact_utils import ( + _elements_to_nodes_inplace, + _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, ) -@njit(cache=True) def find_slipping_elements(velocity_slip, velocity_threshold): - """ - This function takes the velocity of elements and checks if they are larger than the threshold velocity. - If the velocity of elements is larger than threshold velocity, that means those elements are slipping. - In other words, kinetic friction will be acting on those elements, not static friction. - This function outputs an array called slip function, this array has a size of the number of elements. - If the velocity of the element is smaller than the threshold velocity slip function value for that element is 1, - which means static friction is acting on that element. If the velocity of the element is larger than - the threshold velocity slip function value for that element is between 0 and 1, which means kinetic friction is acting - on that element. - - Parameters - ---------- - velocity_slip : numpy.ndarray - 2D (dim, blocksize) array containing data with 'float' type. - Rod-like object element velocity. - velocity_threshold : float - Threshold velocity to determine slip. - - Returns - ------- - slip_function : numpy.ndarray - 2D (dim, blocksize) array containing data with 'float' type. - """ - """ - Developer Notes - ----- - Benchmark results, for a blocksize of 100 using timeit - python version: 18.9 µs ± 2.98 µs per loop - this version: 1.96 µs ± 58.3 ns per loop - """ - abs_velocity_slip = _batch_norm(velocity_slip) - slip_points = np.where(np.fabs(abs_velocity_slip) > velocity_threshold) - slip_function = np.ones((velocity_slip.shape[1])) - slip_function[slip_points] = np.fabs( - 1.0 - np.minimum(1.0, abs_velocity_slip[slip_points] / velocity_threshold - 1.0) + raise NotImplementedError( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._find_slipping_elements()\n" + "instead for finding slipping elements." ) - return slip_function -@njit(cache=True) def node_to_element_mass_or_force(input): - """ - This function converts the mass/forces on rod nodes to - elements, where special treatment is necessary at the ends. - - Parameters - ---------- - input: numpy.ndarray - 2D (dim, blocksize) array containing nodal mass/forces - with 'float' type. - - Returns - ------- - output: numpy.ndarray - 2D (dim, blocksize) array containing elemental mass/forces - with 'float' type. - """ - """ - Developer Notes - ----- - Benchmark results, for a blocksize of 100 using timeit - Python version: 18.1 µs ± 1.03 µs per loop - This version: 1.55 µs ± 13.4 ns per loop - """ - blocksize = input.shape[1] - 1 # nelem - output = np.zeros((3, blocksize)) - for i in range(3): - for k in range(0, blocksize): - output[i, k] += 0.5 * (input[i, k] + input[i, k + 1]) - - # Put extra care for the first and last element - output[..., 0] += 0.5 * input[..., 0] - output[..., -1] += 0.5 * input[..., -1] - - return output + raise NotImplementedError( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._node_to_element_mass_or_force()\n" + "instead for converting the mass/forces on rod nodes to elements." + ) def nodes_to_elements(input): @@ -110,25 +42,11 @@ def nodes_to_elements(input): @njit(cache=True) def elements_to_nodes_inplace(vector_in_element_frame, vector_in_node_frame): - """ - Updating nodal forces using the forces computed on elements - Parameters - ---------- - vector_in_element_frame - vector_in_node_frame - - Returns - ------- - Notes - ----- - Benchmark results, for a blocksize of 100 using timeit - Python version: 23.1 µs ± 7.57 µs per loop - This version: 696 ns ± 10.2 ns per loop - """ - for i in range(3): - for k in range(vector_in_element_frame.shape[1]): - vector_in_node_frame[i, k] += 0.5 * vector_in_element_frame[i, k] - vector_in_node_frame[i, k + 1] += 0.5 * vector_in_element_frame[i, k] + raise NotImplementedError( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._elements_to_nodes_inplace()\n" + "instead for updating nodal forces using the forces computed on elements." + ) # base class for interaction @@ -196,7 +114,7 @@ def apply_normal_force(self, system): 1D (blocksize) array containing data with 'int' type. Index of rod-like object elements that are not in contact with the plane. """ - return apply_normal_force_numba( + return _calculate_contact_forces_rod_plane( self.plane_origin, self.plane_normal, self.surface_tol, @@ -211,7 +129,6 @@ def apply_normal_force(self, system): ) -@njit(cache=True) def apply_normal_force_numba( plane_origin, plane_normal, @@ -225,76 +142,12 @@ def apply_normal_force_numba( internal_forces, external_forces, ): - """ - 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. - - Parameters - ---------- - system - - Returns - ------- - magnitude of the plane response - """ - - # Compute plane response force - nodal_total_forces = _batch_vector_sum(internal_forces, external_forces) - element_total_forces = node_to_element_mass_or_force(nodal_total_forces) - - force_component_along_normal_direction = _batch_product_i_ik_to_k( - plane_normal, element_total_forces - ) - forces_along_normal_direction = _batch_product_i_k_to_ik( - plane_normal, force_component_along_normal_direction - ) - - # If the total force component along the plane normal direction is greater than zero that means, - # total force is pushing rod away from the plane not towards the plane. Thus, response force - # applied by the surface has to be zero. - forces_along_normal_direction[ - ..., np.where(force_component_along_normal_direction > 0)[0] - ] = 0.0 - # Compute response force on the element. Plane response force - # has to be away from the surface and towards the element. Thus - # multiply forces along normal direction with negative sign. - plane_response_force = -forces_along_normal_direction - - # Elastic force response due to penetration - element_position = node_to_element_position(position_collection) - distance_from_plane = _batch_product_i_ik_to_k( - plane_normal, (element_position - plane_origin) - ) - plane_penetration = np.minimum(distance_from_plane - radius, 0.0) - elastic_force = -k * _batch_product_i_k_to_ik(plane_normal, plane_penetration) - - # Damping force response due to velocity towards the plane - element_velocity = node_to_element_velocity( - mass=mass, node_velocity_collection=velocity_collection - ) - normal_component_of_element_velocity = _batch_product_i_ik_to_k( - plane_normal, element_velocity - ) - damping_force = -nu * _batch_product_i_k_to_ik( - plane_normal, normal_component_of_element_velocity + raise NotImplementedError( + "This function is removed in v0.3.2. For rod plane contact please use: \n" + "elastica._contact_functions._calculate_contact_forces_rod_plane() \n" + "For detail, refer to issue #113." ) - # Compute total plane response force - plane_response_force_total = plane_response_force + elastic_force + damping_force - - # Check if the rod elements are in contact with plane. - no_contact_point_idx = np.where((distance_from_plane - radius) > surface_tol)[0] - # If rod element does not have any contact with plane, plane cannot apply response - # force on the element. Thus lets set plane response force to 0.0 for the no contact points. - plane_response_force[..., no_contact_point_idx] = 0.0 - plane_response_force_total[..., no_contact_point_idx] = 0.0 - - # Update the external forces - elements_to_nodes_inplace(plane_response_force_total, external_forces) - - return (_batch_norm(plane_response_force), no_contact_point_idx) - # class for anisotropic frictional plane # NOTE: friction coefficients are passed as arrays in the order @@ -387,11 +240,8 @@ def apply_forces(self, system, time=0.0): system time - Returns - ------- - """ - anisotropic_friction( + _calculate_contact_forces_rod_plane_with_anisotropic_friction( self.plane_origin, self.plane_normal, self.surface_tol, @@ -418,7 +268,6 @@ def apply_forces(self, system, time=0.0): ) -@njit(cache=True) def anisotropic_friction( plane_origin, plane_normal, @@ -444,188 +293,10 @@ def anisotropic_friction( internal_torques, external_torques, ): - plane_response_force_mag, no_contact_point_idx = apply_normal_force_numba( - plane_origin, - plane_normal, - surface_tol, - k, - nu, - radius, - mass, - position_collection, - velocity_collection, - internal_forces, - external_forces, - ) - - # First compute component of rod tangent in plane. Because friction forces acts in plane not out of plane. Thus - # axial direction has to be in plane, it cannot be out of plane. We are projecting rod element tangent vector in - # to the plane. So friction forces can only be in plane forces and not out of plane. - tangent_along_normal_direction = _batch_product_i_ik_to_k(plane_normal, tangents) - tangent_perpendicular_to_normal_direction = tangents - _batch_product_i_k_to_ik( - plane_normal, tangent_along_normal_direction - ) - tangent_perpendicular_to_normal_direction_mag = _batch_norm( - tangent_perpendicular_to_normal_direction - ) - # Normalize tangent_perpendicular_to_normal_direction. This is axial direction for plane. Here we are adding - # small tolerance (1e-10) for normalization, in order to prevent division by 0. - axial_direction = _batch_product_k_ik_to_ik( - 1 / (tangent_perpendicular_to_normal_direction_mag + 1e-14), - tangent_perpendicular_to_normal_direction, - ) - element_velocity = node_to_element_velocity( - mass=mass, node_velocity_collection=velocity_collection - ) - # first apply axial kinetic friction - velocity_mag_along_axial_direction = _batch_dot(element_velocity, axial_direction) - velocity_along_axial_direction = _batch_product_k_ik_to_ik( - velocity_mag_along_axial_direction, axial_direction - ) - - # Friction forces depends on the direction of velocity, in other words sign - # of the velocity vector. - velocity_sign_along_axial_direction = np.sign(velocity_mag_along_axial_direction) - # Check top for sign convention - kinetic_mu = 0.5 * ( - kinetic_mu_forward * (1 + velocity_sign_along_axial_direction) - + kinetic_mu_backward * (1 - velocity_sign_along_axial_direction) - ) - # Call slip function to check if elements slipping or not - slip_function_along_axial_direction = find_slipping_elements( - velocity_along_axial_direction, slip_velocity_tol - ) - - # Now rolling kinetic friction - rolling_direction = _batch_vec_oneD_vec_cross(axial_direction, plane_normal) - torque_arm = _batch_product_i_k_to_ik(-plane_normal, radius) - velocity_along_rolling_direction = _batch_dot(element_velocity, rolling_direction) - directors_transpose = _batch_matrix_transpose(director_collection) - # w_rot = Q.T @ omega @ Q @ r - rotation_velocity = _batch_matvec( - directors_transpose, - _batch_cross(omega_collection, _batch_matvec(director_collection, torque_arm)), - ) - rotation_velocity_along_rolling_direction = _batch_dot( - rotation_velocity, rolling_direction - ) - slip_velocity_mag_along_rolling_direction = ( - velocity_along_rolling_direction + rotation_velocity_along_rolling_direction - ) - slip_velocity_along_rolling_direction = _batch_product_k_ik_to_ik( - slip_velocity_mag_along_rolling_direction, rolling_direction - ) - slip_function_along_rolling_direction = find_slipping_elements( - slip_velocity_along_rolling_direction, slip_velocity_tol - ) - # Compute unitized total slip velocity vector. We will use this to distribute the weight of the rod in axial - # and rolling directions. - unitized_total_velocity = ( - slip_velocity_along_rolling_direction + velocity_along_axial_direction - ) - unitized_total_velocity /= _batch_norm(unitized_total_velocity + 1e-14) - # Apply kinetic friction in axial direction. - kinetic_friction_force_along_axial_direction = -( - (1.0 - slip_function_along_axial_direction) - * kinetic_mu - * plane_response_force_mag - * _batch_dot(unitized_total_velocity, axial_direction) - * axial_direction - ) - # If rod element does not have any contact with plane, plane cannot apply friction - # force on the element. Thus lets set kinetic friction force to 0.0 for the no contact points. - kinetic_friction_force_along_axial_direction[..., no_contact_point_idx] = 0.0 - elements_to_nodes_inplace( - kinetic_friction_force_along_axial_direction, external_forces - ) - # Apply kinetic friction in rolling direction. - kinetic_friction_force_along_rolling_direction = -( - (1.0 - slip_function_along_rolling_direction) - * kinetic_mu_sideways - * plane_response_force_mag - * _batch_dot(unitized_total_velocity, rolling_direction) - * rolling_direction - ) - # If rod element does not have any contact with plane, plane cannot apply friction - # force on the element. Thus lets set kinetic friction force to 0.0 for the no contact points. - kinetic_friction_force_along_rolling_direction[..., no_contact_point_idx] = 0.0 - elements_to_nodes_inplace( - kinetic_friction_force_along_rolling_direction, external_forces - ) - # torque = Q @ r @ Fr - external_torques += _batch_matvec( - director_collection, - _batch_cross(torque_arm, kinetic_friction_force_along_rolling_direction), - ) - - # now axial static friction - nodal_total_forces = _batch_vector_sum(internal_forces, external_forces) - element_total_forces = node_to_element_mass_or_force(nodal_total_forces) - force_component_along_axial_direction = _batch_dot( - element_total_forces, axial_direction - ) - force_component_sign_along_axial_direction = np.sign( - force_component_along_axial_direction - ) - # check top for sign convention - static_mu = 0.5 * ( - static_mu_forward * (1 + force_component_sign_along_axial_direction) - + static_mu_backward * (1 - force_component_sign_along_axial_direction) - ) - max_friction_force = ( - slip_function_along_axial_direction * static_mu * plane_response_force_mag - ) - # friction = min(mu N, pushing force) - static_friction_force_along_axial_direction = -( - np.minimum(np.fabs(force_component_along_axial_direction), max_friction_force) - * force_component_sign_along_axial_direction - * axial_direction - ) - # If rod element does not have any contact with plane, plane cannot apply friction - # force on the element. Thus lets set static friction force to 0.0 for the no contact points. - static_friction_force_along_axial_direction[..., no_contact_point_idx] = 0.0 - elements_to_nodes_inplace( - static_friction_force_along_axial_direction, external_forces - ) - - # now rolling static friction - # there is some normal, tangent and rolling directions inconsitency from Elastica - total_torques = _batch_matvec( - directors_transpose, (internal_torques + external_torques) - ) - # Elastica has opposite defs of tangents in interaction.h and rod.cpp - total_torques_along_axial_direction = _batch_dot(total_torques, axial_direction) - force_component_along_rolling_direction = _batch_dot( - element_total_forces, rolling_direction - ) - noslip_force = -( - ( - radius * force_component_along_rolling_direction - - 2.0 * total_torques_along_axial_direction - ) - / 3.0 - / radius - ) - max_friction_force = ( - slip_function_along_rolling_direction - * static_mu_sideways - * plane_response_force_mag - ) - noslip_force_sign = np.sign(noslip_force) - static_friction_force_along_rolling_direction = ( - np.minimum(np.fabs(noslip_force), max_friction_force) - * noslip_force_sign - * rolling_direction - ) - # If rod element does not have any contact with plane, plane cannot apply friction - # force on the element. Thus lets set plane static friction force to 0.0 for the no contact points. - static_friction_force_along_rolling_direction[..., no_contact_point_idx] = 0.0 - elements_to_nodes_inplace( - static_friction_force_along_rolling_direction, external_forces - ) - external_torques += _batch_matvec( - director_collection, - _batch_cross(torque_arm, static_friction_force_along_rolling_direction), + raise NotImplementedError( + "This function is removed in v0.3.2. For anisotropic_friction please use: \n" + "elastica._contact_functions._calculate_contact_forces_rod_plane_with_anisotropic_friction() \n" + "For detail, refer to issue #113." ) @@ -670,100 +341,28 @@ def sum_over_elements(input): return output -@njit(cache=True) def node_to_element_position(node_position_collection): - """ - This function computes the position of the elements - from the nodal values. - Here we define a separate function because benchmark results - showed that using Numba, we get more than 3 times faster calculation. - - Parameters - ---------- - node_position_collection: numpy.ndarray - 2D (dim, blocksize) array containing nodal positions with - 'float' type. - - Returns - ------- - element_position_collection: numpy.ndarray - 2D (dim, blocksize) array containing elemental positions with - 'float' type. - """ - """ - Developer Notes - ----- - Benchmark results, for a blocksize of 100, - - Python version: 3.5 µs ± 149 ns per loop - - This version: 729 ns ± 14.3 ns per loop - - """ - n_elem = node_position_collection.shape[1] - 1 - element_position_collection = np.empty((3, n_elem)) - for k in range(n_elem): - element_position_collection[0, k] = 0.5 * ( - node_position_collection[0, k + 1] + node_position_collection[0, k] - ) - element_position_collection[1, k] = 0.5 * ( - node_position_collection[1, k + 1] + node_position_collection[1, k] - ) - element_position_collection[2, k] = 0.5 * ( - node_position_collection[2, k + 1] + node_position_collection[2, k] - ) - - return element_position_collection + raise NotImplementedError( + "This function is removed in v0.3.2. For node-to-element_position() interpolation please use: \n" + "elastica.contact_utils._node_to_element_position() for rod position \n" + "For detail, refer to issue #113." + ) -@njit(cache=True) def node_to_element_velocity(mass, node_velocity_collection): - """ - This function computes the velocity of the elements - from the nodal values. Uses the velocity of center of mass - in order to conserve momentum during computation. - - Parameters - ---------- - mass: numpy.ndarray - 2D (dim, blocksize) array containing nodal masses with - 'float' type. - node_velocity_collection: numpy.ndarray - 2D (dim, blocksize) array containing nodal velocities with - 'float' type. - - Returns - ------- - element_velocity_collection: numpy.ndarray - 2D (dim, blocksize) array containing elemental velocities with - 'float' type. - """ - n_elem = node_velocity_collection.shape[1] - 1 - element_velocity_collection = np.empty((3, n_elem)) - for k in range(n_elem): - element_velocity_collection[0, k] = ( - mass[k + 1] * node_velocity_collection[0, k + 1] - + mass[k] * node_velocity_collection[0, k] - ) - element_velocity_collection[1, k] = ( - mass[k + 1] * node_velocity_collection[1, k + 1] - + mass[k] * node_velocity_collection[1, k] - ) - element_velocity_collection[2, k] = ( - mass[k + 1] * node_velocity_collection[2, k + 1] - + mass[k] * node_velocity_collection[2, k] - ) - element_velocity_collection[:, k] /= mass[k + 1] + mass[k] - - return element_velocity_collection + raise NotImplementedError( + "This function is removed in v0.3.2. For node-to-element_velocity() interpolation please use: \n" + "elastica.contact_utils._node_to_element_velocity() for rod velocity. \n" + "For detail, refer to issue #113." + ) def node_to_element_pos_or_vel(vector_in_node_frame): # Remove the function beyond v0.4.0 raise NotImplementedError( "This function is removed in v0.3.0. For node-to-element interpolation please use: \n" - "elastica.interaction.node_to_element_position() for rod position \n" - "elastica.interaction.node_to_element_velocity() for rod velocity. \n" + "elastica.contact_utils._node_to_element_position() for rod position \n" + "elastica.contact_utils._node_to_element_velocity() for rod velocity. \n" "For detail, refer to issue #80." ) @@ -819,7 +418,7 @@ def slender_body_forces( f = np.empty((tangents.shape[0], tangents.shape[1])) total_length = sum_over_elements(lengths) - element_velocity = node_to_element_velocity( + element_velocity = _node_to_element_velocity( mass=mass, node_velocity_collection=velocity_collection ) @@ -903,9 +502,6 @@ def apply_forces(self, system, time=0.0): ---------- system - Returns - ------- - """ stokes_force = slender_body_forces( @@ -916,7 +512,7 @@ def apply_forces(self, system, time=0.0): system.radius, system.mass, ) - elements_to_nodes_inplace(stokes_force, system.external_forces) + _elements_to_nodes_inplace(stokes_force, system.external_forces) # base class for interaction @@ -942,7 +538,7 @@ def apply_normal_force(self, system): ------- magnitude of the plane response """ - return apply_normal_force_numba_rigid_body( + return _calculate_contact_forces_cylinder_plane( self.plane_origin, self.plane_normal, self.surface_tol, @@ -968,54 +564,8 @@ def apply_normal_force_numba_rigid_body( external_forces, ): - # Compute plane response force - # total_forces = system.internal_forces + system.external_forces - total_forces = external_forces - force_component_along_normal_direction = _batch_product_i_ik_to_k( - plane_normal, total_forces - ) - forces_along_normal_direction = _batch_product_i_k_to_ik( - plane_normal, force_component_along_normal_direction - ) - # If the total force component along the plane normal direction is greater than zero that means, - # total force is pushing rod away from the plane not towards the plane. Thus, response force - # applied by the surface has to be zero. - forces_along_normal_direction[ - ..., np.where(force_component_along_normal_direction > 0)[0] - ] = 0.0 - # Compute response force on the element. Plane response force - # has to be away from the surface and towards the element. Thus - # multiply forces along normal direction with negative sign. - plane_response_force = -forces_along_normal_direction - - # Elastic force response due to penetration - element_position = position_collection - distance_from_plane = _batch_product_i_ik_to_k( - plane_normal, (element_position - plane_origin) - ) - plane_penetration = np.minimum(distance_from_plane - length / 2, 0.0) - elastic_force = -k * _batch_product_i_k_to_ik(plane_normal, plane_penetration) - - # Damping force response due to velocity towards the plane - element_velocity = velocity_collection - normal_component_of_element_velocity = _batch_product_i_ik_to_k( - plane_normal, element_velocity - ) - damping_force = -nu * _batch_product_i_k_to_ik( - plane_normal, normal_component_of_element_velocity + raise NotImplementedError( + "This function is removed in v0.3.2. For cylinder plane contact please use: \n" + "elastica._contact_functions._calculate_contact_forces_cylinder_plane() \n" + "For detail, refer to issue #113." ) - - # Compute total plane response force - plane_response_force_total = plane_response_force + elastic_force + damping_force - - # Check if the rigid body is in contact with plane. - no_contact_point_idx = np.where((distance_from_plane - length / 2) > surface_tol)[0] - # If rod element does not have any contact with plane, plane cannot apply response - # force on the element. Thus lets set plane response force to 0.0 for the no contact points. - plane_response_force[..., no_contact_point_idx] = 0.0 - plane_response_force_total[..., no_contact_point_idx] = 0.0 - - # Update the external forces - external_forces += plane_response_force_total - - return (_batch_norm(plane_response_force), no_contact_point_idx) diff --git a/elastica/joint.py b/elastica/joint.py index 57b061194..e37d6f058 100644 --- a/elastica/joint.py +++ b/elastica/joint.py @@ -1,11 +1,8 @@ __doc__ = """ Module containing joint classes to connect multiple rods together. """ - -from elastica._linalg import _batch_product_k_ik_to_ik from elastica._rotations import _inv_rotate from elastica.typing import SystemType, RodType -from math import sqrt -import numba import numpy as np +import logging class FreeJoint: @@ -364,97 +361,47 @@ def get_relative_rotation_two_systems( ) -@numba.njit(cache=True) +# everything below this comment should be removed beyond v0.4.0 def _dot_product(a, b): - sum = 0.0 - for i in range(3): - sum += a[i] * b[i] - return sum + raise NotImplementedError( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._dot_product()\n" + "instead for find the dot product between a and b." + ) -@numba.njit(cache=True) def _norm(a): - return sqrt(_dot_product(a, a)) + raise NotImplementedError( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._norm()\n" + "instead for finding the norm of a." + ) -@numba.njit(cache=True) def _clip(x, low, high): - return max(low, min(x, high)) + raise NotImplementedError( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._clip()\n" + "instead for clipping x." + ) -# Can this be made more efficient than 2 comp, 1 or? -@numba.njit(cache=True) def _out_of_bounds(x, low, high): - return (x < low) or (x > high) + raise NotImplementedError( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._out_of_bounds()\n" + "instead for checking if x is out of bounds." + ) -@numba.njit(cache=True) def _find_min_dist(x1, e1, x2, e2): - e1e1 = _dot_product(e1, e1) - e1e2 = _dot_product(e1, e2) - e2e2 = _dot_product(e2, e2) - - x1e1 = _dot_product(x1, e1) - x1e2 = _dot_product(x1, e2) - x2e1 = _dot_product(e1, x2) - x2e2 = _dot_product(x2, e2) - - s = 0.0 - t = 0.0 - - parallel = abs(1.0 - e1e2 ** 2 / (e1e1 * e2e2)) < 1e-6 - if parallel: - # Some are parallel, so do processing - t = (x2e1 - x1e1) / e1e1 # Comes from taking dot of e1 with a normal - t = _clip(t, 0.0, 1.0) - s = (x1e2 + t * e1e2 - x2e2) / e2e2 # Same as before - s = _clip(s, 0.0, 1.0) - else: - # Using the Cauchy-Binet formula on eq(7) in docstring referenc - s = (e1e1 * (x1e2 - x2e2) + e1e2 * (x2e1 - x1e1)) / (e1e1 * e2e2 - (e1e2) ** 2) - t = (e1e2 * s + x2e1 - x1e1) / e1e1 - - if _out_of_bounds(s, 0.0, 1.0) or _out_of_bounds(t, 0.0, 1.0): - # potential_s = -100.0 - # potential_t = -100.0 - # potential_d = -100.0 - # overall_minimum_distance = 1e20 - - # Fill in the possibilities - potential_t = (x2e1 - x1e1) / e1e1 - s = 0.0 - t = _clip(potential_t, 0.0, 1.0) - potential_d = _norm(x1 + e1 * t - x2) - overall_minimum_distance = potential_d - - potential_t = (x2e1 + e1e2 - x1e1) / e1e1 - potential_t = _clip(potential_t, 0.0, 1.0) - potential_d = _norm(x1 + e1 * potential_t - x2 - e2) - if potential_d < overall_minimum_distance: - s = 1.0 - t = potential_t - overall_minimum_distance = potential_d - - potential_s = (x1e2 - x2e2) / e2e2 - potential_s = _clip(potential_s, 0.0, 1.0) - potential_d = _norm(x2 + potential_s * e2 - x1) - if potential_d < overall_minimum_distance: - s = potential_s - t = 0.0 - overall_minimum_distance = potential_d - - potential_s = (x1e2 + e1e2 - x2e2) / e2e2 - potential_s = _clip(potential_s, 0.0, 1.0) - potential_d = _norm(x2 + potential_s * e2 - x1 - e1) - if potential_d < overall_minimum_distance: - s = potential_s - t = 1.0 - - # Return distance, contact point of system 2, contact point of system 1 - return x2 + s * e2 - x1 - t * e1, x2 + s * e2, x1 - t * e1 - - -@numba.njit(cache=True) + raise NotImplementedError( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._find_min_dist()\n" + "instead for finding minimum distance between contact points." + ) + + def _calculate_contact_forces_rod_rigid_body( x_collection_rod, edge_collection_rod, @@ -475,125 +422,13 @@ def _calculate_contact_forces_rod_rigid_body( velocity_damping_coefficient, friction_coefficient, ): - # We already pass in only the first n_elem x - n_points = x_collection_rod.shape[1] - cylinder_total_contact_forces = np.zeros((3)) - cylinder_total_contact_torques = np.zeros((3)) - for i in range(n_points): - # Element-wise bounding box - x_selected = x_collection_rod[..., i] - # x_cylinder is already a (,) array from outised - del_x = x_selected - x_cylinder_tip - norm_del_x = _norm(del_x) - - # If outside then don't process - if norm_del_x >= (radii_sum[i] + length_sum[i]): - continue - - # find the shortest line segment between the two centerline - # segments : differs from normal cylinder-cylinder intersection - distance_vector, x_cylinder_contact_point, _ = _find_min_dist( - x_selected, edge_collection_rod[..., i], x_cylinder_tip, edge_cylinder - ) - distance_vector_length = _norm(distance_vector) - distance_vector /= distance_vector_length - - gamma = radii_sum[i] - distance_vector_length - - # If distance is large, don't worry about it - if gamma < -1e-5: - continue - - rod_elemental_forces = 0.5 * ( - external_forces_rod[..., i] - + external_forces_rod[..., i + 1] - + internal_forces_rod[..., i] - + internal_forces_rod[..., i + 1] - ) - equilibrium_forces = -rod_elemental_forces + external_forces_cylinder[..., 0] - - normal_force = _dot_product(equilibrium_forces, distance_vector) - # Following line same as np.where(normal_force < 0.0, -normal_force, 0.0) - normal_force = abs(min(normal_force, 0.0)) - - # CHECK FOR GAMMA > 0.0, heaviside but we need to overload it in numba - # As a quick fix, use this instead - mask = (gamma > 0.0) * 1.0 - - # Compute contact spring force - contact_force = contact_k * gamma * distance_vector - interpenetration_velocity = velocity_cylinder[..., 0] - 0.5 * ( - velocity_rod[..., i] + velocity_rod[..., i + 1] - ) - # Compute contact damping - normal_interpenetration_velocity = ( - _dot_product(interpenetration_velocity, distance_vector) * distance_vector - ) - contact_damping_force = -contact_nu * normal_interpenetration_velocity - - # magnitude* direction - net_contact_force = 0.5 * mask * (contact_damping_force + contact_force) - - # Compute friction - slip_interpenetration_velocity = ( - interpenetration_velocity - normal_interpenetration_velocity - ) - slip_interpenetration_velocity_mag = np.linalg.norm( - slip_interpenetration_velocity - ) - slip_interpenetration_velocity_unitized = slip_interpenetration_velocity / ( - slip_interpenetration_velocity_mag + 1e-14 - ) - # Compute friction force in the slip direction. - damping_force_in_slip_direction = ( - velocity_damping_coefficient * slip_interpenetration_velocity_mag - ) - # Compute Coulombic friction - coulombic_friction_force = friction_coefficient * np.linalg.norm( - net_contact_force - ) - # Compare damping force in slip direction and kinetic friction and minimum is the friction force. - friction_force = ( - -min(damping_force_in_slip_direction, coulombic_friction_force) - * slip_interpenetration_velocity_unitized - ) - # Update contact force - net_contact_force += friction_force - - # Torques acting on the cylinder - moment_arm = x_cylinder_contact_point - x_cylinder_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 - cylinder_total_contact_forces += 2.0 * net_contact_force - cylinder_total_contact_torques += np.cross( - moment_arm, 2.0 * net_contact_force - ) - elif i == n_points: - external_forces_rod[..., i] -= 4 / 3 * net_contact_force - external_forces_rod[..., i + 1] -= 2 / 3 * net_contact_force - cylinder_total_contact_forces += 2.0 * net_contact_force - cylinder_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 - cylinder_total_contact_forces += 2.0 * net_contact_force - cylinder_total_contact_torques += np.cross( - moment_arm, 2.0 * net_contact_force - ) - - # Update the cylinder external forces and torques - external_forces_cylinder[..., 0] += cylinder_total_contact_forces - external_torques_cylinder[..., 0] += ( - cylinder_director_collection @ cylinder_total_contact_torques + raise NotImplementedError( + "This function is removed in v0.3.2. Please use\n" + "elastica._contact_functions._calculate_contact_forces_rod_cylinder()\n" + "instead for calculating rod cylinder contact forces." ) -@numba.njit(cache=True) def _calculate_contact_forces_rod_rod( x_collection_rod_one, radius_rod_one, @@ -612,105 +447,13 @@ def _calculate_contact_forces_rod_rod( contact_k, contact_nu, ): - # 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] - edge_collection_rod_one = _batch_product_k_ik_to_ik(length_rod_one, tangent_rod_one) - edge_collection_rod_two = _batch_product_k_ik_to_ik(length_rod_two, tangent_rod_two) - - for i in range(n_points_rod_one): - for j in range(n_points_rod_two): - radii_sum = radius_rod_one[i] + radius_rod_two[j] - length_sum = length_rod_one[i] + length_rod_two[j] - # Element-wise bounding box - x_selected_rod_one = x_collection_rod_one[..., i] - x_selected_rod_two = x_collection_rod_two[..., j] - - del_x = x_selected_rod_one - x_selected_rod_two - norm_del_x = _norm(del_x) - - # If outside then don't process - if norm_del_x >= (radii_sum + length_sum): - continue - - # find the shortest line segment between the two centerline - # segments : differs from normal cylinder-cylinder intersection - distance_vector, _, _ = _find_min_dist( - x_selected_rod_one, - edge_collection_rod_one[..., i], - x_selected_rod_two, - edge_collection_rod_two[..., j], - ) - distance_vector_length = _norm(distance_vector) - distance_vector /= distance_vector_length - - gamma = radii_sum - distance_vector_length - - # If distance is large, don't worry about it - if gamma < -1e-5: - continue - - rod_one_elemental_forces = 0.5 * ( - external_forces_rod_one[..., i] - + external_forces_rod_one[..., i + 1] - + internal_forces_rod_one[..., i] - + internal_forces_rod_one[..., i + 1] - ) - - rod_two_elemental_forces = 0.5 * ( - external_forces_rod_two[..., j] - + external_forces_rod_two[..., j + 1] - + internal_forces_rod_two[..., j] - + internal_forces_rod_two[..., j + 1] - ) - - equilibrium_forces = -rod_one_elemental_forces + rod_two_elemental_forces - - normal_force = _dot_product(equilibrium_forces, distance_vector) - # Following line same as np.where(normal_force < 0.0, -normal_force, 0.0) - normal_force = abs(min(normal_force, 0.0)) - - # CHECK FOR GAMMA > 0.0, heaviside but we need to overload it in numba - # As a quick fix, use this instead - mask = (gamma > 0.0) * 1.0 + raise NotImplementedError( + "This function is removed in v0.3.2. Please use\n" + "elastica._contact_functions._calculate_contact_forces_rod_rod()\n" + "instead for calculating rod rod contact forces." + ) - contact_force = contact_k * gamma - interpenetration_velocity = 0.5 * ( - (velocity_rod_one[..., i] + velocity_rod_one[..., i + 1]) - - (velocity_rod_two[..., j] + velocity_rod_two[..., j + 1]) - ) - contact_damping_force = contact_nu * _dot_product( - interpenetration_velocity, distance_vector - ) - # magnitude* direction - net_contact_force = ( - normal_force + 0.5 * mask * (contact_damping_force + contact_force) - ) * distance_vector - - # Add it to the rods at the end of the day - if i == 0: - external_forces_rod_one[..., i] -= net_contact_force * 2 / 3 - external_forces_rod_one[..., i + 1] -= net_contact_force * 4 / 3 - elif i == n_points_rod_one: - external_forces_rod_one[..., i] -= net_contact_force * 4 / 3 - external_forces_rod_one[..., i + 1] -= net_contact_force * 2 / 3 - else: - external_forces_rod_one[..., i] -= net_contact_force - external_forces_rod_one[..., i + 1] -= net_contact_force - - if j == 0: - external_forces_rod_two[..., j] += net_contact_force * 2 / 3 - external_forces_rod_two[..., j + 1] += net_contact_force * 4 / 3 - elif j == n_points_rod_two: - external_forces_rod_two[..., j] += net_contact_force * 4 / 3 - external_forces_rod_two[..., j + 1] += net_contact_force * 2 / 3 - else: - external_forces_rod_two[..., j] += net_contact_force - external_forces_rod_two[..., j + 1] += net_contact_force - - -@numba.njit(cache=True) def _calculate_contact_forces_self_rod( x_collection_rod, radius_rod, @@ -721,97 +464,21 @@ def _calculate_contact_forces_self_rod( contact_k, contact_nu, ): - # 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) - - for i in range(n_points_rod): - skip = 1 + np.ceil(0.8 * np.pi * radius_rod[i] / length_rod[i]) - for j in range(i - skip, -1, -1): - radii_sum = radius_rod[i] + radius_rod[j] - length_sum = length_rod[i] + length_rod[j] - # Element-wise bounding box - x_selected_rod_index_i = x_collection_rod[..., i] - x_selected_rod_index_j = x_collection_rod[..., j] - - del_x = x_selected_rod_index_i - x_selected_rod_index_j - norm_del_x = _norm(del_x) - - # If outside then don't process - if norm_del_x >= (radii_sum + length_sum): - continue - - # find the shortest line segment between the two centerline - # segments : differs from normal cylinder-cylinder intersection - distance_vector, _, _ = _find_min_dist( - x_selected_rod_index_i, - edge_collection_rod_one[..., i], - x_selected_rod_index_j, - edge_collection_rod_one[..., j], - ) - distance_vector_length = _norm(distance_vector) - distance_vector /= distance_vector_length - - gamma = radii_sum - distance_vector_length - - # If distance is large, don't worry about it - if gamma < -1e-5: - continue + raise NotImplementedError( + "This function is removed in v0.3.2. Please use\n" + "elastica._contact_functions._calculate_contact_forces_self_rod()\n" + "instead for calculating rod self-contact forces." + ) - # CHECK FOR GAMMA > 0.0, heaviside but we need to overload it in numba - # As a quick fix, use this instead - mask = (gamma > 0.0) * 1.0 - contact_force = contact_k * gamma - interpenetration_velocity = 0.5 * ( - (velocity_rod[..., i] + velocity_rod[..., i + 1]) - - (velocity_rod[..., j] + velocity_rod[..., j + 1]) - ) - contact_damping_force = contact_nu * _dot_product( - interpenetration_velocity, distance_vector - ) - - # magnitude* direction - net_contact_force = ( - 0.5 * mask * (contact_damping_force + contact_force) - ) * distance_vector - - # Add it to the rods at the end of the day - # if i == 0: - # external_forces_rod[...,i] -= net_contact_force *2/3 - # external_forces_rod[...,i+1] -= net_contact_force * 4/3 - if i == n_points_rod: - external_forces_rod[..., i] -= net_contact_force * 4 / 3 - external_forces_rod[..., i + 1] -= net_contact_force * 2 / 3 - else: - external_forces_rod[..., i] -= net_contact_force - external_forces_rod[..., i + 1] -= net_contact_force - - if j == 0: - external_forces_rod[..., j] += net_contact_force * 2 / 3 - external_forces_rod[..., j + 1] += net_contact_force * 4 / 3 - # elif j == n_points_rod: - # external_forces_rod[..., j] += net_contact_force * 4/3 - # external_forces_rod[..., j+1] += net_contact_force * 2/3 - else: - external_forces_rod[..., j] += net_contact_force - external_forces_rod[..., j + 1] += net_contact_force - - -@numba.njit(cache=True) def _aabbs_not_intersecting(aabb_one, aabb_two): - """Returns true if not intersecting else false""" - 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]): - return 1 - if (aabb_one[2, 1] < aabb_two[2, 0]) | (aabb_one[2, 0] > aabb_two[2, 1]): - return 1 - - return 0 + raise NotImplementedError( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._aabbs_not_intersecting()\n" + "instead for checking aabbs intersection." + ) -@numba.njit(cache=True) def _prune_using_aabbs_rod_rigid_body( rod_one_position_collection, rod_one_radius_collection, @@ -821,39 +488,13 @@ def _prune_using_aabbs_rod_rigid_body( cylinder_radius, cylinder_length, ): - max_possible_dimension = np.zeros((3,)) - aabb_rod = np.empty((3, 2)) - aabb_cylinder = np.empty((3, 2)) - max_possible_dimension[...] = np.max(rod_one_radius_collection) + np.max( - rod_one_length_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] - ) - - # Is actually Q^T * d but numba complains about performance so we do - # d^T @ Q - cylinder_dimensions_in_local_FOR = np.array( - [cylinder_radius, cylinder_radius, 0.5 * cylinder_length] + raise NotImplementedError( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._prune_using_aabbs_rod_cylinder()\n" + "instead for checking rod cylinder intersection." ) - cylinder_dimensions_in_world_FOR = np.zeros_like(cylinder_dimensions_in_local_FOR) - for i in range(3): - for j in range(3): - cylinder_dimensions_in_world_FOR[i] += ( - cylinder_director[j, i, 0] * cylinder_dimensions_in_local_FOR[j] - ) - - max_possible_dimension = np.abs(cylinder_dimensions_in_world_FOR) - aabb_cylinder[..., 0] = cylinder_position[..., 0] - max_possible_dimension - aabb_cylinder[..., 1] = cylinder_position[..., 0] + max_possible_dimension - return _aabbs_not_intersecting(aabb_cylinder, aabb_rod) -@numba.njit(cache=True) def _prune_using_aabbs_rod_rod( rod_one_position_collection, rod_one_radius_collection, @@ -862,34 +503,12 @@ def _prune_using_aabbs_rod_rod( rod_two_radius_collection, rod_two_length_collection, ): - max_possible_dimension = np.zeros((3,)) - aabb_rod_one = np.empty((3, 2)) - aabb_rod_two = np.empty((3, 2)) - max_possible_dimension[...] = np.max(rod_one_radius_collection) + np.max( - rod_one_length_collection - ) - for i in range(3): - aabb_rod_one[i, 0] = ( - np.min(rod_one_position_collection[i]) - max_possible_dimension[i] - ) - aabb_rod_one[i, 1] = ( - np.max(rod_one_position_collection[i]) + max_possible_dimension[i] - ) - - max_possible_dimension[...] = np.max(rod_two_radius_collection) + np.max( - rod_two_length_collection + raise NotImplementedError( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._prune_using_aabbs_rod_rod()\n" + "instead for checking rod rod intersection." ) - for i in range(3): - aabb_rod_two[i, 0] = ( - np.min(rod_two_position_collection[i]) - max_possible_dimension[i] - ) - aabb_rod_two[i, 1] = ( - np.max(rod_two_position_collection[i]) + max_possible_dimension[i] - ) - - return _aabbs_not_intersecting(aabb_rod_two, aabb_rod_one) - class ExternalContact(FreeJoint): """ @@ -954,6 +573,16 @@ def __init__(self, k, nu, velocity_damping_coefficient=0, friction_coefficient=0 super().__init__(k, nu) self.velocity_damping_coefficient = velocity_damping_coefficient self.friction_coefficient = friction_coefficient + log = logging.getLogger(self.__class__.__name__) + log.warning( + # Remove warning and add error if ExternalContact is used in v0.3.3 + # Remove the option to use ExternalContact, beyond v0.3.3 + "The option to use the ExternalContact joint for the rod-rod and rod-cylinder contact is now deprecated.\n" + "Instead, for rod-rod contact or rod-cylinder contact,use RodRodContact or RodCylinderContact from the add-on Contact mixin class.\n" + "For reference see the classes elastica.contact_forces.RodRodContact() and elastica.contact_forces.RodCylinderContact().\n" + "For usage check examples/RigidbodyCases/RodRigidBodyContact/rod_cylinder_contact.py and examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py.\n" + " The option to use the ExternalContact joint for the rod-rod and rod-cylinder will be removed in the future (v0.3.3).\n" + ) def apply_forces( self, @@ -963,6 +592,14 @@ def apply_forces( index_two, ): # del index_one, index_two + from elastica.contact_utils import ( + _prune_using_aabbs_rod_cylinder, + _prune_using_aabbs_rod_rod, + ) + from elastica._contact_functions import ( + _calculate_contact_forces_rod_cylinder, + _calculate_contact_forces_rod_rod, + ) # TODO: raise error during the initialization if rod one is rigid body. @@ -971,7 +608,7 @@ def apply_forces( cylinder_two = rod_two # First, check for a global AABB bounding box, and see whether that # intersects - if _prune_using_aabbs_rod_rigid_body( + if _prune_using_aabbs_rod_cylinder( rod_one.position_collection, rod_one.radius, rod_one.lengths, @@ -991,7 +628,7 @@ def apply_forces( rod_one.position_collection[..., 1:] + rod_one.position_collection[..., :-1] ) - _calculate_contact_forces_rod_rigid_body( + _calculate_contact_forces_rod_cylinder( rod_element_position, rod_one.lengths * rod_one.tangents, cylinder_two.position_collection[..., 0], @@ -1058,9 +695,21 @@ class SelfContact(FreeJoint): def __init__(self, k, nu): super().__init__(k, nu) + log = logging.getLogger(self.__class__.__name__) + log.warning( + # Remove warning and add error if SelfContact is used in v0.3.3 + # Remove the option to use SelfContact, beyond v0.3.3 + "The option to use the SelfContact joint for the rod self contact is now deprecated.\n" + "Instead, for rod self contact use RodSelfContact from the add-on Contact mixin class.\n" + "For reference see the class elastica.contact_forces.RodSelfContact(), and for usage check examples/RodContactCase/RodSelfContact/solenoids.py.\n" + "The option to use the SelfContact joint for the rod self contact will be removed in the future (v0.3.3).\n" + ) def apply_forces(self, rod_one: RodType, index_one, rod_two: SystemType, index_two): # del index_one, index_two + from elastica._contact_functions import ( + _calculate_contact_forces_self_rod, + ) _calculate_contact_forces_self_rod( rod_one.position_collection[ diff --git a/elastica/memory_block/memory_block_rigid_body.py b/elastica/memory_block/memory_block_rigid_body.py index 36bd26e54..827363784 100644 --- a/elastica/memory_block/memory_block_rigid_body.py +++ b/elastica/memory_block/memory_block_rigid_body.py @@ -1,13 +1,13 @@ __doc__ = """Create block-structure class for collection of rigid body systems.""" import numpy as np -from typing import Sequence, List +from typing import Sequence, Literal from elastica.rigidbody import RigidBodyBase from elastica.rigidbody.data_structures import _RigidRodSymplecticStepperMixin class MemoryBlockRigidBody(RigidBodyBase, _RigidRodSymplecticStepperMixin): - def __init__(self, systems: Sequence, system_idx_list: List[int]): + def __init__(self, systems: Sequence, system_idx_list: Sequence[np.int64]): self.n_bodies = len(systems) self.n_elems = self.n_bodies @@ -15,15 +15,15 @@ def __init__(self, systems: Sequence, system_idx_list: List[int]): self.system_idx_list = np.array(system_idx_list, dtype=np.int64) # Allocate block structure using system collection. - self.allocate_block_variables_scalars(systems) - self.allocate_block_variables_vectors(systems) - self.allocate_block_variables_matrix(systems) - self.allocate_block_variables_for_symplectic_stepper(systems) + self._allocate_block_variables_scalars(systems) + self._allocate_block_variables_vectors(systems) + self._allocate_block_variables_matrix(systems) + self._allocate_block_variables_for_symplectic_stepper(systems) # Initialize the mixin class for symplectic time-stepper. _RigidRodSymplecticStepperMixin.__init__(self) - def allocate_block_variables_scalars(self, systems: Sequence): + def _allocate_block_variables_scalars(self, systems: Sequence): """ This function takes system collection and allocates the variables for block-structure and references allocated variables back to the systems. @@ -55,19 +55,14 @@ def allocate_block_variables_scalars(self, systems: Sequence): (len(map_scalar_dofs_in_rigid_bodies), self.n_elems) ) - for k, v in map_scalar_dofs_in_rigid_bodies.items(): - self.__dict__[k] = np.lib.stride_tricks.as_strided( - self.scalar_dofs_in_rigid_bodies[v], (self.n_elems,) - ) - - for k, v in map_scalar_dofs_in_rigid_bodies.items(): - for system_idx, system in enumerate(systems): - self.__dict__[k][..., system_idx : system_idx + 1] = system.__dict__[k] - system.__dict__[k] = np.ndarray.view( - self.__dict__[k][..., system_idx : system_idx + 1] - ) + self._map_system_properties_to_block_memory( + mapping_dict=map_scalar_dofs_in_rigid_bodies, + systems=systems, + block_memory=self.scalar_dofs_in_rigid_bodies, + value_type="scalar", + ) - def allocate_block_variables_vectors(self, systems: Sequence): + def _allocate_block_variables_vectors(self, systems: Sequence): """ This function takes system collection and allocates the vector variables for block-structure and references allocated vector variables back to the systems. @@ -96,28 +91,21 @@ def allocate_block_variables_vectors(self, systems: Sequence): (len(map_vector_dofs_in_rigid_bodies), 3 * self.n_elems) ) - for k, v in map_vector_dofs_in_rigid_bodies.items(): - self.__dict__[k] = np.lib.stride_tricks.as_strided( - self.vector_dofs_in_rigid_bodies[v], (3, self.n_elems) - ) - - for k, v in map_vector_dofs_in_rigid_bodies.items(): - for system_idx, system in enumerate(systems): - self.__dict__[k][..., system_idx : system_idx + 1] = system.__dict__[ - k - ].copy() - system.__dict__[k] = np.ndarray.view( - self.__dict__[k][..., system_idx : system_idx + 1] - ) + self._map_system_properties_to_block_memory( + mapping_dict=map_vector_dofs_in_rigid_bodies, + systems=systems, + block_memory=self.vector_dofs_in_rigid_bodies, + value_type="vector", + ) - def allocate_block_variables_matrix(self, systems: Sequence): + def _allocate_block_variables_matrix(self, systems: Sequence): """ This function takes system collection and allocates the matrix variables for block-structure and references allocated matrix variables back to the systems. Parameters ---------- - system + systems Returns ------- @@ -139,21 +127,14 @@ def allocate_block_variables_matrix(self, systems: Sequence): (len(map_matrix_dofs_in_rigid_bodies), 9 * self.n_elems) ) - for k, v in map_matrix_dofs_in_rigid_bodies.items(): - self.__dict__[k] = np.lib.stride_tricks.as_strided( - self.matrix_dofs_in_rigid_bodies[v], (3, 3, self.n_elems) - ) - - for k, v in map_matrix_dofs_in_rigid_bodies.items(): - for system_idx, system in enumerate(systems): - self.__dict__[k][..., system_idx : system_idx + 1] = system.__dict__[ - k - ].copy() - system.__dict__[k] = np.ndarray.view( - self.__dict__[k][..., system_idx : system_idx + 1] - ) + self._map_system_properties_to_block_memory( + mapping_dict=map_matrix_dofs_in_rigid_bodies, + systems=systems, + block_memory=self.matrix_dofs_in_rigid_bodies, + value_type="tensor", + ) - def allocate_block_variables_for_symplectic_stepper(self, systems: Sequence): + def _allocate_block_variables_for_symplectic_stepper(self, systems: Sequence): """ This function takes system collection and allocates the variables used by symplectic stepper for block-structure and references allocated variables back to the systems. @@ -181,11 +162,12 @@ def allocate_block_variables_for_symplectic_stepper(self, systems: Sequence): "alpha_collection": 3, } self.rate_collection = np.zeros((len(map_rate_collection), 3 * self.n_elems)) - for k, v in map_rate_collection.items(): - self.__dict__[k] = np.lib.stride_tricks.as_strided( - self.rate_collection[v], (3, self.n_elems) - ) - + self._map_system_properties_to_block_memory( + mapping_dict=map_rate_collection, + systems=systems, + block_memory=self.rate_collection, + value_type="vector", + ) # For Dynamic state update of position Verlet create references self.v_w_collection = np.lib.stride_tricks.as_strided( self.rate_collection[0:2], (2, 3 * self.n_elems) @@ -195,11 +177,55 @@ def allocate_block_variables_for_symplectic_stepper(self, systems: Sequence): self.rate_collection[2:-1], (2, 3 * self.n_elems) ) - for k, v in map_rate_collection.items(): + def _map_system_properties_to_block_memory( + self, + mapping_dict: dict, + systems: Sequence, + block_memory: np.ndarray, + value_type: Literal["scalar", "vector", "tensor"], + ) -> None: + """Map system (rigid bodies) properties to memory blocks. + + Parameters + ---------- + mapping_dict: dict + Dictionary with attribute names as keys and block row index as values. + systems: Sequence + A sequence containing Cosserat rod objects to map from. + block_memory: ndarray + Memory block that, at the end of the method execution, contains all designated + attributes of all systems. + value_type: str + A string that indicates the shape of the attribute. + Options among "scalar", "vector", and "tensor". + + """ + if value_type == "scalar": + view_shape = (self.n_elems,) + + elif value_type == "vector": + view_shape = (3, self.n_elems) + + elif value_type == "tensor": + view_shape = (3, 3, self.n_elems) + + else: + raise ValueError( + "Incorrect value type. Must be one of scalar, vector, and tensor." + ) + + for k, v in mapping_dict.items(): + self.__dict__[k] = np.lib.stride_tricks.as_strided( + block_memory[v], + shape=view_shape, + ) + for system_idx, system in enumerate(systems): - self.__dict__[k][..., system_idx : system_idx + 1] = system.__dict__[ - k - ].copy() + self.__dict__[k][..., system_idx : system_idx + 1] = ( + system.__dict__[k] + if value_type == "scalar" + else system.__dict__[k].copy() + ) system.__dict__[k] = np.ndarray.view( self.__dict__[k][..., system_idx : system_idx + 1] ) diff --git a/elastica/memory_block/memory_block_rod.py b/elastica/memory_block/memory_block_rod.py index 91b1c4d19..233351989 100644 --- a/elastica/memory_block/memory_block_rod.py +++ b/elastica/memory_block/memory_block_rod.py @@ -1,6 +1,6 @@ __doc__ = """Create block-structure class for collection of Cosserat rod systems.""" import numpy as np -from typing import Sequence +from typing import Sequence, Literal, Callable from elastica.memory_block.memory_block_rod_base import ( MemoryBlockRodBase, make_block_memory_metadata, @@ -142,45 +142,30 @@ def __init__(self, systems: Sequence, system_idx_list): self.ghost_voronoi_idx[2::3][n_straight_rods - 1] + 1 ) - # Compute the start and end of the rod nodes again. This time, boundary cells are added. - self.start_idx_in_rod_nodes[n_straight_rods:] = ( - self.periodic_boundary_nodes_idx[0, 0::3] + 1 - ) - self.end_idx_in_rod_nodes[ - n_straight_rods: - ] = self.periodic_boundary_nodes_idx[0, 1::3] - - self.start_idx_in_rod_elems[n_straight_rods:] = ( - self.periodic_boundary_elems_idx[0, 0::2] + 1 - ) - self.end_idx_in_rod_elems[ - n_straight_rods: - ] = self.periodic_boundary_elems_idx[0, 1::2] - - self.start_idx_in_rod_voronoi[n_straight_rods:] = ( - self.periodic_boundary_voronoi_idx[0, :] + 1 - ) - else: - # Compute the start and end of the rod nodes again. This time, boundary cells are added. - self.start_idx_in_rod_nodes[:] = ( - self.periodic_boundary_nodes_idx[0, 0::3] + 1 - ) - self.end_idx_in_rod_nodes[:] = self.periodic_boundary_nodes_idx[0, 1::3] + # Compute the start and end of the rod nodes again. This time, boundary cells are added. + self.start_idx_in_rod_nodes[n_straight_rods:] = ( + self.periodic_boundary_nodes_idx[0, 0::3] + 1 + ) + self.end_idx_in_rod_nodes[ + n_straight_rods: + ] = self.periodic_boundary_nodes_idx[0, 1::3] - self.start_idx_in_rod_elems[:] = ( - self.periodic_boundary_elems_idx[0, 0::2] + 1 - ) - self.end_idx_in_rod_elems[:] = self.periodic_boundary_elems_idx[0, 1::2] + self.start_idx_in_rod_elems[n_straight_rods:] = ( + self.periodic_boundary_elems_idx[0, 0::2] + 1 + ) + self.end_idx_in_rod_elems[ + n_straight_rods: + ] = self.periodic_boundary_elems_idx[0, 1::2] - self.start_idx_in_rod_voronoi[:] = ( - self.periodic_boundary_voronoi_idx[0, :] + 1 - ) + self.start_idx_in_rod_voronoi[n_straight_rods:] = ( + self.periodic_boundary_voronoi_idx[0, :] + 1 + ) # Allocate block structure using system collection. - self.allocate_block_variables_in_nodes(systems) - self.allocate_block_variables_in_elements(systems) - self.allocate_blocks_variables_in_voronoi(systems) - self.allocate_blocks_variables_for_symplectic_stepper(systems) + self._allocate_block_variables_in_nodes(systems) + self._allocate_block_variables_in_elements(systems) + self._allocate_blocks_variables_in_voronoi(systems) + self._allocate_blocks_variables_for_symplectic_stepper(systems) # Reset ghosts of mass, rest length and rest voronoi length to 1. Otherwise # since ghosts are not modified, this causes a division by zero error. @@ -215,7 +200,7 @@ def __init__(self, systems: Sequence, system_idx_list): # Initialize the mixin class for symplectic time-stepper. _RodSymplecticStepperMixin.__init__(self) - def allocate_block_variables_in_nodes(self, systems: Sequence): + def _allocate_block_variables_in_nodes(self, systems: Sequence): """ This function takes system collection and allocates the variables on node for block-structure and references allocated variables back to the @@ -236,23 +221,13 @@ def allocate_block_variables_in_nodes(self, systems: Sequence): self.scalar_dofs_in_rod_nodes = np.zeros( (len(map_scalar_dofs_in_rod_nodes), self.n_nodes) ) - for k, v in map_scalar_dofs_in_rod_nodes.items(): - self.__dict__[k] = np.lib.stride_tricks.as_strided( - self.scalar_dofs_in_rod_nodes[v], (self.n_nodes,) - ) - - for k, v in map_scalar_dofs_in_rod_nodes.items(): - for system_idx, system in enumerate(systems): - start_idx = self.start_idx_in_rod_nodes[system_idx] - end_idx = self.end_idx_in_rod_nodes[system_idx] - self.__dict__[k][..., start_idx:end_idx] = system.__dict__[k].copy() - system.__dict__[k] = np.ndarray.view( - self.__dict__[k][..., start_idx:end_idx] - ) - # synchronize the periodic node boundaries - _synchronize_periodic_boundary_of_scalar_collection( - self.__dict__[k], self.periodic_boundary_nodes_idx - ) + self._map_system_properties_to_block_memory( + mapping_dict=map_scalar_dofs_in_rod_nodes, + systems=systems, + block_memory=self.scalar_dofs_in_rod_nodes, + domain_type="node", + value_type="scalar", + ) # Things in nodes that are vectors # 0 ("position_collection", float64[:, :]), @@ -267,28 +242,15 @@ def allocate_block_variables_in_nodes(self, systems: Sequence): self.vector_dofs_in_rod_nodes = np.zeros( (len(map_vector_dofs_in_rod_nodes), 3 * self.n_nodes) ) - for k, v in map_vector_dofs_in_rod_nodes.items(): - self.__dict__[k] = np.lib.stride_tricks.as_strided( - self.vector_dofs_in_rod_nodes[v], (3, self.n_nodes) - ) - - for k, v in map_vector_dofs_in_rod_nodes.items(): - for system_idx, system in enumerate(systems): - start_idx = self.start_idx_in_rod_nodes[system_idx] - end_idx = self.end_idx_in_rod_nodes[system_idx] - self.__dict__[k][..., start_idx:end_idx] = system.__dict__[k].copy() - system.__dict__[k] = np.ndarray.view( - self.__dict__[k][..., start_idx:end_idx] - ) - # synchronize the periodic node boundaries - _synchronize_periodic_boundary_of_vector_collection( - self.__dict__[k], self.periodic_boundary_nodes_idx - ) - - # Things in nodes that are matrices - # Null set + self._map_system_properties_to_block_memory( + mapping_dict=map_vector_dofs_in_rod_nodes, + systems=systems, + block_memory=self.vector_dofs_in_rod_nodes, + domain_type="node", + value_type="vector", + ) - def allocate_block_variables_in_elements(self, systems: Sequence): + def _allocate_block_variables_in_elements(self, systems: Sequence): """ This function takes system collection and allocates the variables on elements for block-structure and references allocated variables back to the @@ -323,23 +285,13 @@ def allocate_block_variables_in_elements(self, systems: Sequence): self.scalar_dofs_in_rod_elems = np.zeros( (len(map_scalar_dofs_in_rod_elems), self.n_elems) ) - for k, v in map_scalar_dofs_in_rod_elems.items(): - self.__dict__[k] = np.lib.stride_tricks.as_strided( - self.scalar_dofs_in_rod_elems[v], (self.n_elems,) - ) - - for k, v in map_scalar_dofs_in_rod_elems.items(): - for system_idx, system in enumerate(systems): - start_idx = self.start_idx_in_rod_elems[system_idx] - end_idx = self.end_idx_in_rod_elems[system_idx] - self.__dict__[k][..., start_idx:end_idx] = system.__dict__[k].copy() - system.__dict__[k] = np.ndarray.view( - self.__dict__[k][..., start_idx:end_idx] - ) - # synchronize the periodic element boundaries - _synchronize_periodic_boundary_of_scalar_collection( - self.__dict__[k], self.periodic_boundary_elems_idx - ) + self._map_system_properties_to_block_memory( + mapping_dict=map_scalar_dofs_in_rod_elems, + systems=systems, + block_memory=self.scalar_dofs_in_rod_elems, + domain_type="element", + value_type="scalar", + ) # Things in elements that are vectors # 0 ("tangents", float64[:, :]), @@ -359,23 +311,13 @@ def allocate_block_variables_in_elements(self, systems: Sequence): self.vector_dofs_in_rod_elems = np.zeros( (len(map_vector_dofs_in_rod_elems), 3 * self.n_elems) ) - for k, v in map_vector_dofs_in_rod_elems.items(): - self.__dict__[k] = np.lib.stride_tricks.as_strided( - self.vector_dofs_in_rod_elems[v], (3, self.n_elems) - ) - - for k, v in map_vector_dofs_in_rod_elems.items(): - for system_idx, system in enumerate(systems): - start_idx = self.start_idx_in_rod_elems[system_idx] - end_idx = self.end_idx_in_rod_elems[system_idx] - self.__dict__[k][..., start_idx:end_idx] = system.__dict__[k].copy() - system.__dict__[k] = np.ndarray.view( - self.__dict__[k][..., start_idx:end_idx] - ) - # synchronize the periodic element boundaries - _synchronize_periodic_boundary_of_vector_collection( - self.__dict__[k], self.periodic_boundary_elems_idx - ) + self._map_system_properties_to_block_memory( + mapping_dict=map_vector_dofs_in_rod_elems, + systems=systems, + block_memory=self.vector_dofs_in_rod_elems, + domain_type="element", + value_type="vector", + ) # Things in elements that are matrices # 0 ("director_collection", float64[:, :, :]), @@ -391,25 +333,15 @@ def allocate_block_variables_in_elements(self, systems: Sequence): self.matrix_dofs_in_rod_elems = np.zeros( (len(map_matrix_dofs_in_rod_elems), 9 * self.n_elems) ) - for k, v in map_matrix_dofs_in_rod_elems.items(): - self.__dict__[k] = np.lib.stride_tricks.as_strided( - self.matrix_dofs_in_rod_elems[v], (3, 3, self.n_elems) - ) - - for k, v in map_matrix_dofs_in_rod_elems.items(): - for system_idx, system in enumerate(systems): - start_idx = self.start_idx_in_rod_elems[system_idx] - end_idx = self.end_idx_in_rod_elems[system_idx] - self.__dict__[k][..., start_idx:end_idx] = system.__dict__[k].copy() - system.__dict__[k] = np.ndarray.view( - self.__dict__[k][..., start_idx:end_idx] - ) - # synchronize the periodic element boundaries - _synchronize_periodic_boundary_of_matrix_collection( - self.__dict__[k], self.periodic_boundary_elems_idx - ) + self._map_system_properties_to_block_memory( + mapping_dict=map_matrix_dofs_in_rod_elems, + systems=systems, + block_memory=self.matrix_dofs_in_rod_elems, + domain_type="element", + value_type="tensor", + ) - def allocate_blocks_variables_in_voronoi(self, systems: Sequence): + def _allocate_blocks_variables_in_voronoi(self, systems: Sequence): """ This function takes system collection and allocates the variables on voronoi for block-structure and references allocated variables back to the @@ -434,23 +366,13 @@ def allocate_blocks_variables_in_voronoi(self, systems: Sequence): self.scalar_dofs_in_rod_voronois = np.zeros( (len(map_scalar_dofs_in_rod_voronois), self.n_voronoi) ) - for k, v in map_scalar_dofs_in_rod_voronois.items(): - self.__dict__[k] = np.lib.stride_tricks.as_strided( - self.scalar_dofs_in_rod_voronois[v], (self.n_voronoi,) - ) - - for k, v in map_scalar_dofs_in_rod_voronois.items(): - for system_idx, system in enumerate(systems): - start_idx = self.start_idx_in_rod_voronoi[system_idx] - end_idx = self.end_idx_in_rod_voronoi[system_idx] - self.__dict__[k][..., start_idx:end_idx] = system.__dict__[k].copy() - system.__dict__[k] = np.ndarray.view( - self.__dict__[k][..., start_idx:end_idx] - ) - # synchronize the periodic voronoi boundaries - _synchronize_periodic_boundary_of_scalar_collection( - self.__dict__[k], self.periodic_boundary_voronoi_idx - ) + self._map_system_properties_to_block_memory( + mapping_dict=map_scalar_dofs_in_rod_voronois, + systems=systems, + block_memory=self.scalar_dofs_in_rod_voronois, + domain_type="voronoi", + value_type="scalar", + ) # Things in voronoi that are vectors # 0 ("kappa", float64[:, :]), @@ -464,23 +386,13 @@ def allocate_blocks_variables_in_voronoi(self, systems: Sequence): self.vector_dofs_in_rod_voronois = np.zeros( (len(map_vector_dofs_in_rod_voronois), 3 * self.n_voronoi) ) - for k, v in map_vector_dofs_in_rod_voronois.items(): - self.__dict__[k] = np.lib.stride_tricks.as_strided( - self.vector_dofs_in_rod_voronois[v], (3, self.n_voronoi) - ) - - for k, v in map_vector_dofs_in_rod_voronois.items(): - for system_idx, system in enumerate(systems): - start_idx = self.start_idx_in_rod_voronoi[system_idx] - end_idx = self.end_idx_in_rod_voronoi[system_idx] - self.__dict__[k][..., start_idx:end_idx] = system.__dict__[k].copy() - system.__dict__[k] = np.ndarray.view( - self.__dict__[k][..., start_idx:end_idx] - ) - # synchronize the periodic voronoi boundaries - _synchronize_periodic_boundary_of_vector_collection( - self.__dict__[k], self.periodic_boundary_voronoi_idx - ) + self._map_system_properties_to_block_memory( + mapping_dict=map_vector_dofs_in_rod_voronois, + systems=systems, + block_memory=self.vector_dofs_in_rod_voronois, + domain_type="voronoi", + value_type="vector", + ) # Things in voronoi that are matrices # 0 ("bend_matrix", float64[:, :, :]), @@ -488,26 +400,15 @@ def allocate_blocks_variables_in_voronoi(self, systems: Sequence): self.matrix_dofs_in_rod_voronois = np.zeros( (len(map_matrix_dofs_in_rod_voronois), 9 * self.n_voronoi) ) + self._map_system_properties_to_block_memory( + mapping_dict=map_matrix_dofs_in_rod_voronois, + systems=systems, + block_memory=self.matrix_dofs_in_rod_voronois, + domain_type="voronoi", + value_type="tensor", + ) - for k, v in map_matrix_dofs_in_rod_voronois.items(): - self.__dict__[k] = np.lib.stride_tricks.as_strided( - self.matrix_dofs_in_rod_voronois[v], (3, 3, self.n_voronoi) - ) - - for k, v in map_matrix_dofs_in_rod_voronois.items(): - for system_idx, system in enumerate(systems): - start_idx = self.start_idx_in_rod_voronoi[system_idx] - end_idx = self.end_idx_in_rod_voronoi[system_idx] - self.__dict__[k][..., start_idx:end_idx] = system.__dict__[k].copy() - system.__dict__[k] = np.ndarray.view( - self.__dict__[k][..., start_idx:end_idx] - ) - # synchronize the periodic voronoi boundaries - _synchronize_periodic_boundary_of_matrix_collection( - self.__dict__[k], self.periodic_boundary_voronoi_idx - ) - - def allocate_blocks_variables_for_symplectic_stepper(self, systems: Sequence): + def _allocate_blocks_variables_for_symplectic_stepper(self, systems: Sequence): """ This function takes system collection and allocates the variables used by symplectic stepper for block-structure and references allocated variables back to the systems. @@ -535,29 +436,6 @@ def allocate_blocks_variables_for_symplectic_stepper(self, systems: Sequence): "alpha_collection": 3, } self.rate_collection = np.zeros((len(map_rate_collection), 3 * self.n_nodes)) - for k, v in map_rate_collection.items(): - self.__dict__[k] = np.lib.stride_tricks.as_strided( - self.rate_collection[v], (3, self.n_nodes) - ) - - self.__dict__["velocity_collection"] = np.lib.stride_tricks.as_strided( - self.rate_collection[0], (3, self.n_nodes) - ) - - self.__dict__["omega_collection"] = np.lib.stride_tricks.as_strided( - self.rate_collection[1], - (3, self.n_elems), - ) - - self.__dict__["acceleration_collection"] = np.lib.stride_tricks.as_strided( - self.rate_collection[2], - (3, self.n_nodes), - ) - - self.__dict__["alpha_collection"] = np.lib.stride_tricks.as_strided( - self.rate_collection[3], - (3, self.n_elems), - ) # For Dynamic state update of position Verlet create references self.v_w_collection = np.lib.stride_tricks.as_strided( @@ -565,41 +443,137 @@ def allocate_blocks_variables_for_symplectic_stepper(self, systems: Sequence): ) self.dvdt_dwdt_collection = np.lib.stride_tricks.as_strided( - self.rate_collection[2:-1], (2, 3 * self.n_nodes) + self.rate_collection[2:], (2, 3 * self.n_nodes) ) # Copy systems variables on nodes to block structure map_rate_collection_dofs_in_rod_nodes = { "velocity_collection": 0, - "acceleration_collection": 1, + "acceleration_collection": 2, } - for k, v in map_rate_collection_dofs_in_rod_nodes.items(): - for system_idx, system in enumerate(systems): - start_idx = self.start_idx_in_rod_nodes[system_idx] - end_idx = self.end_idx_in_rod_nodes[system_idx] - self.__dict__[k][..., start_idx:end_idx] = system.__dict__[k].copy() - system.__dict__[k] = np.ndarray.view( - self.__dict__[k][..., start_idx:end_idx] - ) - # synchronize the periodic node boundaries - _synchronize_periodic_boundary_of_vector_collection( - self.__dict__[k], self.periodic_boundary_nodes_idx - ) + self._map_system_properties_to_block_memory( + mapping_dict=map_rate_collection_dofs_in_rod_nodes, + systems=systems, + block_memory=self.rate_collection, + domain_type="node", + value_type="vector", + ) - # Copy systems variables on nodes to block structure + # Copy systems variables on elements to block structure map_rate_collection_dofs_in_rod_elems = { - "omega_collection": 0, - "alpha_collection": 1, + "omega_collection": 1, + "alpha_collection": 3, } - for k, v in map_rate_collection_dofs_in_rod_elems.items(): + self._map_system_properties_to_block_memory( + mapping_dict=map_rate_collection_dofs_in_rod_elems, + systems=systems, + block_memory=self.rate_collection, + domain_type="element", + value_type="vector", + ) + + def _map_system_properties_to_block_memory( + self, + mapping_dict: dict, + systems: Sequence, + block_memory: np.ndarray, + domain_type: Literal["node", "element", "voronoi"], + value_type: Literal["scalar", "vector", "tensor"], + ) -> None: + """Map system (Cosserat rods) properties to memory blocks. + + This method take domain types (node, element, voronoi) and value + types (scalar, vector, tensor) as inputs and compute internally how to + construct the mapping properly. + + Parameters + ---------- + mapping_dict: dict + Dictionary with attribute names as keys and block row index as values. + systems: Sequence + A sequence containing Cosserat rod objects to map from. + block_memory: ndarray + Memory block that, at the end of the method execution, contains all designated + attributes of all systems. + domain_type: str + A string that indicates the discretized domain where the attributes reside. + Options among "node", "element", and "voronoi". + value_type: str + A string that indicates the shape of the attribute. + Options among "scalar", "vector", and "tensor". + + """ + + # typedef + start_idx_list: np.ndarray + end_idx_list: np.ndarray + periodic_boundary_idx: np.ndarray + synchronize_periodic_boundary: Callable + domain_num: np.int64 + view_shape: tuple + + if domain_type == "node": + start_idx_list = self.start_idx_in_rod_nodes.view() + end_idx_list = self.end_idx_in_rod_nodes.view() + domain_num = self.n_nodes + periodic_boundary_idx = self.periodic_boundary_nodes_idx.view() + + elif domain_type == "element": + start_idx_list = self.start_idx_in_rod_elems.view() + end_idx_list = self.end_idx_in_rod_elems.view() + domain_num = self.n_elems + periodic_boundary_idx = self.periodic_boundary_elems_idx.view() + + elif domain_type == "voronoi": + start_idx_list = self.start_idx_in_rod_voronoi.view() + end_idx_list = self.end_idx_in_rod_voronoi.view() + domain_num = self.n_voronoi + periodic_boundary_idx = self.periodic_boundary_voronoi_idx.view() + + else: + raise ValueError( + "Incorrect domain type. Must be one of node, element, and voronoi" + ) + + if value_type == "scalar": + view_shape = (domain_num,) + synchronize_periodic_boundary = ( + _synchronize_periodic_boundary_of_scalar_collection + ) + + elif value_type == "vector": + view_shape = (3, domain_num) + synchronize_periodic_boundary = ( + _synchronize_periodic_boundary_of_vector_collection + ) + + elif value_type == "tensor": + view_shape = (3, 3, domain_num) + synchronize_periodic_boundary = ( + _synchronize_periodic_boundary_of_matrix_collection + ) + + else: + raise ValueError( + "Incorrect value type. Must be one of scalar, vector, and tensor." + ) + + for k, v in mapping_dict.items(): + # Map class attributes to block memory + self.__dict__[k] = np.lib.stride_tricks.as_strided( + block_memory[v], + shape=view_shape, + ) + + # Copy system attributes into block memory, then make system attributes + # views into the block memory for system_idx, system in enumerate(systems): - start_idx = self.start_idx_in_rod_elems[system_idx] - end_idx = self.end_idx_in_rod_elems[system_idx] + start_idx = start_idx_list[system_idx] + end_idx = end_idx_list[system_idx] self.__dict__[k][..., start_idx:end_idx] = system.__dict__[k].copy() system.__dict__[k] = np.ndarray.view( self.__dict__[k][..., start_idx:end_idx] ) - # synchronize the periodic node boundaries - _synchronize_periodic_boundary_of_vector_collection( - self.__dict__[k], self.periodic_boundary_elems_idx - ) + + # Synchronize periodic boundaries + synchronize_periodic_boundary(self.__dict__[k], periodic_boundary_idx) diff --git a/elastica/memory_block/memory_block_rod_base.py b/elastica/memory_block/memory_block_rod_base.py index 06eea7149..023ea9091 100644 --- a/elastica/memory_block/memory_block_rod_base.py +++ b/elastica/memory_block/memory_block_rod_base.py @@ -1,23 +1,32 @@ __doc__ = """Create block-structure class for collection of Cosserat rod systems.""" import numpy as np +from typing import Iterable -def make_block_memory_metadata(n_elems_in_rods): +def make_block_memory_metadata(n_elems_in_rods: np.ndarray) -> Iterable: """ - This function, takes number of elements of each rod as an numpy array and computes, + This function, takes number of elements of each rod as a numpy array and computes, ghost nodes, elements and voronoi element indexes and numbers and returns it. Parameters ---------- - n_elems_in_rods + n_elems_in_rods: ndarray + An integer array containing the number of elements in each of the n rod. Returns ------- - + n_elems_with_ghosts: int64 + Total number of elements with ghost elements included. There are two ghost elements + between each pair of two rods adjacent in memory block. + ghost_nodes_idx: ndarray + An integer array of length n - 1 containing the indices of ghost nodes in memory block. + ghost_elements_idx: ndarray + An integer array of length 2 * (n - 1) containing the indices of ghost elements in memory block. + ghost_voronoi_idx: ndarray + An integer array of length 2 * (n - 1) containing the indices of ghost Voronoi nodes in memory block. """ - n_nodes_in_rods = n_elems_in_rods + 1 - n_voronois_in_rods = n_elems_in_rods - 1 + n_nodes_in_rods = n_elems_in_rods + 1 n_rods = n_elems_in_rods.shape[0] # Gap between two rods have one ghost node @@ -27,28 +36,18 @@ def make_block_memory_metadata(n_elems_in_rods): # Gap between two rods have three ghost voronois : comes out to n_nodes_with_ghosts - 2 # n_voronoi_with_ghosts = np.sum(n_voronois_in_rods) + 3 * (n_rods - 1) - # To be nulled - ghost_nodes_idx = np.zeros(((n_rods - 1),), dtype=np.int64) - ghost_nodes_idx[:] = n_nodes_in_rods[:-1] - ghost_nodes_idx = np.cumsum(ghost_nodes_idx) + ghost_nodes_idx = np.cumsum(n_nodes_in_rods[:-1], dtype=np.int64) # Add [0, 1, 2, ... n_rods-2] to the ghost_nodes idx to accommodate miscounting ghost_nodes_idx += np.arange(0, n_rods - 1, dtype=np.int64) ghost_elems_idx = np.zeros((2 * (n_rods - 1),), dtype=np.int64) - ghost_elems_idx[::2] = n_elems_in_rods[:-1] - ghost_elems_idx[1::2] = 1 - ghost_elems_idx = np.cumsum(ghost_elems_idx) - # Add [0, 0, 1, 1, 2, 2, ... n_rods-2, n_rods-2] to the ghost_elems idx to accommodate miscounting - ghost_elems_idx += np.repeat(np.arange(0, n_rods - 1, dtype=np.int64), 2) + ghost_elems_idx[::2] = ghost_nodes_idx - 1 + ghost_elems_idx[1::2] = ghost_nodes_idx.copy() ghost_voronoi_idx = np.zeros((3 * (n_rods - 1),), dtype=np.int64) - ghost_voronoi_idx[::3] = n_voronois_in_rods[:-1] - ghost_voronoi_idx[1::3] = 1 - ghost_voronoi_idx[2::3] = 1 - ghost_voronoi_idx = np.cumsum(ghost_voronoi_idx) - # Add [0, 0, 0, 1, 1, 1, 2, 2, 2, ... n_rods-2, n_rods-2, n_rods-2] to the ghost_voronoi idx - # to accommodate miscounting - ghost_voronoi_idx += np.repeat(np.arange(0, n_rods - 1, dtype=np.int64), 3) + ghost_voronoi_idx[::3] = ghost_nodes_idx - 2 + ghost_voronoi_idx[1::3] = ghost_nodes_idx - 1 + ghost_voronoi_idx[2::3] = ghost_nodes_idx.copy() return n_elems_with_ghosts, ghost_nodes_idx, ghost_elems_idx, ghost_voronoi_idx diff --git a/elastica/mesh/mesh_initializer.py b/elastica/mesh/mesh_initializer.py new file mode 100644 index 000000000..2320c1858 --- /dev/null +++ b/elastica/mesh/mesh_initializer.py @@ -0,0 +1,243 @@ +__doc__ = """ Mesh Initializer using Pyvista """ + +import pyvista as pv +import numpy as np + + +class Mesh: + """ + This Mesh Initializer class uses pyvista to import mesh files in the + STL or OBJ file formats and initializes the necessary mesh information. + + How to initialize a mesh? + ------------------------- + + mesh = Mesh(r"") + + Notes: + ------ + + - Please be sure to add .stl / .obj at the end of the filepath, if already present, ignore. + + Attributes: + ----------- + + mesh.faces: + - Stores the coordinates of the 3 vertices of each of the n faces of the imported mesh. + - Dimension: (3 spatial coordinates, 3 vertices, n faces) + + mesh.face_normals: + - Stores the coordinates of the unit normal vector of each of the n faces. + - Dimension: (3 spatial coordinates, n faces) + + mesh.face_centers: + - Stores the coordinates of the position vector of each of the n face centers. + - Dimension: (3 spatial coordinates, n faces) + + mesh.mesh_scale: + - Stores the 3 dimensions of the smallest box that could fit the mesh. + - Dimension: (3 spatial lengths) + + mesh.mesh_center: + - Stores the coordinates of the position vector of the center of the smallest box that could fit the mesh. + - Dimension: (3 spatial coordinates) + + mesh.mesh_orientation: + - store the 3 orthonormal basis vectors that define the mesh orientation. + - Dimension: (3 spatial coordinates, 3 orthonormal basis vectors) + - Initial value: [[1,0,0],[0,1,0],[0,0,1]] + + Methods: + -------- + + mesh.mesh_update(): + Parameters: None + - This method updates/refreshes the mesh attributes in pyelastica geometry. + - By default this method is called at initialization and after every method that might change the mesh attributes. + + mesh.visualize(): + Parameters: None + - This method visualizes the mesh using pyvista. + + mesh.translate(): + Parameters: {numpy.ndarray-(3 spatial coordinates)} + ex : mesh.translate(np.array([1,1,1])) + - This method translates the mesh by a given vector. + - By default, the mesh's center is at the origin; + by calling this method, the mesh's center is translated to the given vector. + + mesh.scale(): + Parameters: {numpy.ndarray-(3 spatial constants)} + ex : mesh.scale(np.array([1,1,1])) + - This method scales the mesh by a given factor in respective axes. + + mesh.rotate(): + Parameters: {rotation_axis: unit vector[numpy.ndarray-(3 spatial coordinates)], angle: in degrees[float]} + ex : mesh.rotate(np.array([1,0,0]), 90) + - This method rotates the mesh by a given angle about a given axis. + """ + + def __init__(self, filepath: str) -> None: + self.mesh = pv.read(filepath) + self.orientation_cube = pv.Cube() + self.mesh_update() + + def mesh_update(self) -> None: + """ + This method updates/refreshes the mesh attributes in pyelastica geometry. + This needs to be performed at the first initialization as well as + after every method that might change the mesh attributes. + """ + self.mesh_center = self.mesh.center + self.face_normals = self.face_normal_calculation(self.mesh.face_normals) + self.faces = self.face_calculation( + self.mesh.faces, self.mesh.points, self.mesh.n_faces + ) + self.face_centers = self.face_center_calculation(self.faces, self.mesh.n_faces) + self.mesh_scale = self.mesh_scale_calculation(self.mesh.bounds) + self.mesh_orientation = self.orientation_calculation( + self.orientation_cube.face_normals + ) + + def face_calculation( + self, pvfaces: np.ndarray, meshpoints: np.ndarray, n_faces: int + ) -> np.ndarray: + """ + This function converts the faces from pyvista to pyelastica geometry + + What the function does?: + ------------------------ + + # The pyvista's 'faces' attribute returns the connectivity array of the faces of the mesh. + ex: [3, 0, 1, 2, 4, 0, 1, 3, 4] + The faces array is organized as: + [n0, p0_0, p0_1, ..., p0_n, n1, p1_0, p1_1, ..., p1_n, ...] + ,where n0 is the number of points in face 0, and pX_Y is the Y'th point in face X. + For more info, refer to the api reference here - https://docs.pyvista.org/version/stable/api/core/_autosummary/pyvista.PolyData.faces.html + + # The pyvista's 'points' attribute returns the individual vertices of the mesh with no connection information. + ex: [-1. 1. -1.] + [ 1. -1. -1.] + [ 1. 1. -1.] + + # This function takes the 'mesh.points' and numbers them as 0, 1, 2 ..., n_faces - 1; + then establishes connection between verticies of same cell/face through the 'mesh.faces' array + and returns an array with dimension (3 spatial coordinates, 3 vertices, n faces), where n_faces is the number of faces in the mesh. + + Notes: + ------ + + - This function works only if each face of the mesh has equal no. of vertices i.e + all the faces of the mesh has similar geometry. + """ + n_of_vertices_in_each_face = pvfaces[0] + faces = np.zeros((3, n_of_vertices_in_each_face, n_faces)) + vertice_no = 0 + + for i in range(n_faces): + vertice_no += 1 + for j in range(n_of_vertices_in_each_face): + faces[..., j, i] = meshpoints[pvfaces[vertice_no]] + vertice_no += 1 + + return faces + + def face_normal_calculation(self, pyvista_face_normals: np.ndarray) -> np.ndarray: + """ + This function converts the face normals from pyvista to pyelastica geometry, + in pyelastica the face are stored in the format of (n_faces, 3 spatial coordinates), + this is converted into (3 spatial coordinates, n_faces). + """ + face_normals = np.transpose(pyvista_face_normals) + + return face_normals + + def face_center_calculation(self, faces: np.ndarray, n_faces: int) -> np.ndarray: + """ + This function calculates the position vector of each face of the mesh + simply by averaging all the vertices of every face/cell. + """ + face_centers = np.zeros((3, n_faces)) + + for i in range(n_faces): + for j in range(3): + vertices = len(faces[j][..., i]) + temp_sum = faces[j][..., i].sum() + face_centers[j][i] = temp_sum / vertices + + return face_centers + + def mesh_scale_calculation(self, bounds: np.ndarray) -> np.ndarray: + """ + This function calculates scale of the mesh, + for that it calculates the maximum distance between mesh's farthest verticies in each axis. + """ + scale = np.zeros(3) + axis = 0 + for i in range(0, 5, 2): + scale[axis] = bounds[i + 1] - bounds[i] + axis += 1 + + return scale + + def orientation_calculation(self, cube_face_normals: np.ndarray) -> np.ndarray: + """ + This function calculates the orientation of the mesh + by using a dummy cube utility from pyvista. + This dummy cube has minimal values for lengths to minimize the calculation time. + + How the orientation is calculated?: + ------------------------------------ + The dummy cube's orthogonal face normals are extracted to keep record of the orientation of cube; + the cube is rotated with the mesh everytime the mesh.rotate() is called, so the orientation + of the cube is same as the mesh. + Lastly we find the orientation of the mesh by extracting unit face normal vector for each axis. + """ + orientation = np.zeros((3, 3)) + axis = 0 + for i in range(1, 6, 2): + orientation[axis] = cube_face_normals[i] + axis += 1 + + return orientation + + def visualize(self) -> None: + """ + This function visualizes the mesh using pyvista. + """ + pyvista_plotter = pv.Plotter() + pyvista_plotter.add_mesh(self.mesh) + pyvista_plotter.show() + + def translate(self, target_center: np.ndarray) -> None: + """ + Parameters: {numpy.ndarray-(3 spatial coordinates)} + ex : mesh.translate(np.array([1,1,1])) + + This method moves the mesh by center to the + the target point given by the user. + """ + self.mesh = self.mesh.translate(target_center) + self.mesh_update() + + def scale(self, factor: np.ndarray) -> None: + """ + Parameters: {numpy.ndarray-(3 spatial constants)} + ex : mesh.scale(np.array([1,1,1])) + + This method scales the mesh by the given factor. + """ + self.mesh = self.mesh.scale(factor) + self.mesh_update() + + def rotate(self, axis: np.ndarray, angle: float) -> None: + """ + Parameters: {rotation_axis: unit vector[numpy.ndarray-(3 spatial coordinates)], angle: in degrees[float]} + ex : mesh.rotate(np.array([1,0,0]), 90) + + This method rotates the mesh by the given angle + on the give rotation axis. + """ + self.mesh = self.mesh.rotate_vector(axis, angle) + self.orientation_cube = self.orientation_cube.rotate_vector(axis, angle) + self.mesh_update() diff --git a/elastica/modules/__init__.py b/elastica/modules/__init__.py index 889bf69fd..a801adb73 100644 --- a/elastica/modules/__init__.py +++ b/elastica/modules/__init__.py @@ -10,3 +10,4 @@ from .forcing import Forcing from .callbacks import CallBacks from .damping import Damping +from .contact import Contact diff --git a/elastica/modules/base_system.py b/elastica/modules/base_system.py index bc26f0d83..2cb82b3f7 100644 --- a/elastica/modules/base_system.py +++ b/elastica/modules/base_system.py @@ -11,6 +11,7 @@ from elastica.rod import RodBase from elastica.rigidbody import RigidBodyBase +from elastica.surface import SurfaceBase from elastica.modules.memory_block import construct_memory_block_structures from elastica._synchronize_periodic_boundary import _ConstrainPeriodicBoundaries @@ -54,7 +55,7 @@ def __init__(self): # We need to initialize our mixin classes super(BaseSystemCollection, self).__init__() # List of system types/bases that are allowed - self.allowed_sys_types = (RodBase, RigidBodyBase) + self.allowed_sys_types = (RodBase, RigidBodyBase, SurfaceBase) # List of systems to be integrated self._systems = [] # Flag Finalize: Finalizing twice will cause an error, @@ -168,9 +169,20 @@ def finalize(self): # Toggle the finalize_flag self._finalize_flag = True + # sort _feature_group_synchronize so that _call_contacts is at the end + _call_contacts_index = [] + for idx, feature in enumerate(self._feature_group_synchronize): + if feature.__name__ == "_call_contacts": + _call_contacts_index.append(idx) + + # Move to the _call_contacts to the end of the _feature_group_synchronize list. + for index in _call_contacts_index: + self._feature_group_synchronize.append( + self._feature_group_synchronize.pop(index) + ) def synchronize(self, time: float): - # Collection call _featuer_group_synchronize + # Collection call _feature_group_synchronize for feature in self._feature_group_synchronize: feature(time) diff --git a/elastica/modules/contact.py b/elastica/modules/contact.py new file mode 100644 index 000000000..4f0120c50 --- /dev/null +++ b/elastica/modules/contact.py @@ -0,0 +1,171 @@ +__doc__ = """ +Contact +------- + +Provides the contact interface to apply contact forces between objects +(rods, rigid bodies, surfaces). +""" + +from elastica.typing import SystemType, AllowedContactType + + +class Contact: + """ + 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. + """ + + def __init__(self): + self._contacts = [] + super(Contact, self).__init__() + self._feature_group_synchronize.append(self._call_contacts) + self._feature_group_finalize.append(self._finalize_contact) + + def detect_contact_between( + self, first_system: SystemType, second_system: AllowedContactType + ): + """ + This method adds contact detection between two objects using the selected contact class. + You need to input the two objects that are to be connected. + + Parameters + ---------- + first_system : SystemType + Rod or rigid body object + second_system : AllowedContactType + Rod, rigid body or surface object + + Returns + ------- + + """ + sys_idx = [None] * 2 + for i_sys, sys in enumerate((first_system, second_system)): + sys_idx[i_sys] = self._get_sys_idx_if_valid(sys) + + # Create _Contact object, cache it and return to user + _contact = _Contact(*sys_idx) + self._contacts.append(_contact) + + return _contact + + def _finalize_contact(self) -> None: + + # dev : the first indices stores the + # (first_rod_idx, second_rod_idx) + # to apply the contacts to + # Technically we can use another array but it its one more book-keeping + # step. Being lazy, I put them both in the same array + self._contacts[:] = [(*contact.id(), contact()) for contact in self._contacts] + + # check contact order + for ( + first_sys_idx, + second_sys_idx, + contact, + ) in self._contacts: + contact._check_systems_validity( + self._systems[first_sys_idx], + self._systems[second_sys_idx], + ) + + def _call_contacts(self, time: float): + for ( + first_sys_idx, + second_sys_idx, + contact, + ) in self._contacts: + contact.apply_contact( + self._systems[first_sys_idx], + self._systems[second_sys_idx], + ) + + +class _Contact: + """ + Contact module private class + + Attributes + ---------- + _first_sys_idx: int + _second_sys_idx: int + _contact_cls: list + *args + Variable length argument list. + **kwargs + Arbitrary keyword arguments. + """ + + def __init__( + self, + first_sys_idx: int, + second_sys_idx: int, + ) -> None: + """ + + Parameters + ---------- + first_sys_idx + second_sys_idx + """ + self.first_sys_idx = first_sys_idx + self.second_sys_idx = second_sys_idx + self._contact_cls = None + + def using(self, contact_cls: object, *args, **kwargs): + """ + This method is a module to set which contact class is used to apply contact + between user defined rod-like objects. + + Parameters + ---------- + contact_cls: object + User defined contact class. + *args + Variable length argument list + **kwargs + Arbitrary keyword arguments. + + Returns + ------- + + """ + from elastica.contact_forces import NoContact + + assert issubclass( + contact_cls, NoContact + ), "{} is not a valid contact class. Did you forget to derive from NoContact?".format( + contact_cls + ) + self._contact_cls = contact_cls + self._args = args + self._kwargs = kwargs + return self + + def id(self): + return ( + self.first_sys_idx, + self.second_sys_idx, + ) + + def __call__(self, *args, **kwargs): + if not self._contact_cls: + raise RuntimeError( + "No contacts provided to to establish contact between rod-like object id {0}" + " and {1}, but a Contact" + " was intended as per code. Did you forget to" + " call the `using` method?".format(*self.id()) + ) + + try: + return self._contact_cls(*self._args, **self._kwargs) + except (TypeError, IndexError): + raise TypeError( + r"Unable to construct contact class.\n" + r"Did you provide all necessary contact properties?" + ) diff --git a/elastica/modules/memory_block.py b/elastica/modules/memory_block.py index da4f353ec..21cb9b50f 100644 --- a/elastica/modules/memory_block.py +++ b/elastica/modules/memory_block.py @@ -5,6 +5,7 @@ from elastica.rod import RodBase from elastica.rigidbody import RigidBodyBase +from elastica.surface import SurfaceBase from elastica.memory_block import MemoryBlockCosseratRod, MemoryBlockRigidBody @@ -34,6 +35,9 @@ def construct_memory_block_structures(systems): temp_list_for_rigid_body_systems.append(sys_to_be_added) temp_list_for_rigid_body_systems_idx.append(system_idx) + elif issubclass(sys_to_be_added.__class__, SurfaceBase): + pass + else: raise TypeError( "{0}\n" @@ -43,7 +47,9 @@ def construct_memory_block_structures(systems): "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}".format(sys_to_be_added.__class__, RodBase, RigidBodyBase) + "{1} {2} {3}".format( + sys_to_be_added.__class__, RodBase, RigidBodyBase, SurfaceBase + ) ) if temp_list_for_cosserat_rod_systems: diff --git a/elastica/surface/__init__.py b/elastica/surface/__init__.py new file mode 100644 index 000000000..efe78d06c --- /dev/null +++ b/elastica/surface/__init__.py @@ -0,0 +1,3 @@ +__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 new file mode 100644 index 000000000..a68a5be6e --- /dev/null +++ b/elastica/surface/plane.py @@ -0,0 +1,31 @@ +__doc__ = """""" + +from elastica.surface.surface_base import SurfaceBase +import numpy as np +from numpy.testing import assert_allclose +from elastica.utils import Tolerance + + +class Plane(SurfaceBase): + def __init__(self, plane_origin: np.ndarray, plane_normal: np.ndarray): + """ + Plane surface initializer. + + Parameters + ---------- + plane_origin: np.ndarray + Origin of the plane. + Expect (3,1)-shaped array. + plane_normal: np.ndarray + The normal vector of the plane, must be normalized. + Expect (3,1)-shaped array. + """ + + assert_allclose( + np.linalg.norm(plane_normal), + 1, + atol=Tolerance.atol(), + err_msg="plane normal is not a unit vector", + ) + self.normal = np.asarray(plane_normal).reshape(3) + self.origin = np.asarray(plane_origin).reshape(3, 1) diff --git a/elastica/surface/surface_base.py b/elastica/surface/surface_base.py new file mode 100644 index 000000000..8d60d2f06 --- /dev/null +++ b/elastica/surface/surface_base.py @@ -0,0 +1,18 @@ +__doc__ = """Base class for surfaces""" + + +class SurfaceBase: + """ + Base class for all surfaces. + + Notes + ----- + All new surface classes must be derived from this SurfaceBase class. + + """ + + def __init__(self): + """ + SurfaceBase does not take any arguments. + """ + pass diff --git a/elastica/typing.py b/elastica/typing.py index b4382648f..d70a16433 100644 --- a/elastica/typing.py +++ b/elastica/typing.py @@ -1,7 +1,9 @@ from elastica.rod import RodBase from elastica.rigidbody import RigidBodyBase +from elastica.surface import SurfaceBase from typing import Type, Union RodType = Type[RodBase] SystemType = Union[RodType, Type[RigidBodyBase]] +AllowedContactType = Union[SystemType, Type[SurfaceBase]] diff --git a/elastica/version.py b/elastica/version.py index a9febb449..c4fd5eff1 100644 --- a/elastica/version.py +++ b/elastica/version.py @@ -1 +1 @@ -VERSION = "0.3.1" +VERSION = "0.3.2" diff --git a/examples/Binder/1_Timoshenko_Beam.ipynb b/examples/Binder/1_Timoshenko_Beam.ipynb index 3176827b5..a8b81fdfa 100644 --- a/examples/Binder/1_Timoshenko_Beam.ipynb +++ b/examples/Binder/1_Timoshenko_Beam.ipynb @@ -27,7 +27,9 @@ } }, "outputs": [], - "source": [] + "source": [ + "!pip install \"pyelastica[examples,docs]\"" + ] }, { "cell_type": "code", diff --git a/examples/Binder/2_Slithering_Snake.ipynb b/examples/Binder/2_Slithering_Snake.ipynb index fd41d9c75..d0dbc3b31 100644 --- a/examples/Binder/2_Slithering_Snake.ipynb +++ b/examples/Binder/2_Slithering_Snake.ipynb @@ -19,6 +19,15 @@ "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[examples,docs]\"" + ] + }, { "cell_type": "code", "execution_count": null, @@ -422,12 +431,12 @@ " # zero mean.\n", "\n", " # Number of steps in one period.\n", - " period_step = int(1.0 / (time_per_period[-1] - time_per_period[-2])) + 1\n", - " number_of_period = int(time_per_period[-1]) + 1\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 - 1):\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", @@ -716,7 +725,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3.9.12 ('base')", "language": "python", "name": "python3" }, @@ -730,7 +739,12 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.7" + "version": "3.9.12" + }, + "vscode": { + "interpreter": { + "hash": "69b409ac0f88f35940b70a7cc6f81becc62e64d51a49713aaa6660602c575037" + } } }, "nbformat": 4, diff --git a/examples/CatenaryCase/catenary.py b/examples/CatenaryCase/catenary.py new file mode 100644 index 000000000..e8f796715 --- /dev/null +++ b/examples/CatenaryCase/catenary.py @@ -0,0 +1,129 @@ +import numpy as np +from elastica import * + +from post_processing import ( + plot_video, + plot_catenary, +) + + +class CatenarySimulator(BaseSystemCollection, Constraints, Forcing, Damping, CallBacks): + pass + + +catenary_sim = CatenarySimulator() +final_time = 10 +damping_constant = 0.3 +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 +base_radius = 0.01 +base_area = np.pi * (base_radius ** 2) +volume = base_area * base_length +mass = 0.2 +density = mass / volume +E = 1e4 +poisson_ratio = 0.5 +shear_modulus = E / (poisson_ratio + 1.0) + +base_rod = CosseratRod.straight_rod( + n_elem, + start, + direction, + normal, + base_length, + base_radius, + density, + youngs_modulus=E, + shear_modulus=shear_modulus, +) + +catenary_sim.append(base_rod) + +# add damping +catenary_sim.dampen(base_rod).using( + AnalyticalLinearDamper, + damping_constant=damping_constant, + time_step=time_step, +) + +# Add gravity +catenary_sim.add_forcing_to(base_rod).using( + GravityForces, acc_gravity=-9.80665 * normal +) + +# fix catenary ends +catenary_sim.constrain(base_rod).using( + FixedConstraint, constrained_position_idx=(0, -1), constrained_director_idx=(0, -1) +) + + +# Add call backs +class CatenaryCallBack(CallBackBaseClass): + """ + Call back function for continuum snake + """ + + def __init__(self, step_skip: int, callback_params: dict): + 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["step"].append(current_step) + self.callback_params["position"].append(system.position_collection.copy()) + self.callback_params["radius"].append(system.radius.copy()) + self.callback_params["internal_force"].append(system.internal_forces.copy()) + + return + + +pp_list = defaultdict(list) +catenary_sim.collect_diagnostics(base_rod).using( + CatenaryCallBack, step_skip=step_skip, callback_params=pp_list +) + + +catenary_sim.finalize() + + +timestepper = PositionVerlet() + +integrate(timestepper, catenary_sim, final_time, total_steps) +position = np.array(pp_list["position"]) +b = np.min(position[-1][2]) + +SAVE_VIDEO = True +if SAVE_VIDEO: + # plotting the videos + filename_video = "catenary.mp4" + plot_video( + pp_list, + 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( + pp_list, + xlim=(0, base_length), + ylim=(b, 0.0), + ) diff --git a/examples/CatenaryCase/post_processing.py b/examples/CatenaryCase/post_processing.py new file mode 100644 index 000000000..ba6af0e36 --- /dev/null +++ b/examples/CatenaryCase/post_processing.py @@ -0,0 +1,76 @@ +import numpy as np +import matplotlib + +matplotlib.use("Agg") # Must be before importing matplotlib.pyplot or pylab! +from matplotlib import pyplot as plt +from tqdm import tqdm +import scipy as sci + + +def plot_video( + plot_params: dict, + video_name="video.mp4", + fps=15, + xlim=(0, 4), + ylim=(-1, 1), +): + import matplotlib.animation as manimation + + positions_over_time = np.array(plot_params["position"]) + + 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) + ax = fig.add_subplot(111) + ax.set_xlim(*xlim) + ax.set_ylim(*ylim) + ax.set_xlabel("x [m]", fontsize=16) + 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]) + 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 plot_catenary(plot_params: dict, xlim=(0, 1), ylim=(-0.5, 0.5), base_length=1.0): + """ + Catenary analytical solution from Routh, Edward John (1891). "Chapter X: On Strings". A Treatise on Analytical Statics. University Press. + """ + matplotlib.use("TkAgg") + position = np.array(plot_params["position"]) + lowest_point = np.min(position[-1][2]) + x_catenary = np.linspace(0, base_length, 100) + + def f_non_elastic_catenary(x): + return x * (1 - np.cosh(1 / (2 * x))) - lowest_point + + a = sci.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( + x_catenary, + y_catenary, + label="Catenary (Analytical Solution)", + linewidth=3, + linestyle="dashed", + ) + plt.xlim(xlim) + plt.ylim(ylim) + plt.title("Catenary Final Shape") + plt.grid() + plt.legend() + plt.xlabel("x [m]", fontsize=16) + plt.ylabel("y [m]", fontsize=16) + plt.savefig("plot.png", dpi=300) + plt.show() diff --git a/examples/ContinuumSnakeCase/continuum_snake.py b/examples/ContinuumSnakeCase/continuum_snake.py index e253f0413..8987b1554 100644 --- a/examples/ContinuumSnakeCase/continuum_snake.py +++ b/examples/ContinuumSnakeCase/continuum_snake.py @@ -13,7 +13,12 @@ class SnakeSimulator( - ea.BaseSystemCollection, ea.Constraints, ea.Forcing, ea.Damping, ea.CallBacks + ea.BaseSystemCollection, + ea.Constraints, + ea.Forcing, + ea.Damping, + ea.CallBacks, + ea.Contact, ): pass @@ -76,8 +81,10 @@ def run_snake( ) # Add friction forces - origin_plane = np.array([0.0, -base_radius, 0.0]) - normal_plane = normal + ground_plane = ea.Plane( + plane_origin=np.array([0.0, -base_radius, 0.0]), plane_normal=normal + ) + snake_sim.append(ground_plane) slip_velocity_tol = 1e-8 froude = 0.1 mu = base_length / (period * period * np.abs(gravitational_acc) * froude) @@ -85,12 +92,10 @@ def run_snake( [mu, 1.5 * mu, 2.0 * mu] ) # [forward, backward, sideways] static_mu_array = np.zeros(kinetic_mu_array.shape) - snake_sim.add_forcing_to(shearable_rod).using( - ea.AnisotropicFrictionalPlane, + snake_sim.detect_contact_between(shearable_rod, ground_plane).using( + ea.RodPlaneContactWithAnisotropicFriction, 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, diff --git a/examples/ContinuumSnakeWithLiftingWaveCase/continuum_snake_postprocessing.py b/examples/ContinuumSnakeWithLiftingWaveCase/continuum_snake_postprocessing.py new file mode 100644 index 000000000..37ad5ab94 --- /dev/null +++ b/examples/ContinuumSnakeWithLiftingWaveCase/continuum_snake_postprocessing.py @@ -0,0 +1,187 @@ +import numpy as np +from matplotlib import pyplot as plt +from matplotlib.colors import to_rgb +from tqdm import tqdm + + +def plot_snake_velocity( + plot_params: dict, + period, + filename="slithering_snake_velocity.png", + SAVE_FIGURE=False, +): + time_per_period = np.array(plot_params["time"]) / period + avg_velocity = np.array(plot_params["avg_velocity"]) + + [ + velocity_in_direction_of_rod, + velocity_in_rod_roll_dir, + _, + _, + ] = compute_projected_velocity(plot_params, period) + + fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150) + plt.rcParams.update({"font.size": 16}) + ax = fig.add_subplot(111) + ax.grid(visible=True, which="minor", color="k", linestyle="--") + ax.grid(visible=True, which="major", color="k", linestyle="-") + ax.plot( + time_per_period[:], velocity_in_direction_of_rod[:, 2], "r-", label="forward" + ) + ax.plot( + time_per_period[:], + velocity_in_rod_roll_dir[:, 0], + c=to_rgb("xkcd:bluish"), + label="lateral", + ) + ax.plot(time_per_period[:], avg_velocity[:, 1], "k-", label="normal") + ax.set_ylabel("Velocity [m/s]", fontsize=16) + ax.set_xlabel("Time [s]", fontsize=16) + fig.legend(prop={"size": 20}) + plt.show() + # 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()) + + if SAVE_FIGURE: + fig.savefig(filename) + + +def plot_video( + plot_params: dict, + video_name="video.mp4", + fps=15, + xlim=(0, 4), + ylim=(-1, 1), +): # (time step, x/y/z, node) + import matplotlib.animation as manimation + + positions_over_time = np.array(plot_params["position"]) + + 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) + ax = fig.add_subplot(111) + ax.set_xlim(*xlim) + ax.set_ylim(*ylim) + ax.set_xlabel("z [m]", fontsize=16) + ax.set_ylabel("x [m]", fontsize=16) + rod_lines_2d = ax.plot(positions_over_time[0][0], positions_over_time[0][1])[0] + # plt.axis("equal") + with writer.saving(fig, video_name, dpi=150): + 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][1]) + 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 compute_projected_velocity(plot_params: dict, period): + + time_per_period = np.array(plot_params["time"]) / period + avg_velocity = np.array(plot_params["avg_velocity"]) + 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 + # 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 + # 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 + # zero mean. + + # Number of steps in one period. + period_step = int(1.0 / (time_per_period[-1] - time_per_period[-2])) + number_of_period = int(time_per_period[-1]) + + # Center of mass position averaged in one period + center_of_mass_averaged_over_one_period = np.zeros((number_of_period - 2, 3)) + for i in range(1, number_of_period - 1): + # position of center of mass averaged over one period + center_of_mass_averaged_over_one_period[i - 1] = np.mean( + center_of_mass[(i + 1) * period_step : (i + 2) * period_step] + - center_of_mass[(i + 0) * period_step : (i + 1) * period_step], + axis=0, + ) + # Average the rod directions over multiple periods and get the direction of the rod. + direction_of_rod = np.mean(center_of_mass_averaged_over_one_period, axis=0) + direction_of_rod /= np.linalg.norm(direction_of_rod, ord=2) + + # Compute the projected rod velocity in the direction of the rod + velocity_mag_in_direction_of_rod = np.einsum( + "ji,i->j", avg_velocity, direction_of_rod + ) + velocity_in_direction_of_rod = np.einsum( + "j,i->ji", velocity_mag_in_direction_of_rod, direction_of_rod + ) + + # Get the lateral or roll velocity of the rod after subtracting its projected + # 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 + # for fastest forward velocity. We start after first period, because of the ramping up happens + # in first period. + average_velocity_over_simulation = np.mean( + velocity_in_direction_of_rod[period_step * 2 :], axis=0 + ) + + return ( + velocity_in_direction_of_rod, + velocity_in_rod_roll_dir, + average_velocity_over_simulation[2], + average_velocity_over_simulation[0], + ) + + +def plot_curvature( + plot_params: dict, + rest_lengths, + period, + save_fig=False, + filename="continuum_snake_curvature", +): + s = np.cumsum(rest_lengths) + L0 = s[-1] + s = s / L0 + s = s[:-1].copy() + x = np.linspace(0, 1, 100) + curvature = np.array(plot_params["curvature"]) + time = np.array(plot_params["time"]) + peak_time = period * 0.125 + dt = time[1] - time[0] + peak_idx = int(peak_time / (dt)) + plt.rcParams.update({"font.size": 16}) + fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150) + ax = fig.add_subplot(111) + try: + for i in range(peak_idx * 8, peak_idx * 8 * 2, peak_idx): + ax.plot(s, curvature[i, 0, :] * L0, "k") + except: + print("Simulation time not long enough to plot curvature") + ax.plot( + x, 7 * np.cos(2 * np.pi * x - 0.80), "--", label="stereotypical snake curvature" + ) + ax.set_ylabel(r"$\kappa$", fontsize=16) + ax.set_xlabel("s", fontsize=16) + ax.set_xlim(0, 1) + ax.set_ylim(-10, 10) + fig.legend(prop={"size": 16}) + plt.show() + if save_fig: + fig.savefig(filename) + + # 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()) diff --git a/examples/ContinuumSnakeWithLiftingWaveCase/continuum_snake_with_lifting_wave.py b/examples/ContinuumSnakeWithLiftingWaveCase/continuum_snake_with_lifting_wave.py new file mode 100755 index 000000000..fec1fc909 --- /dev/null +++ b/examples/ContinuumSnakeWithLiftingWaveCase/continuum_snake_with_lifting_wave.py @@ -0,0 +1,275 @@ +__doc__ = """Snake friction case from X. Zhang et. al. Nat. Comm. 2021""" + +import os +import numpy as np +import pickle + +from elastica import * + +from continuum_snake_postprocessing import ( + plot_snake_velocity, + plot_video, + compute_projected_velocity, + plot_curvature, +) +from snake_forcing import ( + MuscleTorquesLifting, +) +from snake_contact import SnakeRodPlaneContact + + +class SnakeSimulator( + BaseSystemCollection, Constraints, Forcing, Damping, CallBacks, Contact +): + pass + + +def run_snake( + b_coeff_lat, + PLOT_FIGURE=False, + SAVE_FIGURE=False, + SAVE_VIDEO=False, + SAVE_RESULTS=False, +): + # Initialize the simulation class + snake_sim = SnakeSimulator() + + # Simulation parameters + period = 2.0 + final_time = 20.0 + time_step = 5e-5 + total_steps = int(final_time / time_step) + rendering_fps = 100 + step_skip = int(1.0 / (rendering_fps * time_step)) + + # collection of snake characteristics + n_elem = 25 + base_length = 0.35 + base_radius = 0.009 + snake_torque_ratio = 30.0 + snake_torque_liftratio = 10.0 + + start = np.array([0.0, 0.0, 0.0 + base_radius]) + direction = np.array([1.0, 0.0, 0.0]) + normal = np.array([0.0, 0.0, 1.0]) + density = 1000 + E = 1e6 + poisson_ratio = 0.5 + shear_modulus = E / (poisson_ratio + 1.0) + + shearable_rod = 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) + damping_constant = 1e-1 + + # use linear damping with constant damping ratio + snake_sim.dampen(shearable_rod).using( + AnalyticalLinearDamper, + damping_constant=damping_constant, + time_step=time_step, + ) + + # Add gravitational forces + gravitational_acc = -9.80665 + + snake_sim.add_forcing_to(shearable_rod).using( + GravityForces, acc_gravity=np.array([0.0, 0.0, gravitational_acc]) + ) + + # 1. Add muscle torques -- lateral wave + # Define lateral wave parameters + lateral_wave_length = 1.0 + lateral_amp = b_coeff_lat[:-1] + + lateral_ratio = 1.0 # switch of lateral wave + snake_sim.add_forcing_to(shearable_rod).using( + MuscleTorques, + base_length=base_length, + b_coeff=snake_torque_ratio * lateral_ratio * lateral_amp, + period=period, + wave_number=2.0 * np.pi / (lateral_wave_length), + phase_shift=0.0, + rest_lengths=shearable_rod.rest_lengths, + ramp_up_time=period, + direction=normal, + with_spline=True, + ) + + # 2. Add muscle torques -- lifting wave + # Define lifting wave parameters + lift_wave_length = lateral_wave_length + lift_amp = np.array([1e-3, 2e-3, 2e-3, 2e-3, 2e-3, 1e-3]) + + lift_ratio = 1.0 # switch of lifting wave + phase = 0.5 + snake_sim.add_forcing_to(shearable_rod).using( + MuscleTorquesLifting, + b_coeff=snake_torque_liftratio * lift_ratio * lift_amp, + period=period, + wave_number=2.0 * np.pi / (lift_wave_length), + phase_shift=phase * period, + rest_lengths=shearable_rod.rest_lengths, + ramp_up_time=0.01, + direction=normal, + with_spline=True, + switch_on_time=2.0, + ) + + # Some common parameters first - define friction ratio etc. + 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] + normal_plane = normal + origin_plane = np.array([0.0, 0.0, 0.0]) + ground_plane = Plane(plane_normal=normal_plane, plane_origin=origin_plane) + snake_sim.append(ground_plane) + + snake_sim.detect_contact_between(shearable_rod, ground_plane).using( + SnakeRodPlaneContact, + k=1e2, + nu=1e-1, + slip_velocity_tol=slip_velocity_tol, + kinetic_mu_array=kinetic_mu_array, + ) + + # Add call backs + class ContinuumSnakeCallBack(CallBackBaseClass): + """ + Call back function for continuum snake + """ + + def __init__(self, step_skip: int, callback_params: dict): + 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["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() + ) + self.callback_params["curvature"].append(system.kappa.copy()) + + return + + pp_list = defaultdict(list) + snake_sim.collect_diagnostics(shearable_rod).using( + ContinuumSnakeCallBack, step_skip=step_skip, callback_params=pp_list + ) + + snake_sim.finalize() + + timestepper = PositionVerlet() + integrate(timestepper, snake_sim, final_time, total_steps) + + if PLOT_FIGURE: + filename_plot = "continuum_snake_velocity.png" + plot_snake_velocity(pp_list, period, filename_plot, SAVE_FIGURE) + plot_curvature(pp_list, shearable_rod.rest_lengths, period, SAVE_FIGURE) + + if SAVE_VIDEO: + filename_video = "continuum_snake_with_lifting_wave.mp4" + plot_video( + pp_list, + video_name=filename_video, + fps=rendering_fps, + xlim=(0, 3), + ylim=(-1, 1), + ) + + if SAVE_RESULTS: + + filename = "continuum_snake.dat" + file = open(filename, "wb") + pickle.dump(pp_list, file) + file.close() + + # Compute the average forward velocity. These will be used for optimization. + [_, _, avg_forward, avg_lateral] = compute_projected_velocity(pp_list, period) + + return avg_forward, avg_lateral, pp_list + + +if __name__ == "__main__": + # Options + PLOT_FIGURE = True + SAVE_FIGURE = False + SAVE_VIDEO = True + SAVE_RESULTS = True + CMA_OPTION = False + + if CMA_OPTION: + import cma + + SAVE_OPTIMIZED_COEFFICIENTS = False + + def optimize_snake(spline_coefficient): + [avg_forward, _, _] = run_snake( + spline_coefficient, + PLOT_FIGURE=False, + SAVE_FIGURE=False, + SAVE_VIDEO=False, + SAVE_RESULTS=False, + ) + return -avg_forward + + # Optimize snake 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, 7 * [0], 0.5) + + # Save the optimized coefficients to a file + filename_data = "optimized_coefficients.txt" + if SAVE_OPTIMIZED_COEFFICIENTS: + assert filename_data != "", "provide a file name for coefficients" + np.savetxt(filename_data, optimized_spline_coefficients, delimiter=",") + + else: + # Add muscle forces on the rod + if os.path.exists("optimized_coefficients.txt"): + t_coeff_optimized = np.genfromtxt( + "optimized_coefficients.txt", delimiter="," + ) + else: + wave_length = 1.0 + t_coeff_optimized = np.array( + [4e-3, 4e-3, 4e-3, 4e-3, 4e-3, 4e-3] + # [3.4e-3, 3.3e-3, 5.7e-3, 2.8e-3, 3.0e-3, 3.0e-3] + ) + t_coeff_optimized = np.hstack((t_coeff_optimized, wave_length)) + + # run the simulation + [avg_forward, avg_lateral, pp_list] = run_snake( + t_coeff_optimized, PLOT_FIGURE, SAVE_FIGURE, SAVE_VIDEO, SAVE_RESULTS + ) + + print("average forward velocity:", avg_forward) + print("average forward lateral:", avg_lateral) diff --git a/examples/ContinuumSnakeWithLiftingWaveCase/snake_contact.py b/examples/ContinuumSnakeWithLiftingWaveCase/snake_contact.py new file mode 100755 index 000000000..68dea81dd --- /dev/null +++ b/examples/ContinuumSnakeWithLiftingWaveCase/snake_contact.py @@ -0,0 +1,346 @@ +__doc__ = """Rod plane contact with anistropic friction (no static friction)""" + +import numpy as np +from elastica._linalg import ( + _batch_norm, + _batch_product_i_k_to_ik, + _batch_product_k_ik_to_ik, + _batch_vec_oneD_vec_cross, + _batch_matvec, + _batch_product_i_ik_to_k, + _batch_dot, + _batch_matrix_transpose, + _batch_cross, + _batch_vector_sum, +) + +from elastica.contact_utils import ( + _node_to_element_position, + _node_to_element_velocity, + _find_slipping_elements, + _elements_to_nodes_inplace, + _node_to_element_mass_or_force, +) +from numba import njit +from elastica.rod import RodBase +from elastica.surface import Plane +from elastica.contact_forces import NoContact +from elastica.typing import RodType, SystemType, AllowedContactType + + +@njit(cache=True) +def apply_normal_force_numba( + plane_origin, + plane_normal, + surface_tol, + k, + nu, + radius, + mass, + position_collection, + velocity_collection, + internal_forces, + external_forces, +): + """ + 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. + + Parameters + ---------- + system + + Returns + ------- + magnitude of the plane response + """ + + # Compute plane response force + nodal_total_forces = _batch_vector_sum(internal_forces, external_forces) + element_total_forces = _node_to_element_mass_or_force(nodal_total_forces) + + force_component_along_normal_direction = _batch_product_i_ik_to_k( + plane_normal, element_total_forces + ) + forces_along_normal_direction = _batch_product_i_k_to_ik( + plane_normal, force_component_along_normal_direction + ) + + # If the total force component along the plane normal direction is greater than zero that means, + # total force is pushing rod away from the plane not towards the plane. Thus, response force + # applied by the surface has to be zero. + forces_along_normal_direction[ + ..., np.where(force_component_along_normal_direction > 0)[0] + ] = 0.0 + # Compute response force on the element. Plane response force + # has to be away from the surface and towards the element. Thus + # multiply forces along normal direction with negative sign. + plane_response_force = -forces_along_normal_direction + + # Elastic force response due to penetration + element_position = _node_to_element_position(position_collection) + distance_from_plane = _batch_product_i_ik_to_k( + plane_normal, (element_position - plane_origin) + ) + plane_penetration = np.minimum(distance_from_plane - radius, 0.0) + elastic_force = -k * _batch_product_i_k_to_ik(plane_normal, plane_penetration) + + # Damping force response due to velocity towards the plane + element_velocity = _node_to_element_velocity( + mass=mass, node_velocity_collection=velocity_collection + ) + normal_component_of_element_velocity = _batch_product_i_ik_to_k( + plane_normal, element_velocity + ) + damping_force = -nu * _batch_product_i_k_to_ik( + plane_normal, normal_component_of_element_velocity + ) + + # Compute total plane response force + plane_response_force_total = elastic_force + damping_force + + # Check if the rod elements are in contact with plane. + no_contact_point_idx = np.where((distance_from_plane - radius) > surface_tol)[0] + # If rod element does not have any contact with plane, plane cannot apply response + # force on the element. Thus lets set plane response force to 0.0 for the no contact points. + plane_response_force[..., no_contact_point_idx] = 0.0 + plane_response_force_total[..., no_contact_point_idx] = 0.0 + + # Update the external forces + _elements_to_nodes_inplace(plane_response_force_total, external_forces) + + return (_batch_norm(plane_response_force), no_contact_point_idx) + + +@njit(cache=True) +def anisotropic_friction( + plane_origin, + plane_normal, + surface_tol, + slip_velocity_tol, + k, + nu, + kinetic_mu_forward, + kinetic_mu_backward, + kinetic_mu_sideways, + radius, + mass, + tangents, + position_collection, + director_collection, + velocity_collection, + omega_collection, + internal_forces, + external_forces, +): + plane_response_force_mag, no_contact_point_idx = apply_normal_force_numba( + plane_origin, + plane_normal, + surface_tol, + k, + nu, + radius, + mass, + position_collection, + velocity_collection, + internal_forces, + external_forces, + ) + + # First compute component of rod tangent in plane. Because friction forces acts in plane not out of plane. Thus + # axial direction has to be in plane, it cannot be out of plane. We are projecting rod element tangent vector in + # to the plane. So friction forces can only be in plane forces and not out of plane. + tangent_along_normal_direction = _batch_product_i_ik_to_k(plane_normal, tangents) + tangent_perpendicular_to_normal_direction = tangents - _batch_product_i_k_to_ik( + plane_normal, tangent_along_normal_direction + ) + tangent_perpendicular_to_normal_direction_mag = _batch_norm( + tangent_perpendicular_to_normal_direction + ) + # Normalize tangent_perpendicular_to_normal_direction. This is axial direction for plane. Here we are adding + # small tolerance (1e-10) for normalization, in order to prevent division by 0. + axial_direction = _batch_product_k_ik_to_ik( + 1 / (tangent_perpendicular_to_normal_direction_mag + 1e-14), + tangent_perpendicular_to_normal_direction, + ) + element_velocity = _node_to_element_velocity( + mass=mass, node_velocity_collection=velocity_collection + ) + # first apply axial kinetic friction + velocity_mag_along_axial_direction = _batch_dot(element_velocity, axial_direction) + velocity_along_axial_direction = _batch_product_k_ik_to_ik( + velocity_mag_along_axial_direction, axial_direction + ) + + # Friction forces depends on the direction of velocity, in other words sign + # of the velocity vector. + velocity_sign_along_axial_direction = np.sign(velocity_mag_along_axial_direction) + # Check top for sign convention + kinetic_mu = 0.5 * ( + kinetic_mu_forward * (1 + velocity_sign_along_axial_direction) + + kinetic_mu_backward * (1 - velocity_sign_along_axial_direction) + ) + # Call slip function to check if elements slipping or not + slip_function_along_axial_direction = _find_slipping_elements( + velocity_along_axial_direction, slip_velocity_tol + ) + + # Now rolling kinetic friction + rolling_direction = _batch_vec_oneD_vec_cross(axial_direction, plane_normal) + torque_arm = _batch_product_i_k_to_ik(-plane_normal, radius) + velocity_along_rolling_direction = _batch_dot(element_velocity, rolling_direction) + directors_transpose = _batch_matrix_transpose(director_collection) + # w_rot = Q.T @ omega @ Q @ r + rotation_velocity = _batch_matvec( + directors_transpose, + _batch_cross(omega_collection, _batch_matvec(director_collection, torque_arm)), + ) + rotation_velocity_along_rolling_direction = _batch_dot( + rotation_velocity, rolling_direction + ) + slip_velocity_mag_along_rolling_direction = ( + velocity_along_rolling_direction + rotation_velocity_along_rolling_direction + ) + slip_velocity_along_rolling_direction = _batch_product_k_ik_to_ik( + slip_velocity_mag_along_rolling_direction, rolling_direction + ) + slip_function_along_rolling_direction = _find_slipping_elements( + slip_velocity_along_rolling_direction, slip_velocity_tol + ) + + unitized_total_velocity = element_velocity / _batch_norm(element_velocity + 1e-14) + # Apply kinetic friction in axial direction. + kinetic_friction_force_along_axial_direction = -( + (1.0 - slip_function_along_axial_direction) + * kinetic_mu + * plane_response_force_mag + * _batch_dot(unitized_total_velocity, axial_direction) + * axial_direction + ) + # If rod element does not have any contact with plane, plane cannot apply friction + # force on the element. Thus lets set kinetic friction force to 0.0 for the no contact points. + kinetic_friction_force_along_axial_direction[..., no_contact_point_idx] = 0.0 + _elements_to_nodes_inplace( + kinetic_friction_force_along_axial_direction, external_forces + ) + # Apply kinetic friction in rolling direction. + kinetic_friction_force_along_rolling_direction = -( + (1.0 - slip_function_along_rolling_direction) + * kinetic_mu_sideways + * plane_response_force_mag + * _batch_dot(unitized_total_velocity, rolling_direction) + * rolling_direction + ) + # If rod element does not have any contact with plane, plane cannot apply friction + # force on the element. Thus lets set kinetic friction force to 0.0 for the no contact points. + kinetic_friction_force_along_rolling_direction[..., no_contact_point_idx] = 0.0 + _elements_to_nodes_inplace( + kinetic_friction_force_along_rolling_direction, external_forces + ) + + +class SnakeRodPlaneContact(NoContact): + """ + This class is for applying contact forces between a snake rod and a plane with friction. + First system is always rod and second system is always plane. + + How to define contact between rod and plane. + >>> simulator.detect_contact_between(rod, plane).using( + ... SnakeRodPlaneContact, + ... k=1e4, + ... nu=10, + ... slip_velocity_tol = 1e-4, + ... kinetic_mu_array = np.array([1.0,2.0,3.0]), + ... ) + """ + + def __init__( + self, + k: float, + nu: float, + slip_velocity_tol: float, + kinetic_mu_array: np.ndarray, + ): + """ + Parameters + ---------- + k : float + Contact spring constant. + nu : float + Contact damping constant. + slip_velocity_tol: float + Velocity tolerance to determine if the element is slipping or not. + kinetic_mu_array: numpy.ndarray + 1D (3,) array containing data with 'float' type. + [forward, backward, sideways] kinetic friction coefficients. + """ + super().__init__() + self.k = k + self.nu = nu + self.surface_tol = 1e-4 + self.slip_velocity_tol = slip_velocity_tol + ( + self.kinetic_mu_forward, + self.kinetic_mu_backward, + self.kinetic_mu_sideways, + ) = kinetic_mu_array + + def _check_systems_validity( + self, + system_one: SystemType, + system_two: AllowedContactType, + ) -> None: + """ + This checks the contact order and type of a SystemType object and an AllowedContactType object. + For the RodSphereContact class first_system should be a rod and second_system should be a plane. + Parameters + ---------- + system_one + SystemType + system_two + AllowedContactType + """ + if not issubclass(system_one.__class__, RodBase) or not issubclass( + system_two.__class__, Plane + ): + raise TypeError( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a rod, second should be a plane".format( + system_one.__class__, system_two.__class__ + ) + ) + + def apply_contact(self, system_one: RodType, system_two: SystemType) -> None: + """ + In the case of contact with the plane, this function computes the plane reaction force on the element. + + Parameters + ---------- + system_one: object + Rod object. + system_two: object + Plane object. + + """ + anisotropic_friction( + system_two.origin, + system_two.normal, + self.surface_tol, + self.slip_velocity_tol, + self.k, + self.nu, + self.kinetic_mu_forward, + self.kinetic_mu_backward, + self.kinetic_mu_sideways, + system_one.radius, + system_one.mass, + system_one.tangents, + system_one.position_collection, + system_one.director_collection, + system_one.velocity_collection, + system_one.omega_collection, + system_one.internal_forces, + system_one.external_forces, + ) diff --git a/examples/ContinuumSnakeWithLiftingWaveCase/snake_forcing.py b/examples/ContinuumSnakeWithLiftingWaveCase/snake_forcing.py new file mode 100755 index 000000000..3b471acad --- /dev/null +++ b/examples/ContinuumSnakeWithLiftingWaveCase/snake_forcing.py @@ -0,0 +1,180 @@ +__doc__ = """ External forces and Actions (in learning) of the snake terrain case.""" + +import numpy as np +from elastica._linalg import _batch_matvec +from elastica.utils import _bspline +from numba import njit +from elastica._linalg import ( + _batch_norm, + _batch_product_k_ik_to_ik, + _batch_vec_oneD_vec_cross, +) +from elastica.external_forces import NoForces +from elastica.external_forces import ( + inplace_addition, + inplace_substraction, +) + + +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 + + 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. + switch_on_time: float + time to switch on the muscle activation. + """ + + def __init__( + self, + b_coeff, + period, + wave_number, + phase_shift, + direction, + rest_lengths, + ramp_up_time=0.0, + with_spline=False, + switch_on_time=0.0, + ): + """ + + Parameters + ---------- + b_coeff: nump.ndarray + 1D array containing data with 'float' type. + Beta coefficients for beta-spline. + period: float + Period of traveling wave. + wave_number: float + Wave number of traveling wave. + phase_shift: float + Phase shift of traveling wave. + direction: numpy.ndarray + 1D (dim) array containing data with 'float' type. + ramp_up_time: float + Applied muscle torques are ramped up until ramp up time. + with_spline: boolean + Option to use beta-spline. + switch_on_time: float + time to switch on the muscle activation. + + """ + super().__init__() + + self.direction = direction # Direction torque applied + self.angular_frequency = 2.0 * np.pi / period + self.wave_number = wave_number + self.phase_shift = phase_shift + + assert ramp_up_time >= 0.0 + self.ramp_up_time = ramp_up_time + assert switch_on_time >= 0.0 + self.switch_on_time = switch_on_time + + # s is the position of nodes on the rod, we go from node=1 to node=nelem-1, because there is no + # torques applied by first and last node on elements. Reason is that we cannot apply torque in an + # infinitesimal segment at the beginning and end of rod, because there is no additional element + # (at element=-1 or element=n_elem+1) to provide internal torques to cancel out an external + # torque. This coupled with the requirement that the sum of all muscle torques has + # to be zero results in this condition. + self.s = np.cumsum(rest_lengths) + self.s /= self.s[-1] + + if with_spline: + assert b_coeff.size != 0, "Beta spline coefficient array (t_coeff) is empty" + spline, ctr_pts, ctr_coeffs = _bspline(b_coeff) + self.spline = spline(self.s) + + else: + self.spline = np.full_like(self.s) + + def apply_torques(self, system, time: np.float64 = 0.0): + self.compute_muscle_torques( + time, + self.spline, + self.s, + self.angular_frequency, + self.wave_number, + self.phase_shift, + self.ramp_up_time, + self.direction, + self.switch_on_time, + system.tangents, + system.director_collection, + system.external_torques, + ) + + @staticmethod + @njit(cache=True) + def compute_muscle_torques( + time, + spline, + s, + angular_frequency, + wave_number, + phase_shift, + ramp_up_time, + direction, + switch_on_time, + tangents, + director_collection, + external_torques, + ): + if time > switch_on_time: + # Ramp up the muscle torque + factor = min(1.0, (time - switch_on_time) / ramp_up_time) + # From the node 1 to node nelem-1 + # Magnitude of the torque. Am = beta(s) * sin(2pi*t/T + 2pi*s/lambda + phi) + # There is an inconsistency with paper and Elastica cpp implementation. In paper sign in + # front of wave number is positive, in Elastica cpp it is negative. + torque_mag = ( + factor + * spline + * np.sin( + angular_frequency * (time - switch_on_time - phase_shift) + - wave_number * s + ) + ) + # Head and tail of the snake is opposite compared to elastica cpp. We need to iterate torque_mag + # from last to first element. + # compute torque direction for lifting wave. + # Here, direction of each element is computed separately + # based on the rod tangent and normal direction. This is implemented to + # correct the binormal direction when snake undergoes lateral bending + avg_element_direction = 0.5 * (tangents[..., :-1] + tangents[..., 1:]) + torque_direction = _batch_vec_oneD_vec_cross( + avg_element_direction, direction + ) + torque_direction_unit = _batch_product_k_ik_to_ik( + 1 / (_batch_norm(torque_direction) + 1e-14), + torque_direction, + ) + torque = _batch_product_k_ik_to_ik( + torque_mag[-2::-1], torque_direction_unit + ) + + inplace_addition( + external_torques[..., 1:], + _batch_matvec(director_collection[..., 1:], torque), + ) + inplace_substraction( + external_torques[..., :-1], + _batch_matvec(director_collection[..., :-1], torque), + ) diff --git a/examples/ExperimentalCases/ParallelConnectionExample/parallel_connection_example.py b/examples/ExperimentalCases/ParallelConnectionExample/parallel_connection_example.py index d56ed276f..a902f5042 100644 --- a/examples/ExperimentalCases/ParallelConnectionExample/parallel_connection_example.py +++ b/examples/ExperimentalCases/ParallelConnectionExample/parallel_connection_example.py @@ -7,6 +7,12 @@ 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 ( plot_position, plot_video, diff --git a/examples/FrictionValidationCases/axial_friction.py b/examples/FrictionValidationCases/axial_friction.py index 9c7d654dc..d500a1e60 100644 --- a/examples/FrictionValidationCases/axial_friction.py +++ b/examples/FrictionValidationCases/axial_friction.py @@ -7,7 +7,9 @@ ) -class AxialFrictionSimulator(ea.BaseSystemCollection, ea.Constraints, ea.Forcing): +class AxialFrictionSimulator( + ea.BaseSystemCollection, ea.Constraints, ea.Forcing, ea.Contact +): pass @@ -74,13 +76,13 @@ def simulate_axial_friction_with(force=0.0): slip_velocity_tol = 1e-4 static_mu_array = np.array([0.8, 0.4, 0.4]) # [forward, backward, sideways] kinetic_mu_array = np.array([0.4, 0.2, 0.2]) # [forward, backward, sideways] + friction_plane = ea.Plane(plane_origin=origin_plane, plane_normal=normal_plane) + axial_friction_sim.append(friction_plane) - axial_friction_sim.add_forcing_to(shearable_rod).using( - ea.AnisotropicFrictionalPlane, + axial_friction_sim.detect_contact_between(shearable_rod, friction_plane).using( + ea.RodPlaneContactWithAnisotropicFriction, k=10.0, nu=1e-4, - 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, diff --git a/examples/FrictionValidationCases/rolling_friction_initial_velocity.py b/examples/FrictionValidationCases/rolling_friction_initial_velocity.py index c74e9b857..c42edd7ad 100644 --- a/examples/FrictionValidationCases/rolling_friction_initial_velocity.py +++ b/examples/FrictionValidationCases/rolling_friction_initial_velocity.py @@ -9,7 +9,7 @@ class RollingFrictionInitialVelocitySimulator( - ea.BaseSystemCollection, ea.Constraints, ea.Forcing, ea.Damping + ea.BaseSystemCollection, ea.Constraints, ea.Forcing, ea.Damping, ea.Contact ): pass @@ -88,13 +88,15 @@ def simulate_rolling_friction_initial_velocity_with(IFactor=0.0): slip_velocity_tol = 1e-6 static_mu_array = np.array([0.4, 0.4, 0.4]) # [forward, backward, sideways] kinetic_mu_array = np.array([0.2, 0.2, 0.2]) # [forward, backward, sideways] + friction_plane = ea.Plane(plane_origin=origin_plane, plane_normal=normal_plane) + rolling_friction_initial_velocity_sim.append(friction_plane) - rolling_friction_initial_velocity_sim.add_forcing_to(shearable_rod).using( - ea.AnisotropicFrictionalPlane, + rolling_friction_initial_velocity_sim.detect_contact_between( + shearable_rod, friction_plane + ).using( + ea.RodPlaneContactWithAnisotropicFriction, k=10.0, nu=1e-4, - 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, diff --git a/examples/FrictionValidationCases/rolling_friction_on_inclined_plane.py b/examples/FrictionValidationCases/rolling_friction_on_inclined_plane.py index 57b04cd9c..6685b2524 100644 --- a/examples/FrictionValidationCases/rolling_friction_on_inclined_plane.py +++ b/examples/FrictionValidationCases/rolling_friction_on_inclined_plane.py @@ -9,7 +9,7 @@ class RollingFrictionOnInclinedPlaneSimulator( - ea.BaseSystemCollection, ea.Constraints, ea.Forcing + ea.BaseSystemCollection, ea.Constraints, ea.Forcing, ea.Contact ): pass @@ -74,13 +74,15 @@ def simulate_rolling_friction_on_inclined_plane_with(alpha_s=0.0): slip_velocity_tol = 1e-4 static_mu_array = np.array([0.4, 0.4, 0.4]) # [forward, backward, sideways] kinetic_mu_array = np.array([0.2, 0.2, 0.2]) # [forward, backward, sideways] + friction_plane = ea.Plane(plane_origin=origin_plane, plane_normal=normal_plane) + rolling_friction_on_inclined_plane_sim.append(friction_plane) - rolling_friction_on_inclined_plane_sim.add_forcing_to(shearable_rod).using( - ea.AnisotropicFrictionalPlane, + rolling_friction_on_inclined_plane_sim.detect_contact_between( + shearable_rod, friction_plane + ).using( + ea.RodPlaneContactWithAnisotropicFriction, k=10.0, nu=1e-4, - 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, diff --git a/examples/FrictionValidationCases/rolling_friction_torque.py b/examples/FrictionValidationCases/rolling_friction_torque.py index 350a6cdef..f4a0c22f0 100644 --- a/examples/FrictionValidationCases/rolling_friction_torque.py +++ b/examples/FrictionValidationCases/rolling_friction_torque.py @@ -9,7 +9,7 @@ class RollingFrictionTorqueSimulator( - ea.BaseSystemCollection, ea.Constraints, ea.Forcing + ea.BaseSystemCollection, ea.Constraints, ea.Forcing, ea.Contact ): pass @@ -78,12 +78,15 @@ def simulate_rolling_friction_torque_with(C_s=0.0): static_mu_array = np.array([0.4, 0.4, 0.4]) # [forward, backward, sideways] kinetic_mu_array = np.array([0.2, 0.2, 0.2]) # [forward, backward, sideways] - rolling_friction_torque_sim.add_forcing_to(shearable_rod).using( - ea.AnisotropicFrictionalPlane, + friction_plane = ea.Plane(plane_origin=origin_plane, plane_normal=normal_plane) + rolling_friction_torque_sim.append(friction_plane) + + rolling_friction_torque_sim.detect_contact_between( + shearable_rod, friction_plane + ).using( + ea.RodPlaneContactWithAnisotropicFriction, k=10.0, nu=1e-4, - 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, diff --git a/examples/MuscularSnake/muscle_forces.py b/examples/MuscularSnake/muscle_forces.py index b2a24de3b..6abf18f0d 100644 --- a/examples/MuscularSnake/muscle_forces.py +++ b/examples/MuscularSnake/muscle_forces.py @@ -77,7 +77,7 @@ def __init__( self.step = step self.counter = 0 - def apply_forces(self, system, time: np.float = 0.0): + def apply_forces(self, system, time: np.float64 = 0.0): forces = self._apply_forces( self.amplitude, self.wave_number, diff --git a/examples/MuscularSnake/muscular_snake.py b/examples/MuscularSnake/muscular_snake.py index 9aef09253..4741b37bf 100644 --- a/examples/MuscularSnake/muscular_snake.py +++ b/examples/MuscularSnake/muscular_snake.py @@ -20,6 +20,7 @@ class MuscularSnakeSimulator( ea.Forcing, ea.CallBacks, ea.Damping, + ea.Contact, ): pass @@ -346,12 +347,13 @@ class MuscularSnakeSimulator( [1.0 * mu, 1.5 * mu, 2.0 * mu] ) # [forward, backward, sideways] static_mu_array = 2 * kinetic_mu_array -muscular_snake_simulator.add_forcing_to(snake_body).using( - ea.AnisotropicFrictionalPlane, +friction_plane = ea.Plane(plane_origin=origin_plane, plane_normal=normal_plane) +muscular_snake_simulator.append(friction_plane) + +muscular_snake_simulator.detect_contact_between(snake_body, friction_plane).using( + ea.RodPlaneContactWithAnisotropicFriction, k=1e1, nu=40, - 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, diff --git a/examples/MuscularSnake/post_processing.py b/examples/MuscularSnake/post_processing.py index d79bc7e86..c50b33318 100644 --- a/examples/MuscularSnake/post_processing.py +++ b/examples/MuscularSnake/post_processing.py @@ -117,7 +117,7 @@ def plot_video_with_surface( video_name_3D = folder_name + "3D_" + video_name with writer.saving(fig, video_name_3D, dpi): - with plt.style.context("seaborn-whitegrid"): + with plt.style.context("seaborn-v0_8-whitegrid"): for time_idx in tqdm(range(0, sim_time.shape[0], int(step))): for rod_idx in range(n_visualized_rods): @@ -208,7 +208,7 @@ def plot_video_with_surface( video_name_2D = folder_name + "2D_xy_" + video_name with writer.saving(fig, video_name_2D, dpi): - with plt.style.context("seaborn-whitegrid"): + with plt.style.context("seaborn-v0_8-whitegrid"): for time_idx in tqdm(range(0, sim_time.shape[0], int(step))): for rod_idx in range(n_visualized_rods): @@ -299,7 +299,7 @@ def plot_video_with_surface( video_name_2D = folder_name + "2D_zy_" + video_name with writer.saving(fig, video_name_2D, dpi): - with plt.style.context("seaborn-whitegrid"): + with plt.style.context("seaborn-v0_8-whitegrid"): for time_idx in tqdm(range(0, sim_time.shape[0], int(step))): for rod_idx in range(n_visualized_rods): @@ -392,7 +392,7 @@ def plot_video_with_surface( video_name_2D = folder_name + "2D_xz_" + video_name with writer.saving(fig, video_name_2D, dpi): - with plt.style.context("seaborn-whitegrid"): + with plt.style.context("seaborn-v0_8-whitegrid"): for time_idx in tqdm(range(0, sim_time.shape[0], int(step))): for rod_idx in range(n_visualized_rods): diff --git a/examples/README.md b/examples/README.md index 489ea4492..06f214be2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -26,17 +26,20 @@ Examples can serve as a starting template for customized usages. * __Purpose__: Physical convergence test of simple pendulum with flexible rod. * __Features__: CosseratRod, HingeBC, GravityForces * [ContinuumSnakeCase](./ContinuumSnakeCase) - * __Purpose__: Demonstrate simple case of modeling biological creature using PyElastica. The example use friction to create slithering snake, and optimize the speed using [CMA](https://github.com/CMA-ES/pycma). - * __Features__: CosseratRod, MuscleTorques, AnisotropicFrictionalPlane, Gravity, CMA Optimization - * [MuscularSnake](./MuscularSnake) - * __Purpose__: Example of [Parallel connection module](../elastica/experimental/connection_contact_joint/parallel_connection.py) and customized [Force module](./MuscularSnake/muscle_forces.py) to implement muscular snake. - * __Features__: MuscleForces(custom implemented) + * __Purpose__: Demonstrate simple case of modeling biological creature using PyElastica. The example uses friction to create slithering snake, and optimize the speed using [CMA](https://github.com/CMA-ES/pycma). + * __Features__: CosseratRod, MuscleTorques, RodPlaneContactWithAnisotropicFriction, Gravity, CMA Optimization +* [ContinuumSnakeWithLiftingWaveCase](./ContinuumSnakeWithLiftingWaveCase) + * __Purpose__: Demonstrate simple case of modeling biological creature using PyElastica. The example uses friction to create slithering snake with lift. + * __Features__: CosseratRod, MuscleTorquesLifting(custom implemented), SnakeRodPlaneContact(custom implemented), Gravity +* [MuscularSnake](./MuscularSnake) + * __Purpose__: Example of [Parallel connection module](../elastica/experimental/connection_contact_joint/parallel_connection.py) and customized [Force module](./MuscularSnake/muscle_forces.py) to implement muscular snake. + * __Features__: MuscleForces(custom implemented) * [ButterflyCase](./ButterflyCase) * __Purpose__: Demonstrate simple restoration with initial strain. * __Features__: CosseratRod * [FrictionValidationCases](./FrictionValidationCases) * __Purpose__: Physical validation of rolling and translational friction. - * __Features__: CosseratRod, UniformForces, AnisotropicFrictionalPlane + * __Features__: CosseratRod, UniformForces, RodPlaneContactWithAnisotropicFriction * [JointCases](./JointCases) * __Purpose__: Demonstrate various joint usage with Cosserat Rod. * __Features__: FreeJoint, FixedJoint, HingeJoint, OneEndFixedRod, EndpointForcesSinusoidal @@ -45,27 +48,27 @@ Examples can serve as a starting template for customized usages. * __Features__: Cylinder, Sphere * [RodRigidBodyContact](./RigidBodyCases/RodRigidBodyContact) * __Purpose__: Demonstrate contact between cylinder and rod, for different intial conditions. - * __Features__: Cylinder, CosseratRods, ExternalContact + * __Features__: Cylinder, CosseratRods, RodCylinderContact * [HelicalBucklingCase](./HelicalBucklingCase) * __Purpose__: Demonstrate helical buckling with extreme twisting boundary condition. * __Features__: HelicalBucklingBC * [ContinuumFlagellaCase](./ContinuumFlagellaCase) * __Purpose__: Demonstrate flagella modeling using PyElastica. * __Features__: SlenderBodyTheory, MuscleTorques, - * [MuscularFlagella](./MuscularFlagella) - * __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) +* [MuscularFlagella](./MuscularFlagella) + * __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, ExternalContact + * __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, SelfContact, Link-Writhe-Twist + * __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, SelfContact, Link-Writhe-Twist + * __Features__: CosseratRod, SelonoidsBC, RodSelfContact, Link-Writhe-Twist * [BoundaryConditionsCases](./BoundaryConditionsCases) * __Purpose__: Demonstrate the usage of boundary conditions for constraining the movement of the system. * __Features__: GeneralConstraint, CosseratRod @@ -75,6 +78,9 @@ Examples can serve as a starting template for customized usages. * [RingRodCase](./RingRodCase) * __Purpose__: Demonstrate simulation of ring rod. * __Features__: RingCosseratRod, OneEndFixedRod, GravityForce +* [CatenaryCase](./CatenaryCase) + * __Purpose__: Demonstrate simulation of cosserat rod under gravity with fixed ends, compared with Catenary Analytical Solution from Routh, Edward John (1891). ["Chapter X: On Strings"](https://books.google.com/books?id=3N5JAAAAMAAJ&pg=PA315#v=onepage&q&f=false). A Treatise on Analytical Statics. University Press. + * __Features__: CosseratRod, FixedConstraint, GravityForce ## Functional Examples diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact.py index f39bf8f9c..54bd378e8 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact.py @@ -7,7 +7,7 @@ def rod_cylinder_contact_case(inclination_angle=0.0): class RodCylinderParallelContact( ea.BaseSystemCollection, ea.Constraints, - ea.Connections, + ea.Contact, ea.CallBacks, ea.Forcing, ea.Damping, @@ -67,8 +67,10 @@ class RodCylinderParallelContact( rod_cylinder_parallel_contact_simulator.append(rigid_body) # Add contact between rigid body and rod - rod_cylinder_parallel_contact_simulator.connect(rod, rigid_body).using( - ea.ExternalContact, + rod_cylinder_parallel_contact_simulator.detect_contact_between( + rod, rigid_body + ).using( + ea.RodCylinderContact, k=5e4, nu=0.1, ) diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction.py index e06029e91..ce67ca84f 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction.py @@ -9,7 +9,7 @@ def rod_cylinder_contact_friction_case( class RodCylinderParallelContact( ea.BaseSystemCollection, ea.Constraints, - ea.Connections, + ea.Contact, ea.CallBacks, ea.Forcing, ea.Damping, @@ -93,8 +93,10 @@ class RodCylinderParallelContact( ) # Add contact between rigid body and rod - rod_cylinder_parallel_contact_simulator.connect(rod, rigid_body).using( - ea.ExternalContact, + rod_cylinder_parallel_contact_simulator.detect_contact_between( + rod, rigid_body + ).using( + ea.RodCylinderContact, k=1e5, nu=100, velocity_damping_coefficient=1e5, diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_validation.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_validation.py index ab9d53992..9c97ba71d 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_validation.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_validation.py @@ -6,7 +6,7 @@ class SingleRodSingleCylinderInteractionSimulator( ea.BaseSystemCollection, ea.Constraints, - ea.Connections, + ea.Contact, ea.Forcing, ea.CallBacks, ea.Damping, @@ -109,7 +109,9 @@ class SingleRodSingleCylinderInteractionSimulator( ) single_rod_sim.append(cylinder) -single_rod_sim.connect(rod1, cylinder).using(ea.ExternalContact, 1e2, 0.1) +single_rod_sim.detect_contact_between(rod1, cylinder).using( + ea.RodCylinderContact, 1e2, 0.1 +) # Add call backs 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 cc9b30083..71e7b12ab 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_with_y_normal.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_with_y_normal.py @@ -6,7 +6,7 @@ class SingleRodSingleCylinderInteractionSimulator( ea.BaseSystemCollection, ea.Constraints, - ea.Connections, + ea.Contact, ea.Forcing, ea.CallBacks, ea.Damping, @@ -87,7 +87,9 @@ class SingleRodSingleCylinderInteractionSimulator( ) single_rod_sim.append(cylinder) -single_rod_sim.connect(rod1, cylinder).using(ea.ExternalContact, 1e2, 0.1) +single_rod_sim.detect_contact_between(rod1, cylinder).using( + ea.RodCylinderContact, 1e2, 0.1 +) # Add call backs diff --git a/examples/RodContactCase/RodRodContact/rod_rod_contact_inclined_validation.py b/examples/RodContactCase/RodRodContact/rod_rod_contact_inclined_validation.py index 4b4729e73..6a4c1fa33 100644 --- a/examples/RodContactCase/RodRodContact/rod_rod_contact_inclined_validation.py +++ b/examples/RodContactCase/RodRodContact/rod_rod_contact_inclined_validation.py @@ -9,7 +9,7 @@ class InclinedRodRodContact( ea.BaseSystemCollection, ea.Constraints, - ea.Connections, + ea.Contact, ea.Forcing, ea.Damping, ea.CallBacks, @@ -89,8 +89,8 @@ class InclinedRodRodContact( inclined_rod_rod_contact_sim.append(rod_two) # Contact between two rods -inclined_rod_rod_contact_sim.connect(rod_one, rod_two).using( - ea.ExternalContact, k=1e3, nu=0.0 +inclined_rod_rod_contact_sim.detect_contact_between(rod_one, rod_two).using( + ea.RodRodContact, k=1e3, nu=0.0 ) # add damping diff --git a/examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py b/examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py index 06a1159ff..4fa3eba60 100644 --- a/examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py +++ b/examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py @@ -9,7 +9,7 @@ class ParallelRodRodContact( ea.BaseSystemCollection, ea.Constraints, - ea.Connections, + ea.Contact, ea.Forcing, ea.Damping, ea.CallBacks, @@ -86,8 +86,8 @@ class ParallelRodRodContact( parallel_rod_rod_contact_sim.append(rod_two) # Contact between two rods -parallel_rod_rod_contact_sim.connect(rod_one, rod_two).using( - ea.ExternalContact, k=1e3, nu=0.001 +parallel_rod_rod_contact_sim.detect_contact_between(rod_one, rod_two).using( + ea.RodRodContact, k=1e3, nu=0.001 ) # add damping diff --git a/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py b/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py index d6ee6f105..a23b6094f 100644 --- a/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py +++ b/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py @@ -9,7 +9,7 @@ class PlectonemesCase( ea.BaseSystemCollection, ea.Constraints, - ea.Connections, + ea.Contact, ea.Forcing, ea.CallBacks, ea.Damping, @@ -165,7 +165,9 @@ def constrain_rates(self, rod, time): ) # Add self contact to prevent penetration -plectonemes_sim.connect(sherable_rod, sherable_rod).using(ea.SelfContact, k=1e4, nu=10) +plectonemes_sim.detect_contact_between(sherable_rod, sherable_rod).using( + ea.RodSelfContact, k=1e4, nu=10 +) # Add callback functions for plotting position of the rod later on class RodCallBack(ea.CallBackBaseClass): diff --git a/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py b/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py index e59028027..ce973ea0e 100644 --- a/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py +++ b/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py @@ -9,7 +9,7 @@ class SolenoidCase( ea.BaseSystemCollection, ea.Constraints, - ea.Connections, + ea.Contact, ea.Forcing, ea.CallBacks, ea.Damping, @@ -176,7 +176,9 @@ def constrain_rates(self, rod, time): ) # Add self contact to prevent penetration -solenoid_sim.connect(sherable_rod, sherable_rod).using(ea.SelfContact, k=1e4, nu=10) +solenoid_sim.detect_contact_between(sherable_rod, sherable_rod).using( + ea.RodSelfContact, k=1e4, nu=10 +) # Add callback functions for plotting position of the rod later on class RodCallBack(ea.CallBackBaseClass): diff --git a/poetry.lock b/poetry.lock index 5a5aaedde..f8af7df37 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "accessible-pygments" @@ -27,13 +27,13 @@ files = [ [[package]] name = "autoflake8" -version = "0.4.0" +version = "0.4.1" description = "Tool to automatically fix some issues reported by flake8 (forked from autoflake)." optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "autoflake8-0.4.0-py3-none-any.whl", hash = "sha256:f3a528a7794e9e5974b5ec342dc697c35fbc87de333603be5df6e87e828ca340"}, - {file = "autoflake8-0.4.0.tar.gz", hash = "sha256:32520226af273e4e0b058329b19b68ea444c2f419bd9f26d633c762c5aca3500"}, + {file = "autoflake8-0.4.1-py3-none-any.whl", hash = "sha256:fdf663b627993ac38e5b55b7d742c388fb2a4f34798a052f43eecc5e8d629e9d"}, + {file = "autoflake8-0.4.1.tar.gz", hash = "sha256:c17da499bd2b71ba02fb11fe53ff1ad83d7dae6efb0f115fd1344f467797c679"}, ] [package.dependencies] @@ -41,33 +41,39 @@ pyflakes = ">=2.3.0" [[package]] name = "babel" -version = "2.12.1" +version = "2.14.0" description = "Internationalization utilities" optional = true python-versions = ">=3.7" files = [ - {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"}, - {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, + {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, + {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, ] [package.dependencies] pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + [[package]] name = "beautifulsoup4" -version = "4.12.2" +version = "4.12.3" description = "Screen-scraping library" optional = true python-versions = ">=3.6.0" files = [ - {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, - {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, ] [package.dependencies] soupsieve = ">1.2" [package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] html5lib = ["html5lib"] lxml = ["lxml"] @@ -102,108 +108,123 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2023.7.22" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] name = "cfgv" -version = "3.3.1" +version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.8" files = [ - {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, - {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] [[package]] name = "charset-normalizer" -version = "3.1.0" +version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, - {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, - {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, - {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, - {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, - {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, - {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [[package]] @@ -266,77 +287,74 @@ files = [ [[package]] name = "contourpy" -version = "1.0.7" +version = "1.1.1" description = "Python library for calculating contours of 2D quadrilateral grids" -optional = true +optional = false python-versions = ">=3.8" files = [ - {file = "contourpy-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:95c3acddf921944f241b6773b767f1cbce71d03307270e2d769fd584d5d1092d"}, - {file = "contourpy-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc1464c97579da9f3ab16763c32e5c5d5bb5fa1ec7ce509a4ca6108b61b84fab"}, - {file = "contourpy-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8acf74b5d383414401926c1598ed77825cd530ac7b463ebc2e4f46638f56cce6"}, - {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c71fdd8f1c0f84ffd58fca37d00ca4ebaa9e502fb49825484da075ac0b0b803"}, - {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f99e9486bf1bb979d95d5cffed40689cb595abb2b841f2991fc894b3452290e8"}, - {file = "contourpy-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87f4d8941a9564cda3f7fa6a6cd9b32ec575830780677932abdec7bcb61717b0"}, - {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9e20e5a1908e18aaa60d9077a6d8753090e3f85ca25da6e25d30dc0a9e84c2c6"}, - {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a877ada905f7d69b2a31796c4b66e31a8068b37aa9b78832d41c82fc3e056ddd"}, - {file = "contourpy-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6381fa66866b0ea35e15d197fc06ac3840a9b2643a6475c8fff267db8b9f1e69"}, - {file = "contourpy-1.0.7-cp310-cp310-win32.whl", hash = "sha256:3c184ad2433635f216645fdf0493011a4667e8d46b34082f5a3de702b6ec42e3"}, - {file = "contourpy-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:3caea6365b13119626ee996711ab63e0c9d7496f65641f4459c60a009a1f3e80"}, - {file = "contourpy-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed33433fc3820263a6368e532f19ddb4c5990855e4886088ad84fd7c4e561c71"}, - {file = "contourpy-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:38e2e577f0f092b8e6774459317c05a69935a1755ecfb621c0a98f0e3c09c9a5"}, - {file = "contourpy-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ae90d5a8590e5310c32a7630b4b8618cef7563cebf649011da80874d0aa8f414"}, - {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130230b7e49825c98edf0b428b7aa1125503d91732735ef897786fe5452b1ec2"}, - {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58569c491e7f7e874f11519ef46737cea1d6eda1b514e4eb5ac7dab6aa864d02"}, - {file = "contourpy-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d43960d809c4c12508a60b66cb936e7ed57d51fb5e30b513934a4a23874fae"}, - {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:152fd8f730c31fd67fe0ffebe1df38ab6a669403da93df218801a893645c6ccc"}, - {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9056c5310eb1daa33fc234ef39ebfb8c8e2533f088bbf0bc7350f70a29bde1ac"}, - {file = "contourpy-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a9d7587d2fdc820cc9177139b56795c39fb8560f540bba9ceea215f1f66e1566"}, - {file = "contourpy-1.0.7-cp311-cp311-win32.whl", hash = "sha256:4ee3ee247f795a69e53cd91d927146fb16c4e803c7ac86c84104940c7d2cabf0"}, - {file = "contourpy-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:5caeacc68642e5f19d707471890f037a13007feba8427eb7f2a60811a1fc1350"}, - {file = "contourpy-1.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fd7dc0e6812b799a34f6d12fcb1000539098c249c8da54f3566c6a6461d0dbad"}, - {file = "contourpy-1.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0f9d350b639db6c2c233d92c7f213d94d2e444d8e8fc5ca44c9706cf72193772"}, - {file = "contourpy-1.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e96a08b62bb8de960d3a6afbc5ed8421bf1a2d9c85cc4ea73f4bc81b4910500f"}, - {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:031154ed61f7328ad7f97662e48660a150ef84ee1bc8876b6472af88bf5a9b98"}, - {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e9ebb4425fc1b658e13bace354c48a933b842d53c458f02c86f371cecbedecc"}, - {file = "contourpy-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efb8f6d08ca7998cf59eaf50c9d60717f29a1a0a09caa46460d33b2924839dbd"}, - {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6c180d89a28787e4b73b07e9b0e2dac7741261dbdca95f2b489c4f8f887dd810"}, - {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b8d587cc39057d0afd4166083d289bdeff221ac6d3ee5046aef2d480dc4b503c"}, - {file = "contourpy-1.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:769eef00437edf115e24d87f8926955f00f7704bede656ce605097584f9966dc"}, - {file = "contourpy-1.0.7-cp38-cp38-win32.whl", hash = "sha256:62398c80ef57589bdbe1eb8537127321c1abcfdf8c5f14f479dbbe27d0322e66"}, - {file = "contourpy-1.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:57119b0116e3f408acbdccf9eb6ef19d7fe7baf0d1e9aaa5381489bc1aa56556"}, - {file = "contourpy-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30676ca45084ee61e9c3da589042c24a57592e375d4b138bd84d8709893a1ba4"}, - {file = "contourpy-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e927b3868bd1e12acee7cc8f3747d815b4ab3e445a28d2e5373a7f4a6e76ba1"}, - {file = "contourpy-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:366a0cf0fc079af5204801786ad7a1c007714ee3909e364dbac1729f5b0849e5"}, - {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ba9bb365446a22411f0673abf6ee1fea3b2cf47b37533b970904880ceb72f3"}, - {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:71b0bf0c30d432278793d2141362ac853859e87de0a7dee24a1cea35231f0d50"}, - {file = "contourpy-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7281244c99fd7c6f27c1c6bfafba878517b0b62925a09b586d88ce750a016d2"}, - {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b6d0f9e1d39dbfb3977f9dd79f156c86eb03e57a7face96f199e02b18e58d32a"}, - {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7f6979d20ee5693a1057ab53e043adffa1e7418d734c1532e2d9e915b08d8ec2"}, - {file = "contourpy-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5dd34c1ae752515318224cba7fc62b53130c45ac6a1040c8b7c1a223c46e8967"}, - {file = "contourpy-1.0.7-cp39-cp39-win32.whl", hash = "sha256:c5210e5d5117e9aec8c47d9156d1d3835570dd909a899171b9535cb4a3f32693"}, - {file = "contourpy-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:60835badb5ed5f4e194a6f21c09283dd6e007664a86101431bf870d9e86266c4"}, - {file = "contourpy-1.0.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ce41676b3d0dd16dbcfabcc1dc46090aaf4688fd6e819ef343dbda5a57ef0161"}, - {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a011cf354107b47c58ea932d13b04d93c6d1d69b8b6dce885e642531f847566"}, - {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31a55dccc8426e71817e3fe09b37d6d48ae40aae4ecbc8c7ad59d6893569c436"}, - {file = "contourpy-1.0.7-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69f8ff4db108815addd900a74df665e135dbbd6547a8a69333a68e1f6e368ac2"}, - {file = "contourpy-1.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efe99298ba37e37787f6a2ea868265465410822f7bea163edcc1bd3903354ea9"}, - {file = "contourpy-1.0.7-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a1e97b86f73715e8670ef45292d7cc033548266f07d54e2183ecb3c87598888f"}, - {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc331c13902d0f50845099434cd936d49d7a2ca76cb654b39691974cb1e4812d"}, - {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24847601071f740837aefb730e01bd169fbcaa610209779a78db7ebb6e6a7051"}, - {file = "contourpy-1.0.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abf298af1e7ad44eeb93501e40eb5a67abbf93b5d90e468d01fc0c4451971afa"}, - {file = "contourpy-1.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:64757f6460fc55d7e16ed4f1de193f362104285c667c112b50a804d482777edd"}, - {file = "contourpy-1.0.7.tar.gz", hash = "sha256:d8165a088d31798b59e91117d1f5fc3df8168d8b48c4acc10fc0df0d0bdbcc5e"}, + {file = "contourpy-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:46e24f5412c948d81736509377e255f6040e94216bf1a9b5ea1eaa9d29f6ec1b"}, + {file = "contourpy-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e48694d6a9c5a26ee85b10130c77a011a4fedf50a7279fa0bdaf44bafb4299d"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a66045af6cf00e19d02191ab578a50cb93b2028c3eefed999793698e9ea768ae"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ebf42695f75ee1a952f98ce9775c873e4971732a87334b099dde90b6af6a916"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6aec19457617ef468ff091669cca01fa7ea557b12b59a7908b9474bb9674cf0"}, + {file = "contourpy-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:462c59914dc6d81e0b11f37e560b8a7c2dbab6aca4f38be31519d442d6cde1a1"}, + {file = "contourpy-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6d0a8efc258659edc5299f9ef32d8d81de8b53b45d67bf4bfa3067f31366764d"}, + {file = "contourpy-1.1.1-cp310-cp310-win32.whl", hash = "sha256:d6ab42f223e58b7dac1bb0af32194a7b9311065583cc75ff59dcf301afd8a431"}, + {file = "contourpy-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:549174b0713d49871c6dee90a4b499d3f12f5e5f69641cd23c50a4542e2ca1eb"}, + {file = "contourpy-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:407d864db716a067cc696d61fa1ef6637fedf03606e8417fe2aeed20a061e6b2"}, + {file = "contourpy-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe80c017973e6a4c367e037cb31601044dd55e6bfacd57370674867d15a899b"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e30aaf2b8a2bac57eb7e1650df1b3a4130e8d0c66fc2f861039d507a11760e1b"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3de23ca4f381c3770dee6d10ead6fff524d540c0f662e763ad1530bde5112532"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:566f0e41df06dfef2431defcfaa155f0acfa1ca4acbf8fd80895b1e7e2ada40e"}, + {file = "contourpy-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b04c2f0adaf255bf756cf08ebef1be132d3c7a06fe6f9877d55640c5e60c72c5"}, + {file = "contourpy-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d0c188ae66b772d9d61d43c6030500344c13e3f73a00d1dc241da896f379bb62"}, + {file = "contourpy-1.1.1-cp311-cp311-win32.whl", hash = "sha256:0683e1ae20dc038075d92e0e0148f09ffcefab120e57f6b4c9c0f477ec171f33"}, + {file = "contourpy-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:8636cd2fc5da0fb102a2504fa2c4bea3cbc149533b345d72cdf0e7a924decc45"}, + {file = "contourpy-1.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:560f1d68a33e89c62da5da4077ba98137a5e4d3a271b29f2f195d0fba2adcb6a"}, + {file = "contourpy-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24216552104ae8f3b34120ef84825400b16eb6133af2e27a190fdc13529f023e"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56de98a2fb23025882a18b60c7f0ea2d2d70bbbcfcf878f9067234b1c4818442"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:07d6f11dfaf80a84c97f1a5ba50d129d9303c5b4206f776e94037332e298dda8"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1eaac5257a8f8a047248d60e8f9315c6cff58f7803971170d952555ef6344a7"}, + {file = "contourpy-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19557fa407e70f20bfaba7d55b4d97b14f9480856c4fb65812e8a05fe1c6f9bf"}, + {file = "contourpy-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:081f3c0880712e40effc5f4c3b08feca6d064cb8cfbb372ca548105b86fd6c3d"}, + {file = "contourpy-1.1.1-cp312-cp312-win32.whl", hash = "sha256:059c3d2a94b930f4dafe8105bcdc1b21de99b30b51b5bce74c753686de858cb6"}, + {file = "contourpy-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f44d78b61740e4e8c71db1cf1fd56d9050a4747681c59ec1094750a658ceb970"}, + {file = "contourpy-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:70e5a10f8093d228bb2b552beeb318b8928b8a94763ef03b858ef3612b29395d"}, + {file = "contourpy-1.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8394e652925a18ef0091115e3cc191fef350ab6dc3cc417f06da66bf98071ae9"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5bd5680f844c3ff0008523a71949a3ff5e4953eb7701b28760805bc9bcff217"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66544f853bfa85c0d07a68f6c648b2ec81dafd30f272565c37ab47a33b220684"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0c02b75acfea5cab07585d25069207e478d12309557f90a61b5a3b4f77f46ce"}, + {file = "contourpy-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41339b24471c58dc1499e56783fedc1afa4bb018bcd035cfb0ee2ad2a7501ef8"}, + {file = "contourpy-1.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f29fb0b3f1217dfe9362ec55440d0743fe868497359f2cf93293f4b2701b8251"}, + {file = "contourpy-1.1.1-cp38-cp38-win32.whl", hash = "sha256:f9dc7f933975367251c1b34da882c4f0e0b2e24bb35dc906d2f598a40b72bfc7"}, + {file = "contourpy-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:498e53573e8b94b1caeb9e62d7c2d053c263ebb6aa259c81050766beb50ff8d9"}, + {file = "contourpy-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ba42e3810999a0ddd0439e6e5dbf6d034055cdc72b7c5c839f37a7c274cb4eba"}, + {file = "contourpy-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c06e4c6e234fcc65435223c7b2a90f286b7f1b2733058bdf1345d218cc59e34"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca6fab080484e419528e98624fb5c4282148b847e3602dc8dbe0cb0669469887"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93df44ab351119d14cd1e6b52a5063d3336f0754b72736cc63db59307dabb718"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eafbef886566dc1047d7b3d4b14db0d5b7deb99638d8e1be4e23a7c7ac59ff0f"}, + {file = "contourpy-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efe0fab26d598e1ec07d72cf03eaeeba8e42b4ecf6b9ccb5a356fde60ff08b85"}, + {file = "contourpy-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f08e469821a5e4751c97fcd34bcb586bc243c39c2e39321822060ba902eac49e"}, + {file = "contourpy-1.1.1-cp39-cp39-win32.whl", hash = "sha256:bfc8a5e9238232a45ebc5cb3bfee71f1167064c8d382cadd6076f0d51cff1da0"}, + {file = "contourpy-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:c84fdf3da00c2827d634de4fcf17e3e067490c4aea82833625c4c8e6cdea0887"}, + {file = "contourpy-1.1.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:229a25f68046c5cf8067d6d6351c8b99e40da11b04d8416bf8d2b1d75922521e"}, + {file = "contourpy-1.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a10dab5ea1bd4401c9483450b5b0ba5416be799bbd50fc7a6cc5e2a15e03e8a3"}, + {file = "contourpy-1.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4f9147051cb8fdb29a51dc2482d792b3b23e50f8f57e3720ca2e3d438b7adf23"}, + {file = "contourpy-1.1.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a75cc163a5f4531a256f2c523bd80db509a49fc23721b36dd1ef2f60ff41c3cb"}, + {file = "contourpy-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b53d5769aa1f2d4ea407c65f2d1d08002952fac1d9e9d307aa2e1023554a163"}, + {file = "contourpy-1.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11b836b7dbfb74e049c302bbf74b4b8f6cb9d0b6ca1bf86cfa8ba144aedadd9c"}, + {file = "contourpy-1.1.1.tar.gz", hash = "sha256:96ba37c2e24b7212a77da85004c38e7c4d155d3e72a45eeaf22c1f03f607e8ab"}, ] [package.dependencies] -numpy = ">=1.16" +numpy = {version = ">=1.16,<2.0", markers = "python_version <= \"3.11\""} [package.extras] -bokeh = ["bokeh", "chromedriver", "selenium"] -docs = ["furo", "sphinx-copybutton"] -mypy = ["contourpy[bokeh]", "docutils-stubs", "mypy (==0.991)", "types-Pillow"] -test = ["Pillow", "matplotlib", "pytest"] -test-no-images = ["pytest"] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.4.1)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "wurlitzer"] [[package]] name = "coverage" @@ -405,24 +423,28 @@ toml = ["tomli"] [[package]] name = "cycler" -version = "0.11.0" +version = "0.12.1" description = "Composable style cycles" -optional = true -python-versions = ">=3.6" +optional = false +python-versions = ">=3.8" files = [ - {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, - {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, + {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, + {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, ] +[package.extras] +docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] +tests = ["pytest", "pytest-cov", "pytest-xdist"] + [[package]] name = "distlib" -version = "0.3.6" +version = "0.3.8" description = "Distribution utilities" optional = false python-versions = "*" files = [ - {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, - {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] [[package]] @@ -438,13 +460,13 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.1.1" +version = "1.2.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"}, - {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"}, + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, ] [package.extras] @@ -452,18 +474,19 @@ test = ["pytest (>=6)"] [[package]] name = "filelock" -version = "3.12.0" +version = "3.13.1" description = "A platform independent file lock." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "filelock-3.12.0-py3-none-any.whl", hash = "sha256:ad98852315c2ab702aeb628412cbf7e95b7ce8c3bf9565670b4eaecf1db370a9"}, - {file = "filelock-3.12.0.tar.gz", hash = "sha256:fc03ae43288c013d2ea83c8597001b1129db351aad9c57fe2409327916b8e718"}, + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, ] [package.extras] -docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] [[package]] name = "flake8" @@ -483,78 +506,78 @@ pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "fonttools" -version = "4.43.0" +version = "4.49.0" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.43.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ab80e7d6bb01316d5fc8161a2660ca2e9e597d0880db4927bc866c76474472ef"}, - {file = "fonttools-4.43.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82d8e687a42799df5325e7ee12977b74738f34bf7fde1c296f8140efd699a213"}, - {file = "fonttools-4.43.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d08a694b280d615460563a6b4e2afb0b1b9df708c799ec212bf966652b94fc84"}, - {file = "fonttools-4.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d654d3e780e0ceabb1f4eff5a3c042c67d4428d0fe1ea3afd238a721cf171b3"}, - {file = "fonttools-4.43.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:20fc43783c432862071fa76da6fa714902ae587bc68441e12ff4099b94b1fcef"}, - {file = "fonttools-4.43.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:33c40a657fb87ff83185828c0323032d63a4df1279d5c1c38e21f3ec56327803"}, - {file = "fonttools-4.43.0-cp310-cp310-win32.whl", hash = "sha256:b3813f57f85bbc0e4011a0e1e9211f9ee52f87f402e41dc05bc5135f03fa51c1"}, - {file = "fonttools-4.43.0-cp310-cp310-win_amd64.whl", hash = "sha256:05056a8c9af048381fdb17e89b17d45f6c8394176d01e8c6fef5ac96ea950d38"}, - {file = "fonttools-4.43.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da78f39b601ed0b4262929403186d65cf7a016f91ff349ab18fdc5a7080af465"}, - {file = "fonttools-4.43.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5056f69a18f3f28ab5283202d1efcfe011585d31de09d8560f91c6c88f041e92"}, - {file = "fonttools-4.43.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcc01cea0a121fb0c009993497bad93cae25e77db7dee5345fec9cce1aaa09cd"}, - {file = "fonttools-4.43.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee728d5af70f117581712966a21e2e07031e92c687ef1fdc457ac8d281016f64"}, - {file = "fonttools-4.43.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b5e760198f0b87e42478bb35a6eae385c636208f6f0d413e100b9c9c5efafb6a"}, - {file = "fonttools-4.43.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af38f5145258e9866da5881580507e6d17ff7756beef175d13213a43a84244e9"}, - {file = "fonttools-4.43.0-cp311-cp311-win32.whl", hash = "sha256:25620b738d4533cfc21fd2a4f4b667e481f7cb60e86b609799f7d98af657854e"}, - {file = "fonttools-4.43.0-cp311-cp311-win_amd64.whl", hash = "sha256:635658464dccff6fa5c3b43fe8f818ae2c386ee6a9e1abc27359d1e255528186"}, - {file = "fonttools-4.43.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a682fb5cbf8837d1822b80acc0be5ff2ea0c49ca836e468a21ffd388ef280fd3"}, - {file = "fonttools-4.43.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3d7adfa342e6b3a2b36960981f23f480969f833d565a4eba259c2e6f59d2674f"}, - {file = "fonttools-4.43.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa67d1e720fdd902fde4a59d0880854ae9f19fc958f3e1538bceb36f7f4dc92"}, - {file = "fonttools-4.43.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77e5113233a2df07af9dbf493468ce526784c3b179c0e8b9c7838ced37c98b69"}, - {file = "fonttools-4.43.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:57c22e5f9f53630d458830f710424dce4f43c5f0d95cb3368c0f5178541e4db7"}, - {file = "fonttools-4.43.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:206808f9717c9b19117f461246372a2c160fa12b9b8dbdfb904ab50ca235ba0a"}, - {file = "fonttools-4.43.0-cp312-cp312-win32.whl", hash = "sha256:f19c2b1c65d57cbea25cabb80941fea3fbf2625ff0cdcae8900b5fb1c145704f"}, - {file = "fonttools-4.43.0-cp312-cp312-win_amd64.whl", hash = "sha256:7c76f32051159f8284f1a5f5b605152b5a530736fb8b55b09957db38dcae5348"}, - {file = "fonttools-4.43.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e3f8acc6ef4a627394021246e099faee4b343afd3ffe2e517d8195b4ebf20289"}, - {file = "fonttools-4.43.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a68b71adc3b3a90346e4ac92f0a69ab9caeba391f3b04ab6f1e98f2c8ebe88e3"}, - {file = "fonttools-4.43.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ace0fd5afb79849f599f76af5c6aa5e865bd042c811e4e047bbaa7752cc26126"}, - {file = "fonttools-4.43.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f9660e70a2430780e23830476332bc3391c3c8694769e2c0032a5038702a662"}, - {file = "fonttools-4.43.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:48078357984214ccd22d7fe0340cd6ff7286b2f74f173603a1a9a40b5dc25afe"}, - {file = "fonttools-4.43.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d27d960e10cf7617d70cf3104c32a69b008dde56f2d55a9bed4ba6e3df611544"}, - {file = "fonttools-4.43.0-cp38-cp38-win32.whl", hash = "sha256:a6a2e99bb9ea51e0974bbe71768df42c6dd189308c22f3f00560c3341b345646"}, - {file = "fonttools-4.43.0-cp38-cp38-win_amd64.whl", hash = "sha256:030355fbb0cea59cf75d076d04d3852900583d1258574ff2d7d719abf4513836"}, - {file = "fonttools-4.43.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:52e77f23a9c059f8be01a07300ba4c4d23dc271d33eed502aea5a01ab5d2f4c1"}, - {file = "fonttools-4.43.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a530fa28c155538d32214eafa0964989098a662bd63e91e790e6a7a4e9c02da"}, - {file = "fonttools-4.43.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70f021a6b9eb10dfe7a411b78e63a503a06955dd6d2a4e130906d8760474f77c"}, - {file = "fonttools-4.43.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:812142a0e53cc853964d487e6b40963df62f522b1b571e19d1ff8467d7880ceb"}, - {file = "fonttools-4.43.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ace51902ab67ef5fe225e8b361039e996db153e467e24a28d35f74849b37b7ce"}, - {file = "fonttools-4.43.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8dfd8edfce34ad135bd69de20c77449c06e2c92b38f2a8358d0987737f82b49e"}, - {file = "fonttools-4.43.0-cp39-cp39-win32.whl", hash = "sha256:e5d53eddaf436fa131042f44a76ea1ead0a17c354ab9de0d80e818f0cb1629f1"}, - {file = "fonttools-4.43.0-cp39-cp39-win_amd64.whl", hash = "sha256:93c5b6d77baf28f306bc13fa987b0b13edca6a39dc2324eaca299a74ccc6316f"}, - {file = "fonttools-4.43.0-py3-none-any.whl", hash = "sha256:e4bc589d8da09267c7c4ceaaaa4fc01a7908ac5b43b286ac9279afe76407c384"}, - {file = "fonttools-4.43.0.tar.gz", hash = "sha256:b62a53a4ca83c32c6b78cac64464f88d02929779373c716f738af6968c8c821e"}, + {file = "fonttools-4.49.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d970ecca0aac90d399e458f0b7a8a597e08f95de021f17785fb68e2dc0b99717"}, + {file = "fonttools-4.49.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac9a745b7609f489faa65e1dc842168c18530874a5f5b742ac3dd79e26bca8bc"}, + {file = "fonttools-4.49.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ba0e00620ca28d4ca11fc700806fd69144b463aa3275e1b36e56c7c09915559"}, + {file = "fonttools-4.49.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdee3ab220283057e7840d5fb768ad4c2ebe65bdba6f75d5d7bf47f4e0ed7d29"}, + {file = "fonttools-4.49.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ce7033cb61f2bb65d8849658d3786188afd80f53dad8366a7232654804529532"}, + {file = "fonttools-4.49.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:07bc5ea02bb7bc3aa40a1eb0481ce20e8d9b9642a9536cde0218290dd6085828"}, + {file = "fonttools-4.49.0-cp310-cp310-win32.whl", hash = "sha256:86eef6aab7fd7c6c8545f3ebd00fd1d6729ca1f63b0cb4d621bccb7d1d1c852b"}, + {file = "fonttools-4.49.0-cp310-cp310-win_amd64.whl", hash = "sha256:1fac1b7eebfce75ea663e860e7c5b4a8831b858c17acd68263bc156125201abf"}, + {file = "fonttools-4.49.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:edc0cce355984bb3c1d1e89d6a661934d39586bb32191ebff98c600f8957c63e"}, + {file = "fonttools-4.49.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:83a0d9336de2cba86d886507dd6e0153df333ac787377325a39a2797ec529814"}, + {file = "fonttools-4.49.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36c8865bdb5cfeec88f5028e7e592370a0657b676c6f1d84a2108e0564f90e22"}, + {file = "fonttools-4.49.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33037d9e56e2562c710c8954d0f20d25b8386b397250d65581e544edc9d6b942"}, + {file = "fonttools-4.49.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8fb022d799b96df3eaa27263e9eea306bd3d437cc9aa981820850281a02b6c9a"}, + {file = "fonttools-4.49.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33c584c0ef7dc54f5dd4f84082eabd8d09d1871a3d8ca2986b0c0c98165f8e86"}, + {file = "fonttools-4.49.0-cp311-cp311-win32.whl", hash = "sha256:cbe61b158deb09cffdd8540dc4a948d6e8f4d5b4f3bf5cd7db09bd6a61fee64e"}, + {file = "fonttools-4.49.0-cp311-cp311-win_amd64.whl", hash = "sha256:fc11e5114f3f978d0cea7e9853627935b30d451742eeb4239a81a677bdee6bf6"}, + {file = "fonttools-4.49.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d647a0e697e5daa98c87993726da8281c7233d9d4ffe410812a4896c7c57c075"}, + {file = "fonttools-4.49.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f3bbe672df03563d1f3a691ae531f2e31f84061724c319652039e5a70927167e"}, + {file = "fonttools-4.49.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bebd91041dda0d511b0d303180ed36e31f4f54b106b1259b69fade68413aa7ff"}, + {file = "fonttools-4.49.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4145f91531fd43c50f9eb893faa08399816bb0b13c425667c48475c9f3a2b9b5"}, + {file = "fonttools-4.49.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ea329dafb9670ffbdf4dbc3b0e5c264104abcd8441d56de77f06967f032943cb"}, + {file = "fonttools-4.49.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c076a9e548521ecc13d944b1d261ff3d7825048c338722a4bd126d22316087b7"}, + {file = "fonttools-4.49.0-cp312-cp312-win32.whl", hash = "sha256:b607ea1e96768d13be26d2b400d10d3ebd1456343eb5eaddd2f47d1c4bd00880"}, + {file = "fonttools-4.49.0-cp312-cp312-win_amd64.whl", hash = "sha256:a974c49a981e187381b9cc2c07c6b902d0079b88ff01aed34695ec5360767034"}, + {file = "fonttools-4.49.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b85ec0bdd7bdaa5c1946398cbb541e90a6dfc51df76dfa88e0aaa41b335940cb"}, + {file = "fonttools-4.49.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:af20acbe198a8a790618ee42db192eb128afcdcc4e96d99993aca0b60d1faeb4"}, + {file = "fonttools-4.49.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d418b1fee41a1d14931f7ab4b92dc0bc323b490e41d7a333eec82c9f1780c75"}, + {file = "fonttools-4.49.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b44a52b8e6244b6548851b03b2b377a9702b88ddc21dcaf56a15a0393d425cb9"}, + {file = "fonttools-4.49.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7c7125068e04a70739dad11857a4d47626f2b0bd54de39e8622e89701836eabd"}, + {file = "fonttools-4.49.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29e89d0e1a7f18bc30f197cfadcbef5a13d99806447c7e245f5667579a808036"}, + {file = "fonttools-4.49.0-cp38-cp38-win32.whl", hash = "sha256:9d95fa0d22bf4f12d2fb7b07a46070cdfc19ef5a7b1c98bc172bfab5bf0d6844"}, + {file = "fonttools-4.49.0-cp38-cp38-win_amd64.whl", hash = "sha256:768947008b4dc552d02772e5ebd49e71430a466e2373008ce905f953afea755a"}, + {file = "fonttools-4.49.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:08877e355d3dde1c11973bb58d4acad1981e6d1140711230a4bfb40b2b937ccc"}, + {file = "fonttools-4.49.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fdb54b076f25d6b0f0298dc706acee5052de20c83530fa165b60d1f2e9cbe3cb"}, + {file = "fonttools-4.49.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0af65c720520710cc01c293f9c70bd69684365c6015cc3671db2b7d807fe51f2"}, + {file = "fonttools-4.49.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f255ce8ed7556658f6d23f6afd22a6d9bbc3edb9b96c96682124dc487e1bf42"}, + {file = "fonttools-4.49.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d00af0884c0e65f60dfaf9340e26658836b935052fdd0439952ae42e44fdd2be"}, + {file = "fonttools-4.49.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:263832fae27481d48dfafcc43174644b6706639661e242902ceb30553557e16c"}, + {file = "fonttools-4.49.0-cp39-cp39-win32.whl", hash = "sha256:0404faea044577a01bb82d47a8fa4bc7a54067fa7e324785dd65d200d6dd1133"}, + {file = "fonttools-4.49.0-cp39-cp39-win_amd64.whl", hash = "sha256:b050d362df50fc6e38ae3954d8c29bf2da52be384649ee8245fdb5186b620836"}, + {file = "fonttools-4.49.0-py3-none-any.whl", hash = "sha256:af281525e5dd7fa0b39fb1667b8d5ca0e2a9079967e14c4bfe90fd1cd13e0f18"}, + {file = "fonttools-4.49.0.tar.gz", hash = "sha256:ebf46e7f01b7af7861310417d7c49591a85d99146fc23a5ba82fdb28af156321"}, ] [package.extras] -all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.0.0)", "xattr", "zopfli (>=0.1.4)"] +all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] graphite = ["lz4 (>=1.7.4.2)"] -interpolatable = ["munkres", "scipy"] -lxml = ["lxml (>=4.0,<5)"] +interpolatable = ["munkres", "pycairo", "scipy"] +lxml = ["lxml (>=4.0)"] pathops = ["skia-pathops (>=0.5.0)"] plot = ["matplotlib"] repacker = ["uharfbuzz (>=0.23.0)"] symfont = ["sympy"] type1 = ["xattr"] ufo = ["fs (>=2.2.0,<3)"] -unicode = ["unicodedata2 (>=15.0.0)"] +unicode = ["unicodedata2 (>=15.1.0)"] woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] [[package]] name = "identify" -version = "2.5.24" +version = "2.5.35" description = "File identification library for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"}, - {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"}, + {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, + {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, ] [package.extras] @@ -562,13 +585,13 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [[package]] @@ -584,40 +607,40 @@ files = [ [[package]] name = "importlib-metadata" -version = "6.6.0" +version = "7.0.2" description = "Read metadata from Python packages" -optional = true -python-versions = ">=3.7" +optional = false +python-versions = ">=3.8" files = [ - {file = "importlib_metadata-6.6.0-py3-none-any.whl", hash = "sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed"}, - {file = "importlib_metadata-6.6.0.tar.gz", hash = "sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705"}, + {file = "importlib_metadata-7.0.2-py3-none-any.whl", hash = "sha256:f4bc4c0c070c490abf4ce96d715f68e95923320370efb66143df00199bb6c100"}, + {file = "importlib_metadata-7.0.2.tar.gz", hash = "sha256:198f568f3230878cb1b44fbd7975f87906c22336dba2e4a7f05278c281fbd792"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "importlib-resources" -version = "5.12.0" +version = "6.3.0" description = "Read resources from Python packages" -optional = true -python-versions = ">=3.7" +optional = false +python-versions = ">=3.8" files = [ - {file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"}, - {file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"}, + {file = "importlib_resources-6.3.0-py3-none-any.whl", hash = "sha256:783407aa1cd05550e3aa123e8f7cfaebee35ffa9cb0242919e2d1e4172222705"}, + {file = "importlib_resources-6.3.0.tar.gz", hash = "sha256:166072a97e86917a9025876f34286f549b9caf1d10b35a1b372bffa1600c6569"}, ] [package.dependencies] zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.collections", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] [[package]] name = "iniconfig" @@ -634,7 +657,7 @@ files = [ name = "jinja2" version = "3.1.3" description = "A very fast and expressive template engine." -optional = false +optional = true python-versions = ">=3.7" files = [ {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, @@ -649,116 +672,148 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "kiwisolver" -version = "1.4.4" +version = "1.4.5" description = "A fast implementation of the Cassowary constraint solver" -optional = true +optional = false python-versions = ">=3.7" files = [ - {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6"}, - {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c"}, - {file = "kiwisolver-1.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de"}, - {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32"}, - {file = "kiwisolver-1.4.4-cp310-cp310-win32.whl", hash = "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408"}, - {file = "kiwisolver-1.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004"}, - {file = "kiwisolver-1.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ea21f66820452a3f5d1655f8704a60d66ba1191359b96541eaf457710a5fc6"}, - {file = "kiwisolver-1.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bc9db8a3efb3e403e4ecc6cd9489ea2bac94244f80c78e27c31dcc00d2790ac2"}, - {file = "kiwisolver-1.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d5b61785a9ce44e5a4b880272baa7cf6c8f48a5180c3e81c59553ba0cb0821ca"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2dbb44c3f7e6c4d3487b31037b1bdbf424d97687c1747ce4ff2895795c9bf69"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6295ecd49304dcf3bfbfa45d9a081c96509e95f4b9d0eb7ee4ec0530c4a96514"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bd472dbe5e136f96a4b18f295d159d7f26fd399136f5b17b08c4e5f498cd494"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf7d9fce9bcc4752ca4a1b80aabd38f6d19009ea5cbda0e0856983cf6d0023f5"}, - {file = "kiwisolver-1.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d6601aed50c74e0ef02f4204da1816147a6d3fbdc8b3872d263338a9052c51"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:877272cf6b4b7e94c9614f9b10140e198d2186363728ed0f701c6eee1baec1da"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:db608a6757adabb32f1cfe6066e39b3706d8c3aa69bbc353a5b61edad36a5cb4"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5853eb494c71e267912275e5586fe281444eb5e722de4e131cddf9d442615626"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f0a1dbdb5ecbef0d34eb77e56fcb3e95bbd7e50835d9782a45df81cc46949750"}, - {file = "kiwisolver-1.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:283dffbf061a4ec60391d51e6155e372a1f7a4f5b15d59c8505339454f8989e4"}, - {file = "kiwisolver-1.4.4-cp311-cp311-win32.whl", hash = "sha256:d06adcfa62a4431d404c31216f0f8ac97397d799cd53800e9d3efc2fbb3cf14e"}, - {file = "kiwisolver-1.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e7da3fec7408813a7cebc9e4ec55afed2d0fd65c4754bc376bf03498d4e92686"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-win32.whl", hash = "sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3"}, - {file = "kiwisolver-1.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166"}, - {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454"}, - {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0"}, - {file = "kiwisolver-1.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d"}, - {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c"}, - {file = "kiwisolver-1.4.4-cp38-cp38-win32.whl", hash = "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191"}, - {file = "kiwisolver-1.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766"}, - {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8"}, - {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897"}, - {file = "kiwisolver-1.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac"}, - {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9"}, - {file = "kiwisolver-1.4.4-cp39-cp39-win32.whl", hash = "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea"}, - {file = "kiwisolver-1.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a"}, - {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:28bc5b299f48150b5f822ce68624e445040595a4ac3d59251703779836eceff9"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:81e38381b782cc7e1e46c4e14cd997ee6040768101aefc8fa3c24a4cc58e98f8"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2a66fdfb34e05b705620dd567f5a03f239a088d5a3f321e7b6ac3239d22aa286"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872b8ca05c40d309ed13eb2e582cab0c5a05e81e987ab9c521bf05ad1d5cf5cb"}, - {file = "kiwisolver-1.4.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:70e7c2e7b750585569564e2e5ca9845acfaa5da56ac46df68414f29fea97be9f"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9f85003f5dfa867e86d53fac6f7e6f30c045673fa27b603c397753bebadc3008"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e307eb9bd99801f82789b44bb45e9f541961831c7311521b13a6c85afc09767"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1792d939ec70abe76f5054d3f36ed5656021dcad1322d1cc996d4e54165cef9"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cb459eea32a4e2cf18ba5fcece2dbdf496384413bc1bae15583f19e567f3b2"}, - {file = "kiwisolver-1.4.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36dafec3d6d6088d34e2de6b85f9d8e2324eb734162fba59d2ba9ed7a2043d5b"}, - {file = "kiwisolver-1.4.4.tar.gz", hash = "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"}, + {file = "kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa"}, + {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525"}, + {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238"}, + {file = "kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90"}, + {file = "kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da"}, + {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac"}, + {file = "kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192"}, + {file = "kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228"}, + {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3"}, + {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20"}, + {file = "kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win32.whl", hash = "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3"}, + {file = "kiwisolver-1.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93"}, + {file = "kiwisolver-1.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18"}, + {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc"}, + {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win32.whl", hash = "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e"}, + {file = "kiwisolver-1.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958"}, + {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342"}, + {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win32.whl", hash = "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f"}, + {file = "kiwisolver-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523"}, + {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd"}, + {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea"}, + {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee"}, + {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"}, ] [[package]] name = "llvmlite" -version = "0.38.1" +version = "0.40.1" description = "lightweight wrapper around basic LLVM functionality" optional = false -python-versions = ">=3.7,<3.11" -files = [ - {file = "llvmlite-0.38.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a7dd2bd1d6406e7789273e3f8a304ed5d9adcfaa5768052fca7dc233a857be98"}, - {file = "llvmlite-0.38.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a5e0ed215a576f0f872f47a70b8cb49864e0aefc8586aff5ce83e3bff47bc23"}, - {file = "llvmlite-0.38.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:633c9026eb43b9903cc4ffbc1c7d5293b2e3ad95d06fa9eab0f6ce6ff6ea15b3"}, - {file = "llvmlite-0.38.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b98da8436dbc29013ea301f1fdb0d596ab53bf0ab65c976d96d00bb6faa0b479"}, - {file = "llvmlite-0.38.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0adce1793d66d009c554809f27baeb6258bf13f6fbaa12eff7443500caec25"}, - {file = "llvmlite-0.38.1-cp310-cp310-win32.whl", hash = "sha256:8c64c90a8b0b7b7e1ed1912ba82c1a3f43cf25affbe06aa3c56c84050edee8ac"}, - {file = "llvmlite-0.38.1-cp310-cp310-win_amd64.whl", hash = "sha256:ab070266f0f51304789a6c20d4be91a9e69683ad9bd4861eb89980e8eb613b3a"}, - {file = "llvmlite-0.38.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ed7528b8b85de930b76407e44b080e4f376b7a007c2879749599ff8e2fe32753"}, - {file = "llvmlite-0.38.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7db018da2863034ad9c73c946625637f3a89635bc70576068bab4bd085eea90d"}, - {file = "llvmlite-0.38.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c1e5805c92e049b4956ed01204c6647de6160ab9aefb0d67ea83ca02a1d889a"}, - {file = "llvmlite-0.38.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5559e46c79b4017c3c25edc3b9512d11adc3689b9046120c685b0905c08d48a5"}, - {file = "llvmlite-0.38.1-cp37-cp37m-win32.whl", hash = "sha256:ef9aa574eff2e15f8c47b255da0db5dab326dc7f76384c307ae35490e2d2489a"}, - {file = "llvmlite-0.38.1-cp37-cp37m-win_amd64.whl", hash = "sha256:84d5a0163c172db2b2ae561d2fc0866fbd9f716cf13f92c0d41ca4338e682672"}, - {file = "llvmlite-0.38.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a263252a68d85450110ec1f2b406c0414e49b04a4d216d31c0515ea1d59c3882"}, - {file = "llvmlite-0.38.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:de8bd61480173930f2a029673e7cd0738fbbb5171dfe490340839ad7301d4cf0"}, - {file = "llvmlite-0.38.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fbfbe546394c39db39a6898a51972aa131c8d6b0628517728b350552f58bdc19"}, - {file = "llvmlite-0.38.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c4f26c6c370e134a909ac555a671fa1376e74c69af0208f25c0979472577a9d"}, - {file = "llvmlite-0.38.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f95f455697c44d7c04ef95fdfce04629f48df08a832d0a0d9eb2363186dbb969"}, - {file = "llvmlite-0.38.1-cp38-cp38-win32.whl", hash = "sha256:41e638a71c85a9a4a33f279c4cd812bc2f84122505b1f6ab8984ec7debb8548b"}, - {file = "llvmlite-0.38.1-cp38-cp38-win_amd64.whl", hash = "sha256:5c07d63df4578f31b39b764d3b4291f70157af7f42e171a8884ae7aaf989d1f7"}, - {file = "llvmlite-0.38.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4e11bd9929dcbd55d5eb5cd7b08bf71b0097ea48cc192b69d102a90dd6e9816f"}, - {file = "llvmlite-0.38.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:edfa2c761cfa56cf76e783290d82e117f829bb691d8d90aa375505204888abac"}, - {file = "llvmlite-0.38.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e609f7312a439b53b6f622d99180c3ff6a3e1e4ceca4d18aca1c5b46f4e3664"}, - {file = "llvmlite-0.38.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f53c3448410cc84d0e1af84dbc0d60ad32779853d40bcc8b1ee3c67ebbe94b1"}, - {file = "llvmlite-0.38.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c8fac4edbadefa4dddf5dc6cca76bc2ae81df211dcd16a6638d60cc41249e56"}, - {file = "llvmlite-0.38.1-cp39-cp39-win32.whl", hash = "sha256:3d76c0fa42390bef56979ed213fbf0150c3fef36f5ea68d3d780d5d725da8c01"}, - {file = "llvmlite-0.38.1-cp39-cp39-win_amd64.whl", hash = "sha256:66462d768c30d5f648ca3361d657b434efa8b09f6cf04d6b6eae66e62e993644"}, - {file = "llvmlite-0.38.1.tar.gz", hash = "sha256:0622a86301fcf81cc50d7ed5b4bebe992c030580d413a8443b328ed4f4d82561"}, +python-versions = ">=3.8" +files = [ + {file = "llvmlite-0.40.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84ce9b1c7a59936382ffde7871978cddcda14098e5a76d961e204523e5c372fb"}, + {file = "llvmlite-0.40.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3673c53cb21c65d2ff3704962b5958e967c6fc0bd0cff772998face199e8d87b"}, + {file = "llvmlite-0.40.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bba2747cf5b4954e945c287fe310b3fcc484e2a9d1b0c273e99eb17d103bb0e6"}, + {file = "llvmlite-0.40.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbd5e82cc990e5a3e343a3bf855c26fdfe3bfae55225f00efd01c05bbda79918"}, + {file = "llvmlite-0.40.1-cp310-cp310-win32.whl", hash = "sha256:09f83ea7a54509c285f905d968184bba00fc31ebf12f2b6b1494d677bb7dde9b"}, + {file = "llvmlite-0.40.1-cp310-cp310-win_amd64.whl", hash = "sha256:7b37297f3cbd68d14a97223a30620589d98ad1890e5040c9e5fc181063f4ed49"}, + {file = "llvmlite-0.40.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a66a5bd580951751b4268f4c3bddcef92682814d6bc72f3cd3bb67f335dd7097"}, + {file = "llvmlite-0.40.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:467b43836b388eaedc5a106d76761e388dbc4674b2f2237bc477c6895b15a634"}, + {file = "llvmlite-0.40.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c23edd196bd797dc3a7860799054ea3488d2824ecabc03f9135110c2e39fcbc"}, + {file = "llvmlite-0.40.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a36d9f244b6680cb90bbca66b146dabb2972f4180c64415c96f7c8a2d8b60a36"}, + {file = "llvmlite-0.40.1-cp311-cp311-win_amd64.whl", hash = "sha256:5b3076dc4e9c107d16dc15ecb7f2faf94f7736cd2d5e9f4dc06287fd672452c1"}, + {file = "llvmlite-0.40.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4a7525db121f2e699809b539b5308228854ccab6693ecb01b52c44a2f5647e20"}, + {file = "llvmlite-0.40.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:84747289775d0874e506f907a4513db889471607db19b04de97d144047fec885"}, + {file = "llvmlite-0.40.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e35766e42acef0fe7d1c43169a8ffc327a47808fae6a067b049fe0e9bbf84dd5"}, + {file = "llvmlite-0.40.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cda71de10a1f48416309e408ea83dab5bf36058f83e13b86a2961defed265568"}, + {file = "llvmlite-0.40.1-cp38-cp38-win32.whl", hash = "sha256:96707ebad8b051bbb4fc40c65ef93b7eeee16643bd4d579a14d11578e4b7a647"}, + {file = "llvmlite-0.40.1-cp38-cp38-win_amd64.whl", hash = "sha256:e44f854dc11559795bcdeaf12303759e56213d42dabbf91a5897aa2d8b033810"}, + {file = "llvmlite-0.40.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f643d15aacd0b0b0dc8b74b693822ba3f9a53fa63bc6a178c2dba7cc88f42144"}, + {file = "llvmlite-0.40.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39a0b4d0088c01a469a5860d2e2d7a9b4e6a93c0f07eb26e71a9a872a8cadf8d"}, + {file = "llvmlite-0.40.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9329b930d699699846623054121ed105fd0823ed2180906d3b3235d361645490"}, + {file = "llvmlite-0.40.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2dbbb8424037ca287983b115a29adf37d806baf7e1bf4a67bd2cffb74e085ed"}, + {file = "llvmlite-0.40.1-cp39-cp39-win32.whl", hash = "sha256:e74e7bec3235a1e1c9ad97d897a620c5007d0ed80c32c84c1d787e7daa17e4ec"}, + {file = "llvmlite-0.40.1-cp39-cp39-win_amd64.whl", hash = "sha256:ff8f31111bb99d135ff296757dc81ab36c2dee54ed4bd429158a96da9807c316"}, + {file = "llvmlite-0.40.1.tar.gz", hash = "sha256:5cdb0d45df602099d833d50bd9e81353a5e036242d3c003c5b294fc61d1986b4"}, ] [[package]] @@ -787,111 +842,127 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" -version = "2.1.2" +version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." -optional = false +optional = true python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, - {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, - {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, - {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, - {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, - {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, - {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [[package]] name = "matplotlib" -version = "3.7.1" +version = "3.7.5" description = "Python plotting package" -optional = true +optional = false python-versions = ">=3.8" files = [ - {file = "matplotlib-3.7.1-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:95cbc13c1fc6844ab8812a525bbc237fa1470863ff3dace7352e910519e194b1"}, - {file = "matplotlib-3.7.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:08308bae9e91aca1ec6fd6dda66237eef9f6294ddb17f0d0b3c863169bf82353"}, - {file = "matplotlib-3.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:544764ba51900da4639c0f983b323d288f94f65f4024dc40ecb1542d74dc0500"}, - {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d94989191de3fcc4e002f93f7f1be5da476385dde410ddafbb70686acf00ea"}, - {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e99bc9e65901bb9a7ce5e7bb24af03675cbd7c70b30ac670aa263240635999a4"}, - {file = "matplotlib-3.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb7d248c34a341cd4c31a06fd34d64306624c8cd8d0def7abb08792a5abfd556"}, - {file = "matplotlib-3.7.1-cp310-cp310-win32.whl", hash = "sha256:ce463ce590f3825b52e9fe5c19a3c6a69fd7675a39d589e8b5fbe772272b3a24"}, - {file = "matplotlib-3.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:3d7bc90727351fb841e4d8ae620d2d86d8ed92b50473cd2b42ce9186104ecbba"}, - {file = "matplotlib-3.7.1-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:770a205966d641627fd5cf9d3cb4b6280a716522cd36b8b284a8eb1581310f61"}, - {file = "matplotlib-3.7.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f67bfdb83a8232cb7a92b869f9355d677bce24485c460b19d01970b64b2ed476"}, - {file = "matplotlib-3.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2bf092f9210e105f414a043b92af583c98f50050559616930d884387d0772aba"}, - {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89768d84187f31717349c6bfadc0e0d8c321e8eb34522acec8a67b1236a66332"}, - {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83111e6388dec67822e2534e13b243cc644c7494a4bb60584edbff91585a83c6"}, - {file = "matplotlib-3.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a867bf73a7eb808ef2afbca03bcdb785dae09595fbe550e1bab0cd023eba3de0"}, - {file = "matplotlib-3.7.1-cp311-cp311-win32.whl", hash = "sha256:fbdeeb58c0cf0595efe89c05c224e0a502d1aa6a8696e68a73c3efc6bc354304"}, - {file = "matplotlib-3.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:c0bd19c72ae53e6ab979f0ac6a3fafceb02d2ecafa023c5cca47acd934d10be7"}, - {file = "matplotlib-3.7.1-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:6eb88d87cb2c49af00d3bbc33a003f89fd9f78d318848da029383bfc08ecfbfb"}, - {file = "matplotlib-3.7.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:cf0e4f727534b7b1457898c4f4ae838af1ef87c359b76dcd5330fa31893a3ac7"}, - {file = "matplotlib-3.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:46a561d23b91f30bccfd25429c3c706afe7d73a5cc64ef2dfaf2b2ac47c1a5dc"}, - {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8704726d33e9aa8a6d5215044b8d00804561971163563e6e6591f9dcf64340cc"}, - {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4cf327e98ecf08fcbb82685acaf1939d3338548620ab8dfa02828706402c34de"}, - {file = "matplotlib-3.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:617f14ae9d53292ece33f45cba8503494ee199a75b44de7717964f70637a36aa"}, - {file = "matplotlib-3.7.1-cp38-cp38-win32.whl", hash = "sha256:7c9a4b2da6fac77bcc41b1ea95fadb314e92508bf5493ceff058e727e7ecf5b0"}, - {file = "matplotlib-3.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:14645aad967684e92fc349493fa10c08a6da514b3d03a5931a1bac26e6792bd1"}, - {file = "matplotlib-3.7.1-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:81a6b377ea444336538638d31fdb39af6be1a043ca5e343fe18d0f17e098770b"}, - {file = "matplotlib-3.7.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:28506a03bd7f3fe59cd3cd4ceb2a8d8a2b1db41afede01f66c42561b9be7b4b7"}, - {file = "matplotlib-3.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8c587963b85ce41e0a8af53b9b2de8dddbf5ece4c34553f7bd9d066148dc719c"}, - {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8bf26ade3ff0f27668989d98c8435ce9327d24cffb7f07d24ef609e33d582439"}, - {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:def58098f96a05f90af7e92fd127d21a287068202aa43b2a93476170ebd99e87"}, - {file = "matplotlib-3.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f883a22a56a84dba3b588696a2b8a1ab0d2c3d41be53264115c71b0a942d8fdb"}, - {file = "matplotlib-3.7.1-cp39-cp39-win32.whl", hash = "sha256:4f99e1b234c30c1e9714610eb0c6d2f11809c9c78c984a613ae539ea2ad2eb4b"}, - {file = "matplotlib-3.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:3ba2af245e36990facf67fde840a760128ddd71210b2ab6406e640188d69d136"}, - {file = "matplotlib-3.7.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3032884084f541163f295db8a6536e0abb0db464008fadca6c98aaf84ccf4717"}, - {file = "matplotlib-3.7.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a2cb34336110e0ed8bb4f650e817eed61fa064acbefeb3591f1b33e3a84fd96"}, - {file = "matplotlib-3.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b867e2f952ed592237a1828f027d332d8ee219ad722345b79a001f49df0936eb"}, - {file = "matplotlib-3.7.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:57bfb8c8ea253be947ccb2bc2d1bb3862c2bccc662ad1b4626e1f5e004557042"}, - {file = "matplotlib-3.7.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:438196cdf5dc8d39b50a45cb6e3f6274edbcf2254f85fa9b895bf85851c3a613"}, - {file = "matplotlib-3.7.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:21e9cff1a58d42e74d01153360de92b326708fb205250150018a52c70f43c290"}, - {file = "matplotlib-3.7.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75d4725d70b7c03e082bbb8a34639ede17f333d7247f56caceb3801cb6ff703d"}, - {file = "matplotlib-3.7.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:97cc368a7268141afb5690760921765ed34867ffb9655dd325ed207af85c7529"}, - {file = "matplotlib-3.7.1.tar.gz", hash = "sha256:7b73305f25eab4541bd7ee0b96d87e53ae9c9f1823be5659b806cd85786fe882"}, + {file = "matplotlib-3.7.5-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:4a87b69cb1cb20943010f63feb0b2901c17a3b435f75349fd9865713bfa63925"}, + {file = "matplotlib-3.7.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d3ce45010fefb028359accebb852ca0c21bd77ec0f281952831d235228f15810"}, + {file = "matplotlib-3.7.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbea1e762b28400393d71be1a02144aa16692a3c4c676ba0178ce83fc2928fdd"}, + {file = "matplotlib-3.7.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec0e1adc0ad70ba8227e957551e25a9d2995e319c29f94a97575bb90fa1d4469"}, + {file = "matplotlib-3.7.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6738c89a635ced486c8a20e20111d33f6398a9cbebce1ced59c211e12cd61455"}, + {file = "matplotlib-3.7.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1210b7919b4ed94b5573870f316bca26de3e3b07ffdb563e79327dc0e6bba515"}, + {file = "matplotlib-3.7.5-cp310-cp310-win32.whl", hash = "sha256:068ebcc59c072781d9dcdb82f0d3f1458271c2de7ca9c78f5bd672141091e9e1"}, + {file = "matplotlib-3.7.5-cp310-cp310-win_amd64.whl", hash = "sha256:f098ffbaab9df1e3ef04e5a5586a1e6b1791380698e84938d8640961c79b1fc0"}, + {file = "matplotlib-3.7.5-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:f65342c147572673f02a4abec2d5a23ad9c3898167df9b47c149f32ce61ca078"}, + {file = "matplotlib-3.7.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4ddf7fc0e0dc553891a117aa083039088d8a07686d4c93fb8a810adca68810af"}, + {file = "matplotlib-3.7.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0ccb830fc29442360d91be48527809f23a5dcaee8da5f4d9b2d5b867c1b087b8"}, + {file = "matplotlib-3.7.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efc6bb28178e844d1f408dd4d6341ee8a2e906fc9e0fa3dae497da4e0cab775d"}, + {file = "matplotlib-3.7.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b15c4c2d374f249f324f46e883340d494c01768dd5287f8bc00b65b625ab56c"}, + {file = "matplotlib-3.7.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d028555421912307845e59e3de328260b26d055c5dac9b182cc9783854e98fb"}, + {file = "matplotlib-3.7.5-cp311-cp311-win32.whl", hash = "sha256:fe184b4625b4052fa88ef350b815559dd90cc6cc8e97b62f966e1ca84074aafa"}, + {file = "matplotlib-3.7.5-cp311-cp311-win_amd64.whl", hash = "sha256:084f1f0f2f1010868c6f1f50b4e1c6f2fb201c58475494f1e5b66fed66093647"}, + {file = "matplotlib-3.7.5-cp312-cp312-macosx_10_12_universal2.whl", hash = "sha256:34bceb9d8ddb142055ff27cd7135f539f2f01be2ce0bafbace4117abe58f8fe4"}, + {file = "matplotlib-3.7.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c5a2134162273eb8cdfd320ae907bf84d171de948e62180fa372a3ca7cf0f433"}, + {file = "matplotlib-3.7.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:039ad54683a814002ff37bf7981aa1faa40b91f4ff84149beb53d1eb64617980"}, + {file = "matplotlib-3.7.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d742ccd1b09e863b4ca58291728db645b51dab343eebb08d5d4b31b308296ce"}, + {file = "matplotlib-3.7.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:743b1c488ca6a2bc7f56079d282e44d236bf375968bfd1b7ba701fd4d0fa32d6"}, + {file = "matplotlib-3.7.5-cp312-cp312-win_amd64.whl", hash = "sha256:fbf730fca3e1f23713bc1fae0a57db386e39dc81ea57dc305c67f628c1d7a342"}, + {file = "matplotlib-3.7.5-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:cfff9b838531698ee40e40ea1a8a9dc2c01edb400b27d38de6ba44c1f9a8e3d2"}, + {file = "matplotlib-3.7.5-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:1dbcca4508bca7847fe2d64a05b237a3dcaec1f959aedb756d5b1c67b770c5ee"}, + {file = "matplotlib-3.7.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4cdf4ef46c2a1609a50411b66940b31778db1e4b73d4ecc2eaa40bd588979b13"}, + {file = "matplotlib-3.7.5-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:167200ccfefd1674b60e957186dfd9baf58b324562ad1a28e5d0a6b3bea77905"}, + {file = "matplotlib-3.7.5-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:53e64522934df6e1818b25fd48cf3b645b11740d78e6ef765fbb5fa5ce080d02"}, + {file = "matplotlib-3.7.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3e3bc79b2d7d615067bd010caff9243ead1fc95cf735c16e4b2583173f717eb"}, + {file = "matplotlib-3.7.5-cp38-cp38-win32.whl", hash = "sha256:6b641b48c6819726ed47c55835cdd330e53747d4efff574109fd79b2d8a13748"}, + {file = "matplotlib-3.7.5-cp38-cp38-win_amd64.whl", hash = "sha256:f0b60993ed3488b4532ec6b697059897891927cbfc2b8d458a891b60ec03d9d7"}, + {file = "matplotlib-3.7.5-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:090964d0afaff9c90e4d8de7836757e72ecfb252fb02884016d809239f715651"}, + {file = "matplotlib-3.7.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:9fc6fcfbc55cd719bc0bfa60bde248eb68cf43876d4c22864603bdd23962ba25"}, + {file = "matplotlib-3.7.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7cc3078b019bb863752b8b60e8b269423000f1603cb2299608231996bd9d54"}, + {file = "matplotlib-3.7.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e4e9a868e8163abaaa8259842d85f949a919e1ead17644fb77a60427c90473c"}, + {file = "matplotlib-3.7.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fa7ebc995a7d747dacf0a717d0eb3aa0f0c6a0e9ea88b0194d3a3cd241a1500f"}, + {file = "matplotlib-3.7.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3785bfd83b05fc0e0c2ae4c4a90034fe693ef96c679634756c50fe6efcc09856"}, + {file = "matplotlib-3.7.5-cp39-cp39-win32.whl", hash = "sha256:29b058738c104d0ca8806395f1c9089dfe4d4f0f78ea765c6c704469f3fffc81"}, + {file = "matplotlib-3.7.5-cp39-cp39-win_amd64.whl", hash = "sha256:fd4028d570fa4b31b7b165d4a685942ae9cdc669f33741e388c01857d9723eab"}, + {file = "matplotlib-3.7.5-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2a9a3f4d6a7f88a62a6a18c7e6a84aedcaf4faf0708b4ca46d87b19f1b526f88"}, + {file = "matplotlib-3.7.5-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9b3fd853d4a7f008a938df909b96db0b454225f935d3917520305b90680579c"}, + {file = "matplotlib-3.7.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0ad550da9f160737d7890217c5eeed4337d07e83ca1b2ca6535078f354e7675"}, + {file = "matplotlib-3.7.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:20da7924a08306a861b3f2d1da0d1aa9a6678e480cf8eacffe18b565af2813e7"}, + {file = "matplotlib-3.7.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b45c9798ea6bb920cb77eb7306409756a7fab9db9b463e462618e0559aecb30e"}, + {file = "matplotlib-3.7.5-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a99866267da1e561c7776fe12bf4442174b79aac1a47bd7e627c7e4d077ebd83"}, + {file = "matplotlib-3.7.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b6aa62adb6c268fc87d80f963aca39c64615c31830b02697743c95590ce3fbb"}, + {file = "matplotlib-3.7.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e530ab6a0afd082d2e9c17eb1eb064a63c5b09bb607b2b74fa41adbe3e162286"}, + {file = "matplotlib-3.7.5.tar.gz", hash = "sha256:1e5c971558ebc811aa07f54c7b7c677d78aa518ef4c390e14673a09e0860184a"}, ] [package.dependencies] @@ -900,7 +971,7 @@ cycler = ">=0.10" fonttools = ">=4.22.0" importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""} kiwisolver = ">=1.0.1" -numpy = ">=1.20" +numpy = ">=1.20,<2" packaging = ">=20.0" pillow = ">=6.2.0" pyparsing = ">=2.3.1" @@ -1000,115 +1071,121 @@ setuptools = "*" [[package]] name = "numba" -version = "0.55.2" +version = "0.57.1" description = "compiling Python code using LLVM" optional = false -python-versions = ">=3.7,<3.11" -files = [ - {file = "numba-0.55.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:dd05f7c0ce64b6977596aa4e5a44747c6ef414d7989da1c7672337c54381a5ef"}, - {file = "numba-0.55.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e36232eccd172c583b1f021c5c48744c087ae6fc9dc5c5f0dd2cb2286e517bf8"}, - {file = "numba-0.55.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:25410557d0deb1d97397b71e142a36772133986a7dd4fe2935786e2dd149245f"}, - {file = "numba-0.55.2-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:676c081162cc9403706071c1d1d42e479c0741551ab28096ba13859a2e3e9b80"}, - {file = "numba-0.55.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2665ef28e900b3a55bf370daa81c12ebc64cd434116accd60c38a95a159a3182"}, - {file = "numba-0.55.2-cp310-cp310-win32.whl", hash = "sha256:d7ac9ea5feef9536ab8bfbbb3ded1a0617ea8794d7547800d535b7857800f996"}, - {file = "numba-0.55.2-cp310-cp310-win_amd64.whl", hash = "sha256:29b89a68af162acf87adeb8fbf01f6bb1effae4711b28146f95108d82e905624"}, - {file = "numba-0.55.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:6e0f9b5d1c8ea1bdef39b0ad921a9bbf0cc4a88e76d722d756c68f1653787c35"}, - {file = "numba-0.55.2-cp37-cp37m-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:135fb7694928f9f57b4ff5b1be58f20f4771fedd1680636a9affdead96051959"}, - {file = "numba-0.55.2-cp37-cp37m-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:de1f93bd7e2d431451aec20a52ac651a020e98a4ba46797fad860bba338a7e64"}, - {file = "numba-0.55.2-cp37-cp37m-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3eaf53e73e700370163e58257257299ac0d46fea4f244bf5476e4635bc31d808"}, - {file = "numba-0.55.2-cp37-cp37m-win32.whl", hash = "sha256:da4485e0f0b9562f39c78887149b33d13d787aa696553c9257b95575122905ed"}, - {file = "numba-0.55.2-cp37-cp37m-win_amd64.whl", hash = "sha256:5559c6684bf6cce7a22c656d8fef3e7c38ff5fec5153abef5955f6f7cae9f102"}, - {file = "numba-0.55.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:a85779adc5234f7857615d1bd2c7b514314521f9f0163c33017707ed9816e6e6"}, - {file = "numba-0.55.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:16a52a0641c342b09b39f6762dcbe3846e44aa9baaaf4703b2ca42a3aee7346f"}, - {file = "numba-0.55.2-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:46715180f87d5a1f3e4077d207ade66c96fc01159f5b7d49cee2d6ffb9e6539f"}, - {file = "numba-0.55.2-cp38-cp38-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d1c3cef3289fefb5673ceae32024ab5a8a08d4f4380bcb8348d01f1ba570ccff"}, - {file = "numba-0.55.2-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:68bb33eaef1d6155fc1ae4fa6c915b8a42e5052c89a58742254eaad072eab118"}, - {file = "numba-0.55.2-cp38-cp38-win32.whl", hash = "sha256:dfddd633141608a09cbce275fb9fe7aa514918625ace20b0e587898a2d93c030"}, - {file = "numba-0.55.2-cp38-cp38-win_amd64.whl", hash = "sha256:a669212aa66ffee4ad778016ac3819add33f9bcb96b4c384d3099531dd175085"}, - {file = "numba-0.55.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:dcde1a1a3a430fb5f83c7e095b0b6ac7adb5595f50a3ee05babb2964f31613c4"}, - {file = "numba-0.55.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:69b2e823efa40d32b259f5c094476dde2226b92032f17015d8cd7c10472654ce"}, - {file = "numba-0.55.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:20de0139d2267c8f0e2470d4f88540446cd1bf40de0f29f31b7ab9bf25d49b45"}, - {file = "numba-0.55.2-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:09ff4d690abb05ffbb8a29a96d1cf35b46887a26796d3670de104beeec73d639"}, - {file = "numba-0.55.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1105449247f338e49d63eb04a4aaa5c440bb5435df00f718c8e6e7afad841bb0"}, - {file = "numba-0.55.2-cp39-cp39-win32.whl", hash = "sha256:32649584144c35ced239937ab2c416ab22bbc1490ef8d90609c30fff9f6aa1b8"}, - {file = "numba-0.55.2-cp39-cp39-win_amd64.whl", hash = "sha256:8d5760a1e6a48d98d6b9cf774e4d2a64813d981cca60d7b7356af61195a6ca17"}, - {file = "numba-0.55.2.tar.gz", hash = "sha256:e428d9e11d9ba592849ccc9f7a009003eb7d30612007e365afe743ce7118c6f4"}, +python-versions = ">=3.8" +files = [ + {file = "numba-0.57.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db8268eb5093cae2288942a8cbd69c9352f6fe6e0bfa0a9a27679436f92e4248"}, + {file = "numba-0.57.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:643cb09a9ba9e1bd8b060e910aeca455e9442361e80fce97690795ff9840e681"}, + {file = "numba-0.57.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e9fab973d9e82c9f8449f75994a898daaaf821d84f06fbb0b9de2293dd9306"}, + {file = "numba-0.57.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c0602e4f896e6a6d844517c3ab434bc978e7698a22a733cc8124465898c28fa8"}, + {file = "numba-0.57.1-cp310-cp310-win32.whl", hash = "sha256:3d6483c27520d16cf5d122868b79cad79e48056ecb721b52d70c126bed65431e"}, + {file = "numba-0.57.1-cp310-cp310-win_amd64.whl", hash = "sha256:a32ee263649aa3c3587b833d6311305379529570e6c20deb0c6f4fb5bc7020db"}, + {file = "numba-0.57.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c078f84b5529a7fdb8413bb33d5100f11ec7b44aa705857d9eb4e54a54ff505"}, + {file = "numba-0.57.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e447c4634d1cc99ab50d4faa68f680f1d88b06a2a05acf134aa6fcc0342adeca"}, + {file = "numba-0.57.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4838edef2df5f056cb8974670f3d66562e751040c448eb0b67c7e2fec1726649"}, + {file = "numba-0.57.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9b17fbe4a69dcd9a7cd49916b6463cd9a82af5f84911feeb40793b8bce00dfa7"}, + {file = "numba-0.57.1-cp311-cp311-win_amd64.whl", hash = "sha256:93df62304ada9b351818ba19b1cfbddaf72cd89348e81474326ca0b23bf0bae1"}, + {file = "numba-0.57.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8e00ca63c5d0ad2beeb78d77f087b3a88c45ea9b97e7622ab2ec411a868420ee"}, + {file = "numba-0.57.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ff66d5b022af6c7d81ddbefa87768e78ed4f834ab2da6ca2fd0d60a9e69b94f5"}, + {file = "numba-0.57.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:60ec56386076e9eed106a87c96626d5686fbb16293b9834f0849cf78c9491779"}, + {file = "numba-0.57.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c057ccedca95df23802b6ccad86bb318be624af45b5a38bb8412882be57a681"}, + {file = "numba-0.57.1-cp38-cp38-win32.whl", hash = "sha256:5a82bf37444039c732485c072fda21a361790ed990f88db57fd6941cd5e5d307"}, + {file = "numba-0.57.1-cp38-cp38-win_amd64.whl", hash = "sha256:9bcc36478773ce838f38afd9a4dfafc328d4ffb1915381353d657da7f6473282"}, + {file = "numba-0.57.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae50c8c90c2ce8057f9618b589223e13faa8cbc037d8f15b4aad95a2c33a0582"}, + {file = "numba-0.57.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9a1b2b69448e510d672ff9a6b18d2db9355241d93c6a77677baa14bec67dc2a0"}, + {file = "numba-0.57.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3cf78d74ad9d289fbc1e5b1c9f2680fca7a788311eb620581893ab347ec37a7e"}, + {file = "numba-0.57.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f47dd214adc5dcd040fe9ad2adbd2192133c9075d2189ce1b3d5f9d72863ef05"}, + {file = "numba-0.57.1-cp39-cp39-win32.whl", hash = "sha256:a3eac19529956185677acb7f01864919761bfffbb9ae04bbbe5e84bbc06cfc2b"}, + {file = "numba-0.57.1-cp39-cp39-win_amd64.whl", hash = "sha256:9587ba1bf5f3035575e45562ada17737535c6d612df751e811d702693a72d95e"}, + {file = "numba-0.57.1.tar.gz", hash = "sha256:33c0500170d213e66d90558ad6aca57d3e03e97bb11da82e6d87ab793648cb17"}, ] [package.dependencies] -llvmlite = ">=0.38.0rc1,<0.39" -numpy = ">=1.18,<1.23" -setuptools = "*" +importlib-metadata = {version = "*", markers = "python_version < \"3.9\""} +llvmlite = "==0.40.*" +numpy = ">=1.21,<1.25" [[package]] name = "numpy" -version = "1.22.4" -description = "NumPy is the fundamental package for array computing with Python." +version = "1.24.4" +description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.8" files = [ - {file = "numpy-1.22.4-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:ba9ead61dfb5d971d77b6c131a9dbee62294a932bf6a356e48c75ae684e635b3"}, - {file = "numpy-1.22.4-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:1ce7ab2053e36c0a71e7a13a7475bd3b1f54750b4b433adc96313e127b870887"}, - {file = "numpy-1.22.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7228ad13744f63575b3a972d7ee4fd61815b2879998e70930d4ccf9ec721dce0"}, - {file = "numpy-1.22.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43a8ca7391b626b4c4fe20aefe79fec683279e31e7c79716863b4b25021e0e74"}, - {file = "numpy-1.22.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a911e317e8c826ea632205e63ed8507e0dc877dcdc49744584dfc363df9ca08c"}, - {file = "numpy-1.22.4-cp310-cp310-win32.whl", hash = "sha256:9ce7df0abeabe7fbd8ccbf343dc0db72f68549856b863ae3dd580255d009648e"}, - {file = "numpy-1.22.4-cp310-cp310-win_amd64.whl", hash = "sha256:3e1ffa4748168e1cc8d3cde93f006fe92b5421396221a02f2274aab6ac83b077"}, - {file = "numpy-1.22.4-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:59d55e634968b8f77d3fd674a3cf0b96e85147cd6556ec64ade018f27e9479e1"}, - {file = "numpy-1.22.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c1d937820db6e43bec43e8d016b9b3165dcb42892ea9f106c70fb13d430ffe72"}, - {file = "numpy-1.22.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4c5d5eb2ec8da0b4f50c9a843393971f31f1d60be87e0fb0917a49133d257d6"}, - {file = "numpy-1.22.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64f56fc53a2d18b1924abd15745e30d82a5782b2cab3429aceecc6875bd5add0"}, - {file = "numpy-1.22.4-cp38-cp38-win32.whl", hash = "sha256:fb7a980c81dd932381f8228a426df8aeb70d59bbcda2af075b627bbc50207cba"}, - {file = "numpy-1.22.4-cp38-cp38-win_amd64.whl", hash = "sha256:e96d7f3096a36c8754207ab89d4b3282ba7b49ea140e4973591852c77d09eb76"}, - {file = "numpy-1.22.4-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:4c6036521f11a731ce0648f10c18ae66d7143865f19f7299943c985cdc95afb5"}, - {file = "numpy-1.22.4-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:b89bf9b94b3d624e7bb480344e91f68c1c6c75f026ed6755955117de00917a7c"}, - {file = "numpy-1.22.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2d487e06ecbf1dc2f18e7efce82ded4f705f4bd0cd02677ffccfb39e5c284c7e"}, - {file = "numpy-1.22.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3eb268dbd5cfaffd9448113539e44e2dd1c5ca9ce25576f7c04a5453edc26fa"}, - {file = "numpy-1.22.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37431a77ceb9307c28382c9773da9f306435135fae6b80b62a11c53cfedd8802"}, - {file = "numpy-1.22.4-cp39-cp39-win32.whl", hash = "sha256:cc7f00008eb7d3f2489fca6f334ec19ca63e31371be28fd5dad955b16ec285bd"}, - {file = "numpy-1.22.4-cp39-cp39-win_amd64.whl", hash = "sha256:f0725df166cf4785c0bc4cbfb320203182b1ecd30fee6e541c8752a92df6aa32"}, - {file = "numpy-1.22.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0791fbd1e43bf74b3502133207e378901272f3c156c4df4954cad833b1380207"}, - {file = "numpy-1.22.4.zip", hash = "sha256:425b390e4619f58d8526b3dcf656dde069133ae5c240229821f01b5f44ea07af"}, + {file = "numpy-1.24.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64"}, + {file = "numpy-1.24.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4"}, + {file = "numpy-1.24.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6"}, + {file = "numpy-1.24.4-cp310-cp310-win32.whl", hash = "sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc"}, + {file = "numpy-1.24.4-cp310-cp310-win_amd64.whl", hash = "sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810"}, + {file = "numpy-1.24.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7"}, + {file = "numpy-1.24.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5"}, + {file = "numpy-1.24.4-cp311-cp311-win32.whl", hash = "sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d"}, + {file = "numpy-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61"}, + {file = "numpy-1.24.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e"}, + {file = "numpy-1.24.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc"}, + {file = "numpy-1.24.4-cp38-cp38-win32.whl", hash = "sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2"}, + {file = "numpy-1.24.4-cp38-cp38-win_amd64.whl", hash = "sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400"}, + {file = "numpy-1.24.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9"}, + {file = "numpy-1.24.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d"}, + {file = "numpy-1.24.4-cp39-cp39-win32.whl", hash = "sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835"}, + {file = "numpy-1.24.4-cp39-cp39-win_amd64.whl", hash = "sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a"}, + {file = "numpy-1.24.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2"}, + {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, ] [[package]] name = "numpydoc" -version = "1.5.0" +version = "1.6.0" description = "Sphinx extension to support docstrings in Numpy format" optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "numpydoc-1.5.0-py3-none-any.whl", hash = "sha256:c997759fb6fc32662801cece76491eedbc0ec619b514932ffd2b270ae89c07f9"}, - {file = "numpydoc-1.5.0.tar.gz", hash = "sha256:b0db7b75a32367a0e25c23b397842c65e344a1206524d16c8069f0a1c91b5f4c"}, + {file = "numpydoc-1.6.0-py3-none-any.whl", hash = "sha256:b6ddaa654a52bdf967763c1e773be41f1c3ae3da39ee0de973f2680048acafaa"}, + {file = "numpydoc-1.6.0.tar.gz", hash = "sha256:ae7a5380f0a06373c3afe16ccd15bd79bc6b07f2704cbc6f1e7ecc94b4f5fc0d"}, ] [package.dependencies] Jinja2 = ">=2.10" -sphinx = ">=4.2" +sphinx = ">=5" +tabulate = ">=0.8.10" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [package.extras] -testing = ["matplotlib", "pytest", "pytest-cov"] +developer = ["pre-commit (>=3.3)", "tomli"] +doc = ["matplotlib (>=3.5)", "numpy (>=1.22)", "pydata-sphinx-theme (>=0.13.3)", "sphinx (>=7)"] +test = ["matplotlib", "pytest", "pytest-cov"] [[package]] name = "packaging" -version = "23.1" +version = "24.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] name = "pathspec" -version = "0.11.1" +version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, - {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] [[package]] @@ -1198,34 +1275,55 @@ xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "3.5.1" +version = "4.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "platformdirs-3.5.1-py3-none-any.whl", hash = "sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5"}, - {file = "platformdirs-3.5.1.tar.gz", hash = "sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f"}, + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, ] [package.extras] -docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.2.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] [[package]] name = "pluggy" -version = "1.0.0" +version = "1.4.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pooch" +version = "1.8.1" +description = "\"Pooch manages your Python library's sample data files: it automatically downloads and stores them in a local directory, with support for versioning and corruption checks.\"" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pooch-1.8.1-py3-none-any.whl", hash = "sha256:6b56611ac320c239faece1ac51a60b25796792599ce5c0b1bb87bf01df55e0a9"}, + {file = "pooch-1.8.1.tar.gz", hash = "sha256:27ef63097dd9a6e4f9d2694f5cfbf2f0a5defa44fccafec08d601e731d746270"}, +] + +[package.dependencies] +packaging = ">=20.0" +platformdirs = ">=2.5.0" +requests = ">=2.19.0" + +[package.extras] +progress = ["tqdm (>=4.41.0,<5.0.0)"] +sftp = ["paramiko (>=2.7.0)"] +xxhash = ["xxhash (>=1.4.3)"] + [[package]] name = "pre-commit" version = "2.21.0" @@ -1268,13 +1366,13 @@ files = [ [[package]] name = "pydata-sphinx-theme" -version = "0.13.3" +version = "0.14.4" description = "Bootstrap-based Sphinx theme from the PyData community" optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pydata_sphinx_theme-0.13.3-py3-none-any.whl", hash = "sha256:bf41ca6c1c6216e929e28834e404bfc90e080b51915bbe7563b5e6fda70354f0"}, - {file = "pydata_sphinx_theme-0.13.3.tar.gz", hash = "sha256:827f16b065c4fd97e847c11c108bf632b7f2ff53a3bca3272f63f3f3ff782ecc"}, + {file = "pydata_sphinx_theme-0.14.4-py3-none-any.whl", hash = "sha256:ac15201f4c2e2e7042b0cad8b30251433c1f92be762ddcefdb4ae68811d918d9"}, + {file = "pydata_sphinx_theme-0.14.4.tar.gz", hash = "sha256:f5d7a2cb7a98e35b9b49d3b02cec373ad28958c2ed5c9b1ffe6aff6c56e9de5b"}, ] [package.dependencies] @@ -1284,13 +1382,14 @@ beautifulsoup4 = "*" docutils = "!=0.17.0" packaging = "*" pygments = ">=2.7" -sphinx = ">=4.2" +sphinx = ">=5.0" typing-extensions = "*" [package.extras] +a11y = ["pytest-playwright"] dev = ["nox", "pre-commit", "pydata-sphinx-theme[doc,test]", "pyyaml"] -doc = ["ablog (>=0.11.0rc2)", "colorama", "ipyleaflet", "jupyter_sphinx", "linkify-it-py", "matplotlib", "myst-nb", "nbsphinx", "numpy", "numpydoc", "pandas", "plotly", "rich", "sphinx-copybutton", "sphinx-design", "sphinx-favicon (>=1.0.1)", "sphinx-sitemap", "sphinx-togglebutton", "sphinxcontrib-youtube", "sphinxext-rediraffe", "xarray"] -test = ["codecov", "pytest", "pytest-cov", "pytest-regressions"] +doc = ["ablog (>=0.11.0rc2)", "colorama", "ipykernel", "ipyleaflet", "jupyter_sphinx", "jupyterlite-sphinx", "linkify-it-py", "matplotlib", "myst-parser", "nbsphinx", "numpy", "numpydoc", "pandas", "plotly", "rich", "sphinx-autoapi (>=3.0.0)", "sphinx-copybutton", "sphinx-design", "sphinx-favicon (>=1.0.1)", "sphinx-sitemap", "sphinx-togglebutton", "sphinxcontrib-youtube (<1.4)", "sphinxext-rediraffe", "xarray"] +test = ["pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "pyflakes" @@ -1305,27 +1404,28 @@ files = [ [[package]] name = "pygments" -version = "2.15.1" +version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." optional = true python-versions = ">=3.7" files = [ - {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"}, - {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"}, + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [package.extras] plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyparsing" -version = "3.0.9" +version = "3.1.2" description = "pyparsing module - Classes and methods to define and execute parsing grammars" -optional = true +optional = false python-versions = ">=3.6.8" files = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, + {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, + {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, ] [package.extras] @@ -1333,13 +1433,13 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.3.1" +version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.3.1-py3-none-any.whl", hash = "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362"}, - {file = "pytest-7.3.1.tar.gz", hash = "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] @@ -1351,7 +1451,7 @@ pluggy = ">=0.12,<2.0" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" @@ -1389,27 +1489,30 @@ pytest-metadata = "*" [[package]] name = "pytest-metadata" -version = "2.0.4" +version = "3.1.1" description = "pytest plugin for test session metadata" optional = false -python-versions = ">=3.7,<4.0" +python-versions = ">=3.8" files = [ - {file = "pytest_metadata-2.0.4-py3-none-any.whl", hash = "sha256:acb739f89fabb3d798c099e9e0c035003062367a441910aaaf2281bc1972ee14"}, - {file = "pytest_metadata-2.0.4.tar.gz", hash = "sha256:fcc653f65fe3035b478820b5284fbf0f52803622ee3f60a2faed7a7d3ba1f41e"}, + {file = "pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b"}, + {file = "pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8"}, ] [package.dependencies] -pytest = ">=3.0.0,<8.0.0" +pytest = ">=7.0.0" + +[package.extras] +test = ["black (>=22.1.0)", "flake8 (>=4.0.1)", "pre-commit (>=2.17.0)", "tox (>=3.24.5)"] [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" -optional = true +optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] @@ -1417,62 +1520,88 @@ six = ">=1.5" [[package]] name = "pytz" -version = "2023.3" +version = "2024.1" description = "World timezone definitions, modern and historical" optional = true python-versions = "*" files = [ - {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"}, - {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, ] +[[package]] +name = "pyvista" +version = "0.39.1" +description = "Easier Pythonic interface to VTK" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyvista-0.39.1-py3-none-any.whl", hash = "sha256:d4c9c314d0a1151a950a7dba67505e3148385ae0f7974f614252700f48cc01f4"}, + {file = "pyvista-0.39.1.tar.gz", hash = "sha256:0023b7f6dc8a2ca4f79ec5c20a025b44910c1d1fbae9e00e13af73f3fda5d2d6"}, +] + +[package.dependencies] +matplotlib = ">=3.0.1" +numpy = "*" +pillow = "*" +pooch = "*" +scooby = ">=0.5.1" +vtk = "*" + +[package.extras] +all = ["cmocean", "colorcet", "imageio", "ipyvtklink", "ipywidgets", "jupyter-server-proxy", "meshio (>=5.2)", "nest-asyncio", "panel", "pythreejs", "trame (>=2.2.6)", "trame-client (>=2.4.2)", "trame-server (>=2.8.0)", "trame-vtk (>=2.4.0)"] +colormaps = ["cmocean", "colorcet"] +io = ["imageio", "meshio (>=5.2)"] +jupyter = ["ipyvtklink", "ipywidgets", "jupyter-server-proxy", "nest-asyncio", "panel", "pythreejs", "trame (>=2.2.6)", "trame-client (>=2.4.2)", "trame-server (>=2.8.0)", "trame-vtk (>=2.4.0)"] +trame = ["trame (>=2.2.6)", "trame-client (>=2.4.2)", "trame-server (>=2.8.0)", "trame-vtk (>=2.4.0)"] + [[package]] name = "pyyaml" -version = "6.0" +version = "6.0.1" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.6" files = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, - {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, - {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] [[package]] @@ -1545,27 +1674,41 @@ dev = ["click", "doit (>=0.36.0)", "flake8", "mypy", "pycodestyle", "pydevtool", doc = ["matplotlib (>2)", "numpydoc", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"] test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +[[package]] +name = "scooby" +version = "0.9.2" +description = "A Great Dane turned Python environment detective" +optional = false +python-versions = ">=3.8" +files = [ + {file = "scooby-0.9.2-py3-none-any.whl", hash = "sha256:3cbc59de9febf8c8ba1e01bc7d08b4eca18ece3212d38b08a6f45188f88c8ea9"}, + {file = "scooby-0.9.2.tar.gz", hash = "sha256:28df643bb7c2087547b2e2220070e2f89e815ddbc515fbc28dd5df2b0a14293e"}, +] + +[package.extras] +cpu = ["mkl", "psutil"] + [[package]] name = "setuptools" -version = "67.7.2" +version = "69.1.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "setuptools-67.7.2-py3-none-any.whl", hash = "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b"}, - {file = "setuptools-67.7.2.tar.gz", hash = "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990"}, + {file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"}, + {file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -optional = true +optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, @@ -1585,13 +1728,13 @@ files = [ [[package]] name = "soupsieve" -version = "2.4.1" +version = "2.5" description = "A modern CSS selector implementation for Beautiful Soup." optional = true -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "soupsieve-2.4.1-py3-none-any.whl", hash = "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8"}, - {file = "soupsieve-2.4.1.tar.gz", hash = "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"}, + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, ] [[package]] @@ -1757,6 +1900,20 @@ files = [ lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] +[[package]] +name = "tabulate" +version = "0.9.0" +description = "Pretty-print tabular data" +optional = true +python-versions = ">=3.7" +files = [ + {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, + {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, +] + +[package.extras] +widechars = ["wcwidth"] + [[package]] name = "tomli" version = "1.2.3" @@ -1770,92 +1927,133 @@ files = [ [[package]] name = "tqdm" -version = "4.65.0" +version = "4.66.2" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.65.0-py3-none-any.whl", hash = "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671"}, - {file = "tqdm-4.65.0.tar.gz", hash = "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5"}, + {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"}, + {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -dev = ["py-make (>=0.1.0)", "twine", "wheel"] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] [[package]] name = "typing-extensions" -version = "4.5.0" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.10.0" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, ] [[package]] name = "urllib3" -version = "2.0.7" +version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, - {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.23.0" +version = "20.25.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.23.0-py3-none-any.whl", hash = "sha256:6abec7670e5802a528357fdc75b26b9f57d5d92f29c5462ba0fbe45feacc685e"}, - {file = "virtualenv-20.23.0.tar.gz", hash = "sha256:a85caa554ced0c0afbd0d638e7e2d7b5f92d23478d05d17a76daeac8f279f924"}, + {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, + {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, ] [package.dependencies] -distlib = ">=0.3.6,<1" -filelock = ">=3.11,<4" -platformdirs = ">=3.2,<4" +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" [package.extras] -docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=67.7.1)", "time-machine (>=2.9)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "vtk" +version = "9.3.0" +description = "VTK is an open-source toolkit for 3D computer graphics, image processing, and visualization" +optional = false +python-versions = "*" +files = [ + {file = "vtk-9.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7d3492cb6c52b23dc0c6e664938b8119254a77b5e3099106e2567ed0b6473162"}, + {file = "vtk-9.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3f4e86bff7a4cd71bd6205bd18cf4b6ab70956ecf9cbd73e77a95b2210d98ef"}, + {file = "vtk-9.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a827fb5f05ab78b2cbad81f5d3a3d7065fa995cc907cecdfa7a7b76374130ef3"}, + {file = "vtk-9.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:2aae778297817335ddd6698b4c124c109d8ac476512691fe19446614ae43ba56"}, + {file = "vtk-9.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a3cd59108b21f55b873a63878a0decec0a707bd960b59d5e15b37d1ad873590f"}, + {file = "vtk-9.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d2bdd2c60f0fa5d1926c11b72d96dc23caf9ff41781bae76e48edd09fb8aa03"}, + {file = "vtk-9.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a02bf6067cae7abfd7f6b1330c69555b715be8ec71a3c8d6471af45a96e8e56"}, + {file = "vtk-9.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:ff0eedcde5821c023623f70951f2499e9d59e709e288b67a2e2334abafacc322"}, + {file = "vtk-9.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:94678fa0476e113500f3b99e9692b92b83a5b058caace7bac3b5f780b12b36ed"}, + {file = "vtk-9.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:371b96afca3ed41a0bf1cd80a42f4b906ca2f470a13df32f39b22a9169d996d7"}, + {file = "vtk-9.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cfa8d73acbab386b9d6ef8a1a01149fd096a21a23547f10bf0cf98d88300724"}, + {file = "vtk-9.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:75d27b73270a42923ebefd87a8522f7717618c36825b8058c4d3aa8e64d6145d"}, + {file = "vtk-9.3.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:6b4a6f9d4ae16a417edf3cd750da5cb87e9676d1db1da6a6772a9e492567a452"}, + {file = "vtk-9.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbd3536979c177dd12f9365a1072e217d64503596add6986318d466aab565d51"}, + {file = "vtk-9.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:296f185482df591d7b2c2a734f3a68884352efd89cade37f3345ddc4dcb6e019"}, + {file = "vtk-9.3.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:9d5c837a4d865ec80752d8ca8ee719be8341af66601df0da94ee78ae0806bb4b"}, + {file = "vtk-9.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cdfb7e51a63ee2f06b1aa84e643f046b746116397a89cb50d20956731e88209"}, + {file = "vtk-9.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2902c8601ada0e653a4d34ebca0f17768fb559f05fe9f4502dcdda136d847a1e"}, + {file = "vtk-9.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:f0798c2ae607be930656347491c520945984ab657ab00804d159323962e97102"}, + {file = "vtk-9.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d92c9a70902da512dfbcd3f064f825b7b5b6d62edd197d3754549f7c0ff516"}, + {file = "vtk-9.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:17810f82aeee7143057fcb2d963245f57450800a7b913c5d66ed915f09740d3d"}, + {file = "vtk-9.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:7a564f31dbe514276abffb1d3204120ead15506a24ecaa2560009ba304896dae"}, + {file = "vtk-9.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3b22a0d03305160d6612da0a378759083ef7691d0f83f1b1496418777ee3a2a3"}, + {file = "vtk-9.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdd81c9904647ace8d13ad255d8e5293fb81be8125e1a139a707aaf9e6f0e9e2"}, + {file = "vtk-9.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:f0a44c926ba18fd9e2ad7c07ae0adabb4ca62af28c69c96bcbaa884e0b240249"}, +] + +[package.dependencies] +matplotlib = ">=2.0.0" + +[package.extras] +numpy = ["numpy (>=1.9)"] +web = ["wslink (>=1.0.4)"] [[package]] name = "zipp" -version = "3.15.0" +version = "3.17.0" description = "Backport of pathlib-compatible object wrapper for zip files" -optional = true -python-versions = ">=3.7" +optional = false +python-versions = ">=3.8" files = [ - {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, - {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, + {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, + {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"] [extras] docs = ["Sphinx", "docutils", "myst-parser", "numpydoc", "readthedocs-sphinx-search", "sphinx-autodoc-typehints", "sphinx-book-theme"] -examples = ["cma", "matplotlib"] +examples = ["cma"] [metadata] lock-version = "2.0" -python-versions = ">=3.8,<3.11" -content-hash = "49bef829dcfc4387008183a38efbdc5582c254a5d33cbdb4d9c7cccdc7b0e075" +python-versions = ">=3.8,<3.12" +content-hash = "38ae0fd220b96cacfd6b0dfaa6b11f9e6be7d224a0215a73ea21197b93946e4f" diff --git a/pyproject.toml b/pyproject.toml index ab98b0d3b..9d5574f66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "pyelastica" -version = "0.3.1.post1" +version = "0.3.2" 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 = ["GazzolaLab "] @@ -20,11 +20,12 @@ classifiers = [ # Trove classifiers # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers "License :: OSI Approved :: MIT License", -"Development Status :: 3 - Alpha", +"Development Status :: 4 - Beta", "Programming Language :: Python", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", +"Programming Language :: Python :: 3.11", "Programming Language :: Python :: Implementation :: CPython", "Intended Audience :: Science/Research", "Intended Audience :: Education", @@ -36,11 +37,13 @@ packages = [ ] [tool.poetry.dependencies] -python = ">=3.8,<3.11" -numba = "^0.55.0" +python = ">=3.8,<3.12" +numba = "^0.57.0" numpy = "^1.19.2" scipy = "^1.5.2" tqdm = "^4.61.1" +matplotlib = "^3.3.2" +pyvista = "^0.39.1" # A list of all of the optional dependencies, some of which are included in the # below `extras`. They can be opted into by apps. Sphinx = {version = "^6.1", optional = true} @@ -51,7 +54,6 @@ myst-parser = {version = "^1.0", optional = true} numpydoc = {version = "^1.3.1", optional = true} docutils = {version = "^0.18", optional = true} cma = {version = "^3.2.2", optional = true} -matplotlib = {version = "^3.3.2", optional = true} [tool.poetry.dev-dependencies] black = "21.12b0" @@ -77,7 +79,6 @@ docs = [ ] examples = [ "cma", - "matplotlib", ] [tool.black] diff --git a/tests/cube.obj b/tests/cube.obj new file mode 100644 index 000000000..05de19630 --- /dev/null +++ b/tests/cube.obj @@ -0,0 +1,24 @@ +mtllib cube.mtl +o Cube +v 1.000000 -1.000000 -1.000000 +v 1.000000 -1.000000 1.000000 +v -1.000000 -1.000000 1.000000 +v -1.000000 -1.000000 -1.000000 +v 1.000000 1.000000 -1.000000 +v 1.000000 1.000000 1.000000 +v -1.000000 1.000000 1.000000 +v -1.000000 1.000000 -1.000000 +vn 0.000000 -1.000000 0.000000 +vn 0.000000 1.000000 0.000000 +vn 1.000000 0.000000 0.000000 +vn -0.000000 0.000000 1.000000 +vn -1.000000 -0.000000 -0.000000 +vn 0.000000 0.000000 -1.000000 +usemtl Material +s off +f 1 2 3 4 +f 5 6 7 8 +f 1 2 6 5 +f 1 5 8 4 +f 4 3 7 8 +f 2 6 7 3 diff --git a/tests/cube.stl b/tests/cube.stl new file mode 100644 index 000000000..4f2e0e34d Binary files /dev/null and b/tests/cube.stl differ diff --git a/tests/test_boundary_conditions.py b/tests/test_boundary_conditions.py index da0fc9dca..fdcb8d827 100644 --- a/tests/test_boundary_conditions.py +++ b/tests/test_boundary_conditions.py @@ -16,7 +16,7 @@ import pytest from pytest import main from scipy.spatial.transform import Rotation -from tests.test_rod.test_rods import MockTestRod +from tests.test_rod.mock_rod import MockTestRod test_built_in_boundary_condition_impls = [ FreeBC, diff --git a/tests/test_contact_classes.py b/tests/test_contact_classes.py new file mode 100644 index 000000000..160b2fb71 --- /dev/null +++ b/tests/test_contact_classes.py @@ -0,0 +1,1429 @@ +__doc__ = """ Test Wrapper Classes used in contact in Elastica.contact_forces implementation""" + +import numpy as np +from numpy.testing import assert_allclose +from elastica.utils import Tolerance +from elastica.contact_forces import ( + RodRodContact, + RodCylinderContact, + RodSelfContact, + RodSphereContact, + RodPlaneContact, + RodPlaneContactWithAnisotropicFriction, + CylinderPlaneContact, +) +from elastica.typing import RodBase +from elastica.rigidbody import Cylinder, Sphere +from elastica.surface import Plane +import pytest +from elastica.contact_utils import ( + _node_to_element_mass_or_force, +) + + +def mock_rod_init(self): + + "Initializing Rod" + "Details of initialization are given in test_contact_specific_functions.py" + + self.n_elem = 2 + self.position_collection = np.array([[1, 2, 3], [0, 0, 0], [0, 0, 0]]) + self.mass = np.ones(self.n_elem + 1) + self.radius = np.array([1, 1]) + self.lengths = np.array([1, 1]) + self.tangents = np.array([[1.0, 1.0], [0.0, 0.0], [0.0, 0.0]]) + self.internal_forces = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]) + self.external_forces = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]) + self.velocity_collection = np.array( + [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]] + ) + self.omega_collection = np.array([[0.0, 0.0], [0.0, 0.0], [0.0, 0.0]]) + self.external_torques = np.array([[0.0, 0.0], [0.0, 0.0], [0.0, 0.0]]) + self.internal_torques = np.array([[0.0, 0.0], [0.0, 0.0], [0.0, 0.0]]) + + +def mock_cylinder_init(self): + + "Initializing Cylinder" + "Details of initialization are given in test_contact_specific_functions.py" + + self.n_elems = 1 + self.position_collection = np.array([[0], [0], [0]]) + self.director_collection = np.array( + [[[1.0], [0.0], [0.0]], [[0.0], [1.0], [0.0]], [[0.0], [0.0], [1.0]]] + ) + self.radius = np.array([1.0]) + self.length = np.array([2.0]) + self.external_forces = np.array([[0.0], [0.0], [0.0]]) + self.external_torques = np.array([[0.0], [0.0], [0.0]]) + self.velocity_collection = np.array([[0.0], [0.0], [0.0]]) + + +def mock_sphere_init(self): + + "Initializing Sphere" + "Details of initialization are given in test_contact_specific_functions.py" + + self.n_elems = 1 + self.position_collection = np.array([[0], [0], [0]]) + self.director_collection = np.array( + [[[1.0], [0.0], [0.0]], [[0.0], [1.0], [0.0]], [[0.0], [0.0], [1.0]]] + ) + self.radius = np.array([1.0]) + self.velocity_collection = np.array([[0.0], [0.0], [0.0]]) + self.external_forces = np.array([[0.0], [0.0], [0.0]]) + self.external_torques = np.array([[0.0], [0.0], [0.0]]) + + +def mock_plane_init(self): + + "Initializing Plane" + + self.normal = np.asarray([1.0, 0.0, 0.0]).reshape(3) + self.origin = np.asarray([0.0, 0.0, 0.0]).reshape(3, 1) + + +MockRod = type("MockRod", (RodBase,), {"__init__": mock_rod_init}) + +MockCylinder = type("MockCylinder", (Cylinder,), {"__init__": mock_cylinder_init}) + +MockSphere = type("MockSphere", (Sphere,), {"__init__": mock_sphere_init}) + +MockPlane = type("MockPlane", (Plane,), {"__init__": mock_plane_init}) + + +class TestRodCylinderContact: + def test_check_systems_validity_with_invalid_systems( + self, + ): + mock_rod = MockRod() + mock_list = [1, 2, 3] + mock_cylinder = MockCylinder() + rod_cylinder_contact = RodCylinderContact(k=1.0, nu=0.0) + + "Testing Rod Cylinder Contact wrapper with incorrect type for second argument" + with pytest.raises(TypeError) as excinfo: + rod_cylinder_contact._check_systems_validity(mock_rod, mock_list) + assert ( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a rod, second should be a cylinder" + ).format(mock_rod.__class__, mock_list.__class__) == str(excinfo.value) + + "Testing Rod Cylinder Contact wrapper with incorrect type for first argument" + with pytest.raises(TypeError) as excinfo: + rod_cylinder_contact._check_systems_validity(mock_list, mock_rod) + assert ( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a rod, second should be a cylinder" + ).format(mock_list.__class__, mock_rod.__class__) == str(excinfo.value) + + "Testing Rod Cylinder Contact wrapper with incorrect order" + with pytest.raises(TypeError) as excinfo: + rod_cylinder_contact._check_systems_validity(mock_cylinder, mock_rod) + print(excinfo.value) + assert ( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a rod, second should be a cylinder" + ).format(mock_cylinder.__class__, mock_rod.__class__) == str(excinfo.value) + + def test_contact_rod_cylinder_with_collision_with_k_without_nu_and_friction( + self, + ): + + "Testing Rod Cylinder Contact wrapper with Collision with analytical verified values" + + mock_rod = MockRod() + mock_cylinder = MockCylinder() + rod_cylinder_contact = RodCylinderContact(k=1.0, nu=0.0) + rod_cylinder_contact.apply_contact(mock_rod, mock_cylinder) + + """Details and reasoning about the values are given in 'test_contact_specific_functions.py/test_calculate_contact_forces_rod_cylinder()'""" + assert_allclose( + mock_rod.external_forces, + np.array([[0.166666, 0.333333, 0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]), + atol=1e-6, + ) + + assert_allclose( + mock_cylinder.external_forces, np.array([[-0.5], [0.0], [0.0]]), atol=1e-6 + ) + + assert_allclose( + mock_cylinder.external_torques, np.array([[0.0], [0.0], [0.0]]), atol=1e-6 + ) + + def test_contact_rod_cylinder_with_collision_with_nu_without_k_and_friction( + self, + ): + + "Testing Rod Cylinder Contact wrapper with Collision with analytical verified values" + + mock_rod = MockRod() + "Moving rod towards the cylinder with a velocity of -1 in x-axis" + mock_rod.velocity_collection = np.array([[-1, 0, 0], [-1, 0, 0], [-1, 0, 0]]) + mock_cylinder = MockCylinder() + "Moving cylinder towards the rod with a velocity of 1 in x-axis" + mock_cylinder.velocity_collection = np.array([[1], [0], [0]]) + rod_cylinder_contact = RodCylinderContact(k=0.0, nu=1.0) + rod_cylinder_contact.apply_contact(mock_rod, mock_cylinder) + + """Details and reasoning about the values are given in 'test_contact_specific_functions.py/test_calculate_contact_forces_rod_cylinder()'""" + assert_allclose( + mock_rod.external_forces, + np.array([[0.5, 1, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + + assert_allclose( + mock_cylinder.external_forces, np.array([[-1.5], [0], [0]]), atol=1e-6 + ) + + assert_allclose( + mock_cylinder.external_torques, np.array([[0.0], [0.0], [0.0]]), atol=1e-6 + ) + + def test_contact_rod_cylinder_with_collision_with_k_and_nu_without_friction( + self, + ): + + "Testing Rod Cylinder Contact wrapper with Collision with analytical verified values" + + mock_rod = MockRod() + "Moving rod towards the cylinder with a velocity of -1 in x-axis" + mock_rod.velocity_collection = np.array([[-1, 0, 0], [-1, 0, 0], [-1, 0, 0]]) + mock_cylinder = MockCylinder() + "Moving cylinder towards the rod with a velocity of 1 in x-axis" + mock_cylinder.velocity_collection = np.array([[1], [0], [0]]) + rod_cylinder_contact = RodCylinderContact(k=1.0, nu=1.0) + rod_cylinder_contact.apply_contact(mock_rod, mock_cylinder) + + """Details and reasoning about the values are given in 'test_contact_specific_functions.py/test_calculate_contact_forces_rod_cylinder()'""" + assert_allclose( + mock_rod.external_forces, + np.array([[0.666666, 1.333333, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + + assert_allclose( + mock_cylinder.external_forces, np.array([[-2], [0], [0]]), atol=1e-6 + ) + + assert_allclose( + mock_cylinder.external_torques, np.array([[0.0], [0.0], [0.0]]), atol=1e-6 + ) + + def test_contact_rod_cylinder_with_collision_with_k_and_nu_and_friction( + self, + ): + + "Testing Rod Cylinder Contact wrapper with Collision with analytical verified values" + + mock_rod = MockRod() + "Moving rod towards the cylinder with a velocity of -1 in x-axis" + mock_rod.velocity_collection = np.array([[-1, 0, 0], [-1, 0, 0], [-1, 0, 0]]) + mock_cylinder = MockCylinder() + "Moving cylinder towards the rod with a velocity of 1 in x-axis" + mock_cylinder.velocity_collection = np.array([[1], [0], [0]]) + rod_cylinder_contact = RodCylinderContact( + k=1.0, nu=1.0, velocity_damping_coefficient=0.1, friction_coefficient=0.1 + ) + rod_cylinder_contact.apply_contact(mock_rod, mock_cylinder) + + """Details and reasoning about the values are given in 'test_contact_specific_functions.py/test_calculate_contact_forces_rod_cylinder()'""" + assert_allclose( + mock_rod.external_forces, + np.array( + [ + [0.666666, 1.333333, 0], + [0.033333, 0.066666, 0], + [0.033333, 0.066666, 0], + ] + ), + atol=1e-6, + ) + + assert_allclose( + mock_cylinder.external_forces, np.array([[-2], [-0.1], [-0.1]]), atol=1e-6 + ) + + assert_allclose( + mock_cylinder.external_torques, np.array([[0.0], [0.0], [0.0]]), atol=1e-6 + ) + + def test_contact_rod_cylinder_without_collision(self): + + "Testing Rod Cylinder Contact wrapper without Collision with analytical verified values" + + mock_rod = MockRod() + mock_cylinder = MockCylinder() + rod_cylinder_contact = RodCylinderContact(k=1.0, nu=0.5) + + """Setting cylinder position such that there is no collision""" + mock_cylinder.position_collection = np.array([[400], [500], [600]]) + mock_rod_external_forces_before_execution = mock_rod.external_forces.copy() + mock_cylinder_external_forces_before_execution = ( + mock_cylinder.external_forces.copy() + ) + mock_cylinder_external_torques_before_execution = ( + mock_cylinder.external_torques.copy() + ) + rod_cylinder_contact.apply_contact(mock_rod, mock_cylinder) + + assert_allclose( + mock_rod.external_forces, mock_rod_external_forces_before_execution + ) + assert_allclose( + mock_cylinder.external_forces, + mock_cylinder_external_forces_before_execution, + ) + assert_allclose( + mock_cylinder.external_torques, + mock_cylinder_external_torques_before_execution, + ) + + +class TestRodRodContact: + def test_check_systems_validity_with_invalid_systems( + self, + ): + mock_rod_one = MockRod() + mock_list = [1, 2, 3] + rod_rod_contact = RodRodContact(k=1.0, nu=0.0) + + "Testing Rod Rod Contact wrapper with incorrect type for second argument" + with pytest.raises(TypeError) as excinfo: + rod_rod_contact._check_systems_validity(mock_rod_one, mock_list) + assert ( + "Systems provided to the contact class have incorrect order. \n" + " First system is {0} and second system is {1}. \n" + " Both systems must be distinct rods" + ).format(mock_rod_one.__class__, mock_list.__class__) == str(excinfo.value) + + "Testing Rod Rod Contact wrapper with incorrect type for first argument" + with pytest.raises(TypeError) as excinfo: + rod_rod_contact._check_systems_validity(mock_list, mock_rod_one) + assert ( + "Systems provided to the contact class have incorrect order. \n" + " First system is {0} and second system is {1}. \n" + " Both systems must be distinct rods" + ).format(mock_list.__class__, mock_rod_one.__class__) == str(excinfo.value) + + "Testing Rod Rod Contact wrapper with same rod for both arguments" + with pytest.raises(TypeError) as excinfo: + rod_rod_contact._check_systems_validity(mock_rod_one, mock_rod_one) + assert ( + "First rod is identical to second rod. \n" + "Rods must be distinct for RodRodConact. \n" + "If you want self contact, use RodSelfContact instead" + ) == str(excinfo.value) + + def test_contact_with_two_rods_with_collision_with_k_without_nu(self): + + "Testing Rod Rod Contact wrapper with two rods with analytical verified values" + "Test values have been copied from 'test_contact_specific_functions.py/test_calculate_contact_forces_rod_rod()'" + + mock_rod_one = MockRod() + mock_rod_two = MockRod() + mock_rod_two.position_collection = np.array([[4, 5, 6], [0, 0, 0], [0, 0, 0]]) + rod_rod_contact = RodRodContact(k=1.0, nu=0.0) + rod_rod_contact.apply_contact(mock_rod_one, mock_rod_two) + + assert_allclose( + mock_rod_one.external_forces, + np.array([[0, -0.666666, -0.333333], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + assert_allclose( + mock_rod_two.external_forces, + np.array([[0.333333, 0.666666, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + + def test_contact_with_two_rods_with_collision_without_k_with_nu(self): + + "Testing Rod Rod Contact wrapper with two rods with analytical verified values" + "Test values have been copied from 'test_contact_specific_functions.py/test_calculate_contact_forces_rod_rod()'" + + mock_rod_one = MockRod() + mock_rod_two = MockRod() + + """Moving the rods towards each other with a velocity of 1 along the x-axis.""" + mock_rod_one.velocity_collection = np.array([[1, 0, 0], [1, 0, 0], [1, 0, 0]]) + mock_rod_two.velocity_collection = np.array( + [[-1, 0, 0], [-1, 0, 0], [-1, 0, 0]] + ) + mock_rod_two.position_collection = np.array([[4, 5, 6], [0, 0, 0], [0, 0, 0]]) + rod_rod_contact = RodRodContact(k=0.0, nu=1.0) + rod_rod_contact.apply_contact(mock_rod_one, mock_rod_two) + + assert_allclose( + mock_rod_one.external_forces, + np.array( + [[0, -0.333333, -0.166666], [0, 0, 0], [0, 0, 0]], + ), + atol=1e-6, + ) + assert_allclose( + mock_rod_two.external_forces, + np.array([[0.166666, 0.333333, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + + def test_contact_with_two_rods_with_collision_with_k_and_nu(self): + + "Testing RodRod Contact wrapper with two rods with analytical verified values" + "Test values have been copied from 'test_contact_specific_functions.py/test_calculate_contact_forces_rod_rod()'" + + mock_rod_one = MockRod() + mock_rod_two = MockRod() + + """Moving the rods towards each other with a velocity of 1 along the x-axis.""" + mock_rod_one.velocity_collection = np.array([[1, 0, 0], [1, 0, 0], [1, 0, 0]]) + mock_rod_two.velocity_collection = np.array( + [[-1, 0, 0], [-1, 0, 0], [-1, 0, 0]] + ) + mock_rod_two.position_collection = np.array([[4, 5, 6], [0, 0, 0], [0, 0, 0]]) + rod_rod_contact = RodRodContact(k=1.0, nu=1.0) + rod_rod_contact.apply_contact(mock_rod_one, mock_rod_two) + + assert_allclose( + mock_rod_one.external_forces, + np.array( + [[0, -1, -0.5], [0, 0, 0], [0, 0, 0]], + ), + atol=1e-6, + ) + assert_allclose( + mock_rod_two.external_forces, + np.array([[0.5, 1, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + + def test_contact_with_two_rods_without_collision(self): + + "Testing Rod Rod Contact wrapper with two rods with analytical verified values" + + mock_rod_one = MockRod() + mock_rod_two = MockRod() + + "Setting rod two position such that there is no collision" + mock_rod_two.position_collection = np.array( + [[100, 101, 102], [0, 0, 0], [0, 0, 0]] + ) + rod_rod_contact = RodRodContact(k=1.0, nu=1.0) + mock_rod_one_external_forces_before_execution = ( + mock_rod_one.external_forces.copy() + ) + mock_rod_two_external_forces_before_execution = ( + mock_rod_two.external_forces.copy() + ) + rod_rod_contact.apply_contact(mock_rod_one, mock_rod_two) + + assert_allclose( + mock_rod_one.external_forces, mock_rod_one_external_forces_before_execution + ) + assert_allclose( + mock_rod_two.external_forces, mock_rod_two_external_forces_before_execution + ) + + +class TestRodSelfContact: + def test_check_systems_validity_with_invalid_systems( + self, + ): + mock_rod_one = MockRod() + mock_rod_two = MockRod() + mock_list = [1, 2, 3] + self_contact = RodSelfContact(k=1.0, nu=0.0) + + "Testing Self Contact wrapper with incorrect type for second argument" + with pytest.raises(TypeError) as excinfo: + self_contact._check_systems_validity(mock_rod_one, mock_list) + assert ( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system and second system should be the same rod \n" + " If you want rod rod contact, use RodRodContact instead" + ).format(mock_rod_one.__class__, mock_list.__class__) == str(excinfo.value) + + "Testing Self Contact wrapper with incorrect type for first argument" + with pytest.raises(TypeError) as excinfo: + self_contact._check_systems_validity(mock_list, mock_rod_one) + assert ( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system and second system should be the same rod \n" + " If you want rod rod contact, use RodRodContact instead" + ).format(mock_list.__class__, mock_rod_one.__class__) == str(excinfo.value) + + "Testing Self Contact wrapper with different rods" + with pytest.raises(TypeError) as excinfo: + self_contact._check_systems_validity(mock_rod_one, mock_rod_two) + assert ( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system and second system should be the same rod \n" + " If you want rod rod contact, use RodRodContact instead" + ).format(mock_rod_one.__class__, mock_rod_two.__class__) == str(excinfo.value) + + def test_self_contact_with_rod_self_collision(self): + + "Testing Self Contact wrapper rod self collision with analytical verified values" + + mock_rod = MockRod() + + "Test values have been copied from 'test_contact_specific_functions.py/test_calculate_contact_forces_self_rod()'" + mock_rod.n_elems = 3 + mock_rod.position_collection = np.array( + [[1, 4, 4, 1], [0, 0, 1, 1], [0, 0, 0, 0]] + ) + mock_rod.radius = np.array([1, 1, 1]) + mock_rod.lengths = np.array([3, 1, 3]) + mock_rod.tangents = np.array( + [[1.0, 0.0, -1.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]] + ) + mock_rod.velocity_collection = np.array( + [[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]] + ) + mock_rod.internal_forces = np.array( + [[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]] + ) + mock_rod.external_forces = np.array( + [[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]] + ) + self_contact = RodSelfContact(k=1.0, nu=0.0) + self_contact.apply_contact(mock_rod, mock_rod) + + assert_allclose( + mock_rod.external_forces, + np.array( + [[0, 0, 0, 0], [-0.333333, -0.666666, 0.666666, 0.333333], [0, 0, 0, 0]] + ), + atol=1e-6, + ) + + def test_self_contact_with_rod_no_self_collision(self): + + "Testing Self Contact wrapper rod no self collision with analytical verified values" + + mock_rod = MockRod() + + "the initially set rod does not have self collision" + mock_rod_external_forces_before_execution = mock_rod.external_forces.copy() + self_contact = RodSelfContact(k=1.0, nu=1.0) + self_contact.apply_contact(mock_rod, mock_rod) + + assert_allclose( + mock_rod.external_forces, mock_rod_external_forces_before_execution + ) + + +class TestRodSphereContact: + def test_check_systems_validity_with_invalid_systems( + self, + ): + mock_rod = MockRod() + mock_list = [1, 2, 3] + mock_sphere = MockSphere() + rod_sphere_contact = RodSphereContact(k=1.0, nu=0.0) + + "Testing Rod Sphere Contact wrapper with incorrect type for second argument" + with pytest.raises(TypeError) as excinfo: + rod_sphere_contact._check_systems_validity(mock_rod, mock_list) + assert ( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a rod, second should be a sphere" + ).format(mock_rod.__class__, mock_list.__class__) == str(excinfo.value) + + "Testing Rod Sphere Contact wrapper with incorrect type for first argument" + with pytest.raises(TypeError) as excinfo: + rod_sphere_contact._check_systems_validity(mock_list, mock_rod) + assert ( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a rod, second should be a sphere" + ).format(mock_list.__class__, mock_rod.__class__) == str(excinfo.value) + + "Testing Rod Sphere Contact wrapper with incorrect order" + with pytest.raises(TypeError) as excinfo: + rod_sphere_contact._check_systems_validity(mock_sphere, mock_rod) + print(excinfo.value) + assert ( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a rod, second should be a sphere" + ).format(mock_sphere.__class__, mock_rod.__class__) == str(excinfo.value) + + def test_contact_rod_sphere_with_collision_with_k_without_nu_and_friction( + self, + ): + + "Testing Rod Sphere Contact wrapper with Collision with analytical verified values" + + mock_rod = MockRod() + mock_sphere = MockSphere() + rod_sphere_contact = RodSphereContact(k=1.0, nu=0.0) + rod_sphere_contact.apply_contact(mock_rod, mock_sphere) + + """Details and reasoning about the values are given in 'test_contact_specific_functions.py/test_calculate_contact_forces_rod_sphere_with_k_without_nu_and_friction()'""" + assert_allclose( + mock_sphere.external_forces, np.array([[-0.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.166666, 0.333333, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + + +class TestRodPlaneContact: + def initializer( + self, + shift=0.0, + k_w=0.0, + nu_w=0.0, + plane_normal=np.array([0.0, 1.0, 0.0]), + ): + # create rod + rod = MockRod() + start = np.array([0.0, 0.0, 0.0]) + direction = np.array([0.0, 0.0, 1.0]) + end = start + 1.0 * direction + rod.position_collection = np.zeros((3, 3)) + for i in range(0, 3): + rod.position_collection[i, ...] = np.linspace(start[i], end[i], num=3) + rod.director_collection = np.repeat(np.identity(3)[:, :, np.newaxis], 2, axis=2) + rod.lengths = np.ones(2) * 1.0 / 2 + rod.radius = np.repeat(np.array([0.25]), 2, axis=0) + rod.tangents = np.repeat(direction[:, np.newaxis], 2, axis=1) + + # create plane + plane = MockPlane() + plane.origin = np.array([0.0, -rod.radius[0] + shift, 0.0]).reshape(3, 1) + plane.normal = plane_normal.reshape( + 3, + ) + rod_plane_contact = RodPlaneContact(k_w, nu_w) + + fnormal = -10.0 * np.sign(plane_normal[1]) * np.random.random_sample(1).item() + external_forces = np.repeat( + np.array([0.0, fnormal, 0.0]).reshape(3, 1), 3, axis=1 + ) + external_forces[..., 0] *= 0.5 + external_forces[..., -1] *= 0.5 + rod.external_forces = external_forces.copy() + + return rod, plane, rod_plane_contact, external_forces + + def test_check_systems_validity_with_invalid_systems( + self, + ): + mock_rod = MockRod() + mock_plane = MockPlane() + mock_list = [1, 2, 3] + rod_plane_contact = RodPlaneContact(k=1.0, nu=0.0) + + "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 ( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a rod, second should be a plane" + ).format(mock_rod.__class__, mock_list.__class__) == str(excinfo.value) + + "Testing Rod Plane Contact wrapper with incorrect type for first argument" + with pytest.raises(TypeError) as excinfo: + rod_plane_contact._check_systems_validity(mock_list, mock_plane) + assert ( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a rod, second should be a plane" + ).format(mock_list.__class__, mock_plane.__class__) == str(excinfo.value) + + def test_rod_plane_contact_without_contact(self): + """ + This test case tests the forces on rod, when there is no + contact between rod and the plane. + + """ + + shift = -( + (2.0 - 1.0) * np.random.random_sample(1) + 1.0 + ).item() # we move plane away from rod + print("q") + [rod, plane, rod_plane_contact, external_forces] = self.initializer(shift) + + rod_plane_contact.apply_contact(rod, plane) + correct_forces = external_forces # since no contact + assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) + + def test_rod_plane_contact_without_k_and_nu(self): + """ + 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. + + """ + + [rod, plane, rod_plane_contact, external_forces] = self.initializer() + + rod_plane_contact.apply_contact(rod, plane) + + correct_forces = np.zeros((3, 3)) + assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) + + @pytest.mark.parametrize("k_w", [0.1, 0.5, 1.0, 2, 10]) + def test_rod_plane_contact_with_k_without_nu(self, k_w): + """ + Here wall stiffness coefficient changed parametrically + and damping coefficient set to zero . + Parameters + ---------- + k_w + + + """ + + shift = np.random.random_sample(1).item() # we move plane towards to rod + [rod, plane, rod_plane_contact, external_forces] = self.initializer( + shift=shift, k_w=k_w + ) + correct_forces = k_w * np.repeat( + np.array([0.0, shift, 0.0]).reshape(3, 1), 3, axis=1 + ) + correct_forces[..., 0] *= 0.5 + correct_forces[..., -1] *= 0.5 + + rod_plane_contact.apply_contact(rod, plane) + + assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) + + @pytest.mark.parametrize("nu_w", [0.5, 1.0, 5.0, 7.0, 12.0]) + def test_rod_plane_contact_without_k_with_nu(self, nu_w): + """ + Here wall damping coefficient are changed parametrically and + wall response functions tested. + Parameters + ---------- + nu_w + """ + + [rod, plane, rod_plane_contact, external_forces] = self.initializer(nu_w=nu_w) + + normal_velocity = np.random.random_sample(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), + 3, + axis=1, + ) + + correct_forces[..., 0] *= 0.5 + correct_forces[..., -1] *= 0.5 + + rod_plane_contact.apply_contact(rod, plane) + + assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) + + def test_rod_plane_contact_when_rod_is_under_plane(self): + """ + This test case tests plane response forces on the rod + in the case rod is under the plane and pushed towards + the plane. + + """ + + # 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 plane. + plane_normal = np.array([0.0, -1.0, 0.0]) + + [rod, plane, rod_plane_contact, external_forces] = self.initializer( + shift=offset_of_plane_with_respect_to_rod, plane_normal=plane_normal + ) + + rod_plane_contact.apply_contact(rod, plane) + correct_forces = np.zeros((3, 3)) + assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) + + @pytest.mark.parametrize("k_w", [0.1, 0.5, 1.0, 2, 10]) + def test_rod_plane_contact_when_rod_is_under_plane_with_k_without_nu(self, k_w): + """ + In this test case we move the rod under the plane. + Here wall stiffness coefficient changed parametrically + and damping coefficient set to zero . + Parameters + ---------- + k_w + + """ + # 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 - np.random.random_sample(1).item() + + # plane normal changed, it is towards the negative direction, because rod + # is under the plane. + plane_normal = np.array([0.0, -1.0, 0.0]) + + [rod, plane, rod_plane_contact, external_forces] = self.initializer( + 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 + ), + 3, + axis=1, + ) + correct_forces[..., 0] *= 0.5 + correct_forces[..., -1] *= 0.5 + + rod_plane_contact.apply_contact(rod, plane) + + assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) + + @pytest.mark.parametrize("nu_w", [0.5, 1.0, 5.0, 7.0, 12.0]) + def test_rod_plane_contact_when_rod_is_under_plane_without_k_with_nu(self, nu_w): + """ + 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 + ---------- + nu_w + + """ + # 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 plane. + plane_normal = np.array([0.0, -1.0, 0.0]) + + [rod, plane, rod_plane_contact, external_forces] = self.initializer( + shift=offset_of_plane_with_respect_to_rod, + nu_w=nu_w, + plane_normal=plane_normal, + ) + + normal_velocity = np.random.random_sample(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), + 3, + axis=1, + ) + + correct_forces[..., 0] *= 0.5 + correct_forces[..., -1] *= 0.5 + + rod_plane_contact.apply_contact(rod, plane) + + assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) + + +class TestRodPlaneWithAnisotropicFriction: + def initializer( + self, + 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 + ): + + # create rod + rod = MockRod() + start = np.array([0.0, 0.0, 0.0]) + direction = np.array([0.0, 0.0, 1.0]) + end = start + 1.0 * direction + rod.position_collection = np.zeros((3, 3)) + for i in range(0, 3): + rod.position_collection[i, ...] = np.linspace(start[i], end[i], num=3) + rod.director_collection = np.repeat(np.identity(3)[:, :, np.newaxis], 2, axis=2) + rod.lengths = np.ones(2) * 1.0 / 2 + rod.radius = np.repeat(np.array([0.25]), 2, axis=0) + rod.tangents = np.repeat(direction[:, np.newaxis], 2, axis=1) + + # create plane + plane = MockPlane() + plane.origin = np.array([0.0, -rod.radius[0], 0.0]).reshape(3, 1) + plane.normal = np.array([0.0, 1.0, 0.0]).reshape( + 3, + ) + slip_velocity_tol = 1e-2 + rod_plane_contact = RodPlaneContactWithAnisotropicFriction( + 0.0, + 0.0, + slip_velocity_tol, + static_mu_array, # forward, backward, sideways + kinetic_mu_array, # forward, backward, sideways + ) + + fnormal = (10.0 - 5.0) * np.random.random_sample( + 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), 3, axis=1) + external_forces_collection[..., 0] *= 0.5 + external_forces_collection[..., -1] *= 0.5 + rod.external_forces = external_forces_collection.copy() + + return rod, plane, rod_plane_contact, external_forces_collection + + def test_check_systems_validity_with_invalid_systems( + self, + ): + mock_rod = MockRod() + mock_plane = MockPlane() + mock_list = [1, 2, 3] + rod_plane_contact = RodPlaneContactWithAnisotropicFriction( + 0.0, + 0.0, + 1e-2, + np.array([0.0, 0.0, 0.0]), # forward, backward, sideways + np.array([0.0, 0.0, 0.0]), # forward, backward, sideways + ) + + "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 ( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a rod, second should be a plane" + ).format(mock_rod.__class__, mock_list.__class__) == str(excinfo.value) + + "Testing Rod Plane wrapper with incorrect type for first argument" + with pytest.raises(TypeError) as excinfo: + rod_plane_contact._check_systems_validity(mock_list, mock_plane) + assert ( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a rod, second should be a plane" + ).format(mock_list.__class__, mock_plane.__class__) == str(excinfo.value) + + @pytest.mark.parametrize("velocity", [-1.0, -3.0, 1.0, 5.0, 2.0]) + def test_axial_kinetic_friction(self, velocity): + """ + This function tests kinetic friction in forward and backward direction. + All other friction coefficients set to zero. + Parameters + ---------- + velocity + + + + """ + + [rod, plane, rod_plane_contact, external_forces_collection] = self.initializer( + kinetic_mu_array=np.array([1.0, 1.0, 0.0]) + ) + + rod.velocity_collection += np.array([0.0, 0.0, velocity]).reshape(3, 1) + + rod_plane_contact.apply_contact(rod, plane) + + direction_collection = np.repeat( + np.array([0.0, 0.0, 1.0]).reshape(3, 1), 3, 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("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, force_mag + ): + """ + 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 + ---------- + force_mag + """ + [rod, plane, rod_plane_contact, external_forces_collection] = self.initializer( + static_mu_array=np.array([1.0, 1.0, 0.0]), force_mag_long=force_mag + ) + + rod_plane_contact.apply_contact(rod, plane) + correct_forces = np.zeros((3, 3)) + assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) + + @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, force_mag + ): + """ + 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 + ---------- + force_mag + + + """ + + [rod, plane, rod_plane_contact, external_forces_collection] = self.initializer( + static_mu_array=np.array([1.0, 1.0, 0.0]), force_mag_long=force_mag + ) + + rod_plane_contact.apply_contact(rod, plane) + correct_forces = np.zeros((3, 3)) + 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("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, velocity, omega): + """ + 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 + ---------- + velocity + omega + + """ + [rod, plane, rod_plane_contact, external_forces_collection] = self.initializer( + 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) + + rod_plane_contact.apply_contact(rod, plane) + + correct_forces = np.zeros((3, 3)) + 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, 2)) + 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("force_mag", [-20.0, -15.0, 15.0, 20.0]) + def test_static_rolling_friction_total_force_smaller_than_static_friction_force( + self, force_mag + ): + """ + 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 + ---------- + force_mag + + + """ + + [rod, plane, rod_plane_contact, external_forces_collection] = self.initializer( + static_mu_array=np.array([0.0, 0.0, 10.0]), force_mag_side=force_mag + ) + + rod_plane_contact.apply_contact(rod, plane) + + correct_forces = np.zeros((3, 3)) + 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, 2)) + 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("force_mag", [-100.0, -80.0, 65.0, 95.0]) + def test_static_rolling_friction_total_force_larger_than_static_friction_force( + self, force_mag + ): + """ + 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 + ---------- + force_mag + + + """ + + [rod, plane, rod_plane_contact, external_forces_collection] = self.initializer( + static_mu_array=np.array([0.0, 0.0, 1.0]), force_mag_side=force_mag + ) + + rod_plane_contact.apply_contact(rod, plane) + + correct_forces = np.zeros((3, 3)) + 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, 2)) + 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("torque_mag", [-3.0, -1.0, 2.0, 3.5]) + def test_static_rolling_friction_total_torque_smaller_than_static_friction_force( + self, torque_mag + ): + """ + 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 + ---------- + torque_mag + """ + + [rod, plane, rod_plane_contact, external_forces_collection] = self.initializer( + static_mu_array=np.array([0.0, 0.0, 10.0]) + ) + + external_torques = np.zeros((3, 2)) + external_torques[2] = torque_mag + rod.external_torques = external_torques.copy() + + rod_plane_contact.apply_contact(rod, plane) + + correct_forces = np.zeros((3, 3)) + 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, 2)) + 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("torque_mag", [-10.0, -5.0, 6.0, 7.5]) + def test_static_rolling_friction_total_torque_larger_than_static_friction_force( + self, torque_mag + ): + """ + 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 + ---------- + torque_mag + + """ + + [rod, plane, rod_plane_contact, external_forces_collection] = self.initializer( + static_mu_array=np.array([0.0, 0.0, 1.0]) + ) + + external_torques = np.zeros((3, 2)) + external_torques[2] = torque_mag + rod.external_torques = external_torques.copy() + + rod_plane_contact.apply_contact(rod, plane) + + correct_forces = np.zeros((3, 3)) + 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()) + + +class TestCylinderPlaneContact: + def initializer( + self, + shift=0.0, + k_w=0.0, + nu_w=0.0, + plane_normal=np.array([0.0, 1.0, 0.0]), + ): + # create cylinder + cylinder = MockCylinder() + + # create plane + plane = MockPlane() + plane.origin = np.array([0.0, -cylinder.radius[0] + shift, 0.0]).reshape(3, 1) + plane.normal = plane_normal.reshape( + 3, + ) + cylinder_plane_contact = CylinderPlaneContact(k_w, nu_w) + + fnormal = -10.0 * np.sign(plane_normal[1]) * np.random.random_sample(1).item() + external_forces = np.array([0.0, fnormal, 0.0]).reshape(3, 1) + cylinder.external_forces = external_forces.copy() + + return cylinder, plane, cylinder_plane_contact, external_forces + + def test_check_systems_validity_with_invalid_systems( + self, + ): + mock_cylinder = MockCylinder() + mock_plane = MockPlane() + mock_list = [1, 2, 3] + cylinder_plane_contact = CylinderPlaneContact(k=1.0, nu=0.0) + + "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 ( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a cylinder, second should be a plane" + ).format(mock_cylinder.__class__, mock_list.__class__) == str(excinfo.value) + + "Testing Cylinder Plane wrapper with incorrect type for first argument" + with pytest.raises(TypeError) as excinfo: + cylinder_plane_contact._check_systems_validity(mock_list, mock_plane) + assert ( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a cylinder, second should be a plane" + ).format(mock_list.__class__, mock_plane.__class__) == str(excinfo.value) + + def test_cylinder_plane_contact_without_contact(self): + """ + This test case tests the forces on cylinder, when there is no + contact between cylinder and the plane. + + """ + + shift = -( + (2.0 - 1.0) * np.random.random_sample(1) + 1.0 + ).item() # we move plane away from cylinder + print("q") + [cylinder, plane, cylinder_plane_contact, external_forces] = self.initializer( + shift + ) + + cylinder_plane_contact.apply_contact(cylinder, plane) + correct_forces = external_forces # since no contact + assert_allclose(correct_forces, cylinder.external_forces, atol=Tolerance.atol()) + + def test_cylinder_plane_contact_without_k_and_nu(self): + """ + This function tests wall response on the cylinder. Here + wall stiffness coefficient and damping coefficient set + to zero to check only sum of all forces on the cylinder. + + """ + + [cylinder, plane, cylinder_plane_contact, external_forces] = self.initializer() + + cylinder_plane_contact.apply_contact(cylinder, plane) + + correct_forces = np.zeros((3, 1)) + assert_allclose(correct_forces, cylinder.external_forces, atol=Tolerance.atol()) + + @pytest.mark.parametrize("k_w", [0.1, 0.5, 1.0, 2, 10]) + def test_cylinder_plane_contact_with_k_without_nu(self, k_w): + """ + Here wall stiffness coefficient changed parametrically + and damping coefficient set to zero . + Parameters + ---------- + k_w + + + """ + + shift = np.random.random_sample(1).item() # we move plane towards to cylinder + [cylinder, plane, cylinder_plane_contact, external_forces] = self.initializer( + shift=shift, k_w=k_w + ) + correct_forces = k_w * np.array([0.0, shift, 0.0]).reshape(3, 1) + + cylinder_plane_contact.apply_contact(cylinder, plane) + + assert_allclose(correct_forces, cylinder.external_forces, atol=Tolerance.atol()) + + @pytest.mark.parametrize("nu_w", [0.5, 1.0, 5.0, 7.0, 12.0]) + def test_cylinder_plane_contact_without_k_with_nu(self, nu_w): + """ + Here wall damping coefficient are changed parametrically and + wall response functions tested. + Parameters + ---------- + nu_w + """ + + [cylinder, plane, cylinder_plane_contact, external_forces] = self.initializer( + nu_w=nu_w + ) + + normal_velocity = np.random.random_sample(1).item() + cylinder.velocity_collection[..., :] += np.array( + [0.0, -normal_velocity, 0.0] + ).reshape(3, 1) + + correct_forces = nu_w * np.array([0.0, normal_velocity, 0.0]).reshape(3, 1) + + cylinder_plane_contact.apply_contact(cylinder, plane) + + assert_allclose(correct_forces, cylinder.external_forces, atol=Tolerance.atol()) + + def test_cylinder_plane_contact_when_cylinder_is_under_plane(self): + """ + This test case tests plane response forces on the cylinder + in the case cylinder is under the plane and pushed towards + the plane. + + """ + + # we move plane on top of the cylinder. Note that 1.0 is radius of the cylinder. + offset_of_plane_with_respect_to_cylinder = 2.0 * 1.0 + + # plane normal changed, it is towards the negative direction, because cylinder + # is under the plane. + plane_normal = np.array([0.0, -1.0, 0.0]) + + [cylinder, plane, cylinder_plane_contact, external_forces] = self.initializer( + shift=offset_of_plane_with_respect_to_cylinder, plane_normal=plane_normal + ) + + cylinder_plane_contact.apply_contact(cylinder, plane) + correct_forces = np.zeros((3, 1)) + assert_allclose(correct_forces, cylinder.external_forces, atol=Tolerance.atol()) + + @pytest.mark.parametrize("k_w", [0.1, 0.5, 1.0, 2, 10]) + def test_cylinder_plane_contact_when_cylinder_is_under_plane_with_k_without_nu( + self, k_w + ): + """ + In this test case we move the cylinder under the plane. + Here wall stiffness coefficient changed parametrically + and damping coefficient set to zero . + Parameters + ---------- + k_w + + """ + # we move plane on top of the cylinder. Note that 1.0 is radius of the cylinder. + offset_of_plane_with_respect_to_cylinder = 2.0 * 1.0 + + # we move plane towards to cylinder by random distance + shift = ( + offset_of_plane_with_respect_to_cylinder - np.random.random_sample(1).item() + ) + + # plane normal changed, it is towards the negative direction, because cylinder + # is under the plane. + plane_normal = np.array([0.0, -1.0, 0.0]) + + [cylinder, plane, cylinder_plane_contact, external_forces] = self.initializer( + shift=shift, k_w=k_w, plane_normal=plane_normal + ) + + # we have to substract cylinder offset because top part + correct_forces = k_w * np.array( + [0.0, shift - offset_of_plane_with_respect_to_cylinder, 0.0] + ).reshape(3, 1) + + cylinder_plane_contact.apply_contact(cylinder, plane) + + assert_allclose(correct_forces, cylinder.external_forces, atol=Tolerance.atol()) + + @pytest.mark.parametrize("nu_w", [0.5, 1.0, 5.0, 7.0, 12.0]) + def test_cylinder_plane_contact_when_cylinder_is_under_plane_without_k_with_nu( + self, nu_w + ): + """ + 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 + ---------- + nu_w + + """ + # we move plane on top of the cylinder. Note that 1.0 is radius of the cylinder. + offset_of_plane_with_respect_to_cylinder = 2.0 * 1.0 + + # plane normal changed, it is towards the negative direction, because cylinder + # is under the plane. + plane_normal = np.array([0.0, -1.0, 0.0]) + + [cylinder, plane, cylinder_plane_contact, external_forces] = self.initializer( + shift=offset_of_plane_with_respect_to_cylinder, + nu_w=nu_w, + plane_normal=plane_normal, + ) + + normal_velocity = np.random.random_sample(1).item() + cylinder.velocity_collection[..., :] += np.array( + [0.0, -normal_velocity, 0.0] + ).reshape(3, 1) + + correct_forces = nu_w * np.array([0.0, normal_velocity, 0.0]).reshape(3, 1) + cylinder_plane_contact.apply_contact(cylinder, plane) + + assert_allclose(correct_forces, cylinder.external_forces, atol=Tolerance.atol()) diff --git a/tests/test_contact_functions.py b/tests/test_contact_functions.py new file mode 100644 index 000000000..f25d5830b --- /dev/null +++ b/tests/test_contact_functions.py @@ -0,0 +1,684 @@ +__doc__ = """ Test specific functions used in contact in Elastica.contact_forces implementation""" + +import numpy as np +from numpy.testing import assert_allclose +from elastica.typing import RodBase +from elastica.rigidbody import Cylinder, Sphere + +from elastica._contact_functions import ( + _calculate_contact_forces_rod_cylinder, + _calculate_contact_forces_rod_rod, + _calculate_contact_forces_self_rod, + _calculate_contact_forces_rod_sphere, +) + + +def mock_rod_init(self): + + "Initializing Rod" + + """ + This is a small rod with 2 elements; + Initial Parameters: + element's radius = 1, length = 1, + tangent vector for both elements is (1, 0, 0), + stationary rod i.e velocity vector of each node is (0, 0, 0), + internal/external forces vectors are also (0, 0, 0) + """ + + self.n_elems = 2 + self.position_collection = np.array([[1, 2, 3], [0, 0, 0], [0, 0, 0]]) + self.radius = np.array([1, 1]) + self.lengths = np.array([1, 1]) + self.tangents = np.array([[1.0, 1.0], [0.0, 0.0], [0.0, 0.0]]) + self.velocity_collection = np.array( + [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]] + ) + self.internal_forces = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]) + self.external_forces = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]) + + +def mock_cylinder_init(self): + + "Initializing Cylinder" + + """ + This is a rigid body cylinder;, + Initial Parameters: + radius = 1, length = 2, + center positioned at origin i.e (0, 0, 0), + cylinder's upright in x,y,z plane thus the director array, + stationary cylinder i.e velocity vector is (0, 0, 0), + external forces and torques vectors are also (0, 0, 0) + """ + + self.n_elems = 1 + self.position = np.array([[0], [0], [0]]) + self.director = np.array( + [[[1.0], [0.0], [0.0]], [[0.0], [1.0], [0.0]], [[0.0], [0.0], [1.0]]] + ) + self.radius = 1.0 + self.length = 2.0 + self.velocity_collection = np.array([[0.0], [0.0], [0.0]]) + self.external_forces = np.array([[0.0], [0.0], [0.0]]) + self.external_torques = np.array([[0.0], [0.0], [0.0]]) + + +def mock_sphere_init(self): + + "Initializing Sphere" + + """ + This is a rigid body sphere;, + Initial Parameters: + radius = 1, + center positioned at origin i.e (0, 0, 0), + sphere's upright in x,y,z plane thus the director array, + stationary sphere i.e velocity vector is (0, 0, 0), + external forces and torques vectors are also (0, 0, 0) + """ + + self.n_elems = 1 + self.position = np.array([[0], [0], [0]]) + self.director = np.array( + [[[1.0], [0.0], [0.0]], [[0.0], [1.0], [0.0]], [[0.0], [0.0], [1.0]]] + ) + self.radius = 1.0 + self.velocity_collection = np.array([[0.0], [0.0], [0.0]]) + self.external_forces = np.array([[0.0], [0.0], [0.0]]) + self.external_torques = np.array([[0.0], [0.0], [0.0]]) + + +MockRod = type("MockRod", (RodBase,), {"__init__": mock_rod_init}) + +MockCylinder = type("MockCylinder", (Cylinder,), {"__init__": mock_cylinder_init}) + +MockSphere = type("MockSphere", (Sphere,), {"__init__": mock_sphere_init}) + + +class TestCalculateContactForcesRodCylinder: + "Class to test the calculate contact forces rod cylinder function" + + "Testing function with handcrafted/calculated values" + + def test_calculate_contact_forces_rod_cylinder_with_k_without_nu_and_friction( + self, + ): + + "initializing rod parameters" + rod = MockRod() + rod_element_position = 0.5 * ( + rod.position_collection[..., 1:] + rod.position_collection[..., :-1] + ) + + "initializing cylinder parameters" + cylinder = MockCylinder() + x_cyl = ( + cylinder.position[..., 0] + - 0.5 * cylinder.length * cylinder.director[2, :, 0] + ) + + "initializing constants" + """ + Setting contact_k = 1 and other parameters to 0, + so the net forces becomes a function of contact forces only. + """ + k = 1.0 + nu = 0 + velocity_damping_coefficient = 0 + friction_coefficient = 0 + + "Function call" + _calculate_contact_forces_rod_cylinder( + rod_element_position, + rod.lengths * rod.tangents, + cylinder.position[..., 0], + x_cyl, + cylinder.length * cylinder.director[2, :, 0], + rod.radius + cylinder.radius, + rod.lengths + cylinder.length, + rod.internal_forces, + rod.external_forces, + cylinder.external_forces, + cylinder.external_torques, + cylinder.director[:, :, 0], + rod.velocity_collection, + cylinder.velocity_collection, + k, + nu, + velocity_damping_coefficient, + friction_coefficient, + ) + + "Test values" + """ + The two systems were placed such that they are penetrating by 0.5 units and + resulting forces act along the x-axis only. + The net force was calculated by halving the contact force i.e + net force = 0.5 * contact force = -0.25; + where, contact force = k(1) * min distance between colliding elements(-1) * gamma(0.5) = -0.5 + The net force is then divided to the nodes of the rod and the cylinder as per indices. + """ + assert_allclose( + cylinder.external_forces, np.array([[-0.5], [0], [0]]), atol=1e-6 + ) + assert_allclose(cylinder.external_torques, np.array([[0], [0], [0]]), atol=1e-6) + assert_allclose( + rod.external_forces, + np.array([[0.166666, 0.333333, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + + def test_calculate_contact_forces_rod_cylinder_with_nu_without_k_and_friction( + self, + ): + + "initializing rod parameters" + rod = MockRod() + "Moving rod towards the cylinder with a velocity of -1 in x-axis" + rod.velocity_collection = np.array([[-1, 0, 0], [-1, 0, 0], [-1, 0, 0]]) + rod_element_position = 0.5 * ( + rod.position_collection[..., 1:] + rod.position_collection[..., :-1] + ) + + "initializing cylinder parameters" + cylinder = MockCylinder() + "Moving cylinder towards the rod with a velocity of 1 in x-axis" + cylinder.velocity_collection = np.array([[1], [0], [0]]) + x_cyl = ( + cylinder.position[..., 0] + - 0.5 * cylinder.length * cylinder.director[2, :, 0] + ) + + "initializing constants" + """ + Setting contact_nu = 1 and other parameters to 0, + so the net forces becomes a function of contact damping forces only. + """ + k = 0.0 + nu = 1.0 + velocity_damping_coefficient = 0 + friction_coefficient = 0 + + "Function call" + _calculate_contact_forces_rod_cylinder( + rod_element_position, + rod.lengths * rod.tangents, + cylinder.position[..., 0], + x_cyl, + cylinder.length * cylinder.director[2, :, 0], + rod.radius + cylinder.radius, + rod.lengths + cylinder.length, + rod.internal_forces, + rod.external_forces, + cylinder.external_forces, + cylinder.external_torques, + cylinder.director[:, :, 0], + rod.velocity_collection, + cylinder.velocity_collection, + k, + nu, + velocity_damping_coefficient, + friction_coefficient, + ) + + "Test values" + """ + The two systems were placed such that they are penetrating by 0.5 units and + resulting forces act along the x-axis only. + The net force was calculated by halving the contact damping force i.e + net force = 0.5 * contact damping force = -0.75; + where, contact damping force = -nu(1) * penetration velocity(1.5)[x-axis] = -1.5 + The net force is then divided to the nodes of the rod and the cylinder as per indices. + """ + assert_allclose( + cylinder.external_forces, np.array([[-1.5], [0], [0]]), atol=1e-6 + ) + assert_allclose(cylinder.external_torques, np.array([[0], [0], [0]]), atol=1e-6) + assert_allclose( + rod.external_forces, + np.array([[0.5, 1, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + + def test_calculate_contact_forces_rod_cylinder_with_k_and_nu_without_friction( + self, + ): + + "initializing rod parameters" + rod = MockRod() + "Moving rod towards the cylinder with a velocity of -1 in x-axis" + rod.velocity_collection = np.array([[-1, 0, 0], [-1, 0, 0], [-1, 0, 0]]) + rod_element_position = 0.5 * ( + rod.position_collection[..., 1:] + rod.position_collection[..., :-1] + ) + + "initializing cylinder parameters" + cylinder = MockCylinder() + "Moving cylinder towards the rod with a velocity of 1 in x-axis" + cylinder.velocity_collection = np.array([[1], [0], [0]]) + x_cyl = ( + cylinder.position[..., 0] + - 0.5 * cylinder.length * cylinder.director[2, :, 0] + ) + + "initializing constants" + """ + Setting contact_nu = 1 and contact_k = 1, + so the net forces becomes a function of contact damping and contact forces. + """ + k = 1.0 + nu = 1.0 + velocity_damping_coefficient = 0 + friction_coefficient = 0 + + "Function call" + _calculate_contact_forces_rod_cylinder( + rod_element_position, + rod.lengths * rod.tangents, + cylinder.position[..., 0], + x_cyl, + cylinder.length * cylinder.director[2, :, 0], + rod.radius + cylinder.radius, + rod.lengths + cylinder.length, + rod.internal_forces, + rod.external_forces, + cylinder.external_forces, + cylinder.external_torques, + cylinder.director[:, :, 0], + rod.velocity_collection, + cylinder.velocity_collection, + k, + nu, + velocity_damping_coefficient, + friction_coefficient, + ) + + "Test values" + """ + For nu and k dependent case, we just have to add both the forces that were generated above. + """ + assert_allclose(cylinder.external_forces, np.array([[-2], [0], [0]]), atol=1e-6) + assert_allclose(cylinder.external_torques, np.array([[0], [0], [0]]), atol=1e-6) + assert_allclose( + rod.external_forces, + np.array([[0.666666, 1.333333, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + + def test_calculate_contact_forces_rod_cylinder_with_k_and_nu_and_friction(self): + + "initializing rod parameters" + rod = MockRod() + "Moving rod towards the cylinder with a velocity of -1 in x-axis" + rod.velocity_collection = np.array([[-1, 0, 0], [-1, 0, 0], [-1, 0, 0]]) + rod_element_position = 0.5 * ( + rod.position_collection[..., 1:] + rod.position_collection[..., :-1] + ) + + "initializing cylinder parameters" + cylinder = MockCylinder() + "Moving cylinder towards the rod with a velocity of 1 in x-axis" + cylinder.velocity_collection = np.array([[1], [0], [0]]) + x_cyl = ( + cylinder.position[..., 0] + - 0.5 * cylinder.length * cylinder.director[2, :, 0] + ) + + "initializing constants" + k = 1.0 + nu = 1.0 + velocity_damping_coefficient = 0.1 + friction_coefficient = 0.1 + + "Function call" + _calculate_contact_forces_rod_cylinder( + rod_element_position, + rod.lengths * rod.tangents, + cylinder.position[..., 0], + x_cyl, + cylinder.length * cylinder.director[2, :, 0], + rod.radius + cylinder.radius, + rod.lengths + cylinder.length, + rod.internal_forces, + rod.external_forces, + cylinder.external_forces, + cylinder.external_torques, + cylinder.director[:, :, 0], + rod.velocity_collection, + cylinder.velocity_collection, + k, + nu, + velocity_damping_coefficient, + friction_coefficient, + ) + + "Test values" + """ + With friction, we have to subtract the frictional forces from the net contact forces. + from above values the frictional forces are calculated as: + coulombic friction force = friction coefficient(0.1) * net contact force before friction(1) * slip_direction_velocity_unitized(0.5^-2) = 0.07071... (for y and z axis) + slip direction friction = velocity damping coefficient(0.1) * slip_direction_velocity(0.5^-2) * slip_direction_velocity_unitized(0.5^-2) = 0.05 (for y and z axis) + the minimum of the two is slip direction friciton, so the frictional force is that only: + friction force = slip direction friction = 0.05 + after applying sign convention and dividing the force among the nodes of rod and cylinder, + we get the following values. + """ + assert_allclose( + cylinder.external_forces, np.array([[-2], [-0.1], [-0.1]]), atol=1e-6 + ) + assert_allclose(cylinder.external_torques, np.array([[0], [0], [0]]), atol=1e-6) + assert_allclose( + rod.external_forces, + np.array( + [ + [0.666666, 1.333333, 0], + [0.033333, 0.066666, 0], + [0.033333, 0.066666, 0], + ] + ), + atol=1e-6, + ) + + +class TestCalculateContactForcesRodRod: + "Function to test the calculate contact forces rod rod function" + + "Testing function with handcrafted/calculated values" + + def test_calculate_contact_forces_rod_rod_with_k_without_nu(self): + + rod_one = MockRod() + rod_two = MockRod() + """Placing rod two such that its first element just touches the last element of rod one.""" + rod_two.position_collection = np.array([[4, 5, 6], [0, 0, 0], [0, 0, 0]]) + + "initializing constants" + """ + Setting contact_k = 1 and nu to 0, + so the net forces becomes a function of contact forces only. + """ + k = 1.0 + nu = 0.0 + + "Function call" + _calculate_contact_forces_rod_rod( + rod_one.position_collection[..., :-1], + rod_one.radius, + rod_one.lengths, + rod_one.tangents, + rod_one.velocity_collection, + rod_one.internal_forces, + rod_one.external_forces, + rod_two.position_collection[..., :-1], + rod_two.radius, + rod_two.lengths, + rod_two.tangents, + rod_two.velocity_collection, + rod_two.internal_forces, + rod_two.external_forces, + k, + nu, + ) + + "Test values" + """ + Resulting forces act along the x-axis only. + The net force was calculated by halving the contact force i.e + net force = 0.5 * contact force = 0.5; + where, contact force = k(1) * min distance between colliding elements(1) * gamma(1) = 1 + The net force is then divided to the nodes of the two rods as per indices. + """ + assert_allclose( + rod_one.external_forces, + np.array( + [[0, -0.666666, -0.333333], [0, 0, 0], [0, 0, 0]], + ), + atol=1e-6, + ) + assert_allclose( + rod_two.external_forces, + np.array([[0.333333, 0.666666, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + + def test_calculate_contact_forces_rod_rod_without_k_with_nu(self): + + rod_one = MockRod() + rod_two = MockRod() + """Placing rod two such that its first element just touches the last element of rod one.""" + rod_two.position_collection = np.array([[4, 5, 6], [0, 0, 0], [0, 0, 0]]) + + """Moving the rods towards each other with a velocity of 1 along the x-axis.""" + rod_one.velocity_collection = np.array([[1, 0, 0], [1, 0, 0], [1, 0, 0]]) + rod_two.velocity_collection = np.array([[-1, 0, 0], [-1, 0, 0], [-1, 0, 0]]) + + "initializing constants" + """ + Setting contact_nu = 1 and nu to 0, + so the net forces becomes a function of contact damping forces only. + """ + k = 0.0 + nu = 1.0 + + "Function call" + _calculate_contact_forces_rod_rod( + rod_one.position_collection[..., :-1], + rod_one.radius, + rod_one.lengths, + rod_one.tangents, + rod_one.velocity_collection, + rod_one.internal_forces, + rod_one.external_forces, + rod_two.position_collection[..., :-1], + rod_two.radius, + rod_two.lengths, + rod_two.tangents, + rod_two.velocity_collection, + rod_two.internal_forces, + rod_two.external_forces, + k, + nu, + ) + + "Test values" + """ + Resulting forces act along the x-axis only. + The net force was calculated by halving the contact damping force i.e + net force = 0.5 * contact damping force = 0.25; + where, contact damping force = nu(1) * penetration velocity(0.5)[x-axis] = 0.5 + The net force is then divided to the nodes of the two rods as per indices. + """ + assert_allclose( + rod_one.external_forces, + np.array( + [[0, -0.333333, -0.166666], [0, 0, 0], [0, 0, 0]], + ), + atol=1e-6, + ) + assert_allclose( + rod_two.external_forces, + np.array([[0.166666, 0.333333, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + + def test_calculate_contact_forces_rod_rod_with_k_and_nu(self): + + rod_one = MockRod() + rod_two = MockRod() + """Placing rod two such that its first element just touches the last element of rod one.""" + rod_two.position_collection = np.array([[4, 5, 6], [0, 0, 0], [0, 0, 0]]) + + """Moving the rods towards each other with a velocity of 1 along the x-axis.""" + rod_one.velocity_collection = np.array([[1, 0, 0], [1, 0, 0], [1, 0, 0]]) + rod_two.velocity_collection = np.array([[-1, 0, 0], [-1, 0, 0], [-1, 0, 0]]) + + "initializing constants" + """ + Setting contact_nu = 1 and contact_k = 1, + so the net forces becomes a function of contact damping and contact forces. + """ + k = 1.0 + nu = 1.0 + + "Function call" + _calculate_contact_forces_rod_rod( + rod_one.position_collection[..., :-1], + rod_one.radius, + rod_one.lengths, + rod_one.tangents, + rod_one.velocity_collection, + rod_one.internal_forces, + rod_one.external_forces, + rod_two.position_collection[..., :-1], + rod_two.radius, + rod_two.lengths, + rod_two.tangents, + rod_two.velocity_collection, + rod_two.internal_forces, + rod_two.external_forces, + k, + nu, + ) + + "Test values" + """ + For nu and k dependent case, we just have to add both the forces that were generated above. + """ + assert_allclose( + rod_one.external_forces, + np.array( + [[0, -1, -0.5], [0, 0, 0], [0, 0, 0]], + ), + atol=1e-6, + ) + assert_allclose( + rod_two.external_forces, + np.array([[0.5, 1, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + + +def test_calculate_contact_forces_self_rod(): + "Function to test the calculate contact forces self rod function" + + "Testing function with handcrafted/calculated values" + + rod = MockRod() + """Changing rod parameters to establish self contact in rod; + elements are placed such that the a 'U' rod is formed in the x-y plane, + where the rod is penetrating itself by 0.5 units by radius.""" + rod.n_elems = 3 + rod.position_collection = np.array([[1, 4, 4, 1], [0, 0, 1, 1], [0, 0, 0, 0]]) + rod.radius = np.array([1, 1, 1]) + rod.lengths = np.array([3, 1, 3]) + rod.tangents = np.array([[1.0, 0.0, -1.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]]) + rod.velocity_collection = np.array( + [[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]] + ) + rod.internal_forces = np.array( + [[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]] + ) + rod.external_forces = np.array( + [[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]] + ) + + "initializing constants" + k = 1.0 + nu = 1.0 + + "Function call" + _calculate_contact_forces_self_rod( + rod.position_collection[..., :-1], + rod.radius, + rod.lengths, + rod.tangents, + rod.velocity_collection, + rod.external_forces, + k, + nu, + ) + + "Test values" + """Resulting forces act along the y-axis only. + Since the rod is stationary i.e velocity = 0, the net force is a function of contact force only. + The net force was calculated by halving the contact force i.e + net force = 0.5 * contact force = -0.5; + where, contact force = k(1) * minimum distance between colliding elements centres(-1) * gamma(1) = -1 + The net force is then divided to the nodes of the rod as per indices.""" + assert_allclose( + rod.external_forces, + np.array( + [[0, 0, 0, 0], [-0.333333, -0.666666, 0.666666, 0.333333], [0, 0, 0, 0]] + ), + atol=1e-6, + ) + + +class TestCalculateContactForcesRodSphere: + "Class to test the calculate contact forces rod sphere function" + + "Testing function with handcrafted/calculated values" + + def test_calculate_contact_forces_rod_sphere_with_k_without_nu_and_friction( + self, + ): + + "initializing rod parameters" + rod = MockRod() + rod_element_position = 0.5 * ( + rod.position_collection[..., 1:] + rod.position_collection[..., :-1] + ) + + "initializing sphere parameters" + sphere = MockSphere() + x_sph = sphere.position[..., 0] - sphere.radius * sphere.director[2, :, 0] + + "initializing constants" + """ + Setting contact_k = 1 and other parameters to 0, + so the net forces becomes a function of contact forces only. + """ + k = 1.0 + nu = 0 + velocity_damping_coefficient = 0 + friction_coefficient = 0 + + "Function call" + _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, + nu, + velocity_damping_coefficient, + friction_coefficient, + ) + + "Test values" + """ + The two systems were placed such that they are penetrating by 0.5 units and + resulting forces act along the x-axis only. + The net force was calculated by halving the contact force i.e + net force = 0.5 * contact force = -0.25; + where, contact force = k(1) * min distance between colliding elements(-1) * gamma(0.5) = -0.5 + The net force is then divided to the nodes of the rod and the sphere as per indices. + """ + assert_allclose(sphere.external_forces, np.array([[-0.5], [0], [0]]), atol=1e-6) + assert_allclose(sphere.external_torques, np.array([[0], [0], [0]]), atol=1e-6) + assert_allclose( + rod.external_forces, + np.array([[0.166666, 0.333333, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) diff --git a/tests/test_contact_utils.py b/tests/test_contact_utils.py new file mode 100644 index 000000000..e459d8154 --- /dev/null +++ b/tests/test_contact_utils.py @@ -0,0 +1,581 @@ +__doc__ = """ Test helper functions in elastica/contact_util.py used for contact""" + +import pytest +import numpy as np +from numpy.testing import assert_allclose +from elastica.utils import Tolerance +from elastica.typing import RodBase +from elastica.rigidbody import Cylinder, Sphere +from elastica.contact_utils import ( + _dot_product, + _norm, + _clip, + _out_of_bounds, + _find_min_dist, + _aabbs_not_intersecting, + _prune_using_aabbs_rod_cylinder, + _prune_using_aabbs_rod_rod, + _prune_using_aabbs_rod_sphere, + _find_slipping_elements, + _node_to_element_mass_or_force, + _elements_to_nodes_inplace, + _node_to_element_position, + _node_to_element_velocity, +) + + +class TestDotProduct: + "class to test the dot product function" + + @pytest.mark.parametrize("ndim", [3]) + def test_dot_product_using_numpy(self, ndim): + """ + This method was generated by "copilot" in VS code; + This method uses numpy dot product to compare with the output of our function, + numpy dot product uses an optimized implementation that takes advantage of + hardware-specific optimizations such as SIMD. + """ + vector1 = np.random.randn(ndim) + vector2 = np.random.randn(ndim) + dot_product = _dot_product(vector1, vector2) + assert_allclose(dot_product, np.dot(vector1, vector2)) + + def test_dot_product_with_verified_values(self): + "Testing function with analytically verified values" + + "test for parallel vectors" + vec1 = [1, 0, 0] + vec2 = [2, 0, 0] + dot_product = _dot_product(vec1, vec2) + assert_allclose(dot_product, 2) + """Calcutlations : vec1 . vect2 + [1, 0, 0] . [2, 0, 0] + 1*2 + 0*0 + 0*0 + 2 (Answer)""" + + "test for perpendicular vectors" + vec1 = [1, 0, 0] + vec2 = [0, 1, 0] + dot_product = _dot_product(vec1, vec2) + assert_allclose(dot_product, 0) + """Calcutlations : vec1 . vect2 + [1, 0, 0] . [0, 1, 0] + 1*0 + 0*1 + 0*0 + 0 (Answer)""" + + "test for opposite vectors" + vec1 = [1, 0, 0] + vec2 = [-1, 0, 0] + dot_product = _dot_product(vec1, vec2) + assert_allclose(dot_product, -1) + """Calcutlations : vec1 . vect2 + [1, 0, 0] . [-1, 0, 0] + 1*-1 + 0*0 + 0*0 + -1 (Answer)""" + + "test for arbitrary vectors" + vec1 = [1, -2, 3] + vec2 = [-2, 1, 3] + dot_product = _dot_product(vec1, vec2) + assert_allclose(dot_product, 5) + """Calcutlations : vec1 . vect2 + [1, -2, 3] . [-2, 1, 3] + 1*-2 + -2*1 + 3*3 + -2 - 2 + 9 + 5 (Answer)""" + + +def test_norm_with_verified_values(): + "Function to test the _norm function" + + "Testing function with analytically verified values" + + "test for null vector" + vec1 = [0, 0, 0] + norm = _norm(vec1) + assert_allclose(norm, 0) + """Calcutlations : sqrt(0^2 + 0^2 + 0^2) + sqrt(0 + 0 + 0) + 0 (Answer)""" + + "test for unit vector" + vec1 = [1, 1, 1] + norm = _norm(vec1) + assert_allclose(norm, 1.7320508075688772) + """Calcutlations : sqrt(1^2 + 1^2 + 1^2) + sqrt(1 + 1 + 1) + sqrt(3) + 1.7320508075688772 (Answer)""" + + "test for arbitrary natural number vector" + vec1 = [1, 2, 3] + norm = _norm(vec1) + assert_allclose(norm, 3.7416573867739413) + """Calcutlations : sqrt(1^2 + 2^2 + 3^2) + sqrt(1 + 4 + 9) + sqrt(14) + 3.7416573867739413 (Answer)""" + + "test for decimal values vector" + vec1 = [0.001, 0.002, 0.003] + norm = _norm(vec1) + assert_allclose(norm, 0.0037416573867739412) + """Calcutlations : sqrt(0.001^2 + 0.002^2 + 0.003^2) + sqrt(0.000001 + 0.000004 + 0.000009) + sqrt(0.000014) + 0.0037416573867739412 (Answer)""" + + +@pytest.mark.parametrize( + "x, result", + [(0.5, 1), (1.5, 1.5), (2.5, 2)], +) +def test_clip_with_verified_values(x, result): + + "Function to test the _clip function" + + """_clip is a simple comparison function; + The tests are made to get _clip to return 'low', 'x', 'high' in the respective order""" + + low = 1.0 + high = 2.0 + assert _clip(x, low, high) == result + + +@pytest.mark.parametrize( + "x, result", + [(0.5, 1), (1.5, 0), (2.5, 1)], +) +def test_out_of_bounds_with_verified_values(x, result): + + "Function to test the _out_of_bounds function" + + """_out_of_bounds returns 1 if x < low or x > high, else returns 0""" + + low = 1.0 + high = 2.0 + assert _out_of_bounds(x, low, high) == result + + +def test_find_min_dist(): + "Function to test the _find_min_dist function" + + "testing function with analytically verified values" + + "intersecting lines" + x1 = np.array([0, 0, 0]) + e1 = np.array([1, 1, 1]) + # line 1 starts along origin and points towards (1,1,1) + x2 = np.array([0, 1, 0]) + e2 = np.array([1, 0, 1]) + # line 2 starts along (0,1,0) and points towards (1,0,1) + min_dist_vec, contact_point_of_system2, contact_point_of_system1 = _find_min_dist( + x1, e1, x2, e2 + ) + assert_allclose(min_dist_vec, [0, 0, 0]) + # since the lines intersect, the minimum distance is 0. + assert_allclose(contact_point_of_system2, [1, 1, 1]) + # the contact point of line 2 and line 1 is (1,1,1) + assert_allclose(contact_point_of_system1, [-1, -1, -1]) + # the function returns -1 for contact point of system 1 in case of intersecting lines + + "non intersecting lines" + x1 = np.array([0, 0, 0]) + e1 = np.array([1, 0, 0]) + # line 1 starts along origin and points towards (1,0,0) + x2 = np.array([0, 1, 0]) + e2 = np.array([0, 0, 1]) + # line 2 starts along (0,1,0) and points towards (0,0,1) + min_dist_vec, contact_point_of_system2, contact_point_of_system1 = _find_min_dist( + x1, e1, x2, e2 + ) + assert_allclose(min_dist_vec, [0, 1, 0]) + # minimum distance is 1 unit(verified using GeoGebra 3D calculator visualisation) + assert_allclose(contact_point_of_system2, [0, 1, 0]) + assert_allclose(contact_point_of_system1, [0, 0, 0]) + + "parallel lines" + x1 = np.array([0, 0, 0]) + e1 = np.array([1, 0, 0]) + # line 1 starts along origin and points towards (1,0,0) + x2 = np.array([0, 1, 0]) + e2 = np.array([1, 1, 0]) + # line 2 starts along (0,1,0) and points towards (1,1,0) + min_dist_vec, contact_point_of_system2, contact_point_of_system1 = _find_min_dist( + x1, e1, x2, e2 + ) + assert_allclose(min_dist_vec, [0, 1, 0]) + # minimum distance is 1 unit(verified using GeoGebra 3D calculator visualisation) + assert_allclose(contact_point_of_system2, [0, 1, 0]) + assert_allclose(contact_point_of_system1, [0, 0, 0]) + + +def test_aabbs_not_intersecting(): + "Function to test the _aabb_intersecting function" + + "testing function with analytically verified values" + + " intersecting boxes" + aabb_one = np.array([[0, 0], [0, 0], [0, 0]]) + aabb_two = np.array([[0, 0], [0, 0], [0, 0]]) + "both boxes are overlapping perfectly thus intersecting" + assert _aabbs_not_intersecting(aabb_one, aabb_two) == 0 + + " non Intersecting boxes" + aabb_one = np.array([[0, 1], [0, 1], [0, 1]]) + "box one is a unit size cube in the first quadrant with origin as one of its vertices" + aabb_two = np.array([[2, 3], [2, 3], [2, 3]]) + "box two is a unit size cube in the first quadrant with (2,2,2) as the closest vertex to box 1" + assert _aabbs_not_intersecting(aabb_one, aabb_two) == 1 + + +def mock_rod_init(self): + + "Initializing Rod" + + """ + This is a small rod with 2 elements; + Initial Parameters: + element's radius = 1, length = 1, + tangent vector for both elements is (1, 0, 0), + stationary rod i.e velocity vector of each node is (0, 0, 0), + internal/external forces vectors are also (0, 0, 0) + """ + + self.n_elems = 2 + self.position_collection = np.array([[1, 2, 3], [0, 0, 0], [0, 0, 0]]) + self.radius = np.array([1, 1]) + self.lengths = np.array([1, 1]) + self.tangents = np.array([[1.0, 1.0], [0.0, 0.0], [0.0, 0.0]]) + self.velocity_collection = np.array( + [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]] + ) + self.internal_forces = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]) + self.external_forces = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]) + + +def mock_cylinder_init(self): + + "Initializing Cylinder" + + """ + This is a rigid body cylinder;, + Initial Parameters: + radius = 1, length = 2, + center positioned at origin i.e (0, 0, 0), + cylinder's upright in x,y,z plane thus the director array, + stationary cylinder i.e velocity vector is (0, 0, 0), + external forces and torques vectors are also (0, 0, 0) + """ + + self.n_elems = 1 + self.position = np.array([[0], [0], [0]]) + self.director = np.array( + [[[1.0], [0.0], [0.0]], [[0.0], [1.0], [0.0]], [[0.0], [0.0], [1.0]]] + ) + self.radius = 1.0 + self.length = 2.0 + self.velocity_collection = np.array([[0.0], [0.0], [0.0]]) + self.external_forces = np.array([[0.0], [0.0], [0.0]]) + self.external_torques = np.array([[0.0], [0.0], [0.0]]) + + +def mock_sphere_init(self): + + "Initializing Sphere" + + """ + This is a rigid body sphere;, + Initial Parameters: + radius = 1, + center positioned at origin i.e (0, 0, 0), + sphere's upright in x,y,z plane thus the director array, + stationary sphere i.e velocity vector is (0, 0, 0), + external forces and torques vectors are also (0, 0, 0) + """ + + self.n_elems = 1 + self.position = np.array([[0], [0], [0]]) + self.director = np.array( + [[[1.0], [0.0], [0.0]], [[0.0], [1.0], [0.0]], [[0.0], [0.0], [1.0]]] + ) + self.radius = 1.0 + self.velocity_collection = np.array([[0.0], [0.0], [0.0]]) + self.external_forces = np.array([[0.0], [0.0], [0.0]]) + self.external_torques = np.array([[0.0], [0.0], [0.0]]) + + +MockRod = type("MockRod", (RodBase,), {"__init__": mock_rod_init}) + +MockCylinder = type("MockCylinder", (Cylinder,), {"__init__": mock_cylinder_init}) + +MockSphere = type("MockSphere", (Sphere,), {"__init__": mock_sphere_init}) + + +def test_prune_using_aabbs_rod_cylinder(): + "Function to test the prune using aabbs rod cylinder function" + + "Testing function with analytically verified values" + + "Intersecting rod and cylinder" + + """ + Since both the initialized rod and cylinder are overlapping in 3D space at (1, 1, 1); + Hence they are intersectiong and the function should return 0 + """ + rod = MockRod() + cylinder = MockCylinder() + assert ( + _prune_using_aabbs_rod_cylinder( + rod.position_collection, + rod.radius, + rod.lengths, + cylinder.position, + cylinder.director, + cylinder.radius, + cylinder.length, + ) + == 0 + ) + + "Non - Intersecting rod and cylinder" + rod = MockRod() + cylinder = MockCylinder() + + """ + Changing the position of cylinder in 3D space so the rod and cylinder don't overlap/intersect. + """ + cylinder.position = np.array([[20], [3], [4]]) + assert ( + _prune_using_aabbs_rod_cylinder( + rod.position_collection, + rod.radius, + rod.lengths, + cylinder.position, + cylinder.director, + cylinder.radius, + cylinder.length, + ) + == 1 + ) + + +def test_prune_using_aabbs_rod_rod(): + "Function to test the prune using aabbs rod rod function" + + "Testing function with analytically verified values" + + "Intersecting rod and rod" + """ + Since both the rods have same position, node's radius and length, they are overlapping/intersecting in 3D space. + """ + rod_one = MockRod() + rod_two = MockRod() + assert ( + _prune_using_aabbs_rod_rod( + rod_one.position_collection, + rod_one.radius, + rod_one.lengths, + rod_two.position_collection, + rod_two.radius, + rod_two.lengths, + ) + == 0 + ) + + "Non - Intersecting rod and rod" + """ + Changing the position of rod_two in 3D space so the rod_one and rod_two don't overlap/intersect. + """ + rod_two.position_collection = np.array([[20, 21, 22], [0, 0, 0], [0, 0, 0]]) + assert ( + _prune_using_aabbs_rod_rod( + rod_one.position_collection, + rod_one.radius, + rod_one.lengths, + rod_two.position_collection, + rod_two.radius, + rod_two.lengths, + ) + == 1 + ) + + +def test_prune_using_aabbs_rod_sphere(): + "Function to test the prune using aabbs rod sphere function" + + "Testing function with analytically verified values" + + "Intersecting rod and sphere" + """ + Since both the rod and sphere are overlapping in 3D space at (1, 1, 1); + Hence they are intersectiong and the function should return 0 + """ + rod = MockRod() + sphere = MockSphere() + assert ( + _prune_using_aabbs_rod_sphere( + rod.position_collection, + rod.radius, + rod.lengths, + sphere.position, + sphere.director, + sphere.radius, + ) + == 0 + ) + + "Non - Intersecting rod and sphere" + rod = MockRod() + sphere = MockSphere() + + """ + Changing the position of sphere in 3D space so the rod and sphere don't overlap/intersect. + """ + sphere.position = np.array([[20], [3], [4]]) + assert ( + _prune_using_aabbs_rod_sphere( + rod.position_collection, + rod.radius, + rod.lengths, + sphere.position, + sphere.director, + sphere.radius, + ) + == 1 + ) + + +class TestRodPlaneAuxiliaryFunctions: + @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) + def test_linear_interpolation_slip(self, n_elem): + velocity_threshold = 1.0 + + # if slip velocity larger than threshold + velocity_slip = np.repeat( + np.array([0.0, 0.0, 2.0]).reshape(3, 1), n_elem, axis=1 + ) + slip_function = _find_slipping_elements(velocity_slip, velocity_threshold) + correct_slip_function = np.zeros(n_elem) + assert_allclose(correct_slip_function, slip_function, atol=Tolerance.atol()) + + # if slip velocity smaller than threshold + velocity_slip = np.repeat( + np.array([0.0, 0.0, 0.0]).reshape(3, 1), n_elem, axis=1 + ) + slip_function = _find_slipping_elements(velocity_slip, velocity_threshold) + correct_slip_function = np.ones(n_elem) + assert_allclose(correct_slip_function, slip_function, atol=Tolerance.atol()) + + # if slip velocity smaller than threshold but very close to threshold + velocity_slip = np.repeat( + np.array([0.0, 0.0, 1.0 - 1e-6]).reshape(3, 1), n_elem, axis=1 + ) + slip_function = _find_slipping_elements(velocity_slip, velocity_threshold) + correct_slip_function = np.ones(n_elem) + assert_allclose(correct_slip_function, slip_function, atol=Tolerance.atol()) + + # if slip velocity larger than threshold but very close to threshold + velocity_slip = np.repeat( + np.array([0.0, 0.0, 1.0 + 1e-6]).reshape(3, 1), n_elem, axis=1 + ) + slip_function = _find_slipping_elements(velocity_slip, velocity_threshold) + correct_slip_function = np.ones(n_elem) - 1e-6 + assert_allclose(correct_slip_function, slip_function, atol=Tolerance.atol()) + + # if half of the array slip velocity is larger than threshold and half of it + # smaller than threshold + velocity_slip = np.hstack( + ( + np.repeat(np.array([0.0, 0.0, 2.0]).reshape(3, 1), n_elem, axis=1), + np.repeat(np.array([0.0, 0.0, 0.0]).reshape(3, 1), n_elem, axis=1), + ) + ) + slip_function = _find_slipping_elements(velocity_slip, velocity_threshold) + correct_slip_function = np.hstack((np.zeros(n_elem), np.ones(n_elem))) + assert_allclose(correct_slip_function, slip_function, atol=Tolerance.atol()) + + @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) + def test_node_to_element_mass_or_force(self, n_elem): + random_vector = np.random.rand(3).reshape(3, 1) + input = np.repeat(random_vector, n_elem + 1, axis=1) + input[..., 0] *= 0.5 + input[..., -1] *= 0.5 + correct_output = np.repeat(random_vector, n_elem, axis=1) + output = _node_to_element_mass_or_force(input) + assert_allclose(correct_output, output, atol=Tolerance.atol()) + assert_allclose(np.sum(input), np.sum(output), atol=Tolerance.atol()) + + # These functions are used in the case if Numba is available + class TestGeneralAuxiliaryFunctions: + @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) + def test_node_to_element_position(self, n_elem): + """ + This function tests _node_to_element_position function. We are + converting node positions to element positions. Here also + we are using numba to speed up the process. + + Parameters + ---------- + n_elem + + Returns + ------- + + """ + random = np.random.rand() # Adding some random numbers + input_position = random * np.ones((3, n_elem + 1)) + correct_output = random * np.ones((3, n_elem)) + + output = _node_to_element_position(input_position) + assert_allclose(correct_output, output, atol=Tolerance.atol()) + + @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) + def test_node_to_element_velocity(self, n_elem): + """ + This function tests _node_to_element_velocity function. We are + converting node velocities to element velocities. Here also + we are using numba to speed up the process. + + Parameters + ---------- + n_elem + + Returns + ------- + + """ + random = np.random.rand() # Adding some random numbers + input_velocity = random * np.ones((3, n_elem + 1)) + input_mass = 2.0 * random * np.ones(n_elem + 1) + correct_output = random * np.ones((3, n_elem)) + + output = _node_to_element_velocity( + mass=input_mass, node_velocity_collection=input_velocity + ) + assert_allclose(correct_output, output, atol=Tolerance.atol()) + + @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) + def test_elements_to_nodes_inplace(self, n_elem): + """ + This function _elements_to_nodes_inplace. We are + converting node velocities to element velocities. Here also + we are using numba to speed up the process. + + Parameters + ---------- + n_elem + + Returns + ------- + + """ + random = np.random.rand() # Adding some random numbers + vector_in_element_frame = random * np.ones((3, n_elem)) + vector_in_node_frame = np.zeros((3, n_elem + 1)) + correct_output = vector_in_node_frame.copy() + correct_output[:, :n_elem] += 0.5 * vector_in_element_frame + correct_output[:, 1 : n_elem + 1] += 0.5 * vector_in_element_frame + + _elements_to_nodes_inplace(vector_in_element_frame, vector_in_node_frame) + assert_allclose(correct_output, vector_in_node_frame, atol=Tolerance.atol()) diff --git a/tests/test_dissipation.py b/tests/test_dissipation.py index 5d6ae1367..6fc390423 100644 --- a/tests/test_dissipation.py +++ b/tests/test_dissipation.py @@ -13,7 +13,7 @@ import pytest -from tests.test_rod.test_rods import MockTestRod, MockTestRingRod +from tests.test_rod.mock_rod import MockTestRod, MockTestRingRod def test_damper_base(): diff --git a/tests/test_interaction.py b/tests/test_interaction.py index 8d72598c6..8d2b0e13c 100644 --- a/tests/test_interaction.py +++ b/tests/test_interaction.py @@ -9,11 +9,16 @@ find_slipping_elements, AnisotropicFrictionalPlane, node_to_element_mass_or_force, - nodes_to_elements, SlenderBodyTheory, + nodes_to_elements, + elements_to_nodes_inplace, + apply_normal_force_numba_rigid_body, +) +from elastica.contact_utils import ( + _node_to_element_mass_or_force, ) -from tests.test_rod.test_rods import MockTestRod +from tests.test_rod.mock_rod import MockTestRod class BaseRodClass(MockTestRod): @@ -319,63 +324,36 @@ def test_interaction_when_rod_is_under_plane_without_k_with_nu(self, n_elem, nu_ class TestAuxiliaryFunctions: @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - def test_linear_interpolation_slip(self, n_elem): + def test_linear_interpolation_slip_error_message(self, n_elem): velocity_threshold = 1.0 # if slip velocity larger than threshold velocity_slip = np.repeat( np.array([0.0, 0.0, 2.0]).reshape(3, 1), n_elem, axis=1 ) - slip_function = find_slipping_elements(velocity_slip, velocity_threshold) - correct_slip_function = np.zeros(n_elem) - assert_allclose(correct_slip_function, slip_function, atol=Tolerance.atol()) - - # if slip velocity smaller than threshold - velocity_slip = np.repeat( - np.array([0.0, 0.0, 0.0]).reshape(3, 1), n_elem, axis=1 - ) - slip_function = find_slipping_elements(velocity_slip, velocity_threshold) - correct_slip_function = np.ones(n_elem) - assert_allclose(correct_slip_function, slip_function, atol=Tolerance.atol()) - - # if slip velocity smaller than threshold but very close to threshold - velocity_slip = np.repeat( - np.array([0.0, 0.0, 1.0 - 1e-6]).reshape(3, 1), n_elem, axis=1 - ) - slip_function = find_slipping_elements(velocity_slip, velocity_threshold) - correct_slip_function = np.ones(n_elem) - assert_allclose(correct_slip_function, slip_function, atol=Tolerance.atol()) - - # if slip velocity larger than threshold but very close to threshold - velocity_slip = np.repeat( - np.array([0.0, 0.0, 1.0 + 1e-6]).reshape(3, 1), n_elem, axis=1 - ) - slip_function = find_slipping_elements(velocity_slip, velocity_threshold) - correct_slip_function = np.ones(n_elem) - 1e-6 - assert_allclose(correct_slip_function, slip_function, atol=Tolerance.atol()) - - # if half of the array slip velocity is larger than threshold and half of it - # smaller than threshold - velocity_slip = np.hstack( - ( - np.repeat(np.array([0.0, 0.0, 2.0]).reshape(3, 1), n_elem, axis=1), - np.repeat(np.array([0.0, 0.0, 0.0]).reshape(3, 1), n_elem, axis=1), - ) + error_message = ( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._find_slipping_elements()\n" + "instead for finding slipping elements." ) - slip_function = find_slipping_elements(velocity_slip, velocity_threshold) - correct_slip_function = np.hstack((np.zeros(n_elem), np.ones(n_elem))) - assert_allclose(correct_slip_function, slip_function, atol=Tolerance.atol()) + with pytest.raises(NotImplementedError) as error_info: + slip_function = find_slipping_elements(velocity_slip, velocity_threshold) + assert error_info.value.args[0] == error_message @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - def test_node_to_element_mass_or_force(self, n_elem): + def test_node_to_element_mass_or_force_error_message(self, n_elem): random_vector = np.random.rand(3).reshape(3, 1) input = np.repeat(random_vector, n_elem + 1, axis=1) input[..., 0] *= 0.5 input[..., -1] *= 0.5 - correct_output = np.repeat(random_vector, n_elem, axis=1) - output = node_to_element_mass_or_force(input) - assert_allclose(correct_output, output, atol=Tolerance.atol()) - assert_allclose(np.sum(input), np.sum(output), atol=Tolerance.atol()) + error_message = ( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._node_to_element_mass_or_force()\n" + "instead for converting the mass/forces on rod nodes to elements." + ) + with pytest.raises(NotImplementedError) as error_info: + output = node_to_element_mass_or_force(input) + assert error_info.value.args[0] == error_message @pytest.mark.parametrize("n_elem", [2, 10]) def test_not_impl_error_for_nodes_to_elements(self, n_elem): @@ -581,7 +559,7 @@ def test_kinetic_rolling_friction(self, n_elem, velocity, omega): ) assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - forces_on_elements = node_to_element_mass_or_force(external_forces_collection) + forces_on_elements = _node_to_element_mass_or_force(external_forces_collection) correct_torques = np.zeros((3, n_elem)) correct_torques[2] += ( -1.0 @@ -622,7 +600,7 @@ def test_static_rolling_friction_total_force_smaller_than_static_friction_force( 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) + forces_on_elements = _node_to_element_mass_or_force(external_forces_collection) correct_torques = np.zeros((3, n_elem)) correct_torques[2] += ( -1.0 @@ -665,7 +643,7 @@ def test_static_rolling_friction_total_force_larger_than_static_friction_force( ) * 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) + forces_on_elements = _node_to_element_mass_or_force(external_forces_collection) correct_torques = np.zeros((3, n_elem)) correct_torques[2] += ( -1.0 @@ -751,7 +729,7 @@ def test_static_rolling_friction_total_torque_larger_than_static_friction_force( ) assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - forces_on_elements = node_to_element_mass_or_force(external_forces_collection) + 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 @@ -794,7 +772,7 @@ def test_sum_over_elements(self, n_elem): assert_allclose(correct_output, output, atol=Tolerance.atol()) @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - def test_node_to_element_position(self, n_elem): + def test_node_to_element_position_error_message(self, n_elem): """ This function tests node_to_element_position function. We are converting node positions to element positions. Here also @@ -812,11 +790,17 @@ def test_node_to_element_position(self, n_elem): input_position = random * np.ones((3, n_elem + 1)) correct_output = random * np.ones((3, n_elem)) - output = node_to_element_position(input_position) - assert_allclose(correct_output, output, atol=Tolerance.atol()) + error_message = ( + "This function is removed in v0.3.2. For node-to-element_position() interpolation please use: \n" + "elastica.contact_utils._node_to_element_position() for rod position \n" + "For detail, refer to issue #113." + ) + with pytest.raises(NotImplementedError) as error_info: + output = node_to_element_position(input_position) + assert error_info.value.args[0] == error_message @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - def test_node_to_element_velocity(self, n_elem): + def test_node_to_element_velocity_error_message(self, n_elem): """ This function tests node_to_element_velocity function. We are converting node velocities to element velocities. Here also @@ -835,10 +819,16 @@ def test_node_to_element_velocity(self, n_elem): input_mass = 2.0 * random * np.ones(n_elem + 1) correct_output = random * np.ones((3, n_elem)) - output = node_to_element_velocity( - mass=input_mass, node_velocity_collection=input_velocity + error_message = ( + "This function is removed in v0.3.2. For node-to-element_velocity() interpolation please use: \n" + "elastica.contact_utils._node_to_element_velocity() for rod velocity. \n" + "For detail, refer to issue #113." ) - assert_allclose(correct_output, output, atol=Tolerance.atol()) + with pytest.raises(NotImplementedError) as error_info: + output = node_to_element_velocity( + mass=input_mass, node_velocity_collection=input_velocity + ) + assert error_info.value.args[0] == error_message @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) def test_not_impl_error_for_node_to_element_pos_or_vel(self, n_elem): @@ -846,14 +836,41 @@ def test_not_impl_error_for_node_to_element_pos_or_vel(self, n_elem): input_velocity = random * np.ones((3, n_elem + 1)) error_message = ( "This function is removed in v0.3.0. For node-to-element interpolation please use: \n" - "elastica.interaction.node_to_element_position() for rod position \n" - "elastica.interaction.node_to_element_velocity() for rod velocity. \n" + "elastica.contact_utils._node_to_element_position() for rod position \n" + "elastica.contact_utils._node_to_element_velocity() for rod velocity. \n" "For detail, refer to issue #80." ) with pytest.raises(NotImplementedError) as error_info: node_to_element_pos_or_vel(input_velocity) assert error_info.value.args[0] == error_message + @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) + def test_elements_to_nodes_inplace_error_message(self, n_elem): + """ + This function tests _elements_to_nodes_inplace. We are + converting node velocities to element velocities. Here also + we are using numba to speed up the process. + + Parameters + ---------- + n_elem + + Returns + ------- + + """ + random = np.random.rand() # Adding some random numbers + vector_in_element_frame = random * np.ones((3, n_elem)) + vector_in_node_frame = np.zeros((3, n_elem + 1)) + error_message = ( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._elements_to_nodes_inplace()\n" + "instead for updating nodal forces using the forces computed on elements." + ) + with pytest.raises(NotImplementedError) as error_info: + elements_to_nodes_inplace(vector_in_element_frame, vector_in_node_frame) + assert error_info.value.args[0] == error_message + except ImportError: pass @@ -994,3 +1011,42 @@ def test_slender_body_matrix_product_only_xy(self, n_elem): slender_body_theory.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_apply_normal_force_numba_rigid_body_error_message(n_elem): + """ + This function _elements_to_nodes_inplace. We are + converting node velocities to element velocities. Here also + we are using numba to speed up the process. + + Parameters + ---------- + n_elem + + Returns + ------- + + """ + + position_collection = np.zeros((3, n_elem + 1)) + position_collection[0, :] = np.linspace(0, 1.0, n_elem + 1) + + error_message = ( + "This function is removed in v0.3.2. For cylinder plane contact please use: \n" + "elastica._contact_functions._calculate_contact_forces_cylinder_plane() \n" + "For detail, refer to issue #113." + ) + with pytest.raises(NotImplementedError) as error_info: + apply_normal_force_numba_rigid_body( + plane_origin=np.array([0.0, 0.0, 0.0]), + plane_normal=np.array([0.0, 0.0, 1.0]), + surface_tol=1e-4, + k=1.0, + nu=1.0, + length=1.0, + position_collection=position_collection, + velocity_collection=np.zeros((3, n_elem + 1)), + external_forces=np.zeros((3, n_elem + 1)), + ) + assert error_info.value.args[0] == error_message diff --git a/tests/test_joint.py b/tests/test_joint.py index aa4b9579e..d016226a2 100644 --- a/tests/test_joint.py +++ b/tests/test_joint.py @@ -13,6 +13,8 @@ import numpy as np import pytest from scipy.spatial.transform import Rotation +from elastica.joint import ExternalContact, SelfContact + # TODO: change tests and made them independent of rod, at least assigin hardcoded values for forces and torques @@ -372,3 +374,650 @@ def test_fixedjoint(rest_euler_angle): assert_allclose( rod2.external_torques[..., rod2_index], torque_rod2, atol=Tolerance.atol() ) + + +from elastica.joint import ( + _dot_product, + _norm, + _clip, + _out_of_bounds, + _find_min_dist, + _aabbs_not_intersecting, +) + + +@pytest.mark.parametrize("ndim", [2, 3, 5, 10, 20]) +def test_dot_product_error_message(ndim): + vector1 = np.random.randn(ndim) + vector2 = np.random.randn(ndim) + error_message = ( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._dot_product()\n" + "instead for find the dot product between a and b." + ) + with pytest.raises(NotImplementedError) as error_info: + dot_product = _dot_product(vector1, vector2) + assert error_info.value.args[0] == error_message + + +@pytest.mark.parametrize("ndim", [2, 3, 5, 10, 20]) +def test_norm_error_message(ndim): + vec1 = np.random.randn(ndim) + error_message = ( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._norm()\n" + "instead for finding the norm of a." + ) + with pytest.raises(NotImplementedError) as error_info: + norm = _norm(vec1) + assert error_info.value.args[0] == error_message + + +@pytest.mark.parametrize( + "x, result", + [(0.5, 1), (1.5, 1.5), (2.5, 2)], +) +def test_clip_error_message(x, result): + low = 1.0 + high = 2.0 + error_message = ( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._clip()\n" + "instead for clipping x." + ) + with pytest.raises(NotImplementedError) as error_info: + _clip(x, low, high) + assert error_info.value.args[0] == error_message + + +@pytest.mark.parametrize( + "x, result", + [(0.5, 1), (1.5, 1.5), (2.5, 2)], +) +def test_out_of_bounds_error_message(x, result): + low = 1.0 + high = 2.0 + error_message = ( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._out_of_bounds()\n" + "instead for checking if x is out of bounds." + ) + with pytest.raises(NotImplementedError) as error_info: + _out_of_bounds(x, low, high) + assert error_info.value.args[0] == error_message + + +def test_find_min_dist_error_message(): + x1 = np.array([0, 0, 0]) + e1 = np.array([1, 1, 1]) + x2 = np.array([0, 1, 0]) + e2 = np.array([1, 0, 1]) + + error_message = ( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._find_min_dist()\n" + "instead for finding minimum distance between contact points." + ) + with pytest.raises(NotImplementedError) as error_info: + ( + min_dist_vec, + contact_point_of_system2, + contact_point_of_system1, + ) = _find_min_dist(x1, e1, x2, e2) + assert error_info.value.args[0] == error_message + + +def test_aabbs_not_intersecting_error_message(): + aabb_one = np.array([[0, 0], [0, 0], [0, 0]]) + aabb_two = np.array([[0, 0], [0, 0], [0, 0]]) + error_message = ( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._aabbs_not_intersecting()\n" + "instead for checking aabbs intersection." + ) + with pytest.raises(NotImplementedError) as error_info: + _aabbs_not_intersecting(aabb_one, aabb_two) + assert error_info.value.args[0] == error_message + + +from elastica.typing import RodBase, RigidBodyBase +from elastica.joint import ( + _prune_using_aabbs_rod_rigid_body, + _prune_using_aabbs_rod_rod, + _calculate_contact_forces_rod_rigid_body, + _calculate_contact_forces_rod_rod, + _calculate_contact_forces_self_rod, +) + + +def mock_rod_init(self): + + "Initializing Rod" + "Details of initialization are given in test_contact_specific_functions.py" + + self.n_elems = 2 + self.position_collection = np.array([[1, 2, 3], [0, 0, 0], [0, 0, 0]]) + self.radius = np.array([1, 1]) + self.lengths = np.array([1, 1]) + self.tangents = np.array([[1.0, 1.0], [0.0, 0.0], [0.0, 0.0]]) + self.internal_forces = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]) + self.external_forces = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]) + self.velocity_collection = np.array( + [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]] + ) + + +def mock_rigid_body_init(self): + + "Initializing Rigid Body" + "Details of initialization are given in test_contact_specific_functions.py" + + self.n_elems = 1 + self.position_collection = np.array([[0], [0], [0]]) + self.director_collection = np.array( + [[[1.0], [0.0], [0.0]], [[0.0], [1.0], [0.0]], [[0.0], [0.0], [1.0]]] + ) + self.radius = np.array([1.0]) + self.length = np.array([2.0]) + self.external_forces = np.array([[0.0], [0.0], [0.0]]) + self.external_torques = np.array([[0.0], [0.0], [0.0]]) + self.velocity_collection = np.array([[0.0], [0.0], [0.0]]) + + +MockRod = type("MockRod", (RodBase,), {"__init__": mock_rod_init}) + +MockRigidBody = type( + "MockRigidBody", (RigidBodyBase,), {"__init__": mock_rigid_body_init} +) + + +def test_prune_using_aabbs_rod_rigid_body_error_message(): + rod = MockRod() + cylinder = MockRigidBody() + error_message = ( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._prune_using_aabbs_rod_cylinder()\n" + "instead for checking rod cylinder intersection." + ) + with pytest.raises(NotImplementedError) as error_info: + _prune_using_aabbs_rod_rigid_body( + rod.position_collection, + rod.radius, + rod.lengths, + cylinder.position_collection, + cylinder.director_collection, + cylinder.radius, + cylinder.length, + ) + assert error_info.value.args[0] == error_message + + +def test_prune_using_aabbs_rod_rod_error_message(): + rod_one = MockRod() + rod_two = MockRod() + error_message = ( + "This function is removed in v0.3.2. Please use\n" + "elastica.contact_utils._prune_using_aabbs_rod_rod()\n" + "instead for checking rod rod intersection." + ) + with pytest.raises(NotImplementedError) as error_info: + _prune_using_aabbs_rod_rod( + rod_one.position_collection, + rod_one.radius, + rod_one.lengths, + rod_two.position_collection, + rod_two.radius, + rod_two.lengths, + ) + assert error_info.value.args[0] == error_message + + +def test_calculate_contact_forces_rod_rigid_body_error_message(): + + "initializing rod parameters" + rod = MockRod() + rod_element_position = 0.5 * ( + rod.position_collection[..., 1:] + rod.position_collection[..., :-1] + ) + + "initializing cylinder parameters" + cylinder = MockRigidBody() + x_cyl = ( + cylinder.position_collection[..., 0] + - 0.5 * cylinder.length * cylinder.director_collection[2, :, 0] + ) + + "initializing constants" + """ + Setting contact_k = 1 and other parameters to 0, + so the net forces becomes a function of contact forces only. + """ + k = 1.0 + nu = 0 + velocity_damping_coefficient = 0 + friction_coefficient = 0 + + error_message = ( + "This function is removed in v0.3.2. Please use\n" + "elastica._contact_functions._calculate_contact_forces_rod_cylinder()\n" + "instead for calculating rod cylinder contact forces." + ) + + "Function call" + with pytest.raises(NotImplementedError) as error_info: + _calculate_contact_forces_rod_rigid_body( + rod_element_position, + rod.lengths * rod.tangents, + cylinder.position_collection[..., 0], + x_cyl, + cylinder.length * cylinder.director_collection[2, :, 0], + rod.radius + cylinder.radius, + rod.lengths + cylinder.length, + rod.internal_forces, + rod.external_forces, + cylinder.external_forces, + cylinder.external_torques, + cylinder.director_collection[:, :, 0], + rod.velocity_collection, + cylinder.velocity_collection, + k, + nu, + velocity_damping_coefficient, + friction_coefficient, + ) + assert error_info.value.args[0] == error_message + + +def test_calculate_contact_forces_rod_rod_error_message(): + + rod_one = MockRod() + rod_two = MockRod() + """Placing rod two such that its first element just touches the last element of rod one.""" + rod_two.position_collection = np.array([[4, 5, 6], [0, 0, 0], [0, 0, 0]]) + + "initializing constants" + """ + Setting contact_k = 1 and nu to 0, + so the net forces becomes a function of contact forces only. + """ + k = 1.0 + nu = 0.0 + + error_message = ( + "This function is removed in v0.3.2. Please use\n" + "elastica._contact_functions._calculate_contact_forces_rod_rod()\n" + "instead for calculating rod rod contact forces." + ) + + "Function call" + with pytest.raises(NotImplementedError) as error_info: + _calculate_contact_forces_rod_rod( + rod_one.position_collection[..., :-1], + rod_one.radius, + rod_one.lengths, + rod_one.tangents, + rod_one.velocity_collection, + rod_one.internal_forces, + rod_one.external_forces, + rod_two.position_collection[..., :-1], + rod_two.radius, + rod_two.lengths, + rod_two.tangents, + rod_two.velocity_collection, + rod_two.internal_forces, + rod_two.external_forces, + k, + nu, + ) + assert error_info.value.args[0] == error_message + + +def test_calculate_contact_forces_self_rod_error_message(): + "Function to test the calculate contact forces self rod function" + + "Testing function with handcrafted/calculated values" + + rod = MockRod() + + "initializing constants" + k = 1.0 + nu = 1.0 + + error_message = ( + "This function is removed in v0.3.2. Please use\n" + "elastica._contact_functions._calculate_contact_forces_self_rod()\n" + "instead for calculating rod self-contact forces." + ) + + "Function call" + with pytest.raises(NotImplementedError) as error_info: + _calculate_contact_forces_self_rod( + rod.position_collection[..., :-1], + rod.radius, + rod.lengths, + rod.tangents, + rod.velocity_collection, + rod.external_forces, + k, + nu, + ) + assert error_info.value.args[0] == error_message + + +class TestExternalContact: + def test_external_contact_rod_rigid_body_with_collision_with_k_without_nu_and_friction( + self, + ): + + "Testing External Contact wrapper with Collision with analytical verified values" + + mock_rod = MockRod() + mock_rigid_body = MockRigidBody() + ext_contact = ExternalContact(k=1.0, nu=0.0) + ext_contact.apply_forces(mock_rod, 0, mock_rigid_body, 1) + + """Details and reasoning about the values are given in 'test_contact_specific_functions.py/test_claculate_contact_forces_rod_rigid_body()'""" + assert_allclose( + mock_rod.external_forces, + np.array([[0.166666, 0.333333, 0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]), + atol=1e-6, + ) + + assert_allclose( + mock_rigid_body.external_forces, np.array([[-0.5], [0.0], [0.0]]), atol=1e-6 + ) + + assert_allclose( + mock_rigid_body.external_torques, np.array([[0.0], [0.0], [0.0]]), atol=1e-6 + ) + + def test_external_contact_rod_rigid_body_with_collision_with_nu_without_k_and_friction( + self, + ): + + "Testing External Contact wrapper with Collision with analytical verified values" + + mock_rod = MockRod() + "Moving rod towards the cylinder with a velocity of -1 in x-axis" + mock_rod.velocity_collection = np.array([[-1, 0, 0], [-1, 0, 0], [-1, 0, 0]]) + mock_rigid_body = MockRigidBody() + "Moving cylinder towards the rod with a velocity of 1 in x-axis" + mock_rigid_body.velocity_collection = np.array([[1], [0], [0]]) + ext_contact = ExternalContact(k=0.0, nu=1.0) + ext_contact.apply_forces(mock_rod, 0, mock_rigid_body, 1) + + """Details and reasoning about the values are given in 'test_contact_specific_functions.py/test_claculate_contact_forces_rod_rigid_body()'""" + assert_allclose( + mock_rod.external_forces, + np.array([[0.5, 1, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + + assert_allclose( + mock_rigid_body.external_forces, np.array([[-1.5], [0], [0]]), atol=1e-6 + ) + + assert_allclose( + mock_rigid_body.external_torques, np.array([[0.0], [0.0], [0.0]]), atol=1e-6 + ) + + def test_external_contact_rod_rigid_body_with_collision_with_k_and_nu_without_friction( + self, + ): + + "Testing External Contact wrapper with Collision with analytical verified values" + + mock_rod = MockRod() + "Moving rod towards the cylinder with a velocity of -1 in x-axis" + mock_rod.velocity_collection = np.array([[-1, 0, 0], [-1, 0, 0], [-1, 0, 0]]) + mock_rigid_body = MockRigidBody() + "Moving cylinder towards the rod with a velocity of 1 in x-axis" + mock_rigid_body.velocity_collection = np.array([[1], [0], [0]]) + ext_contact = ExternalContact(k=1.0, nu=1.0) + ext_contact.apply_forces(mock_rod, 0, mock_rigid_body, 1) + + """Details and reasoning about the values are given in 'test_contact_specific_functions.py/test_claculate_contact_forces_rod_rigid_body()'""" + assert_allclose( + mock_rod.external_forces, + np.array([[0.666666, 1.333333, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + + assert_allclose( + mock_rigid_body.external_forces, np.array([[-2], [0], [0]]), atol=1e-6 + ) + + assert_allclose( + mock_rigid_body.external_torques, np.array([[0.0], [0.0], [0.0]]), atol=1e-6 + ) + + def test_external_contact_rod_rigid_body_with_collision_with_k_and_nu_and_friction( + self, + ): + + "Testing External Contact wrapper with Collision with analytical verified values" + + mock_rod = MockRod() + "Moving rod towards the cylinder with a velocity of -1 in x-axis" + mock_rod.velocity_collection = np.array([[-1, 0, 0], [-1, 0, 0], [-1, 0, 0]]) + mock_rigid_body = MockRigidBody() + "Moving cylinder towards the rod with a velocity of 1 in x-axis" + mock_rigid_body.velocity_collection = np.array([[1], [0], [0]]) + ext_contact = ExternalContact( + k=1.0, nu=1.0, velocity_damping_coefficient=0.1, friction_coefficient=0.1 + ) + ext_contact.apply_forces(mock_rod, 0, mock_rigid_body, 1) + + """Details and reasoning about the values are given in 'test_contact_specific_functions.py/test_claculate_contact_forces_rod_rigid_body()'""" + assert_allclose( + mock_rod.external_forces, + np.array( + [ + [0.666666, 1.333333, 0], + [0.033333, 0.066666, 0], + [0.033333, 0.066666, 0], + ] + ), + atol=1e-6, + ) + + assert_allclose( + mock_rigid_body.external_forces, np.array([[-2], [-0.1], [-0.1]]), atol=1e-6 + ) + + assert_allclose( + mock_rigid_body.external_torques, np.array([[0.0], [0.0], [0.0]]), atol=1e-6 + ) + + def test_external_contact_rod_rigid_body_without_collision(self): + + "Testing External Contact wrapper without Collision with analytical verified values" + + mock_rod = MockRod() + mock_rigid_body = MockRigidBody() + ext_contact = ExternalContact(k=1.0, nu=0.5) + + """Setting rigid body position such that there is no collision""" + mock_rigid_body.position_collection = np.array([[400], [500], [600]]) + mock_rod_external_forces_before_execution = mock_rod.external_forces.copy() + mock_rigid_body_external_forces_before_execution = ( + mock_rigid_body.external_forces.copy() + ) + mock_rigid_body_external_torques_before_execution = ( + mock_rigid_body.external_torques.copy() + ) + ext_contact.apply_forces(mock_rod, 0, mock_rigid_body, 1) + + assert_allclose( + mock_rod.external_forces, mock_rod_external_forces_before_execution + ) + assert_allclose( + mock_rigid_body.external_forces, + mock_rigid_body_external_forces_before_execution, + ) + assert_allclose( + mock_rigid_body.external_torques, + mock_rigid_body_external_torques_before_execution, + ) + + def test_external_contact_with_two_rods_with_collision_with_k_without_nu(self): + + "Testing External Contact wrapper with two rods with analytical verified values" + "Test values have been copied from 'test_contact_specific_functions.py/test_calculate_contact_forces_rod_rod()'" + + mock_rod_one = MockRod() + mock_rod_two = MockRod() + mock_rod_two.position_collection = np.array([[4, 5, 6], [0, 0, 0], [0, 0, 0]]) + ext_contact = ExternalContact(k=1.0, nu=0.0) + ext_contact.apply_forces(mock_rod_one, 0, mock_rod_two, 0) + + assert_allclose( + mock_rod_one.external_forces, + np.array([[0, -0.666666, -0.333333], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + assert_allclose( + mock_rod_two.external_forces, + np.array([[0.333333, 0.666666, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + + def test_external_contact_with_two_rods_with_collision_without_k_with_nu(self): + + "Testing External Contact wrapper with two rods with analytical verified values" + "Test values have been copied from 'test_contact_specific_functions.py/test_calculate_contact_forces_rod_rod()'" + + mock_rod_one = MockRod() + mock_rod_two = MockRod() + + """Moving the rods towards each other with a velocity of 1 along the x-axis.""" + mock_rod_one.velocity_collection = np.array([[1, 0, 0], [1, 0, 0], [1, 0, 0]]) + mock_rod_two.velocity_collection = np.array( + [[-1, 0, 0], [-1, 0, 0], [-1, 0, 0]] + ) + mock_rod_two.position_collection = np.array([[4, 5, 6], [0, 0, 0], [0, 0, 0]]) + ext_contact = ExternalContact(k=0.0, nu=1.0) + ext_contact.apply_forces(mock_rod_one, 0, mock_rod_two, 0) + + assert_allclose( + mock_rod_one.external_forces, + np.array( + [[0, -0.333333, -0.166666], [0, 0, 0], [0, 0, 0]], + ), + atol=1e-6, + ) + assert_allclose( + mock_rod_two.external_forces, + np.array([[0.166666, 0.333333, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + + def test_external_contact_with_two_rods_with_collision_with_k_and_nu(self): + + "Testing External Contact wrapper with two rods with analytical verified values" + "Test values have been copied from 'test_contact_specific_functions.py/test_calculate_contact_forces_rod_rod()'" + + mock_rod_one = MockRod() + mock_rod_two = MockRod() + + """Moving the rods towards each other with a velocity of 1 along the x-axis.""" + mock_rod_one.velocity_collection = np.array([[1, 0, 0], [1, 0, 0], [1, 0, 0]]) + mock_rod_two.velocity_collection = np.array( + [[-1, 0, 0], [-1, 0, 0], [-1, 0, 0]] + ) + mock_rod_two.position_collection = np.array([[4, 5, 6], [0, 0, 0], [0, 0, 0]]) + ext_contact = ExternalContact(k=1.0, nu=1.0) + ext_contact.apply_forces(mock_rod_one, 0, mock_rod_two, 0) + + assert_allclose( + mock_rod_one.external_forces, + np.array( + [[0, -1, -0.5], [0, 0, 0], [0, 0, 0]], + ), + atol=1e-6, + ) + assert_allclose( + mock_rod_two.external_forces, + np.array([[0.5, 1, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + + def test_external_contact_with_two_rods_without_collision(self): + + "Testing External Contact wrapper with two rods with analytical verified values" + + mock_rod_one = MockRod() + mock_rod_two = MockRod() + + "Setting rod two position such that there is no collision" + mock_rod_two.position_collection = np.array( + [[100, 101, 102], [0, 0, 0], [0, 0, 0]] + ) + ext_contact = ExternalContact(k=1.0, nu=1.0) + mock_rod_one_external_forces_before_execution = ( + mock_rod_one.external_forces.copy() + ) + mock_rod_two_external_forces_before_execution = ( + mock_rod_two.external_forces.copy() + ) + ext_contact.apply_forces(mock_rod_one, 0, mock_rod_two, 0) + + assert_allclose( + mock_rod_one.external_forces, mock_rod_one_external_forces_before_execution + ) + assert_allclose( + mock_rod_two.external_forces, mock_rod_two_external_forces_before_execution + ) + + +class TestSelfContact: + def test_self_contact_with_rod_self_collision(self): + + "Testing Self Contact wrapper rod self collision with analytical verified values" + + mock_rod = MockRod() + + "Test values have been copied from 'test_contact_specific_functions.py/test_calculate_contact_forces_self_rod()'" + mock_rod.n_elems = 3 + mock_rod.position_collection = np.array( + [[1, 4, 4, 1], [0, 0, 1, 1], [0, 0, 0, 0]] + ) + mock_rod.radius = np.array([1, 1, 1]) + mock_rod.lengths = np.array([3, 1, 3]) + mock_rod.tangents = np.array( + [[1.0, 0.0, -1.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0]] + ) + mock_rod.velocity_collection = np.array( + [[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]] + ) + mock_rod.internal_forces = np.array( + [[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]] + ) + mock_rod.external_forces = np.array( + [[0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]] + ) + sel_contact = SelfContact(k=1.0, nu=0.0) + sel_contact.apply_forces(mock_rod, 0, mock_rod, 0) + + assert_allclose( + mock_rod.external_forces, + np.array( + [[0, 0, 0, 0], [-0.333333, -0.666666, 0.666666, 0.333333], [0, 0, 0, 0]] + ), + atol=1e-6, + ) + + def test_self_contact_with_rod_no_self_collision(self): + + "Testing Self Contact wrapper rod no self collision with analytical verified values" + + mock_rod = MockRod() + + "the initially set rod does not have self collision" + mock_rod_external_forces_before_execution = mock_rod.external_forces.copy() + sel_contact = SelfContact(k=1.0, nu=1.0) + sel_contact.apply_forces(mock_rod, 0, mock_rod, 0) + + assert_allclose( + mock_rod.external_forces, mock_rod_external_forces_before_execution + ) diff --git a/tests/test_mesh_init.py b/tests/test_mesh_init.py new file mode 100644 index 000000000..038fb9d34 --- /dev/null +++ b/tests/test_mesh_init.py @@ -0,0 +1,244 @@ +__doc__ = """ Test mesh initialization with attributes in Elastica """ + +from elastica.mesh.mesh_initializer import Mesh +import numpy as np +from numpy.testing import assert_allclose +from sys import platform + + +""" +A dummy cube mesh stl file is used for testing at tests/cube.stl +This dummy file was created using the open source code for creating meshes using 'numpy-stl' +in numpy-stl documentation (https://numpy-stl.readthedocs.io/en/latest/usage.html#initial-usage) +""" + + +def cube_mesh_stl_init(): + """ + This function initializes a new cube mesh in stl file format. + """ + if platform == "win32": + path = r"tests\cube.stl" + else: + path = r"tests/cube.stl" + + mockmesh = Mesh(path) + return mockmesh + + +""" +A dummy cube mesh obj file is used for testing at tests/cube.obj +This dummy file was created manually by guidelines at https://all3dp.com/1/obj-file-format-3d-printing-cad/ +""" + + +def cube_mesh_obj_init(): + """ + This function initializes a new cube mesh in obj file format. + """ + if platform == "win32": + path = r"tests\cube.obj" + else: + path = r"tests/cube.obj" + + mockmesh = Mesh(path) + return mockmesh + + +def test_mesh_faces(): + """ + This functions tests the geometry of faces generated. + """ + + """ + Testing cube mesh in stl file format. + """ + mockmesh = cube_mesh_stl_init() + calculated_faces = np.array( + [ + [ + [-1, 1, -1, -1, -1, -1, 1, 1, 1, -1, -1, -1], + [-1, -1, -1, -1, 1, 1, 1, 1, -1, -1, 1, 1], + [1, 1, -1, -1, 1, -1, 1, 1, 1, 1, 1, -1], + ], + [ + [-1, -1, -1, -1, -1, -1, -1, -1, 1, 1, -1, -1], + [1, 1, -1, 1, -1, 1, -1, 1, 1, 1, -1, -1], + [-1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -1], + ], + [ + [-1, -1, -1, -1, 1, 1, 1, 1, -1, -1, -1, -1], + [-1, -1, 1, 1, 1, 1, -1, -1, -1, 1, -1, 1], + [-1, -1, 1, -1, 1, 1, -1, 1, 1, 1, 1, 1], + ], + ] + ) + + assert_allclose(mockmesh.faces, calculated_faces) + + """ + Testing cube mesh in obj file format. + """ + mockmesh = cube_mesh_obj_init() + calculated_faces = np.array( + [ + [ + [1, 1, 1, 1, -1, 1], + [1, 1, 1, 1, -1, 1], + [-1, -1, 1, -1, -1, -1], + [-1, -1, 1, -1, -1, -1], + ], + [ + [-1, 1, -1, -1, -1, -1], + [-1, 1, -1, 1, -1, 1], + [-1, 1, 1, 1, 1, 1], + [-1, 1, 1, -1, 1, -1], + ], + [ + [-1, -1, -1, -1, -1, 1], + [1, 1, 1, -1, 1, 1], + [1, 1, 1, -1, 1, 1], + [-1, -1, -1, -1, -1, 1], + ], + ] + ) + assert_allclose(mockmesh.faces, calculated_faces) + + +def test_face_normals(): + """ + This functions tests the face normals of the cube mesh. + """ + + """ + Testing cube mesh in stl file format. + """ + mockmesh = cube_mesh_stl_init() + calculated_face_normals = np.array( + [ + [0, 0, -1, -1, 0, 0, 1, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, -1, -1], + [-1, -1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0], + ] + ) + assert_allclose(mockmesh.face_normals, calculated_face_normals) + + """ + Testing cube mesh in obj file format. + """ + mockmesh = cube_mesh_obj_init() + calculated_face_normals = np.array( + [[0, 0, 1, 0, -1, 0], [-1, 1, 0, 0, 0, 0], [0, 0, 0, -1, 0, 1]] + ) + assert_allclose(mockmesh.face_normals, calculated_face_normals) + + +def test_face_centers(): + """ + This functions tests the face centers of the cube mesh. + """ + + """ + Testing cube mesh in stl file format. + """ + mockmesh = cube_mesh_stl_init() + calculated_face_centers = np.array( + [ + [ + -0.333333, + 0.333333, + -1, + -1, + 0.333333, + -0.333333, + 1, + 1, + 0.333333, + -0.333333, + 0.333333, + -0.333333, + ], + [ + -0.333333, + 0.333333, + -0.333333, + 0.333333, + -0.333333, + 0.333333, + -0.333333, + 0.333333, + 1, + 1, + -1, + -1, + ], + [ + -1, + -1, + 0.333333, + -0.333333, + 1, + 1, + -0.333333, + 0.333333, + -0.333333, + 0.333333, + -0.333333, + 0.333333, + ], + ] + ) + assert_allclose(mockmesh.face_centers, calculated_face_centers, atol=1e-6) + + """ + Testing cube mesh in obj file format. + """ + mockmesh = cube_mesh_obj_init() + calculated_face_centers = np.array( + [[0, 0, 1, 0, -1, 0], [-1, 1, 0, 0, 0, 0], [0, 0, 0, -1, 0, 1]] + ) + assert_allclose(mockmesh.face_centers, calculated_face_centers, atol=1e-6) + + +def test_mesh_scale(): + """ + This functions tests the scaling of the cube mesh. + """ + mockmeshstl = cube_mesh_stl_init() + mockmeshobj = cube_mesh_obj_init() + """ + The scale of both the cubes mesh(stl & obj) is 2 in all directions because + its a uniform cube with side 2 situated at origin. + """ + calculated_mesh_scale = np.array([2, 2, 2]) + assert_allclose(mockmeshstl.mesh_scale, calculated_mesh_scale) + assert_allclose(mockmeshobj.mesh_scale, calculated_mesh_scale) + + +def test_mesh_center(): + """ + This functions tests the center of the cube mesh. + """ + mockmeshstl = cube_mesh_stl_init() + mockmeshobj = cube_mesh_obj_init() + """ + Both the cubes(stl & obj) are situated at origin. + """ + calculated_mesh_center = np.array([0, 0, 0]) + assert_allclose(mockmeshstl.mesh_center, calculated_mesh_center) + assert_allclose(mockmeshobj.mesh_center, calculated_mesh_center) + + +def test_mesh_orientation(): + """ + This functions tests the orientation of the cube mesh. + """ + mockmeshstl = cube_mesh_stl_init() + mockmeshobj = cube_mesh_obj_init() + """ + Both the cubes(stl & obj) are situated at origin and the initial orientation is upright + in the general 3-D cartesian plane. + """ + calculated_mesh_orientation = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) + assert_allclose(mockmeshstl.mesh_orientation, calculated_mesh_orientation) + assert_allclose(mockmeshobj.mesh_orientation, calculated_mesh_orientation) diff --git a/tests/test_mesh_methods.py b/tests/test_mesh_methods.py new file mode 100644 index 000000000..317f5ffcf --- /dev/null +++ b/tests/test_mesh_methods.py @@ -0,0 +1,270 @@ +__doc__ = """ Test mesh class's methods in Elastica """ + +from elastica.mesh.mesh_initializer import Mesh +import numpy as np +from numpy.testing import assert_allclose +from sys import platform + + +""" +A dummy cube mesh stl file is used for testing at tests/cube.stl +This dummy file was created using the open source code for creating meshes using 'numpy-stl' +in numpy-stl documentation (https://numpy-stl.readthedocs.io/en/latest/usage.html#initial-usage) +""" + + +def cube_mesh_stl_init(): + """ + This function initializes a new cube mesh in stl file format. + """ + if platform == "win32": + path = r"tests\cube.stl" + else: + path = r"tests/cube.stl" + + mockmesh = Mesh(path) + return mockmesh + + +""" +A dummy cube mesh obj file is used for testing at tests/cube.obj +This dummy file was created manually by guidelines at https://all3dp.com/1/obj-file-format-3d-printing-cad/ +""" + + +def cube_mesh_obj_init(): + """ + This function initializes a new cube mesh in obj file format. + """ + if platform == "win32": + path = r"tests\cube.obj" + else: + path = r"tests/cube.obj" + + mockmesh = Mesh(path) + return mockmesh + + +"""def test_visualize(): + This function tests the visualization of the mesh. + Running this test will open a new window with the mesh visualization. + mockmesh = cube_mesh_init() + try: + mockmesh.visualize() + except: + raise RuntimeError("Visualization failed")""" + + +def test_mesh_translate(): + """ + This function tests the translation of the mesh. + """ + mockmeshstl = cube_mesh_stl_init() + mockmeshobj = cube_mesh_obj_init() + + """ + By default both the cubes's(stl & obj) centers are to be situated at the origin, + lets move its center to [1,1,1] + """ + target_center = np.array([1, 1, 1]) + + """ + This is pyvista's numpy array that stores the bounds of the mesh. + format: [xmin, xmax, ymin, ymax, zmin, zmax] + since we translated the uniform cube with edge 2 to center [1,1,1], + the new bounds are: [0,2,0,2,0,2] + """ + target_bounds = np.array([0, 2, 0, 2, 0, 2]) + + """ + Similarly the face centers will also be translated from initial position, + or moved by [1, 1, 1]; + checkout initial position in test_mesh_init.py. + Since mesh face centers are calculated using mesh vertices, + if the face centers are correct, the mesh vertices are also correct. + """ + target_face_centers_stl = np.array( + [ + [ + 0.666666, + 1.333333, + 0, + 0, + 1.333333, + 0.666666, + 2, + 2, + 1.333333, + 0.666666, + 1.333333, + 0.666666, + ], + [ + 0.666666, + 1.333333, + 0.666666, + 1.333333, + 0.666666, + 1.333333, + 0.666666, + 1.333333, + 2, + 2, + 0, + 0, + ], + [ + 0, + 0, + 1.333333, + 0.666666, + 2, + 2, + 0.666666, + 1.333333, + 0.666666, + 1.333333, + 0.666666, + 1.333333, + ], + ] + ) + target_face_centers_obj = np.array( + [[1, 1, 2, 1, 0, 1], [0, 2, 1, 1, 1, 1], [1, 1, 1, 0, 1, 2]] + ) + "Translating the mesh" + mockmeshstl.translate(target_center) + mockmeshobj.translate(target_center) + + "Testing the translation for both the stl and obj meshes" + assert_allclose(mockmeshstl.mesh_center, target_center) + assert_allclose(mockmeshobj.mesh_center, target_center) + assert_allclose(mockmeshstl.mesh.bounds, target_bounds) + assert_allclose(mockmeshobj.mesh.bounds, target_bounds) + assert_allclose(mockmeshstl.face_centers, target_face_centers_stl, atol=1e-6) + assert_allclose(mockmeshobj.face_centers, target_face_centers_obj, atol=1e-6) + + +def test_mesh_scale(): + """ + This function tests the scaling of the mesh. + """ + mockmeshstl = cube_mesh_stl_init() + mockmeshobj = cube_mesh_obj_init() + scaling_factor = np.array([2, 2, 2]) + + """ + This is pyvista's numpy array that stores the bounds of the mesh. + format: [xmin, xmax, ymin, ymax, zmin, zmax] + since we scaled the uniform cube with edge 2 by 2 situated with center at [0, 0, 0], + the new bounds are: [-2,2,-2,2,-2,2] + """ + target_bounds = np.array([-2, 2, -2, 2, -2, 2]) + + """ + Similarly the face centers will also be translated from initial position, + or multiplied by 2; + checkout initial position in test_mesh_init.py. + Since mesh face centers are calculated using mesh vertices, + if the face centers are correct, the mesh vertices are also correct. + """ + target_face_centers_stl = np.array( + [ + [ + -0.666666, + 0.666666, + -2, + -2, + 0.666666, + -0.666666, + 2, + 2, + 0.666666, + -0.666666, + 0.666666, + -0.666666, + ], + [ + -0.666666, + 0.666666, + -0.666666, + 0.666666, + -0.666666, + 0.666666, + -0.666666, + 0.666666, + 2, + 2, + -2, + -2, + ], + [ + -2, + -2, + 0.666666, + -0.666666, + 2, + 2, + -0.666666, + 0.666666, + -0.666666, + 0.666666, + -0.666666, + 0.666666, + ], + ] + ) + target_face_centers_obj = np.array( + [[0, 0, 2, 0, -2, 0], [-2, 2, 0, 0, 0, 0], [0, 0, 0, -2, 0, 2]] + ) + + "Scaling the mesh" + mockmeshstl.scale(scaling_factor) + mockmeshobj.scale(scaling_factor) + "Testing the scaling for both the stl and obj meshes" + assert_allclose(mockmeshstl.mesh.bounds, target_bounds) + assert_allclose(mockmeshobj.mesh.bounds, target_bounds) + assert_allclose(mockmeshstl.face_centers, target_face_centers_stl, atol=1e-6) + assert_allclose(mockmeshobj.face_centers, target_face_centers_obj, atol=1e-6) + + +def test_mesh_rotate(): + """ + This function tests the rotation of the mesh. + """ + mockmeshstl = cube_mesh_stl_init() + mockmeshobj = cube_mesh_obj_init() + rotation_angle = 90.0 + rotation_axis = np.array([1, 0, 0]) + + """ + Checkout the formatting of bounds in above tests. + """ + target_bounds = np.array([-1, 1, -1, 1, -2, 2]) + + """ + First we scale the uniform cube mesh in y direction + by a factor of 2, so that it becomes a cuboid. + Then we rotate the cuboid by 90 degrees about x axis, + so the longer edge of the cuboid is now in z direction; + then we test the bounds as formatted above. + """ + mockmeshstl.scale(np.array([1, 2, 1])) + mockmeshobj.scale(np.array([1, 2, 1])) + "Rotating the mesh" + mockmeshstl.rotate(rotation_axis, rotation_angle) + mockmeshobj.rotate(rotation_axis, rotation_angle) + "Testing the bounds for both the stl and obj meshes" + assert_allclose(mockmeshstl.mesh.bounds, target_bounds) + assert_allclose(mockmeshobj.mesh.bounds, target_bounds) + + """ + Testing the final orientation of the meshes + """ + correct_orientation_after_rotation = np.array([[1, 0, 0], [0, 0, 1], [0, -1, 0]]) + assert_allclose( + mockmeshstl.mesh_orientation, correct_orientation_after_rotation, atol=1e-6 + ) + assert_allclose( + mockmeshobj.mesh_orientation, correct_orientation_after_rotation, atol=1e-6 + ) diff --git a/tests/test_modules/test_base_system.py b/tests/test_modules/test_base_system.py index 094f2a3ae..ebb3b7c3e 100644 --- a/tests/test_modules/test_base_system.py +++ b/tests/test_modules/test_base_system.py @@ -74,8 +74,16 @@ def test_extend_allowed_types(self, load_collection): from elastica.rod import RodBase from elastica.rigidbody import RigidBodyBase - - assert bsc.allowed_sys_types == (RodBase, RigidBodyBase, int, float, str) + from elastica.surface import SurfaceBase + + assert bsc.allowed_sys_types == ( + RodBase, + RigidBodyBase, + SurfaceBase, + int, + float, + str, + ) def test_extend_correctness(self, load_collection): """ diff --git a/tests/test_modules/test_connections.py b/tests/test_modules/test_connections.py index 59292e740..bdc2715a4 100644 --- a/tests/test_modules/test_connections.py +++ b/tests/test_modules/test_connections.py @@ -4,6 +4,8 @@ from elastica.modules import Connections from elastica.modules.connections import _Connect +from numpy.testing import assert_allclose +from elastica.utils import Tolerance class TestConnect: @@ -172,13 +174,16 @@ def mock_init(self, *args, **kwargs): # Actual test is here, this should not throw with pytest.raises(TypeError) as excinfo: _ = connect() - assert "Unable to construct" in str(excinfo.value) + assert ( + r"Unable to construct connection class.\nDid you provide all necessary joint properties?" + == str(excinfo.value) + ) class TestConnectionsMixin: from elastica.modules import BaseSystemCollection - class SystemCollectionWithConnectionsMixedin(BaseSystemCollection, Connections): + class SystemCollectionWithConnectionsMixin(BaseSystemCollection, Connections): pass # TODO fix link after new PR @@ -188,14 +193,10 @@ class MockRod(RodBase): def __init__(self, *args, **kwargs): self.n_elems = 3 # arbitrary number - # Connections assume that this promise is met - def __len__(self): - return 2 # a random number - @pytest.fixture(scope="function", params=[2, 10]) def load_system_with_connects(self, request): n_sys = request.param - sys_coll_with_connects = self.SystemCollectionWithConnectionsMixedin() + sys_coll_with_connects = self.SystemCollectionWithConnectionsMixin() for i_sys in range(n_sys): sys_coll_with_connects.append(self.MockRod(2, 3, 4, 5)) return sys_coll_with_connects @@ -224,51 +225,51 @@ def load_system_with_connects(self, request): def test_connect_with_illegal_index_throws( self, load_system_with_connects, sys_idx ): - scwc = load_system_with_connects + system_collection_with_connections = load_system_with_connects with pytest.raises(AssertionError) as excinfo: - scwc.connect(*sys_idx) + system_collection_with_connections.connect(*sys_idx) assert "exceeds number of" in str(excinfo.value) with pytest.raises(AssertionError) as excinfo: - scwc.connect(*[np.int_(x) for x in sys_idx]) + system_collection_with_connections.connect(*[np.int_(x) for x in sys_idx]) assert "exceeds number of" in str(excinfo.value) def test_connect_with_unregistered_system_throws(self, load_system_with_connects): - scwc = load_system_with_connects + system_collection_with_connections = load_system_with_connects # Register this rod mock_rod_registered = self.MockRod(5, 5, 5, 5) - scwc.append(mock_rod_registered) + system_collection_with_connections.append(mock_rod_registered) # Don't register this rod mock_rod = self.MockRod(2, 3, 4, 5) with pytest.raises(ValueError) as excinfo: - scwc.connect(mock_rod, mock_rod_registered) + system_collection_with_connections.connect(mock_rod, mock_rod_registered) assert "was not found, did you" in str(excinfo.value) # Switch arguments with pytest.raises(ValueError) as excinfo: - scwc.connect(mock_rod_registered, mock_rod) + system_collection_with_connections.connect(mock_rod_registered, mock_rod) assert "was not found, did you" in str(excinfo.value) def test_connect_with_illegal_system_throws(self, load_system_with_connects): - scwc = load_system_with_connects + system_collection_with_connections = load_system_with_connects # Register this rod mock_rod_registered = self.MockRod(5, 5, 5, 5) - scwc.append(mock_rod_registered) + system_collection_with_connections.append(mock_rod_registered) # Not a rod, but a list! mock_rod = [1, 2, 3, 5] with pytest.raises(TypeError) as excinfo: - scwc.connect(mock_rod, mock_rod_registered) + system_collection_with_connections.connect(mock_rod, mock_rod_registered) assert "not a sys" in str(excinfo.value) # Switch arguments with pytest.raises(TypeError) as excinfo: - scwc.connect(mock_rod_registered, mock_rod) + system_collection_with_connections.connect(mock_rod_registered, mock_rod) assert "not a sys" in str(excinfo.value) """ @@ -276,30 +277,32 @@ def test_connect_with_illegal_system_throws(self, load_system_with_connects): """ def test_connect_registers_and_returns_Connect(self, load_system_with_connects): - scwc = load_system_with_connects + system_collection_with_connections = load_system_with_connects mock_rod_one = self.MockRod(2, 3, 4, 5) - scwc.append(mock_rod_one) + system_collection_with_connections.append(mock_rod_one) mock_rod_two = self.MockRod(4, 5) - scwc.append(mock_rod_two) + system_collection_with_connections.append(mock_rod_two) - _mock_connect = scwc.connect(mock_rod_one, mock_rod_two) - assert _mock_connect in scwc._connections + _mock_connect = system_collection_with_connections.connect( + mock_rod_one, mock_rod_two + ) + assert _mock_connect in system_collection_with_connections._connections assert _mock_connect.__class__ == _Connect # check sane defaults provided for connection indices - assert None in _mock_connect.id() and None in _mock_connect.id() + assert _mock_connect.id()[2] is None and _mock_connect.id()[3] is None from elastica.joint import FreeJoint @pytest.fixture def load_rod_with_connects(self, load_system_with_connects): - scwc = load_system_with_connects + system_collection_with_connections = load_system_with_connects mock_rod_one = self.MockRod(2, 3, 4, 5) - scwc.append(mock_rod_one) + system_collection_with_connections.append(mock_rod_one) mock_rod_two = self.MockRod(5.0, 5.0) - scwc.append(mock_rod_two) + system_collection_with_connections.append(mock_rod_two) def mock_init(self, *args, **kwargs): pass @@ -310,28 +313,129 @@ def mock_init(self, *args, **kwargs): ) # Constrain any and all systems - scwc.connect(0, 1).using(MockConnect, 2, 42) # index based connect - scwc.connect(mock_rod_one, mock_rod_two).using( + system_collection_with_connections.connect(0, 1).using( + MockConnect, 2, 42 + ) # index based connect + system_collection_with_connections.connect(mock_rod_one, mock_rod_two).using( MockConnect, 2, 3 ) # system based connect - scwc.connect(0, mock_rod_one).using( + system_collection_with_connections.connect(0, mock_rod_one).using( MockConnect, 1, 2 ) # index/system based connect - return scwc, MockConnect + return system_collection_with_connections, MockConnect def test_connect_finalize_correctness(self, load_rod_with_connects): - scwc, connect_cls = load_rod_with_connects + system_collection_with_connections, connect_cls = load_rod_with_connects - scwc._finalize_connections() + system_collection_with_connections._finalize_connections() - for (fidx, sidx, fconnect, sconnect, connect) in scwc._connections: + for ( + fidx, + sidx, + fconnect, + sconnect, + connect, + ) in system_collection_with_connections._connections: assert type(fidx) is int assert type(sidx) is int assert fconnect is None assert sconnect is None assert type(connect) is connect_cls - def test_connect_call_on_systems(self): - # TODO Finish after the architecture is complete - pass + @pytest.fixture + def load_rod_with_connects_and_indices(self, load_system_with_connects): + system_collection_with_connections_and_indices = load_system_with_connects + + mock_rod_one = self.MockRod(1.0, 2.0, 3.0, 4.0) + mock_rod_one.position_collection = np.array( + [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 0.0, 0.0], [3.0, 0.0, 0.0]] + ) + mock_rod_one.velocity_collection = np.array( + [[1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 0.0]] + ) + mock_rod_one.external_forces = np.array( + [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]] + ) + system_collection_with_connections_and_indices.append(mock_rod_one) + mock_rod_two = self.MockRod(1.0, 1.0) + mock_rod_two.position_collection = np.array( + [[0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [4.0, 0.0, 0.0], [6.0, 0.0, 0.0]] + ) + mock_rod_two.velocity_collection = np.array( + [[2.0, 0.0, 0.0], [2.0, 0.0, 0.0], [-2.0, 0.0, 0.0], [-2.0, 0.0, 0.0]] + ) + mock_rod_two.external_forces = np.array( + [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]] + ) + system_collection_with_connections_and_indices.append(mock_rod_two) + + def mock_init(self, *args, **kwargs): + self.k = 1.0 + self.nu = 0.1 + + # in place class + MockConnect = type( + "MockConnect", (self.FreeJoint, object), {"__init__": mock_init} + ) + + # Constrain any and all systems + system_collection_with_connections_and_indices.connect( + mock_rod_one, mock_rod_two, 0, 0 + ).using( + MockConnect, 2, 42 + ) # with connection indices + return system_collection_with_connections_and_indices, MockConnect + + def test_connect_call_on_systems(self, load_rod_with_connects_and_indices): + ( + system_collection_with_connections_and_indices, + connect_cls, + ) = load_rod_with_connects_and_indices + + system_collection_with_connections_and_indices._finalize_connections() + system_collection_with_connections_and_indices._call_connections() + + for ( + fidx, + sidx, + fconnect, + sconnect, + connect, + ) in system_collection_with_connections_and_indices._connections: + end_distance_vector = ( + system_collection_with_connections_and_indices._systems[ + sidx + ].position_collection[..., sconnect] + - system_collection_with_connections_and_indices._systems[ + fidx + ].position_collection[..., fconnect] + ) + elastic_force = connect.k * end_distance_vector + + relative_velocity = ( + system_collection_with_connections_and_indices._systems[ + sidx + ].velocity_collection[..., sconnect] + - system_collection_with_connections_and_indices._systems[ + fidx + ].velocity_collection[..., fconnect] + ) + damping_force = connect.nu * relative_velocity + + contact_force = elastic_force + damping_force + + assert_allclose( + system_collection_with_connections_and_indices._systems[ + fidx + ].external_forces[..., fconnect], + contact_force, + atol=Tolerance.atol(), + ) + assert_allclose( + system_collection_with_connections_and_indices._systems[ + sidx + ].external_forces[..., sconnect], + -1 * contact_force, + atol=Tolerance.atol(), + ) diff --git a/tests/test_modules/test_contact.py b/tests/test_modules/test_contact.py new file mode 100644 index 000000000..b8ad8b795 --- /dev/null +++ b/tests/test_modules/test_contact.py @@ -0,0 +1,392 @@ +__doc__ = """ Test modules for contact """ +import numpy as np +import pytest +from elastica.modules import Contact +from elastica.modules.contact import _Contact +from numpy.testing import assert_allclose +from elastica.utils import Tolerance + + +class TestContact: + @pytest.fixture(scope="function") + def load_contact(self, request): + # contact between 15th and 23rd rod + return _Contact(15, 23) + + @pytest.mark.parametrize("illegal_contact", [int, list]) + def test_using_with_illegal_contact_throws_assertion_error( + self, load_contact, illegal_contact + ): + with pytest.raises(AssertionError) as excinfo: + load_contact.using(illegal_contact) + assert "{} is not a valid contact class. Did you forget to derive from NoContact?".format( + illegal_contact + ) == str( + excinfo.value + ) + + from elastica.contact_forces import NoContact, RodRodContact, RodSelfContact + + @pytest.mark.parametrize( + "legal_contact", [NoContact, RodRodContact, RodSelfContact] + ) + def test_using_with_legal_contact(self, load_contact, legal_contact): + contact = load_contact + contact.using(legal_contact, 3, 4.0, "5", k=1, l_var="2", j=3.0) + + assert contact._contact_cls == legal_contact + assert contact._args == (3, 4.0, "5") + assert contact._kwargs == {"k": 1, "l_var": "2", "j": 3.0} + + def test_id(self, load_contact): + contact = load_contact + # This is purely for coverage purposes, no actual test + # since its a simple return + assert contact.id() == (15, 23) + + def test_call_without_setting_contact_throws_runtime_error(self, load_contact): + contact = load_contact + + with pytest.raises(RuntimeError) as excinfo: + contact() + assert "No contacts provided to to establish contact between rod-like object id {0} and {1}, but a Contact was intended as per code. Did you forget to call the `using` method?".format( + *contact.id() + ) == str( + excinfo.value + ) + + def test_call_improper_args_throws(self, load_contact): + # Example of bad initiailization function + # This needs at least four args which the user might + # forget to pass later on + def mock_init(self, *args, **kwargs): + self.nu = args[3] # Need at least four args + self.k = kwargs.get("k") + + # in place class + MockContact = type( + "MockContact", (self.NoContact, object), {"__init__": mock_init} + ) + + # The user thinks 4.0 goes to nu, but we don't accept it because of error in + # construction og a Contact class + contact = load_contact + contact.using(MockContact, 4.0, k=1, l_var="2", j=3.0) + + # Actual test is here, this should not throw + with pytest.raises(TypeError) as excinfo: + _ = contact() + assert ( + r"Unable to construct contact class.\nDid you provide all necessary contact properties?" + == str(excinfo.value) + ) + + +class TestContactMixin: + from elastica.modules import BaseSystemCollection + + class SystemCollectionWithContactMixin(BaseSystemCollection, Contact): + pass + + from elastica.rod import RodBase + from elastica.rigidbody import RigidBodyBase + from elastica.surface import SurfaceBase + + class MockRod(RodBase): + def __init__(self, *args, **kwargs): + self.n_elems = 2 + self.position_collection = np.array([[1, 2, 3], [0, 0, 0], [0, 0, 0]]) + self.radius = np.array([1, 1]) + self.lengths = np.array([1, 1]) + self.tangents = np.array([[1.0, 1.0], [0.0, 0.0], [0.0, 0.0]]) + self.velocity_collection = np.array( + [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]] + ) + self.internal_forces = np.array( + [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]] + ) + self.external_forces = np.array( + [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]] + ) + + class MockRigidBody(RigidBodyBase): + def __init__(self, *args, **kwargs): + self.n_elems = 1 + + class MockSurface(SurfaceBase): + def __init__(self, *args, **kwargs): + self.n_facets = 1 + + @pytest.fixture(scope="function", params=[2, 10]) + def load_system_with_contacts(self, request): + n_sys = request.param + sys_coll_with_contacts = self.SystemCollectionWithContactMixin() + for i_sys in range(n_sys): + sys_coll_with_contacts.append(self.MockRod(2, 3, 4, 5)) + return sys_coll_with_contacts + + """ The following calls test _get_sys_idx_if_valid from BaseSystem indirectly, + and are here because of legacy reasons. I have not removed them because there + are Contacts require testing against multiple indices, which is still use + ful to cross-verify against. + + START + """ + + @pytest.mark.parametrize( + "sys_idx", + [ + (12, 3), + (3, 12), + (-12, 3), + (-3, 12), + (12, -3), + (-12, -3), + (3, -12), + (-3, -12), + ], + ) + def test_contact_with_illegal_index_throws( + self, load_system_with_contacts, sys_idx + ): + system_collection_with_contacts = load_system_with_contacts + + with pytest.raises(AssertionError) as excinfo: + system_collection_with_contacts.detect_contact_between(*sys_idx) + assert "exceeds number of" in str(excinfo.value) + + with pytest.raises(AssertionError) as excinfo: + system_collection_with_contacts.detect_contact_between( + *[np.int_(x) for x in sys_idx] + ) + assert "exceeds number of" in str(excinfo.value) + + def test_contact_with_unregistered_system_throws(self, load_system_with_contacts): + system_collection_with_contacts = load_system_with_contacts + + # Register this rod + mock_rod_registered = self.MockRod(5, 5, 5, 5) + system_collection_with_contacts.append(mock_rod_registered) + # Don't register this rod + mock_rod = self.MockRod(2, 3, 4, 5) + + with pytest.raises(ValueError) as excinfo: + system_collection_with_contacts.detect_contact_between( + mock_rod, mock_rod_registered + ) + assert "was not found, did you" in str(excinfo.value) + + # Switch arguments + with pytest.raises(ValueError) as excinfo: + system_collection_with_contacts.detect_contact_between( + mock_rod_registered, mock_rod + ) + assert "was not found, did you" in str(excinfo.value) + + def test_contact_with_illegal_system_throws(self, load_system_with_contacts): + system_collection_with_contacts = load_system_with_contacts + + # Register this rod + mock_rod_registered = self.MockRod(5, 5, 5, 5) + system_collection_with_contacts.append(mock_rod_registered) + + # Not a rod, but a list! + mock_rod = [1, 2, 3, 5] + + with pytest.raises(TypeError) as excinfo: + system_collection_with_contacts.detect_contact_between( + mock_rod, mock_rod_registered + ) + assert "not a sys" in str(excinfo.value) + + # Switch arguments + with pytest.raises(TypeError) as excinfo: + system_collection_with_contacts.detect_contact_between( + mock_rod_registered, mock_rod + ) + assert "not a sys" in str(excinfo.value) + + """ + END of testing BaseSystem calls + """ + + def test_contact_registers_and_returns_Contact(self, load_system_with_contacts): + system_collection_with_contacts = load_system_with_contacts + + mock_rod_one = self.MockRod(2, 3, 4, 5) + system_collection_with_contacts.append(mock_rod_one) + + mock_rod_two = self.MockRod(4, 5) + system_collection_with_contacts.append(mock_rod_two) + + _mock_contact = system_collection_with_contacts.detect_contact_between( + mock_rod_one, mock_rod_two + ) + assert _mock_contact in system_collection_with_contacts._contacts + assert _mock_contact.__class__ == _Contact + + from elastica.contact_forces import NoContact + + @pytest.fixture + def load_rod_with_contacts(self, load_system_with_contacts): + system_collection_with_contacts = load_system_with_contacts + + mock_rod_one = self.MockRod(2, 3, 4, 5) + system_collection_with_contacts.append(mock_rod_one) + mock_rod_two = self.MockRod(5.0, 5.0) + system_collection_with_contacts.append(mock_rod_two) + + def mock_init(self, *args, **kwargs): + pass + + # in place class + MockContact = type( + "MockContact", (self.NoContact, object), {"__init__": mock_init} + ) + + # Constrain any and all systems + system_collection_with_contacts.detect_contact_between(0, 1).using( + MockContact + ) # index based contact + system_collection_with_contacts.detect_contact_between( + mock_rod_one, mock_rod_two + ).using( + MockContact + ) # system based contact + system_collection_with_contacts.detect_contact_between(0, mock_rod_one).using( + MockContact + ) # index/system based contact + return system_collection_with_contacts, MockContact + + def test_contact_finalize_correctness(self, load_rod_with_contacts): + system_collection_with_contacts, contact_cls = load_rod_with_contacts + + system_collection_with_contacts._finalize_contact() + + for (fidx, sidx, contact) in system_collection_with_contacts._contacts: + assert type(fidx) is int + assert type(sidx) is int + assert type(contact) is contact_cls + + @pytest.fixture + def load_contact_objects_with_incorrect_order(self, load_system_with_contacts): + system_collection_with_contacts = load_system_with_contacts + + mock_rod = self.MockRod(2, 3, 4, 5) + system_collection_with_contacts.append(mock_rod) + mock_rigid_body = self.MockRigidBody(5.0, 5.0) + system_collection_with_contacts.append(mock_rigid_body) + + def mock_init(self, *args, **kwargs): + pass + + # in place class + MockContact = type( + "MockContact", (self.NoContact, object), {"__init__": mock_init} + ) + + # incorrect order contact + system_collection_with_contacts.detect_contact_between( + mock_rigid_body, mock_rod + ).using( + MockContact + ) # rigid body before rod + + return system_collection_with_contacts, MockContact + + def test_contact_check_order(self, load_contact_objects_with_incorrect_order): + ( + system_collection_with_contacts, + contact_cls, + ) = load_contact_objects_with_incorrect_order + + mock_rod = self.MockRod(2, 3, 4, 5) + mock_rigid_body = self.MockRigidBody(5.0, 5.0) + + with pytest.raises(TypeError) as excinfo: + system_collection_with_contacts._finalize_contact() + assert ( + "Systems provided to the contact class have incorrect order. \n" + " First system is {0} and second system is {1}. \n" + " If the first system is a rod, the second system can be a rod, rigid body or surface. \n" + " If the first system is a rigid body, the second system can be a rigid body or surface." + ).format(mock_rigid_body.__class__, mock_rod.__class__) == str(excinfo.value) + + @pytest.fixture + def load_system_with_rods_in_contact(self, load_system_with_contacts): + system_collection_with_rods_in_contact = load_system_with_contacts + + mock_rod_one = self.MockRod(1.0, 2.0, 3.0, 4.0) + system_collection_with_rods_in_contact.append(mock_rod_one) + mock_rod_two = self.MockRod(1.0, 1.0) + "Move second rod above first rod to make contact in parallel" + mock_rod_two.position_collection = np.array( + [[1, 2, 3], [0.5, 0.5, 0.5], [0, 0, 0]] + ) + system_collection_with_rods_in_contact.append(mock_rod_two) + + # in place class + from elastica.contact_forces import RodRodContact + + # Constrain any and all systems + system_collection_with_rods_in_contact.detect_contact_between( + mock_rod_one, mock_rod_two + ).using( + RodRodContact, + k=1.0, + nu=0.1, + ) + return system_collection_with_rods_in_contact + + def test_contact_call_on_systems(self, load_system_with_rods_in_contact): + + system_collection_with_rods_in_contact = load_system_with_rods_in_contact + + system_collection_with_rods_in_contact._finalize_contact() + system_collection_with_rods_in_contact._call_contacts(time=0) + + from elastica.contact_forces import _calculate_contact_forces_rod_rod + + for ( + fidx, + sidx, + contact, + ) in system_collection_with_rods_in_contact._contacts: + system_one = system_collection_with_rods_in_contact._systems[fidx] + system_two = system_collection_with_rods_in_contact._systems[sidx] + external_forces_system_one = np.zeros_like(system_one.external_forces) + external_forces_system_two = np.zeros_like(system_two.external_forces) + + _calculate_contact_forces_rod_rod( + system_one.position_collection[ + ..., :-1 + ], # Discount last node, we want element start position + system_one.radius, + system_one.lengths, + system_one.tangents, + system_one.velocity_collection, + system_one.internal_forces, + external_forces_system_one, + system_two.position_collection[ + ..., :-1 + ], # Discount last node, we want element start position + system_two.radius, + system_two.lengths, + system_two.tangents, + system_two.velocity_collection, + system_two.internal_forces, + external_forces_system_two, + contact.k, + contact.nu, + ) + + assert_allclose( + system_collection_with_rods_in_contact._systems[fidx].external_forces, + external_forces_system_one, + atol=Tolerance.atol(), + ) + assert_allclose( + system_collection_with_rods_in_contact._systems[sidx].external_forces, + external_forces_system_two, + atol=Tolerance.atol(), + ) diff --git a/tests/test_modules/test_memory_block.py b/tests/test_modules/test_memory_block.py deleted file mode 100644 index a78a03397..000000000 --- a/tests/test_modules/test_memory_block.py +++ /dev/null @@ -1,93 +0,0 @@ -__doc__ = """" Test modules to construct memory block """ - -import pytest -import numpy as np - -from elastica.rod import RodBase -from elastica.modules.memory_block import construct_memory_block_structures -from elastica.memory_block.memory_block_rod import MemoryBlockCosseratRod - - -class BaseRodForTesting(RodBase): - def __init__(self, n_elems): - self.n_elems = n_elems # np.random.randint(10, 30 + 1) - self.n_nodes = self.n_elems + 1 - self.n_voronoi = self.n_elems - 1 - self.ring_rod_flag = False - - # Things that are scalar mapped on nodes - self.mass = np.random.randn(self.n_nodes) - - # Things that are vectors mapped on nodes - self.position_collection = np.random.randn(3, self.n_nodes) - self.velocity_collection = np.random.randn(3, self.n_nodes) - self.acceleration_collection = np.random.randn(3, self.n_nodes) - self.internal_forces = np.random.randn(3, self.n_nodes) - self.external_forces = np.random.randn(3, self.n_nodes) - - # Things that are scalar mapped on elements - self.radius = np.random.rand(self.n_elems) - self.volume = np.random.rand(self.n_elems) - self.density = np.random.rand(self.n_elems) - self.lengths = np.random.rand(self.n_elems) - self.rest_lengths = self.lengths.copy() - self.dilatation = np.random.rand(self.n_elems) - self.dilatation_rate = np.random.rand(self.n_elems) - - # Things that are vector mapped on elements - self.omega_collection = np.random.randn(3, self.n_elems) - self.alpha_collection = np.random.randn(3, self.n_elems) - self.tangents = np.random.randn(3, self.n_elems) - self.sigma = np.random.randn(3, self.n_elems) - self.rest_sigma = np.random.randn(3, self.n_elems) - self.internal_torques = np.random.randn(3, self.n_elems) - self.external_torques = np.random.randn(3, self.n_elems) - self.internal_stress = np.random.randn(3, self.n_elems) - - # Things that are matrix mapped on elements - self.director_collection = np.zeros((3, 3, self.n_elems)) - for i in range(3): - for j in range(3): - self.director_collection[i, j, ...] = 3 * i + j - # self.director_collection *= np.random.randn() - self.mass_second_moment_of_inertia = np.random.randn() * np.ones( - (3, 3, self.n_elems) - ) - self.inv_mass_second_moment_of_inertia = np.random.randn() * np.ones( - (3, 3, self.n_elems) - ) - self.shear_matrix = np.random.randn() * np.ones((3, 3, self.n_elems)) - - # Things that are scalar mapped on voronoi - self.voronoi_dilatation = np.random.rand(self.n_voronoi) - self.rest_voronoi_lengths = np.random.rand(self.n_voronoi) - - # Things that are vectors mapped on voronoi - self.kappa = np.random.randn(3, self.n_voronoi) - self.rest_kappa = np.random.randn(3, self.n_voronoi) - self.internal_couple = np.random.randn(3, self.n_voronoi) - - # Things that are matrix mapped on voronoi - self.bend_matrix = np.random.randn() * np.ones((3, 3, self.n_voronoi)) - - -@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. - - Parameters - ---------- - n_rods - - Returns - ------- - - """ - - systems = [BaseRodForTesting(np.random.randint(10, 30 + 1)) for _ in range(n_rods)] - - memory_block_list = construct_memory_block_structures(systems) - - assert issubclass(memory_block_list[0].__class__, MemoryBlockCosseratRod) diff --git a/tests/test_modules/test_memory_block_base.py b/tests/test_modules/test_memory_block_base.py new file mode 100644 index 000000000..9dde39234 --- /dev/null +++ b/tests/test_modules/test_memory_block_base.py @@ -0,0 +1,119 @@ +__doc__ = """ Test make_block_memory_metadata and make_block_memory_periodic_boundary_metadata functions """ + +import pytest +import numpy as np +from numpy.testing import assert_array_equal +from elastica.memory_block.memory_block_rod_base import ( + make_block_memory_metadata, + make_block_memory_periodic_boundary_metadata, +) + + +@pytest.mark.parametrize( + "n_elems_in_rods, outputs", + [ + ( + np.array([5], dtype=np.int64), + [ + np.int64(5), + np.array([], dtype=np.int64), + np.array([], dtype=np.int64), + np.array([], dtype=np.int64), + ], + ), + ( + np.array([5, 5], dtype=np.int64), + [ + np.int64(12), + np.array([6], dtype=np.int64), + np.array([5, 6], dtype=np.int64), + np.array([4, 5, 6], dtype=np.int64), + ], + ), + ( + np.array([1, 1, 1], dtype=np.int64), + [ + np.int64(7), + np.array([2, 5], dtype=np.int64), + np.array([1, 2, 4, 5], dtype=np.int64), + np.array([0, 1, 2, 3, 4, 5], dtype=np.int64), + ], + ), + ( + np.array([1, 2, 3], dtype=np.int64), + [ + np.int64(10), + np.array([2, 6], dtype=np.int64), + np.array([1, 2, 5, 6], dtype=np.int64), + np.array([0, 1, 2, 4, 5, 6], dtype=np.int64), + ], + ), + ( + np.array([10, 10, 5, 5], dtype=np.int64), + [ + np.int64(36), + np.array([11, 23, 30], dtype=np.int64), + np.array([10, 11, 22, 23, 29, 30], dtype=np.int64), + np.array([9, 10, 11, 21, 22, 23, 28, 29, 30], dtype=np.int64), + ], + ), + ], +) +def test_make_block_memory_metadata(n_elems_in_rods, outputs): + ( + n_elems_with_ghosts, + ghost_nodes_idx, + ghost_elems_idx, + ghost_voronoi_idx, + ) = make_block_memory_metadata(n_elems_in_rods) + + assert_array_equal(n_elems_with_ghosts, outputs[0]) + assert_array_equal(ghost_nodes_idx, outputs[1]) + assert_array_equal(ghost_elems_idx, outputs[2]) + assert_array_equal(ghost_voronoi_idx, outputs[3]) + + +@pytest.mark.parametrize( + "n_elems_in_ring_rods", + [ + np.array([10], dtype=np.int64), + np.array([2, 4], dtype=np.int64), + np.array([4, 5, 7], dtype=np.int64), + np.array([10, 10, 10, 10], dtype=np.int64), + ], +) +def test_make_block_memory_periodic_boundary_metadata(n_elems_in_ring_rods): + ( + n_elem, + periodic_boundary_node_idx, + periodic_boundary_elems_idx, + periodic_boundary_voronoi_idx, + ) = make_block_memory_periodic_boundary_metadata(n_elems_in_ring_rods) + + n_ring_rods = len(n_elems_in_ring_rods) + expected_n_elem = n_elems_in_ring_rods + 2 + expected_node_idx = np.empty((2, 3 * n_ring_rods), dtype=np.int64) + expected_element_idx = np.empty((2, 2 * n_ring_rods), dtype=np.int64) + expected_voronoi_idx = np.empty((2, n_ring_rods), dtype=np.int64) + + accumulation = np.hstack((0, np.cumsum(n_elems_in_ring_rods[:-1] + 4))) + + expected_node_idx[0, 0::3] = accumulation + expected_node_idx[0, 1::3] = accumulation + n_elems_in_ring_rods + 1 + expected_node_idx[0, 2::3] = accumulation + n_elems_in_ring_rods + 2 + expected_node_idx[1, 0::3] = accumulation + n_elems_in_ring_rods + expected_node_idx[1, 1::3] = accumulation + 1 + expected_node_idx[1, 2::3] = accumulation + 2 + + expected_element_idx[0, 0::2] = accumulation + expected_element_idx[0, 1::2] = accumulation + n_elems_in_ring_rods + 1 + expected_element_idx[1, 0::2] = accumulation + n_elems_in_ring_rods + expected_element_idx[1, 1::2] = accumulation + 1 + + expected_voronoi_idx[0, :] = accumulation + expected_voronoi_idx[1, :] = accumulation + n_elems_in_ring_rods + + assert_array_equal(n_elem, expected_n_elem) + assert_array_equal(periodic_boundary_node_idx, expected_node_idx) + assert_array_equal(periodic_boundary_elems_idx, expected_element_idx) + assert_array_equal(periodic_boundary_voronoi_idx, expected_voronoi_idx) diff --git a/tests/test_modules/test_memory_block_cosserat_rod.py b/tests/test_modules/test_memory_block_cosserat_rod.py new file mode 100644 index 000000000..7a76c852f --- /dev/null +++ b/tests/test_modules/test_memory_block_cosserat_rod.py @@ -0,0 +1,224 @@ +__doc__ = """" Test modules to construct memory block for Cosserat rods """ + +import pytest +import random +import numpy as np +from numpy.testing import assert_array_equal + +from elastica.rod import RodBase +from elastica.modules.memory_block import construct_memory_block_structures +from elastica.memory_block.memory_block_rod import MemoryBlockCosseratRod + + +class BaseRodForTesting(RodBase): + def __init__(self, n_elems: np.int64, ring_rod_flag: bool): + self.n_elems = n_elems # np.random.randint(10, 30 + 1) + self.n_nodes = n_elems if ring_rod_flag else n_elems + 1 + self.n_voronoi = n_elems if ring_rod_flag else n_elems - 1 + self.ring_rod_flag = ring_rod_flag + + # Things that are scalar mapped on nodes + self.mass = np.random.randn(self.n_nodes) + + # Things that are vectors mapped on nodes + self.position_collection = np.random.randn(3, self.n_nodes) + self.velocity_collection = np.random.randn(3, self.n_nodes) + self.acceleration_collection = np.random.randn(3, self.n_nodes) + self.internal_forces = np.random.randn(3, self.n_nodes) + self.external_forces = np.random.randn(3, self.n_nodes) + + # Things that are scalar mapped on elements + self.radius = np.random.rand(self.n_elems) + self.volume = np.random.rand(self.n_elems) + self.density = np.random.rand(self.n_elems) + self.lengths = np.random.rand(self.n_elems) + self.rest_lengths = self.lengths.copy() + self.dilatation = np.random.rand(self.n_elems) + self.dilatation_rate = np.random.rand(self.n_elems) + + # Things that are vector mapped on elements + self.omega_collection = np.random.randn(3, self.n_elems) + self.alpha_collection = np.random.randn(3, self.n_elems) + self.tangents = np.random.randn(3, self.n_elems) + self.sigma = np.random.randn(3, self.n_elems) + self.rest_sigma = np.random.randn(3, self.n_elems) + self.internal_torques = np.random.randn(3, self.n_elems) + self.external_torques = np.random.randn(3, self.n_elems) + self.internal_stress = np.random.randn(3, self.n_elems) + + # Things that are matrix mapped on elements + self.director_collection = np.tile( + np.eye(3).reshape(3, 3, 1), (1, 1, self.n_elems) + ) + self.mass_second_moment_of_inertia = np.random.randn() * np.ones( + (3, 3, self.n_elems) + ) + self.inv_mass_second_moment_of_inertia = np.random.randn() * np.ones( + (3, 3, self.n_elems) + ) + self.shear_matrix = np.random.randn() * np.ones((3, 3, self.n_elems)) + + # Things that are scalar mapped on voronoi + self.voronoi_dilatation = np.random.rand(self.n_voronoi) + self.rest_voronoi_lengths = np.random.rand(self.n_voronoi) + + # Things that are vectors mapped on voronoi + self.kappa = np.random.randn(3, self.n_voronoi) + self.rest_kappa = np.random.randn(3, self.n_voronoi) + self.internal_couple = np.random.randn(3, self.n_voronoi) + + # Things that are matrix mapped on voronoi + self.bend_matrix = np.random.randn() * np.ones((3, 3, self.n_voronoi)) + + +@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. + + Parameters + ---------- + n_rods + + Returns + ------- + + """ + + systems = [ + BaseRodForTesting(np.random.randint(10, 30 + 1), ring_rod_flag=False) + for _ in range(n_rods) + ] + + memory_block_list = construct_memory_block_structures(systems) + + assert issubclass(memory_block_list[0].__class__, MemoryBlockCosseratRod) + + +@pytest.mark.parametrize("n_straight_rods", [0, 1, 2, 5]) +@pytest.mark.parametrize("n_ring_rods", [0, 1, 2, 5]) +def test_memory_block_rod(n_straight_rods, n_ring_rods): + """ + Test memory block logic for Cosserat rods. This test suite supports + a mixture of straight rods and ring rods, the order of which is internally + randomized. Correct system indexing is required within the memory block + implementation to pass this test. + + Parameters + ---------- + n_straight_rods: int + Number of straight rods. + n_ring_rods: int + Number of ring rods. + + """ + + n_rods = n_straight_rods + n_ring_rods + + # Skip test if both are zero + if n_rods == 0: + pytest.skip() + + # Define a temporary list of systems + n_elems = np.random.randint(low=10, high=31, size=(n_rods,)) + systems = [ + BaseRodForTesting(n_elems=n_elems[k], ring_rod_flag=False) + for k in range(n_straight_rods) + ] + [ + BaseRodForTesting(n_elems=n_elems[k + n_straight_rods], ring_rod_flag=True) + for k in range(n_ring_rods) + ] + random.shuffle(systems) + system_idx_list = np.arange(0, n_rods) + + # Initialize memory blocks + memory_block = MemoryBlockCosseratRod( + systems=systems, system_idx_list=system_idx_list + ) + attr_list = dir(memory_block) + + # Test basic attributes + expected_n_elems = np.sum(n_elems) + 2 * n_ring_rods + 2 * (n_rods - 1) + + assert memory_block.n_rods == n_rods + assert memory_block.n_elems == expected_n_elems + assert memory_block.n_nodes == expected_n_elems + 1 + assert memory_block.n_voronoi == expected_n_elems - 1 + + start_idx_dict = { + "node": memory_block.start_idx_in_rod_nodes.view(), + "element": memory_block.start_idx_in_rod_elems.view(), + "voronoi": memory_block.start_idx_in_rod_voronoi.view(), + } + + end_idx_dict = { + "node": memory_block.end_idx_in_rod_nodes.view(), + "element": memory_block.end_idx_in_rod_elems.view(), + "voronoi": memory_block.end_idx_in_rod_voronoi.view(), + } + + expected_attr_dict = { + "mass": "node", + "position_collection": "node", + "internal_forces": "node", + "external_forces": "node", + "radius": "element", + "volume": "element", + "density": "element", + "lengths": "element", + "rest_lengths": "element", + "dilatation": "element", + "dilatation_rate": "element", + "tangents": "element", + "sigma": "element", + "rest_sigma": "element", + "internal_torques": "element", + "external_torques": "element", + "internal_stress": "element", + "director_collection": "element", + "mass_second_moment_of_inertia": "element", + "inv_mass_second_moment_of_inertia": "element", + "shear_matrix": "element", + "voronoi_dilatation": "voronoi", + "rest_voronoi_lengths": "voronoi", + "kappa": "voronoi", + "rest_kappa": "voronoi", + "internal_couple": "voronoi", + "bend_matrix": "voronoi", + "velocity_collection": "node", + "omega_collection": "element", + "acceleration_collection": "node", + "alpha_collection": "element", + } + + # Cross check: make sure memory block and rod attributes are views of each other + for attr, domain in expected_attr_dict.items(): + # Check if the memory block has the attribute + assert attr in attr_list + + start_idx = start_idx_dict[domain] + end_idx = end_idx_dict[domain] + + block_view = memory_block.__dict__[attr].view() + for i, k in enumerate(memory_block.system_idx_list): + # Assert that the rod's and memory block's attributes share memory + assert np.shares_memory( + block_view[..., start_idx[i] : end_idx[i]], systems[k].__dict__[attr] + ) + + # Assert that the rod's and memory block's attributes are equal in values + assert_array_equal( + block_view[..., start_idx[i] : end_idx[i]], systems[k].__dict__[attr] + ) + + # Self check: make sure memory block attributes do not share memory with each other + for attr_x in expected_attr_dict: + for attr_y in expected_attr_dict: + if attr_x == attr_y: + continue + + assert not np.may_share_memory( + memory_block.__dict__[attr_x], + memory_block.__dict__[attr_y], + ) diff --git a/tests/test_modules/test_memory_block_rigid_body.py b/tests/test_modules/test_memory_block_rigid_body.py new file mode 100644 index 000000000..5616d0a93 --- /dev/null +++ b/tests/test_modules/test_memory_block_rigid_body.py @@ -0,0 +1,133 @@ +import pytest +import numpy as np +import scipy as sp + +from numpy.testing import assert_array_equal + +from elastica.rigidbody import RigidBodyBase +from elastica.modules.memory_block import construct_memory_block_structures +from elastica.memory_block.memory_block_rigid_body import MemoryBlockRigidBody + + +class MockRigidBodyForTesting(RigidBodyBase): + def __init__(self): + super().__init__() + self.radius = np.random.uniform(low=1, high=5) + self.length = np.random.uniform(low=1, high=10) + self.density = np.random.uniform(low=1, high=10) + self.volume = self.length * self.radius * self.radius + self.mass = np.array(self.volume * self.density) + + self.position_collection = np.random.rand(3, 1) + self.external_forces = np.random.rand(3, 1) + self.external_torques = np.random.rand(3, 1) + + self.director_collection = sp.linalg.qr(np.random.rand(3, 3))[0].reshape( + 3, 3, 1 + ) + + mass_second_moi_diag = ( + np.random.uniform(low=1, high=2, size=(3,)) + * self.radius + * self.length + * self.density + ) + + self.mass_second_moment_of_inertia = np.diag(mass_second_moi_diag).reshape( + (3, 3, 1) + ) + self.inv_mass_second_moment_of_inertia = np.diag( + 1.0 / mass_second_moi_diag + ).reshape((3, 3, 1)) + + self.velocity_collection = np.random.rand(3, 1) + self.acceleration_collection = np.random.rand(3, 1) + self.omega_collection = np.random.rand(3, 1) + self.alpha_collection = np.random.rand(3, 1) + + +@pytest.mark.parametrize("n_bodies", [1, 2, 5, 6]) +def test_construct_memory_block_structures_for_rigid_bodies(n_bodies): + """ + This test is only testing the validity of created rigid-body block-structure class, + using the construct_memory_block_structures function. + + Parameters + ---------- + n_bodies: int + Number of rigid bodies to pass into memory block constructor. + """ + + systems = [MockRigidBodyForTesting() for _ in range(n_bodies)] + + memory_block_list = construct_memory_block_structures(systems) + + assert issubclass(memory_block_list[0].__class__, MemoryBlockRigidBody) + + +@pytest.mark.parametrize("n_bodies", [1, 2, 5, 6]) +def test_memory_block_rigid_body(n_bodies): + """ + Test memory block logic for rigid bodies. + + Parameters + ---------- + n_bodies: int + Number of rigid bodies to be passed into memory block. + """ + + systems = [MockRigidBodyForTesting() for _ in range(n_bodies)] + system_idx_list = np.arange(n_bodies) + + memory_block = MemoryBlockRigidBody(systems, system_idx_list) + + assert memory_block.n_bodies == n_bodies + assert memory_block.n_elems == n_bodies + assert memory_block.n_nodes == n_bodies + + attr_list = dir(memory_block) + + expected_attr_list = [ + "mass", + "radius", + "volume", + "density", + "length", + "position_collection", + "external_forces", + "external_torques", + "director_collection", + "mass_second_moment_of_inertia", + "inv_mass_second_moment_of_inertia", + "velocity_collection", + "omega_collection", + "acceleration_collection", + "alpha_collection", + ] + + # Cross check: make sure memory block and rod attributes are views of each other + for attr in expected_attr_list: + # Check if the memory block has the attribute + assert attr in attr_list + + block_view = memory_block.__dict__[attr].view() + + for k in memory_block.system_idx_list: + # Assert that the rod's and memory block's attributes share memory + assert np.shares_memory( + block_view[..., k : k + 1], systems[k].__dict__[attr] + ) + + # Assert that the rod's and memory block's attributes are equal in values + assert_array_equal(block_view[..., k : k + 1], systems[k].__dict__[attr]) + + # Self check: make sure memory block attributes do not share memory with each other + for attr_x in expected_attr_list: + for attr_y in expected_attr_list: + if attr_x == attr_y: + continue + + assert not np.may_share_memory( + memory_block.__dict__[attr_x], + memory_block.__dict__[attr_y], + ) diff --git a/tests/test_rigid_body/test_sphere.py b/tests/test_rigid_body/test_sphere.py index 5ce9b3411..7f4a6daf4 100644 --- a/tests/test_rigid_body/test_sphere.py +++ b/tests/test_rigid_body/test_sphere.py @@ -78,7 +78,7 @@ def test_sphere_initialization(): ) -def test_cylinder_update_accelerations(): +def test_sphere_update_accelerations(): """ This test is testing the update acceleration method of Sphere class. diff --git a/tests/test_rod/mock_rod.py b/tests/test_rod/mock_rod.py new file mode 100644 index 000000000..499dc7697 --- /dev/null +++ b/tests/test_rod/mock_rod.py @@ -0,0 +1,49 @@ +__doc__ = """Mock Rod classes for testing.""" + +import numpy as np + +from elastica.memory_block.memory_block_rod_base import ( + make_block_memory_periodic_boundary_metadata, +) +from elastica.utils import MaxDimension + + +class MockTestRod: + def __init__(self): + self.n_elems = 32 + self.ring_rod_flag = False + self.position_collection = np.random.randn( + MaxDimension.value(), self.n_elems + 1 + ) + self.director_collection = np.random.randn( + MaxDimension.value(), MaxDimension.value(), self.n_elems + ) + self.velocity_collection = np.random.randn( + MaxDimension.value(), self.n_elems + 1 + ) + self.omega_collection = np.random.randn(MaxDimension.value(), self.n_elems) + self.mass = np.abs(np.random.randn(self.n_elems + 1)) + self.external_forces = np.zeros(self.n_elems + 1) + + +class MockTestRingRod: + def __init__(self): + self.n_elems = 32 + self.ring_rod_flag = True + self.position_collection = np.random.randn(MaxDimension.value(), self.n_elems) + self.director_collection = np.random.randn( + MaxDimension.value(), MaxDimension.value(), self.n_elems + ) + self.velocity_collection = np.random.randn(MaxDimension.value(), self.n_elems) + self.omega_collection = np.random.randn(MaxDimension.value(), self.n_elems) + self.mass = np.abs(np.random.randn(self.n_elems)) + self.external_forces = np.zeros(self.n_elems) + + n_elems_ring_rods = (np.ones(1) * (self.n_elems - 3)).astype("int64") + + ( + _, + self.periodic_boundary_nodes_idx, + self.periodic_boundary_elems_idx, + self.periodic_boundary_voronoi_idx, + ) = make_block_memory_periodic_boundary_metadata(n_elems_ring_rods) diff --git a/tests/test_rod/test_knot_theory.py b/tests/test_rod/test_knot_theory.py index f33122e49..a5630d73a 100644 --- a/tests/test_rod/test_knot_theory.py +++ b/tests/test_rod/test_knot_theory.py @@ -6,7 +6,7 @@ from elastica.utils import MaxDimension -from test_rods import MockTestRod +from mock_rod import MockTestRod from elastica.rod.rod_base import RodBase from elastica.rod.knot_theory import ( diff --git a/tests/test_rod/test_rods.py b/tests/test_rod/test_rods.py deleted file mode 100644 index 2079721f9..000000000 --- a/tests/test_rod/test_rods.py +++ /dev/null @@ -1,430 +0,0 @@ -__doc__ = """ Rod class for testing module """ - -import pytest -import numpy as np - -# from elastica.utils import MaxDimension -from numpy.testing import assert_allclose - -from elastica.rod.data_structures import _bootstrap_from_data -from elastica.rod.data_structures import ( - _KinematicState, - _DynamicState, -) -from elastica.memory_block.memory_block_rod_base import ( - make_block_memory_periodic_boundary_metadata, -) -from elastica.utils import MaxDimension - - -class MockTestRod: - def __init__(self): - self.n_elems = 32 - self.ring_rod_flag = False - self.position_collection = np.random.randn( - MaxDimension.value(), self.n_elems + 1 - ) - self.director_collection = np.random.randn( - MaxDimension.value(), MaxDimension.value(), self.n_elems - ) - self.velocity_collection = np.random.randn( - MaxDimension.value(), self.n_elems + 1 - ) - self.omega_collection = np.random.randn(MaxDimension.value(), self.n_elems) - self.mass = np.abs(np.random.randn(self.n_elems + 1)) - self.external_forces = np.zeros(self.n_elems + 1) - - -class MockTestRingRod: - def __init__(self): - self.n_elems = 32 - self.ring_rod_flag = True - self.position_collection = np.random.randn(MaxDimension.value(), self.n_elems) - self.director_collection = np.random.randn( - MaxDimension.value(), MaxDimension.value(), self.n_elems - ) - self.velocity_collection = np.random.randn(MaxDimension.value(), self.n_elems) - self.omega_collection = np.random.randn(MaxDimension.value(), self.n_elems) - self.mass = np.abs(np.random.randn(self.n_elems)) - self.external_forces = np.zeros(self.n_elems) - - n_elems_ring_rods = (np.ones(1) * (self.n_elems - 3)).astype("int64") - - ( - _, - self.periodic_boundary_nodes_idx, - self.periodic_boundary_elems_idx, - self.periodic_boundary_voronoi_idx, - ) = make_block_memory_periodic_boundary_metadata(n_elems_ring_rods) - - -# Choosing 15 and 31 as nelems to reflect common expected -# use case of blocksize = 2*(k), k = int -# https://docs.pytest.org/en/latest/fixture.html -@pytest.fixture(scope="module", params=[15, 31]) -def load_data_for_bootstrapping_state(request): - """Yield states for bootstrapping""" - n_elem = request.param - n_nodes = n_elem + 1 - dim = 3 - - position_collection = np.random.randn(dim, n_nodes) - velocity_collection = np.random.randn(dim, n_nodes) - acceleration_collection = np.random.randn(dim, n_nodes) - omega_collection = np.random.randn(dim, n_elem) - alpha_collection = np.random.randn(dim, n_elem) - director_collection = np.random.randn(dim, dim, n_elem) - - vector_states = np.hstack( - ( - position_collection, - velocity_collection, - omega_collection, - acceleration_collection, - alpha_collection, - ) - ) - - yield n_elem, vector_states, director_collection - - -@pytest.mark.parametrize("stepper_type", ["explicit"]) -def test_bootstrapping_integrity(load_data_for_bootstrapping_state, stepper_type): - (n_elem, vectors, directors) = load_data_for_bootstrapping_state - all_states = _bootstrap_from_data(stepper_type, *load_data_for_bootstrapping_state) - - assert np.shares_memory( - all_states[2], vectors - ), "Integrity of bootstrapping from vector states compromised" - assert np.shares_memory( - all_states[3], directors - ), "Integrity of bootstrapping from matrix states compromised" - for state in all_states[4:]: - assert np.shares_memory( - state, vectors - ), "Integrity of bootstrapping from vector states compromised" - - -def assert_instance(obj, cls): - assert isinstance(obj, cls), "object is not a {} type".format(cls.__name__) - - -def test_bootstrapping_types_for_explicit_steppers(load_data_for_bootstrapping_state): - all_states = _bootstrap_from_data("explicit", *load_data_for_bootstrapping_state) - from elastica.rod.data_structures import _State, _DerivativeState - - assert_instance(all_states[0], _State) - assert_instance(all_states[1], _DerivativeState) - - test_states = all_states[0] - test_derivatives = all_states[1] - - assert np.shares_memory( - test_states.kinematic_rate_collection, test_derivatives.rate_collection - ), "Explicit states does not share memory" - - -@pytest.mark.xfail -def test_bootstrapping_types_for_symplectic_steppers(load_data_for_bootstrapping_state): - """For block structure we drop the boot strap from data function. Thus this test fails.""" - all_states = _bootstrap_from_data("symplectic", *load_data_for_bootstrapping_state) - from elastica.rod.data_structures import ( - _KinematicState, - _DynamicState, - ) - - assert_instance(all_states[0], _KinematicState) - assert_instance(all_states[1], _DynamicState) - - -# TODO Add realistic example with states used in a real time-stepper to solve some ODE -class LoadStates: - """Mixin class for testing explicit and symplectic - stepper behaviors that manipulate state objects - """ - - Vectors = None - Directors = None - States = { - "Position": None, - "Directors": None, - "Velocity": None, - "Omega": None, - "Acceleration": None, - "Alpha": None, - } - - @pytest.fixture - def load_states(self, load_data_for_bootstrapping_state): - (n_elem, vectors, directors) = load_data_for_bootstrapping_state - self.Vectors = vectors.copy() - self.Directors = directors.copy() - # Stepper Type found in base-classes TestExplicitStepperStateBehavior - # and TestSymplecticStepperStateBehavior below. - all_states = _bootstrap_from_data( - self.StepperType, *load_data_for_bootstrapping_state - ) - - # Create copies of states (position, velocity etc) into - # the states dictionary. We then do the SAME manipulation - # of self.States and all_states to check their correctness - for src_state, tgt_key in zip(all_states[2:], self.States): - self.States[tgt_key] = src_state.copy() - - # all_states[0,1] here depends on the StepperType used above - # if TestExplicitStepperStateBehavior.StepperType, then [_State, _DerivativeState] - # if TestSymplecticStepperStateBehavior.StepperType, then [_KinematicState, _DynamicState] - return all_states[0], all_states[1] - - -class TestExplicitStepperStateBehavior(LoadStates): - # TODO : update tests after including Rodrigues rotation properly - StepperType = "explicit" - - @pytest.mark.parametrize("mul_type", ["Pre multiply", "Post multiply"]) - def test_derivative_rmul(self, load_states, mul_type): - _, derivative = load_states - dt = np.random.randn() - if mul_type == "Pre multiply": - test_derivative = dt * derivative - elif mul_type == "Post multiply": - test_derivative = derivative * dt - else: - raise RuntimeError("Shouldn't be here") - assert_instance(test_derivative, np.ndarray) - correct_derivative = dt * self.Vectors - assert np.all(np.in1d(test_derivative.ravel(), correct_derivative.ravel())) - - def test_state_add(self, load_states): - state, derivative = load_states - - def func(x, y): - return x + 1.0 * y - - test_state = func(state, derivative) - - from elastica.rod.data_structures import _State - - assert_instance(test_state, _State) - - assert test_state.n_nodes == state.n_nodes, "Nodes unequal" - assert_allclose( - test_state.position_collection, - func(self.States["Position"], self.States["Velocity"]), - ) - # FIXME How to test directors again? - assert np.all( - np.in1d( - func(self.States["Velocity"], self.States["Acceleration"]).ravel(), - test_state.kinematic_rate_collection.ravel(), - ) - ) - assert np.all( - np.in1d( - func(self.States["Omega"], self.States["Alpha"]).ravel(), - test_state.kinematic_rate_collection.ravel(), - ) - ) - - def test_state_iadd(self, load_states): - state, derivative = load_states - - scalar = 2.0 - - def inplace_func(x, y): - x.__iadd__(scalar * y) - - def func(x, y): - return x + scalar * y - - inplace_func(state, derivative) - - assert_allclose( - state.position_collection, - func(self.States["Position"], self.States["Velocity"]), - ) - assert np.all( - np.in1d( - func(self.States["Velocity"], self.States["Acceleration"]).ravel(), - state.kinematic_rate_collection.ravel(), - ) - ) - assert np.all( - np.in1d( - func(self.States["Omega"], self.States["Alpha"]).ravel(), - state.kinematic_rate_collection.ravel(), - ) - ) - - -# Choosing 15 and 31 as nelems to reflect common expected -# use case of blocksize = 2*(k), k = int -# https://docs.pytest.org/en/latest/fixture.html -@pytest.fixture(scope="module", params=[15, 31]) -def load_data_for_symplectic_stepper(request): - """Creates data for symplectic stepper classes _KinematicState and _DynamicStata""" - n_elem = request.param - n_nodes = n_elem + 1 - dim = 3 - - position_collection = np.random.randn(dim, n_nodes) - velocity_collection = np.random.randn(dim, n_nodes) - acceleration_collection = np.random.randn(dim, n_nodes) - omega_collection = np.random.randn(dim, n_elem) - alpha_collection = np.random.randn(dim, n_elem) - director_collection = np.random.randn(dim, dim, n_elem) - - v_w_collection = np.zeros((2, dim * n_nodes)) - v_w_collection[0] = velocity_collection.reshape(dim * n_nodes) - # Stack extra zeros to make dimensions match - v_w_collection[1] = np.hstack( - (omega_collection.reshape(dim * n_elem), np.zeros((3))) - ) - - dvdt_dwdt_collection = np.zeros((2, dim * n_nodes)) - dvdt_dwdt_collection[0] = acceleration_collection.reshape(dim * n_nodes) - # Stack extra zeros to make dimensions match - dvdt_dwdt_collection[1] = np.hstack( - (alpha_collection.reshape(dim * n_elem), np.zeros((3))) - ) - - yield n_elem, position_collection, director_collection, v_w_collection, dvdt_dwdt_collection, velocity_collection, omega_collection - - -class LoadStatesForSymplecticStepper: - """Mixin class for testing symplectic - stepper behaviors that manipulate state objects. - """ - - Vectors = None - Directors = None - States = { - "Position": None, - "Directors": None, - "Velocity": None, - "Omega": None, - "Acceleration": None, - "Alpha": None, - } - - @pytest.fixture - def load_states(self, load_data_for_symplectic_stepper): - dim = 3 - ( - n_elem, - position_collection, - director_collection, - v_w_collection, - dvdt_dwdt_collection, - velocity_collection, - omega_collection, - ) = load_data_for_symplectic_stepper - - kinematic_states = _KinematicState(position_collection, director_collection) - dynamic_states = _DynamicState( - v_w_collection, dvdt_dwdt_collection, velocity_collection, omega_collection - ) - - self.States["Position"] = position_collection.copy() - self.States["Directors"] = director_collection.copy() - self.States["Velocity"] = velocity_collection.copy() - self.States["Omega"] = omega_collection.copy() - self.States["Acceleration"] = ( - dvdt_dwdt_collection[0].reshape(dim, n_elem + 1).copy() - ) - self.States["Alpha"] = ( - dvdt_dwdt_collection[1, 0 : dim * n_elem].reshape(dim, n_elem).copy() - ) - - return kinematic_states, dynamic_states, n_elem - - -class TestSymplecticStepperStateBehavior(LoadStatesForSymplecticStepper): - """This test case is changed for the block structure implementation and it is only testing - symplectic steppers. - """ - - # TODO : update tests after including Rodrigues rotation properly - StepperType = "symplectic" - - def test_dynamic_state_returns_correct_kinematic_rates(self, load_states): - kin_state, dyn_state, _ = load_states - # 0.0 in the function parameter is time=0.0 - # numba complains if this is included - assert np.all( - np.in1d(self.States["Velocity"].ravel(), dyn_state.kinematic_rates(0.0)[0]) - ) - assert np.all( - np.in1d(self.States["Omega"].ravel(), dyn_state.kinematic_rates(0.0)[1]) - ) - - def test_dynamic_state_returns_correct_dynamic_rates(self, load_states): - kin_state, dyn_state, _ = load_states - # 0.0 in the function parameter is time=0.0 - # numba complains if this is included - assert np.all( - np.in1d( - self.States["Acceleration"].ravel(), - dyn_state.dynamic_rates(0.0, 1.0).ravel(), - ) - ) - assert np.all( - np.in1d( - self.States["Alpha"].ravel(), dyn_state.dynamic_rates(0.0, 1.0).ravel() - ) - ) - - def test_dynamic_state_iadd(self, load_states): - from elastica.rod.data_structures import ( - overload_operator_dynamic_numba, - ) - - _, dyn_state, _ = load_states - scalar = 2.0 - - def inplace_func(x, y): - # x.iadd(scalar * y.dynamic_rates(0.0)) # USE THIS WITH JITCLASS - overload_operator_dynamic_numba( - x.rate_collection, y.dynamic_rates(0.0, scalar) - ) - - def func(x, y): - return x + scalar * y - - inplace_func(dyn_state, dyn_state) - - temp = func(self.States["Velocity"], self.States["Acceleration"]) - assert np.all(np.in1d(temp.ravel(), dyn_state.rate_collection.ravel())) - - temp = func(self.States["Omega"], self.States["Alpha"]) - assert np.all(np.in1d(temp.ravel(), dyn_state.rate_collection.ravel())) - - def test_kinematic_state_iadd(self, load_states): - from elastica.rod.data_structures import ( - overload_operator_kinematic_numba, - ) - - kin_state, dyn_state, n_elem = load_states - scalar = 2.0 - - def inplace_func(x, y): - # x.iadd(scalar * y.kinematic_rates(0.0)) # USE THIS WITH JITCLASS - overload_operator_kinematic_numba( - n_elem + 1, - scalar, - x.position_collection, - x.director_collection, - y.velocity_collection, - y.omega_collection, - ) - - def func(x, y): - return x + scalar * y - - inplace_func(kin_state, dyn_state) - - assert_allclose( - kin_state.position_collection, - func(self.States["Position"], self.States["Velocity"]), - ) - # FIXME How to test directors? diff --git a/tests/test_surface/test_plane.py b/tests/test_surface/test_plane.py new file mode 100644 index 000000000..f82d6a0ce --- /dev/null +++ b/tests/test_surface/test_plane.py @@ -0,0 +1,41 @@ +__doc__ = """Tests for plane surface class""" +import numpy as np +from numpy.testing import assert_allclose +from elastica.utils import Tolerance +from elastica.surface import Plane +import pytest + + +# tests Initialisation of plane +def test_plane_initialization(): + """ + This test case is for testing initialization of rigid sphere and it checks the + validity of the members of sphere class. + + Returns + ------- + + """ + # setting up test params + plane_origin = np.random.rand(3).reshape(3, 1) + plane_normal_direction = np.random.rand(3).reshape(3) + plane_normal = plane_normal_direction / np.linalg.norm(plane_normal_direction) + + test_plane = Plane(plane_origin, plane_normal) + # checking plane origin + assert_allclose( + test_plane.origin, + plane_origin, + atol=Tolerance.atol(), + ) + + # checking plane normal + assert_allclose(test_plane.normal, plane_normal, atol=Tolerance.atol()) + + # check unnormalized error message + invalid_plane_normal = np.zeros( + 3, + ) + with pytest.raises(AssertionError) as excinfo: + test_plane = Plane(plane_origin, invalid_plane_normal) + assert "plane normal is not a unit vector" in str(excinfo.value) diff --git a/tests/test_synchronize_periodic_boundary.py b/tests/test_synchronize_periodic_boundary.py index 1ea3badf7..6f6cd2247 100644 --- a/tests/test_synchronize_periodic_boundary.py +++ b/tests/test_synchronize_periodic_boundary.py @@ -9,7 +9,7 @@ ) from elastica.utils import Tolerance import pytest -from tests.test_rod.test_rods import MockTestRingRod +from tests.test_rod.mock_rod import MockTestRingRod @pytest.mark.parametrize("n_elems", [10, 30, 40])