From 49e4c30b84c10f9265a35adbd503a8d82da122f9 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 26 Sep 2025 12:12:06 -0500 Subject: [PATCH 01/85] update version --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cd6dc7d6..59fba5f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pyelastica" -version = "0.3.3post1" +version = "1.0.0" description = "Elastica is a software to simulate the dynamics of filaments that, at every cross-section, can undergo all six possible modes of deformation, allowing the filament to bend, twist, stretch and shear, while interacting with complex environments via muscular activity, surface contact, friction and hydrodynamics." readme = "README.md" authors = [ diff --git a/uv.lock b/uv.lock index 61121713..100ada42 100644 --- a/uv.lock +++ b/uv.lock @@ -1279,7 +1279,7 @@ wheels = [ [[package]] name = "pyelastica" -version = "0.3.3.post1" +version = "1.0.0" source = { editable = "." } dependencies = [ { name = "cma" }, From 5d273275a5096a2d29f34f809c1a5bf8c2397531 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 28 Sep 2025 16:49:18 -0500 Subject: [PATCH 02/85] tests: add behavior test for simulator.close feature --- tests/test_modules/test_callbacks_close.py | 65 ++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 tests/test_modules/test_callbacks_close.py diff --git a/tests/test_modules/test_callbacks_close.py b/tests/test_modules/test_callbacks_close.py new file mode 100644 index 00000000..4f3fe0cb --- /dev/null +++ b/tests/test_modules/test_callbacks_close.py @@ -0,0 +1,65 @@ +__doc__ = """ Test modules for callback """ +import numpy as np +import pytest + +from elastica.callback_functions import CallBackBaseClass + + +class TestCallBacksClosing: + from elastica.modules import BaseSystemCollection + from elastica.modules import CallBacks + + class SystemCollectionWithCallBacksMixedin(BaseSystemCollection, CallBacks): + pass + + def test_callback_closing_test_default_callback_impl(self): + """ + Test if any class derived from CallBackBaseClass can be used + without any error when simulator.close() is called. + This is to check the backward compatibility, as many previous + callback classes are derived from CallBackBaseClass, + but does not have explicit implementation of on_close method. + """ + sys_coll = self.SystemCollectionWithCallBacksMixedin() + sys_coll.extend_allowed_types((int,)) + rod = 0 + + class MockCallback(CallBackBaseClass): + pass + + # build flag check for some MockCallback.on_close() function call + + sys_coll.append(rod) + sys_coll.collect_diagnostics(rod).using(MockCallback) + sys_coll.close() + + def test_callback_closing_custom(self): + """ + Check if on_close is called properly with a custom callback. + """ + sys_coll = self.SystemCollectionWithCallBacksMixedin() + sys_coll.extend_allowed_types((int,)) + rod = 0 + + CLOSE_CALLED_FLAG = [] + + class MockCallback(CallBackBaseClass): + def __init__(self, o): + self.o = o + + def on_close(self): + self.o.append(42) + + sys_coll.append(rod) + sys_coll.collect_diagnostics(rod).using(MockCallback, o=CLOSE_CALLED_FLAG) + sys_coll.close() + + # Before finalize, on_close function should not be hooked. + assert not CLOSE_CALLED_FLAG + + # After finalize, on_close function should be called. + sys_coll.finalize() + sys_coll.close() + + assert len(CLOSE_CALLED_FLAG) == 1 + assert CLOSE_CALLED_FLAG[0] == 42 From 563ffb786a3e8aa568ca49a6c944194532eb3b67 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 28 Sep 2025 16:50:37 -0500 Subject: [PATCH 03/85] feat: fix ExportCallback example case --- elastica/callback_functions.py | 18 +++++++++--------- elastica/modules/memory_block.py | 1 + tests/test_callback_functions.py | 22 +++++----------------- 3 files changed, 15 insertions(+), 26 deletions(-) diff --git a/elastica/callback_functions.py b/elastica/callback_functions.py index 50c13cf7..215989d1 100644 --- a/elastica/callback_functions.py +++ b/elastica/callback_functions.py @@ -43,7 +43,12 @@ def make_callback(self, system: T, time: np.float64, current_step: int) -> None: Simulation step. """ - pass + + def on_close(self) -> None: + """ + This method is called collectively when when .close() is + called by the system collection. + """ class MyCallBack(CallBackBaseClass): @@ -264,15 +269,10 @@ def get_last_saved_path(self) -> Optional[str]: else: return self.save_path.format(self.file_count - 1, self._ext) - def close(self) -> None: + def on_close(self) -> None: """ - Save residual buffer + Save residual buffer. + Can be called using `simulator.close()`. """ if self.buffer_size: self._dump() - - def clear(self) -> None: - """ - Alias to `close` - """ - self.close() diff --git a/elastica/modules/memory_block.py b/elastica/modules/memory_block.py index fa75f744..6cebcc63 100644 --- a/elastica/modules/memory_block.py +++ b/elastica/modules/memory_block.py @@ -3,6 +3,7 @@ Cosserat Rods, Rigid Body etc. """ from typing import cast + from elastica.typing import ( RodType, RigidBodyType, diff --git a/tests/test_callback_functions.py b/tests/test_callback_functions.py index 4629ef35..9db898f9 100644 --- a/tests/test_callback_functions.py +++ b/tests/test_callback_functions.py @@ -171,22 +171,22 @@ def test_export_call_back_step_skip_param(self, step_skip): callback = ExportCallBack(step_skip, "rod", temp_dir_path, "npz") callback.make_callback(mock_rod, 1, step_skip - 1) # Check empty - callback.clear() + callback.on_close() saved_path_name = callback.get_last_saved_path() assert saved_path_name is None, "No file should be saved." # Check saved callback.make_callback(mock_rod, 1, step_skip) - callback.clear() + callback.on_close() saved_path_name = callback.get_last_saved_path() assert saved_path_name is not None, "File should be saved." assert os.path.exists(saved_path_name), "File should be saved" # Check saved file number callback.make_callback(mock_rod, 1, step_skip * 2) - callback.clear() + callback.on_close() callback.make_callback(mock_rod, 1, step_skip * 5) - callback.clear() + callback.on_close() saved_path_name = callback.get_last_saved_path() assert ( str(2) in saved_path_name @@ -222,19 +222,7 @@ def test_export_call_back_close_test(self, rng): ) for step in range(10): callback.make_callback(mock_rod, 1, step) - callback.close() - saved_path_name = callback.get_last_saved_path() - assert os.path.exists(saved_path_name), "File is not saved." - - def test_export_call_back_clear_test(self, rng): - mock_rod = MockRodWithElements(5) - with tempfile.TemporaryDirectory() as temp_dir_path: - callback = ExportCallBack( - 1, "rod", temp_dir_path, "npz", file_save_interval=50 - ) - for step in range(10): - callback.make_callback(mock_rod, 1, step) - callback.clear() + callback.on_close() saved_path_name = callback.get_last_saved_path() assert os.path.exists(saved_path_name), "File is not saved." From 01d0fc33678bf74f1a58774eacf0a5913dc13eb7 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 28 Sep 2025 16:51:01 -0500 Subject: [PATCH 04/85] feat: add simulator.close() method for collective end-call for callbacks --- elastica/modules/base_system.py | 14 +++++++++++++- elastica/modules/callbacks.py | 4 ++++ elastica/modules/memory_block.py | 18 ++---------------- elastica/modules/protocol.py | 6 +++++- elastica/timestepper/protocol.py | 5 ++++- elastica/timestepper/symplectic_steppers.py | 10 +++++++--- 6 files changed, 35 insertions(+), 22 deletions(-) diff --git a/elastica/modules/base_system.py b/elastica/modules/base_system.py index abd7d8c3..a6bd28a3 100644 --- a/elastica/modules/base_system.py +++ b/elastica/modules/base_system.py @@ -5,7 +5,7 @@ Basic coordinating for multiple, smaller systems that have an independently integrable interface (i.e. works with symplectic or explicit routines `timestepper.py`.) """ -from typing import TYPE_CHECKING, Type, Generator, Any, overload +from typing import TYPE_CHECKING, Type, Generator, Any, overload, Callable from typing import final from elastica.typing import ( SystemType, @@ -72,6 +72,9 @@ def __init__(self) -> None: self._feature_group_callback: OperatorGroupFIFO[ OperatorCallbackType, ModuleProtocol ] = OperatorGroupFIFO() + self._feature_group_on_close: OperatorGroupFIFO[Callable, ModuleProtocol] = ( + OperatorGroupFIFO() + ) self._feature_group_finalize: list[OperatorFinalizeType] = [] # We need to initialize our mixin classes super().__init__() @@ -282,6 +285,15 @@ def apply_callbacks(self, time: np.float64, current_step: int) -> None: for func in self._feature_group_callback: func(time=time, current_step=current_step) + @final + def close(self) -> None: + """ + Call close functions for all features. + Features are registered in _feature_group_on_close. + """ + for func in self._feature_group_on_close: + func() + if TYPE_CHECKING: from .protocol import SystemCollectionProtocol diff --git a/elastica/modules/callbacks.py b/elastica/modules/callbacks.py index 5638f8ba..f3b5b5f0 100644 --- a/elastica/modules/callbacks.py +++ b/elastica/modules/callbacks.py @@ -57,6 +57,7 @@ def collect_diagnostics( _callback: ModuleProtocol = _CallBack(sys_idx) self._callback_list.append(_callback) self._feature_group_callback.append_id(_callback) + self._feature_group_on_close.append_id(_callback) return _callback @@ -70,6 +71,9 @@ def _finalize_callback(self: SystemCollectionWithCallbackProtocol) -> None: callback_instance.make_callback, system=self[sys_id] ) self._feature_group_callback.add_operators(callback, [callback_operator]) + self._feature_group_on_close.add_operators( + callback, [callback_instance.on_close] + ) self._callback_list.clear() del self._callback_list diff --git a/elastica/modules/memory_block.py b/elastica/modules/memory_block.py index 6cebcc63..d14900ee 100644 --- a/elastica/modules/memory_block.py +++ b/elastica/modules/memory_block.py @@ -51,25 +51,11 @@ def construct_memory_block_structures( temp_list_for_rigid_body_systems_idx.append(system_idx) elif isinstance(sys_to_be_added, SurfaceBase): + # TODO: Surface type is passive system pass - # surface_system = cast(SurfaceType, sys_to_be_added) - # raise NotImplementedError( - # "Surfaces are not yet implemented in memory block construction." - # ) else: - raise TypeError( - "{0}\n" - "is not a system passing validity\n" - "checks for constructing block structure. If you are sure that\n" - "{0}\n" - "satisfies all criteria for being a system, please add\n" - "it here with correct memory block implementation.\n" - "The allowed types are\n" - "{1} {2} {3}".format( - sys_to_be_added.__class__, RodBase, RigidBodyBase, SurfaceBase - ) - ) + continue # No error:: any typechecking should be finished by BaseSystemCollection._check_type if temp_list_for_cosserat_rod_systems: _memory_blocks.append( diff --git a/elastica/modules/protocol.py b/elastica/modules/protocol.py index 4670a301..e17b23ca 100644 --- a/elastica/modules/protocol.py +++ b/elastica/modules/protocol.py @@ -1,4 +1,4 @@ -from typing import Protocol, Generator, TypeVar, Any, Type, overload, Iterator +from typing import Protocol, Generator, TypeVar, Any, Type, overload, Iterator, Callable from typing import TYPE_CHECKING from typing_extensions import Self # python 3.11: from typing import Self @@ -80,6 +80,10 @@ def apply_callbacks(self, time: np.float64, current_step: int) -> None: ... def finalize(self) -> None: ... + _feature_group_on_close: "OperatorGroupFIFO[Callable, ModuleProtocol]" + + def close(self) -> None: ... + # Mixin Protocols (Used to type Self) class ConnectedSystemCollectionProtocol(SystemCollectionProtocol, Protocol): diff --git a/elastica/timestepper/protocol.py b/elastica/timestepper/protocol.py index 18a92fc4..d228c0ea 100644 --- a/elastica/timestepper/protocol.py +++ b/elastica/timestepper/protocol.py @@ -25,7 +25,10 @@ def n_stages(self) -> int: ... def step_methods(self) -> SteppersOperatorsType: ... def step( - self, SystemCollection: SystemCollectionType, time: np.float64, dt: np.float64 + self, + SystemCollection: SystemCollectionType, + time: np.float64 | float, + dt: np.float64 | float, ) -> np.float64: ... def step_single_instance( diff --git a/elastica/timestepper/symplectic_steppers.py b/elastica/timestepper/symplectic_steppers.py index 4bd355af..c7da9e0a 100644 --- a/elastica/timestepper/symplectic_steppers.py +++ b/elastica/timestepper/symplectic_steppers.py @@ -66,11 +66,15 @@ def n_stages(self: SymplecticStepperProtocol) -> int: def step( self: SymplecticStepperProtocol, SystemCollection: SystemCollectionType, - time: np.float64, - dt: np.float64, + time: np.float64 | float, + dt: np.float64 | float, ) -> np.float64: return SymplecticStepperMixin.do_step( - self, self.steps_and_prefactors, SystemCollection, time, dt + self, + self.steps_and_prefactors, + SystemCollection, + np.float64(time), + np.float64(dt), ) # TODO: Merge with .step method in the future. From 931e9dcbdffbdd98b4f5a8fbe57f6366a64c888e Mon Sep 17 00:00:00 2001 From: Bokun Zheng <56558970+zzzzzbk@users.noreply.github.com> Date: Fri, 17 Oct 2025 13:32:45 -0400 Subject: [PATCH 05/85] fix: compute_additional_segment directions (#509) * Fix direction not updated by time step * Fix knot theory: avoid looping inside if resolve #509 --- elastica/rod/knot_theory.py | 17 +++++++++++------ tests/test_rod/test_knot_theory.py | 9 +++++++-- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/elastica/rod/knot_theory.py b/elastica/rod/knot_theory.py index 64b89e88..44ab7688 100644 --- a/elastica/rod/knot_theory.py +++ b/elastica/rod/knot_theory.py @@ -723,6 +723,9 @@ def _compute_additional_segment( # Direction of the additional point at the end of the rod direction_of_rod_end = center_line[i, :, -1] - center_line[i, :, -2] direction_of_rod_end /= np.linalg.norm(direction_of_rod_end) + + beginning_direction[i, :] = direction_of_rod_begin + end_direction[i, :] = direction_of_rod_end elif type_of_additional_segment == "end_to_end": for i in range(timesize): # Direction of the additional point at the beginning of the rod @@ -731,6 +734,9 @@ def _compute_additional_segment( # Direction of the additional point at the end of the rod direction_of_rod_end = -direction_of_rod_begin + + beginning_direction[i, :] = direction_of_rod_begin + end_direction[i, :] = direction_of_rod_end elif type_of_additional_segment == "net_tangent": for i in range(timesize): # Direction of the additional point at the beginning of the rod @@ -745,19 +751,18 @@ def _compute_additional_segment( direction_of_rod_begin = average_begin - average_end direction_of_rod_begin /= np.linalg.norm(direction_of_rod_begin) direction_of_rod_end = -direction_of_rod_begin + + beginning_direction[i, :] = direction_of_rod_begin + end_direction[i, :] = direction_of_rod_end else: raise NotImplementedError("unavailable type_of_additional_segment is given") # Compute new centerline and beginning/end direction for i in range(timesize): - first_point = center_line[i, :, 0] + segment_length * direction_of_rod_begin - last_point = center_line[i, :, -1] + segment_length * direction_of_rod_end - + first_point = center_line[i, :, 0] + segment_length * beginning_direction[i, :] + last_point = center_line[i, :, -1] + segment_length * end_direction[i, :] new_center_line[i, :, 1:-1] = center_line[i, :, :] new_center_line[i, :, 0] = first_point new_center_line[i, :, -1] = last_point - beginning_direction[i, :] = direction_of_rod_begin - end_direction[i, :] = direction_of_rod_end - return new_center_line, beginning_direction, end_direction diff --git a/tests/test_rod/test_knot_theory.py b/tests/test_rod/test_knot_theory.py index dfe7466e..03004d23 100644 --- a/tests/test_rod/test_knot_theory.py +++ b/tests/test_rod/test_knot_theory.py @@ -183,9 +183,14 @@ def test_knot_theory_compute_additional_segment_integrity(type_str): def test_knot_theory_compute_additional_segment_straight_case( n_elem, segment_length, type_str ): - # If straight rod give, result should be same regardless of type - center_line = np.zeros([1, 3, n_elem]) + # If straight rod give, result should be same regardless of type and time steps + center_line = np.zeros([2, 3, n_elem]) center_line[0, 2, :] = np.linspace(0, 5, n_elem) + + # adding a sine curve to second time step to ensure it does not affect straight case + center_line[1, 1, :] = np.sin(np.linspace(0, 5, n_elem)) + center_line[1, 2, :] = np.cos(np.linspace(0, 5, n_elem)) + ncl, bd, ed = _compute_additional_segment(center_line, segment_length, type_str) assert_allclose(ncl[0, :, 0], np.array([0, 0, -segment_length])) assert_allclose( From b52060591977b99d1d483911309d402bc7701b09 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 10 Oct 2025 14:15:44 -0500 Subject: [PATCH 06/85] specify python version installed for github-action --- .github/workflows/main.yml | 4 ++-- .gitignore | 1 + Makefile | 9 +++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 989f9964..e1b1f169 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -62,7 +62,7 @@ jobs: # Install dependencies - name: Install dependencies run: | - make install-dev-deps + make install-dev-deps PYTHON_VERSION=${{ matrix.python-version }} uv cache prune --ci source .venv/bin/activate # Runs a single command using the runners shell @@ -111,7 +111,7 @@ jobs: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }} - name: Install dependencies run: | - make install-dev-deps + make install-dev-deps PYTHON_VERSION=${{ matrix.python-version }} uv cache prune --ci source .venv/bin/activate - name: Test PyElastica using pytest diff --git a/.gitignore b/.gitignore index da20c6d3..91f44a17 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ __pycache__/ *.so *.swp +.vscode # Distribution / packaging .Python diff --git a/Makefile b/Makefile index 523ab386..b70f1c3b 100644 --- a/Makefile +++ b/Makefile @@ -2,15 +2,24 @@ PYTHON := python3 PYTHONPATH := `pwd` AUTOFLAKE_ARGS := -r +PYTHON_VERSION := #* Installation .PHONY: install install: +ifdef PYTHON_VERSION + uv sync --python $(PYTHON_VERSION) +else uv sync +endif .PHONY: install-dev-deps install-dev-deps: +ifdef PYTHON_VERSION + uv sync --all-groups --all-extras --python $(PYTHON_VERSION) +else uv sync --all-groups --all-extras +endif .PHONY: build From ced1993955177c385e9987ad45202a6d5cf36682 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 26 Sep 2025 16:00:07 -0500 Subject: [PATCH 07/85] Refactor simulation integration in examples and documentations to use timestepper loop explicitly instead of integrate function --- docs/guide/workflow.md | 14 +++++++++----- elastica/timestepper/__init__.py | 2 ++ examples/AxialStretchingCase/axial_stretching.py | 5 ++++- examples/Binder/1_Timoshenko_Beam.ipynb | 10 ++++++---- examples/Binder/2_Slithering_Snake.ipynb | 6 ++++-- .../general_constraint_allow_yaw.py | 5 ++++- examples/ButterflyCase/butterfly.py | 5 ++++- .../cantilever_conservative_distributed_load.py | 5 ++++- .../cantilever_nonconservative_distributed_load.py | 5 ++++- .../cantilever_transversal_load.py | 6 ++++-- .../convergence_cantilever_transversal_load.py | 6 ++++-- examples/CatenaryCase/catenary.py | 5 ++++- .../ContinuumFlagellaCase/continuum_flagella.py | 5 ++++- examples/ContinuumSnakeCase/continuum_snake.py | 5 ++++- .../continuum_snake_with_lifting_wave.py | 5 ++++- .../DynamicCantileverCase/dynamic_cantilever.py | 10 ++++------ .../generic_system_type_fixed_joint.py | 5 ++++- .../generic_system_type_spherical_joint.py | 5 ++++- .../parallel_connection_example.py | 5 ++++- .../flexible_swinging_pendulum.py | 5 ++++- examples/FrictionValidationCases/axial_friction.py | 5 ++++- .../rolling_friction_initial_velocity.py | 7 ++++--- .../rolling_friction_on_inclined_plane.py | 7 ++++--- .../rolling_friction_torque.py | 5 ++++- .../convergence_helicalbuckling.py | 5 ++++- examples/HelicalBucklingCase/helicalbuckling.py | 5 ++++- examples/JointCases/fixed_joint.py | 5 ++++- examples/JointCases/fixed_joint_torsion.py | 5 ++++- examples/JointCases/hinge_joint.py | 5 ++++- examples/JointCases/spherical_joint.py | 5 ++++- examples/KnotCase/knot_simulation.py | 5 ++++- examples/MuscularFlagella/muscular_flagella.py | 5 ++++- examples/MuscularSnake/muscular_snake.py | 5 ++++- examples/RestartExample/restart_example.py | 11 ++++------- .../RodRigidBodyContact/rod_cylinder_contact.py | 7 ++++--- .../rod_cylinder_contact_friction.py | 7 ++++--- .../rod_cylinder_contact_validation.py | 5 ++++- .../rod_cylinder_contact_with_y_normal.py | 5 ++++- .../rigid_cylinder_rotational_motion_case.py | 5 ++++- .../rigid_cylinder_translational_motion_case.py | 5 ++++- .../rigid_sphere_rotational_motion_case.py | 5 ++++- .../rigid_sphere_translational_motion_case.py | 5 ++++- examples/RingRodCase/ring_rod.py | 5 ++++- .../rod_rod_contact_inclined_validation.py | 5 ++++- .../rod_rod_contact_parallel_validation.py | 5 ++++- .../PlectonemesCase/plectoneme_case.py | 5 ++++- .../RodSelfContact/SolenoidsCase/solenoid_case.py | 5 ++++- .../TimoshenkoBeamCase/convergence_timoshenko.py | 5 ++++- examples/TimoshenkoBeamCase/timoshenko.py | 5 ++++- .../tumbling_unconstrained_rod.py | 8 ++++++-- .../ContinuumSnakeVisualization/continuum_snake.py | 5 ++++- 51 files changed, 211 insertions(+), 80 deletions(-) diff --git a/docs/guide/workflow.md b/docs/guide/workflow.md index 0a823c90..100b724e 100644 --- a/docs/guide/workflow.md +++ b/docs/guide/workflow.md @@ -38,8 +38,8 @@ Available components are: | [Constraints](../api/constraints.rst) | | | [Forcing](../api/external_forces.rst) | | | [Connections](../api/connections.rst) | | -| [CallBacks](../api/callback.rst) | | -| [Damping](../api/damping.rst) | | +| [CallBacks](../api/callback.rst) | | +| [Damping](../api/damping.rst) | | :::{Note} We adopted a composition and mixin design paradigm in building elastica. The detail of the implementation is not important in using the package, but we left some references to read [here](../advanced/PackageDesign.md). @@ -232,18 +232,22 @@ This goes through and collects all the rods and applied conditions, preparing th

6. Set Timestepper

-With our system now ready to be run, we need to define which time stepping algorithm to use. Currently, we suggest using the position Verlet algorithm. We also need to define how much time we want to simulate as well as either the time step (dt) or the number of total time steps we want to take. Once we have defined these things, we can run the simulation by calling `integrate()`, which will start the simulation. +With our system now ready to be run, we need to define which time stepping algorithm to use. Currently, we suggest using the position Verlet algorithm. We also need to define how much time we want to simulate as well as either the time step (dt) or the number of total time steps we want to take. Once we have defined these things, we can run the simulation using an a timestepper loop. >> We are still actively testing different integration and time-stepping techniques, `PositionVerlet` is the best default at this moment. ```python from elastica.timestepper.symplectic_steppers import PositionVerlet -from elastica.timestepper import integrate timestepper = PositionVerlet() final_time = 10 # seconds total_steps = int(final_time / dt) -integrate(timestepper, simulator, final_time, total_steps) + +# Explicit timestepper loop +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(simulator, time, dt) ``` More documentation on timestepper and integrator is included [here](../api/time_steppers.rst) diff --git a/elastica/timestepper/__init__.py b/elastica/timestepper/__init__.py index 264c9d8b..d87e7840 100644 --- a/elastica/timestepper/__init__.py +++ b/elastica/timestepper/__init__.py @@ -29,6 +29,8 @@ def extend_stepper_interface( return do_step_method, stepper_methods +# Deprecated: Kept for backward compatibility. +# Recommended to call integration loop explicitly by users. def integrate( stepper: StepperProtocol, systems: SystemCollectionType, diff --git a/examples/AxialStretchingCase/axial_stretching.py b/examples/AxialStretchingCase/axial_stretching.py index a3d733f6..a4f54a81 100644 --- a/examples/AxialStretchingCase/axial_stretching.py +++ b/examples/AxialStretchingCase/axial_stretching.py @@ -131,7 +131,10 @@ def make_callback( total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, stretch_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(stretch_sim, time, dt) if PLOT_FIGURE: # First-order theory with base-length diff --git a/examples/Binder/1_Timoshenko_Beam.ipynb b/examples/Binder/1_Timoshenko_Beam.ipynb index fa2a389f..d71d459a 100644 --- a/examples/Binder/1_Timoshenko_Beam.ipynb +++ b/examples/Binder/1_Timoshenko_Beam.ipynb @@ -57,8 +57,7 @@ "from elastica.external_forces import EndpointForces\n", "\n", "# Import Timestepping Functions\n", - "from elastica.timestepper.symplectic_steppers import PositionVerlet\n", - "from elastica.timestepper import integrate" + "from elastica.timestepper.symplectic_steppers import PositionVerlet" ] }, { @@ -404,7 +403,7 @@ }, "source": [ "## Run Simulation\n", - "We are now ready to perform the simulation. To run the simulation, we `integrate` the `timoshenko_sim` system using the `timestepper` method until `final_time` by taking `total_steps`. As currently setup, the beam simulation takes about 1 minute to run. " + "We are now ready to perform the simulation. To run the simulation, we integrate the `timoshenko_sim` system using the `timestepper` method until `final_time` by taking `total_steps`. As currently setup, the beam simulation takes about 1 minute to run. " ] }, { @@ -417,7 +416,10 @@ }, "outputs": [], "source": [ - "integrate(timestepper, timoshenko_sim, final_time, total_steps)" + "dt = final_time / total_steps\n", + "time = 0.0\n", + "for i in range(total_steps):\n", + " time = timestepper.step(timoshenko_sim, time, dt)" ] }, { diff --git a/examples/Binder/2_Slithering_Snake.ipynb b/examples/Binder/2_Slithering_Snake.ipynb index cec14b8f..00d5cadc 100644 --- a/examples/Binder/2_Slithering_Snake.ipynb +++ b/examples/Binder/2_Slithering_Snake.ipynb @@ -52,7 +52,6 @@ "\n", "# import timestepping functions\n", "from elastica.timestepper.symplectic_steppers import PositionVerlet\n", - "from elastica.timestepper import integrate\n", "\n", "# import call back functions\n", "from elastica.callback_functions import CallBackBaseClass\n", @@ -389,7 +388,10 @@ }, "outputs": [], "source": [ - "integrate(timestepper, snake_sim, final_time, total_steps)" + "dt = final_time / total_steps\n", + "time = 0.0\n", + "for i in range(total_steps):\n", + " time = timestepper.step(snake_sim, time, dt)" ] }, { diff --git a/examples/BoundaryConditionsCases/general_constraint_allow_yaw.py b/examples/BoundaryConditionsCases/general_constraint_allow_yaw.py index 99989ce7..b4fce8f5 100644 --- a/examples/BoundaryConditionsCases/general_constraint_allow_yaw.py +++ b/examples/BoundaryConditionsCases/general_constraint_allow_yaw.py @@ -103,7 +103,10 @@ class GeneralConstraintSimulator( timestepper = ea.PositionVerlet() print("Total steps", total_steps) -ea.integrate(timestepper, general_constraint_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(general_constraint_sim, time, dt) plot_orientation( diff --git a/examples/ButterflyCase/butterfly.py b/examples/ButterflyCase/butterfly.py index 52946306..5fd5d050 100644 --- a/examples/ButterflyCase/butterfly.py +++ b/examples/ButterflyCase/butterfly.py @@ -126,7 +126,10 @@ def make_callback( dt = 0.01 * dl total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, butterfly_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(butterfly_sim, time, dt) if PLOT_FIGURE: # Plot the histories diff --git a/examples/CantileverDistributedLoad/cantilever_conservative_distributed_load.py b/examples/CantileverDistributedLoad/cantilever_conservative_distributed_load.py index 4dfc9cbc..1dca913e 100644 --- a/examples/CantileverDistributedLoad/cantilever_conservative_distributed_load.py +++ b/examples/CantileverDistributedLoad/cantilever_conservative_distributed_load.py @@ -124,7 +124,10 @@ def make_callback(self, system, time, current_step: int): timestepper = ea.PositionVerlet() total_steps = int(final_time / dt) - ea.integrate(timestepper, square_rod_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(square_rod_sim, time, dt) relative_tip_position = np.zeros( 2, diff --git a/examples/CantileverDistributedLoad/cantilever_nonconservative_distributed_load.py b/examples/CantileverDistributedLoad/cantilever_nonconservative_distributed_load.py index 7b3f91fe..6e394cd3 100644 --- a/examples/CantileverDistributedLoad/cantilever_nonconservative_distributed_load.py +++ b/examples/CantileverDistributedLoad/cantilever_nonconservative_distributed_load.py @@ -120,7 +120,10 @@ def make_callback(self, system, time, current_step: int): total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, square_rod_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(square_rod_sim, time, dt) if plot_figure_equilibrium: diff --git a/examples/CantileverTransversalLoadCase/cantilever_transversal_load.py b/examples/CantileverTransversalLoadCase/cantilever_transversal_load.py index c37c2279..56f9b7d4 100644 --- a/examples/CantileverTransversalLoadCase/cantilever_transversal_load.py +++ b/examples/CantileverTransversalLoadCase/cantilever_transversal_load.py @@ -2,7 +2,6 @@ from elastica.boundary_conditions import OneEndFixedBC from elastica.external_forces import EndpointForces from elastica.timestepper.symplectic_steppers import PositionVerlet -from elastica.timestepper import integrate import elastica as ea from examples.convergence_functions import calculate_error_norm from cantilever_transversal_load_postprocessing import adjust_square_cross_section @@ -126,7 +125,10 @@ class SquareRodSimulator( timestepper = PositionVerlet() - integrate(timestepper, square_rod_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(square_rod_sim, time, dt) print(square_rod.position_collection[2, ...]) error, l1, l2, linf = calculate_error_norm( diff --git a/examples/CantileverTransversalLoadCase/convergence_cantilever_transversal_load.py b/examples/CantileverTransversalLoadCase/convergence_cantilever_transversal_load.py index 6a47db3a..c37c0db9 100644 --- a/examples/CantileverTransversalLoadCase/convergence_cantilever_transversal_load.py +++ b/examples/CantileverTransversalLoadCase/convergence_cantilever_transversal_load.py @@ -2,7 +2,6 @@ from elastica.boundary_conditions import OneEndFixedBC from elastica.external_forces import EndpointForces from elastica.timestepper.symplectic_steppers import PositionVerlet -from elastica.timestepper import integrate import elastica as ea from examples.convergence_functions import calculate_error_norm from cantilever_transversal_load_postprocessing import adjust_square_cross_section @@ -126,7 +125,10 @@ class SquareRodSimulator( timestepper = PositionVerlet() - integrate(timestepper, squarerod_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(squarerod_sim, time, dt) print(rod.position_collection[2, ...]) error, l1, l2, linf = calculate_error_norm( diff --git a/examples/CatenaryCase/catenary.py b/examples/CatenaryCase/catenary.py index 6875749e..b228de01 100644 --- a/examples/CatenaryCase/catenary.py +++ b/examples/CatenaryCase/catenary.py @@ -112,7 +112,10 @@ def make_callback( timestepper: ea.typing.StepperProtocol = ea.PositionVerlet() -ea.integrate(timestepper, catenary_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(catenary_sim, time, dt) position = np.array(recorded_history["position"]) b = np.min(position[-1][2]) diff --git a/examples/ContinuumFlagellaCase/continuum_flagella.py b/examples/ContinuumFlagellaCase/continuum_flagella.py index e4314349..2c2f42ca 100644 --- a/examples/ContinuumFlagellaCase/continuum_flagella.py +++ b/examples/ContinuumFlagellaCase/continuum_flagella.py @@ -129,7 +129,10 @@ def make_callback(self, system, time, current_step: int): final_time = (10.0 + 0.01) * period total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, flagella_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(flagella_sim, time, dt) if PLOT_FIGURE: filename_plot = "continuum_flagella_velocity.png" diff --git a/examples/ContinuumSnakeCase/continuum_snake.py b/examples/ContinuumSnakeCase/continuum_snake.py index f998754a..f773c0df 100644 --- a/examples/ContinuumSnakeCase/continuum_snake.py +++ b/examples/ContinuumSnakeCase/continuum_snake.py @@ -192,7 +192,10 @@ def get_slip_velocity(self, system: RodType) -> NDArray[np.float64]: snake_sim.finalize() timestepper = ea.PositionVerlet() - ea.integrate(timestepper, snake_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(snake_sim, time, dt) if PLOT_FIGURE: filename_plot = "continuum_snake_velocity.png" diff --git a/examples/ContinuumSnakeWithLiftingWaveCase/continuum_snake_with_lifting_wave.py b/examples/ContinuumSnakeWithLiftingWaveCase/continuum_snake_with_lifting_wave.py index fec1fc90..f8585a91 100755 --- a/examples/ContinuumSnakeWithLiftingWaveCase/continuum_snake_with_lifting_wave.py +++ b/examples/ContinuumSnakeWithLiftingWaveCase/continuum_snake_with_lifting_wave.py @@ -188,7 +188,10 @@ def make_callback(self, system, time, current_step: int): snake_sim.finalize() timestepper = PositionVerlet() - integrate(timestepper, snake_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(snake_sim, time, dt) if PLOT_FIGURE: filename_plot = "continuum_snake_velocity.png" diff --git a/examples/DynamicCantileverCase/dynamic_cantilever.py b/examples/DynamicCantileverCase/dynamic_cantilever.py index 36f95c16..bc4a97bb 100644 --- a/examples/DynamicCantileverCase/dynamic_cantilever.py +++ b/examples/DynamicCantileverCase/dynamic_cantilever.py @@ -124,12 +124,10 @@ def make_callback(self, system, time, current_step: int): timestepper = ea.PositionVerlet() - ea.integrate( - timestepper, - cantilever_sim, - final_time, - total_steps, - ) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(cantilever_sim, time, dt) # FFT amplitudes = np.abs(fft(recorded_history["deflection"])) diff --git a/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_fixed_joint.py b/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_fixed_joint.py index f0729130..14d66deb 100644 --- a/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_fixed_joint.py +++ b/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_fixed_joint.py @@ -156,7 +156,10 @@ class FixedJointSimulator( dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, fixed_joint_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(fixed_joint_sim, time, dt) PLOT_FIGURE = True SAVE_FIGURE = True diff --git a/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_spherical_joint.py b/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_spherical_joint.py index 065ddbd8..e31bc20b 100644 --- a/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_spherical_joint.py +++ b/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_spherical_joint.py @@ -152,7 +152,10 @@ class SphericalJointSimulator( dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, spherical_joint_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(spherical_joint_sim, time, dt) PLOT_FIGURE = True SAVE_FIGURE = True diff --git a/examples/ExperimentalCases/ParallelConnectionExample/parallel_connection_example.py b/examples/ExperimentalCases/ParallelConnectionExample/parallel_connection_example.py index aac78b94..acca8253 100644 --- a/examples/ExperimentalCases/ParallelConnectionExample/parallel_connection_example.py +++ b/examples/ExperimentalCases/ParallelConnectionExample/parallel_connection_example.py @@ -190,7 +190,10 @@ def make_callback(self, system, time, current_step: int): dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, parallel_connection_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(parallel_connection_sim, time, dt) PLOT_FIGURE = True SAVE_FIGURE = False diff --git a/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum.py b/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum.py index aff47e1a..cdfadb39 100644 --- a/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum.py +++ b/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum.py @@ -126,7 +126,10 @@ def make_callback(self, system, time, current_step: int): timestepper = ea.PositionVerlet() # timestepper = PEFRL() -ea.integrate(timestepper, pendulum_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(pendulum_sim, time, dt) if PLOT_VIDEO: diff --git a/examples/FrictionValidationCases/axial_friction.py b/examples/FrictionValidationCases/axial_friction.py index 428c346a..3e34ca9f 100644 --- a/examples/FrictionValidationCases/axial_friction.py +++ b/examples/FrictionValidationCases/axial_friction.py @@ -95,7 +95,10 @@ def simulate_axial_friction_with(force=0.0): dt = 1e-5 total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, axial_friction_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(axial_friction_sim, time, dt) # compute translational and rotational energy translational_energy = shearable_rod.compute_translational_energy() diff --git a/examples/FrictionValidationCases/rolling_friction_initial_velocity.py b/examples/FrictionValidationCases/rolling_friction_initial_velocity.py index 80efc3e6..3162015e 100644 --- a/examples/FrictionValidationCases/rolling_friction_initial_velocity.py +++ b/examples/FrictionValidationCases/rolling_friction_initial_velocity.py @@ -108,9 +108,10 @@ def simulate_rolling_friction_initial_velocity_with(IFactor=0.0): final_time = 2.0 total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate( - timestepper, rolling_friction_initial_velocity_sim, final_time, total_steps - ) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(rolling_friction_initial_velocity_sim, time, dt) # compute translational and rotational energy translational_energy = shearable_rod.compute_translational_energy() diff --git a/examples/FrictionValidationCases/rolling_friction_on_inclined_plane.py b/examples/FrictionValidationCases/rolling_friction_on_inclined_plane.py index 6b892d77..fd352308 100644 --- a/examples/FrictionValidationCases/rolling_friction_on_inclined_plane.py +++ b/examples/FrictionValidationCases/rolling_friction_on_inclined_plane.py @@ -95,9 +95,10 @@ def simulate_rolling_friction_on_inclined_plane_with(alpha_s=0.0): dt = 1e-6 total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate( - timestepper, rolling_friction_on_inclined_plane_sim, final_time, total_steps - ) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(rolling_friction_on_inclined_plane_sim, time, dt) # compute translational and rotational energy translational_energy = shearable_rod.compute_translational_energy() diff --git a/examples/FrictionValidationCases/rolling_friction_torque.py b/examples/FrictionValidationCases/rolling_friction_torque.py index 6258b096..b462c0ec 100644 --- a/examples/FrictionValidationCases/rolling_friction_torque.py +++ b/examples/FrictionValidationCases/rolling_friction_torque.py @@ -99,7 +99,10 @@ def simulate_rolling_friction_torque_with(C_s=0.0): dt = 1e-6 total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, rolling_friction_torque_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(rolling_friction_torque_sim, time, dt) # compute translational and rotational energy translational_energy = shearable_rod.compute_translational_energy() diff --git a/examples/HelicalBucklingCase/convergence_helicalbuckling.py b/examples/HelicalBucklingCase/convergence_helicalbuckling.py index c69b2259..55737f55 100644 --- a/examples/HelicalBucklingCase/convergence_helicalbuckling.py +++ b/examples/HelicalBucklingCase/convergence_helicalbuckling.py @@ -92,7 +92,10 @@ def simulate_helicalbucklin_beam_with( final_time = 10500 total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, helicalbuckling_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(helicalbuckling_sim, time, dt) # calculate errors and norms # Since we need to evaluate analytical solution only on nodes, n_nodes = n_elems+1 diff --git a/examples/HelicalBucklingCase/helicalbuckling.py b/examples/HelicalBucklingCase/helicalbuckling.py index 39e087ea..019232dd 100644 --- a/examples/HelicalBucklingCase/helicalbuckling.py +++ b/examples/HelicalBucklingCase/helicalbuckling.py @@ -88,7 +88,10 @@ class HelicalBucklingSimulator( final_time = 10500.0 total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, helicalbuckling_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(helicalbuckling_sim, time, dt) if PLOT_FIGURE: plot_helicalbuckling(shearable_rod, SAVE_FIGURE) diff --git a/examples/JointCases/fixed_joint.py b/examples/JointCases/fixed_joint.py index 6c2aaff2..8a856659 100644 --- a/examples/JointCases/fixed_joint.py +++ b/examples/JointCases/fixed_joint.py @@ -118,7 +118,10 @@ class FixedJointSimulator( dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, fixed_joint_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(fixed_joint_sim, time, dt) PLOT_FIGURE = True SAVE_FIGURE = False diff --git a/examples/JointCases/fixed_joint_torsion.py b/examples/JointCases/fixed_joint_torsion.py index 4aa734d7..e616fb41 100644 --- a/examples/JointCases/fixed_joint_torsion.py +++ b/examples/JointCases/fixed_joint_torsion.py @@ -124,7 +124,10 @@ class FixedJointSimulator( dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, fixed_joint_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(fixed_joint_sim, time, dt) plot_orientation( diff --git a/examples/JointCases/hinge_joint.py b/examples/JointCases/hinge_joint.py index 4aaa9519..f5084869 100644 --- a/examples/JointCases/hinge_joint.py +++ b/examples/JointCases/hinge_joint.py @@ -120,7 +120,10 @@ class HingeJointSimulator( dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, hinge_joint_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(hinge_joint_sim, time, dt) PLOT_FIGURE = True SAVE_FIGURE = False diff --git a/examples/JointCases/spherical_joint.py b/examples/JointCases/spherical_joint.py index 6d2492df..e9b78433 100644 --- a/examples/JointCases/spherical_joint.py +++ b/examples/JointCases/spherical_joint.py @@ -121,7 +121,10 @@ class SphericalJointSimulator( dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, spherical_joint_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(spherical_joint_sim, time, dt) PLOT_FIGURE = True SAVE_FIGURE = False diff --git a/examples/KnotCase/knot_simulation.py b/examples/KnotCase/knot_simulation.py index 664c5e5d..5d2e0918 100644 --- a/examples/KnotCase/knot_simulation.py +++ b/examples/KnotCase/knot_simulation.py @@ -173,7 +173,10 @@ def base_target(t: float, rod: RodType) -> Pose: timestepper = ea.PositionVerlet() total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, simulator, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(simulator, time, dt) if GENERATE_3D_VIDEO: filename_video = "knot3D.mp4" diff --git a/examples/MuscularFlagella/muscular_flagella.py b/examples/MuscularFlagella/muscular_flagella.py index 8b8e1363..12764659 100644 --- a/examples/MuscularFlagella/muscular_flagella.py +++ b/examples/MuscularFlagella/muscular_flagella.py @@ -237,7 +237,10 @@ def make_callback(self, system, time, current_step: int): timestepper = ea.PositionVerlet() print("Total steps", total_steps) -ea.integrate(timestepper, muscular_flagella_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(muscular_flagella_sim, time, dt) # Plot the videos diff --git a/examples/MuscularSnake/muscular_snake.py b/examples/MuscularSnake/muscular_snake.py index 61d68f30..ce265f7c 100644 --- a/examples/MuscularSnake/muscular_snake.py +++ b/examples/MuscularSnake/muscular_snake.py @@ -394,7 +394,10 @@ def make_callback(self, system, time, current_step: int): muscular_snake_simulator.finalize() timestepper = ea.PositionVerlet() -ea.integrate(timestepper, muscular_snake_simulator, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(muscular_snake_simulator, time, dt) plot_video_with_surface( diff --git a/examples/RestartExample/restart_example.py b/examples/RestartExample/restart_example.py index 3d85b278..b34ce1df 100644 --- a/examples/RestartExample/restart_example.py +++ b/examples/RestartExample/restart_example.py @@ -84,13 +84,10 @@ class RestartExampleSimulator( timestepper = ea.PositionVerlet() total_steps = int(final_time / dt) -time = ea.integrate( - timestepper, - restart_example_simulator, - final_time, - total_steps, - restart_time=restart_time, -) +dt = final_time / total_steps +time = restart_time +for i in range(total_steps): + time = timestepper.step(restart_example_simulator, time, dt) # Save all the systems appended on the simulator class. Since in this example have only one system, under the # `restart_file_location` directory there is one file called system_0.npz . For each system appended on the simulator diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact.py index a45ae03d..a25f3af6 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact.py @@ -216,9 +216,10 @@ def make_callback(self, system, time, current_step: int): rod_cylinder_parallel_contact_simulator.finalize() timestepper = ea.PositionVerlet() - ea.integrate( - timestepper, rod_cylinder_parallel_contact_simulator, final_time, total_steps - ) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(rod_cylinder_parallel_contact_simulator, time, dt) # Plot the rods plot_video_with_surface( diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction.py index be53c236..f4f58a39 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction.py @@ -237,9 +237,10 @@ def make_callback(self, system, time, current_step: int): rod_cylinder_parallel_contact_simulator.finalize() timestepper = ea.PositionVerlet() - ea.integrate( - timestepper, rod_cylinder_parallel_contact_simulator, final_time, total_steps - ) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(rod_cylinder_parallel_contact_simulator, time, dt) if POST_PROCESSING: # Plot the rods diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_validation.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_validation.py index f1700a77..b7dcfc0b 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_validation.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_validation.py @@ -159,7 +159,10 @@ def make_callback(self, system, time, current_step: int): total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, single_rod_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(single_rod_sim, time, dt) if PLOT_FIGURE: plot_video( diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_with_y_normal.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_with_y_normal.py index 11fc7919..5a6b3680 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_with_y_normal.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_with_y_normal.py @@ -137,7 +137,10 @@ def make_callback(self, system, time, current_step: int): total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, single_rod_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(single_rod_sim, time, dt) if PLOT_FIGURE: plot_video( diff --git a/examples/RigidBodyCases/rigid_cylinder_rotational_motion_case.py b/examples/RigidBodyCases/rigid_cylinder_rotational_motion_case.py index 64271916..71522006 100644 --- a/examples/RigidBodyCases/rigid_cylinder_rotational_motion_case.py +++ b/examples/RigidBodyCases/rigid_cylinder_rotational_motion_case.py @@ -99,7 +99,10 @@ def make_callback(self, system, time, current_step: int): dt = 4.0e-5 total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, rigid_cylinder_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(rigid_cylinder_sim, time, dt) # compute translational and rotational energy translational_energy = cylinder.compute_translational_energy() diff --git a/examples/RigidBodyCases/rigid_cylinder_translational_motion_case.py b/examples/RigidBodyCases/rigid_cylinder_translational_motion_case.py index ce78a84a..b86c6e33 100644 --- a/examples/RigidBodyCases/rigid_cylinder_translational_motion_case.py +++ b/examples/RigidBodyCases/rigid_cylinder_translational_motion_case.py @@ -97,7 +97,10 @@ def make_callback(self, system, time, current_step: int): dt = 4.0e-5 total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, rigid_cylinder_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(rigid_cylinder_sim, time, dt) # compute translational and rotational energy translational_energy = cylinder.compute_translational_energy() diff --git a/examples/RigidBodyCases/rigid_sphere_rotational_motion_case.py b/examples/RigidBodyCases/rigid_sphere_rotational_motion_case.py index 5f38beba..cc1ed157 100644 --- a/examples/RigidBodyCases/rigid_sphere_rotational_motion_case.py +++ b/examples/RigidBodyCases/rigid_sphere_rotational_motion_case.py @@ -90,7 +90,10 @@ def make_callback(self, system, time, current_step: int): dt = 4.0e-5 total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, rigid_sphere_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(rigid_sphere_sim, time, dt) # compute translational and rotational energy translational_energy = sphere.compute_translational_energy() diff --git a/examples/RigidBodyCases/rigid_sphere_translational_motion_case.py b/examples/RigidBodyCases/rigid_sphere_translational_motion_case.py index d4dc17f1..858b393f 100644 --- a/examples/RigidBodyCases/rigid_sphere_translational_motion_case.py +++ b/examples/RigidBodyCases/rigid_sphere_translational_motion_case.py @@ -91,7 +91,10 @@ def make_callback(self, system, time, current_step: int): dt = 4.0e-5 total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, rigid_sphere_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(rigid_sphere_sim, time, dt) # compute translational and rotational energy translational_energy = sphere.compute_translational_energy() diff --git a/examples/RingRodCase/ring_rod.py b/examples/RingRodCase/ring_rod.py index eec6588e..ff203b53 100644 --- a/examples/RingRodCase/ring_rod.py +++ b/examples/RingRodCase/ring_rod.py @@ -106,7 +106,10 @@ def make_callback(self, system, time, current_step: int): ring_sim.finalize() timestepper = ea.PositionVerlet() -ea.integrate(timestepper, ring_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(ring_sim, time, dt) filename_video = "ring_rod.mp4" diff --git a/examples/RodContactCase/RodRodContact/rod_rod_contact_inclined_validation.py b/examples/RodContactCase/RodRodContact/rod_rod_contact_inclined_validation.py index d60940ce..2f4132ee 100644 --- a/examples/RodContactCase/RodRodContact/rod_rod_contact_inclined_validation.py +++ b/examples/RodContactCase/RodRodContact/rod_rod_contact_inclined_validation.py @@ -162,7 +162,10 @@ def make_callback(self, system, time, current_step: int): # Do the simulation timestepper = ea.PositionVerlet() -ea.integrate(timestepper, inclined_rod_rod_contact_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(inclined_rod_rod_contact_sim, time, dt) # plotting the videos filename_video = "inclined_rods_contact.mp4" diff --git a/examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py b/examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py index 042efd70..e8ebfd8e 100644 --- a/examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py +++ b/examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py @@ -159,7 +159,10 @@ def make_callback(self, system, time, current_step: int): # Do the simulation timestepper = ea.PositionVerlet() -ea.integrate(timestepper, parallel_rod_rod_contact_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(parallel_rod_rod_contact_sim, time, dt) # plotting the videos filename_video = "parallel_rods_contact.mp4" diff --git a/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py b/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py index 5f3fb851..af57f47b 100644 --- a/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py +++ b/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py @@ -215,7 +215,10 @@ def make_callback(self, system, time, current_step: int): # Run the simulation time_stepper = ea.PositionVerlet() -ea.integrate(time_stepper, plectonemes_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = time_stepper.step(plectonemes_sim, time, dt) # plotting the videos filename_video = "plectonemes.mp4" diff --git a/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py b/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py index 3ff7d528..1d8cfb7b 100644 --- a/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py +++ b/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py @@ -226,7 +226,10 @@ def make_callback(self, system, time, current_step: int): # Run the simulation time_stepper = ea.PositionVerlet() -ea.integrate(time_stepper, solenoid_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = time_stepper.step(solenoid_sim, time, dt) # plotting the videos filename_video = "solenoid.mp4" diff --git a/examples/TimoshenkoBeamCase/convergence_timoshenko.py b/examples/TimoshenkoBeamCase/convergence_timoshenko.py index ee3262a3..f95f9f1d 100644 --- a/examples/TimoshenkoBeamCase/convergence_timoshenko.py +++ b/examples/TimoshenkoBeamCase/convergence_timoshenko.py @@ -112,7 +112,10 @@ def simulate_timoshenko_beam_with( dt = 0.01 * dl total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, timoshenko_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(timoshenko_sim, time, dt) if PLOT_FIGURE: plot_timoshenko(shearable_rod, end_force, SAVE_FIGURE, ADD_UNSHEARABLE_ROD) diff --git a/examples/TimoshenkoBeamCase/timoshenko.py b/examples/TimoshenkoBeamCase/timoshenko.py index 5f04f803..b473e917 100644 --- a/examples/TimoshenkoBeamCase/timoshenko.py +++ b/examples/TimoshenkoBeamCase/timoshenko.py @@ -135,7 +135,10 @@ def make_callback(self, system, time, current_step: int): total_steps = int(final_time / dt) print("Total steps", total_steps) -ea.integrate(timestepper, timoshenko_sim, final_time, total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(timoshenko_sim, time, dt) if PLOT_FIGURE: plot_timoshenko(shearable_rod, end_force, SAVE_FIGURE, ADD_UNSHEARABLE_ROD) diff --git a/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod.py b/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod.py index f5bfae5a..7c3fd8bc 100644 --- a/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod.py +++ b/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod.py @@ -5,7 +5,7 @@ import elastica as ea from elastica.timestepper.symplectic_steppers import PositionVerlet -from elastica.timestepper import integrate +from tqdm import tqdm from elastica.external_forces import UniformTorques from forces import ( @@ -132,7 +132,11 @@ def make_callback(self, system, time, current_step: int): print("System finalized") timestepper = PositionVerlet() - integrate(timestepper, square_rod_sim, final_time, total_steps) + + dt = final_time / total_steps + time = 0.0 + for i in tqdm(range(total_steps)): + time = timestepper.step(square_rod_sim, time, dt) with open("TumblingUnconstrainedRod.json", "r") as file: analytic_data = json.load(file) diff --git a/examples/Visualization/ContinuumSnakeVisualization/continuum_snake.py b/examples/Visualization/ContinuumSnakeVisualization/continuum_snake.py index 60e1867c..b46b647a 100644 --- a/examples/Visualization/ContinuumSnakeVisualization/continuum_snake.py +++ b/examples/Visualization/ContinuumSnakeVisualization/continuum_snake.py @@ -124,7 +124,10 @@ def make_callback(self, system, time, current_step: int): final_time = (11.0 + 0.01) * period total_steps = int(final_time / dt) print("Total steps", total_steps) - ea.integrate(timestepper, snake_sim, final_time, total_steps) + dt = final_time / total_steps + time = 0.0 + for i in range(total_steps): + time = timestepper.step(snake_sim, time, dt) if SAVE_RESULTS: import pickle From f0951f64353233b7287e1701c00dfc83f2700ff8 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 28 Sep 2025 16:48:52 -0500 Subject: [PATCH 08/85] mypy: fix using same name for time --- examples/KnotCase/knot_simulation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/KnotCase/knot_simulation.py b/examples/KnotCase/knot_simulation.py index 5d2e0918..9d506693 100644 --- a/examples/KnotCase/knot_simulation.py +++ b/examples/KnotCase/knot_simulation.py @@ -183,7 +183,7 @@ def base_target(t: float, rod: RodType) -> Pose: plot_video3D(recorded_history, video_name=filename_video, margin=0.2, fps=10) # Plot knot topological quantities - time = np.asarray(recorded_history["time"]) + timestep = np.asarray(recorded_history["time"]) positions = np.asarray(recorded_history["position"]) orientations = np.asarray(recorded_history["orientation"]) radii = np.asarray(recorded_history["radius"]) @@ -198,9 +198,9 @@ def base_target(t: float, rod: RodType) -> Pose: ) plt.figure() - plt.plot(time, total_twist, label="twist") - plt.plot(time, total_writhe, label="writhe") - plt.plot(time, total_link, label="link") + plt.plot(timestep, total_twist, label="twist") + plt.plot(timestep, total_writhe, label="writhe") + plt.plot(timestep, total_link, label="link") plt.legend() plt.xlabel("time") plt.ylabel("link-writhe-twist quantity") From 5f3b3f97e2398563825a81c79d981d7a0419fd4a Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 17 Oct 2025 14:08:18 -0500 Subject: [PATCH 09/85] move initial callback in base module --- elastica/modules/base_system.py | 3 +++ elastica/modules/callbacks.py | 3 --- tests/test_modules/test_base_system.py | 12 ++++++++++-- tests/test_modules/test_callbacks.py | 15 --------------- 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/elastica/modules/base_system.py b/elastica/modules/base_system.py index a6bd28a3..165d11db 100644 --- a/elastica/modules/base_system.py +++ b/elastica/modules/base_system.py @@ -247,6 +247,9 @@ def finalize(self) -> None: self._feature_group_finalize.clear() del self._feature_group_finalize + # First callback execution + self.apply_callbacks(time=np.float64(0.0), current_step=0) + @final def synchronize(self, time: np.float64) -> None: """ diff --git a/elastica/modules/callbacks.py b/elastica/modules/callbacks.py index f3b5b5f0..74315a09 100644 --- a/elastica/modules/callbacks.py +++ b/elastica/modules/callbacks.py @@ -78,9 +78,6 @@ def _finalize_callback(self: SystemCollectionWithCallbackProtocol) -> None: self._callback_list.clear() del self._callback_list - # First callback execution - self.apply_callbacks(time=np.float64(0.0), current_step=0) - class _CallBack: """ diff --git a/tests/test_modules/test_base_system.py b/tests/test_modules/test_base_system.py index 38a907d1..5dd904ca 100644 --- a/tests/test_modules/test_base_system.py +++ b/tests/test_modules/test_base_system.py @@ -231,8 +231,11 @@ def test_forcing(self, load_collection, legal_forces): from elastica.callback_functions import CallBackBaseClass @pytest.mark.parametrize("legal_callback", [CallBackBaseClass]) - def test_callback(self, load_collection, legal_callback): + def test_callback(self, mocker, load_collection, legal_callback): simulator_class, rod = load_collection + + spy = mocker.spy(legal_callback, "make_callback") + simulator_class.collect_diagnostics(rod).using(legal_callback) simulator_class.finalize() # After finalize check if the created callback object is instance of the class we have given. @@ -243,5 +246,10 @@ def test_callback(self, load_collection, legal_callback): legal_callback, ) - # TODO: this is a dummy test for apply_callbacks find a better way to test them simulator_class.apply_callbacks(time=0, current_step=0) + + assert ( + spy.call_count == 2 + ) # Callback should be called twice: once during the finalize and once during the apply_callbacks + assert spy.call_args[1]["time"] == np.float64(0.0) + assert spy.call_args[1]["current_step"] == 0 diff --git a/tests/test_modules/test_callbacks.py b/tests/test_modules/test_callbacks.py index 09250c17..d5830229 100644 --- a/tests/test_modules/test_callbacks.py +++ b/tests/test_modules/test_callbacks.py @@ -184,18 +184,3 @@ def test_callback_finalize_sorted(self, load_rod_with_callbacks): for x, _ in scwc._callback_list: assert num < x num = x - - def test_first_call_callback_during_finalize(self, mocker, load_rod_with_callbacks): - """ - This test is to check if the callback is called during the finalize. - If this test fails, check if `apply_callbacks` is called during the finalization step. - """ - scwc, callback_cls = load_rod_with_callbacks - callback_features = [d for d in scwc._callback_list] - - spy = mocker.spy(scwc, "apply_callbacks") - scwc._finalize_callback() - - assert spy.call_count == 1 - assert spy.call_args[1]["time"] == np.float64(0.0) - assert spy.call_args[1]["current_step"] == 0 From fbbcc869e2f2e56f3f45a9cdbb1c199bdb6c92ad Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sat, 18 Oct 2025 02:07:07 -0500 Subject: [PATCH 10/85] feat: allow data collections (list, tuple, dict) to be passed for callback --- elastica/callback_functions.py | 11 ++- elastica/modules/callbacks.py | 55 +++++++++++++-- elastica/typing.py | 4 +- tests/test_modules/test_base_system.py | 58 ++++++++++++++++ tests/test_modules/test_callbacks.py | 93 +++++++++++++++++++++++++- 5 files changed, 208 insertions(+), 13 deletions(-) diff --git a/elastica/callback_functions.py b/elastica/callback_functions.py index 215989d1..42345586 100644 --- a/elastica/callback_functions.py +++ b/elastica/callback_functions.py @@ -26,7 +26,12 @@ class CallBackBaseClass(Generic[T]): """ - def make_callback(self, system: T, time: np.float64, current_step: int) -> None: + def make_callback( + self, + system: T, + time: np.float64, + current_step: int, + ) -> None: """ This method is called every time step. Users can define which parameters are called back and recorded. Also users @@ -35,7 +40,7 @@ def make_callback(self, system: T, time: np.float64, current_step: int) -> None: Parameters ---------- - system : object + system : T (SystemType | tuple[SystemType] | dict[Any, SystemType] | list[SystemType]) System is a rod-like object. time : float The time of the simulation. @@ -80,7 +85,7 @@ def __init__(self, step_skip: int, callback_params: dict) -> None: self.callback_params = callback_params def make_callback( - self, system: "RodType | RigidBodyType", time: np.float64, current_step: int + self, system: RodType, time: np.float64, current_step: int ) -> None: if current_step % self.sample_every == 0: diff --git a/elastica/modules/callbacks.py b/elastica/modules/callbacks.py index 74315a09..055d5dc4 100644 --- a/elastica/modules/callbacks.py +++ b/elastica/modules/callbacks.py @@ -1,10 +1,13 @@ +from __future__ import annotations + __doc__ = """ CallBacks ----------- Provides the callBack interface to collect data over time (see `callback_functions.py`). """ -from typing import Type, Any +from types import EllipsisType +from typing import Type, Any, TypeAlias, cast from typing_extensions import Self # 3.11: from typing import Self from elastica.typing import SystemType, SystemIdxType, OperatorFinalizeType from .protocol import ModuleProtocol @@ -17,6 +20,22 @@ from .protocol import SystemCollectionWithCallbackProtocol +SystemIdxDSType: TypeAlias = """ +( + SystemIdxType + | tuple[SystemIdxType, ...] + | list[SystemIdxType] + | dict[Any, SystemIdxType] +) +""" + +SystemDSType: TypeAlias = """ +( + SystemType | tuple[SystemType, ...] | list[SystemType] | dict[Any, SystemType] +) +""" + + class CallBacks: """ CallBacks class is a module for calling callback functions, set by the user. If the user @@ -35,7 +54,8 @@ def __init__(self: SystemCollectionWithCallbackProtocol) -> None: self._feature_group_finalize.append(self._finalize_callback) def collect_diagnostics( - self: SystemCollectionWithCallbackProtocol, system: SystemType + self: SystemCollectionWithCallbackProtocol, + system: SystemDSType | EllipsisType, ) -> ModuleProtocol: """ This method calls user-defined call-back classes for a @@ -51,7 +71,18 @@ def collect_diagnostics( ------- """ - sys_idx: SystemIdxType = self.get_system_index(system) + sys_idx: SystemIdxDSType + if system is Ellipsis: + sys_idx = tuple([self.get_system_index(sys) for sys in self]) + elif isinstance(system, list): + sys_idx = [self.get_system_index(sys) for sys in system] + elif isinstance(system, dict): + sys_idx = {key: self.get_system_index(sys) for key, sys in system.items()} + elif isinstance(system, tuple): + sys_idx = tuple([self.get_system_index(sys) for sys in system]) + else: + # Single entity + sys_idx = self.get_system_index(system) # Create _Constraint object, cache it and return to user _callback: ModuleProtocol = _CallBack(sys_idx) @@ -67,8 +98,18 @@ def _finalize_callback(self: SystemCollectionWithCallbackProtocol) -> None: sys_id = callback.id() callback_instance = callback.instantiate() + system: SystemDSType + if isinstance(sys_id, (tuple, list)): + _T = type(sys_id) + system = _T([self[sys_id_] for sys_id_ in sys_id]) + elif isinstance(sys_id, dict): + sys_id = cast(dict[Any, SystemIdxType], sys_id) + system = {key: self[sys_id_] for key, sys_id_ in sys_id.items()} + else: + system = self[sys_id] + callback_operator = functools.partial( - callback_instance.make_callback, system=self[sys_id] + callback_instance.make_callback, system=system ) self._feature_group_callback.add_operators(callback, [callback_operator]) self._feature_group_on_close.add_operators( @@ -93,7 +134,7 @@ class _CallBack: Arbitrary keyword arguments. """ - def __init__(self, sys_idx: SystemIdxType): + def __init__(self, sys_idx: SystemIdxDSType): """ Parameters @@ -101,7 +142,7 @@ def __init__(self, sys_idx: SystemIdxType): sys_idx: int rod object index """ - self._sys_idx: SystemIdxType = sys_idx + self._sys_idx: SystemIdxDSType = sys_idx self._callback_cls: Type[CallBackBaseClass] self._args: Any self._kwargs: Any @@ -135,7 +176,7 @@ def using( self._kwargs = kwargs return self - def id(self) -> SystemIdxType: + def id(self) -> SystemIdxDSType: return self._sys_idx def instantiate(self) -> CallBackBaseClass: diff --git a/elastica/typing.py b/elastica/typing.py index 25596143..31ef9917 100644 --- a/elastica/typing.py +++ b/elastica/typing.py @@ -1,3 +1,5 @@ +from __future__ import annotations + __doc__ = """ This module contains aliases of type-hints for elastica. @@ -47,7 +49,7 @@ StaticSystemType: TypeAlias = "StaticSystemProtocol" -SystemType: TypeAlias = "SystemProtocol" +SystemType: TypeAlias = SystemProtocol SystemIdxType: TypeAlias = int BlockSystemType: TypeAlias = "BlockSystemProtocol" diff --git a/tests/test_modules/test_base_system.py b/tests/test_modules/test_base_system.py index 5dd904ca..8f4cfa2c 100644 --- a/tests/test_modules/test_base_system.py +++ b/tests/test_modules/test_base_system.py @@ -251,5 +251,63 @@ def test_callback(self, mocker, load_collection, legal_callback): assert ( spy.call_count == 2 ) # Callback should be called twice: once during the finalize and once during the apply_callbacks + assert spy.call_args[1]["system"] == rod assert spy.call_args[1]["time"] == np.float64(0.0) assert spy.call_args[1]["current_step"] == 0 + + @pytest.mark.parametrize("legal_callback", [CallBackBaseClass]) + def test_callback_in_data_structure(self, mocker, load_collection, legal_callback): + simulator_class, rod = load_collection + + spy = mocker.spy(legal_callback, "make_callback") + + simulator_class.collect_diagnostics((rod, rod)).using(legal_callback) + simulator_class.finalize() + # After finalize check if the created callback object is instance of the class we have given. + assert isinstance( + simulator_class._feature_group_callback._operator_collection[-1][ + -1 + ].func.__self__, + legal_callback, + ) + + simulator_class.apply_callbacks(time=0, current_step=0) + + assert ( + spy.call_count == 2 + ) # Callback should be called twice: once during the finalize and once during the apply_callbacks + assert spy.call_args[1]["system"] == (rod, rod) + assert spy.call_args[1]["time"] == np.float64(0.0) + assert spy.call_args[1]["current_step"] == 0 + + @pytest.mark.parametrize("legal_callback", [CallBackBaseClass]) + def test_callback_in_ellipsis(self, mocker, load_collection, legal_callback): + simulator_class, rod = load_collection + simulator_class.extend_allowed_types((int,)) + + simulator_class.append(rod) + + spy = mocker.spy(legal_callback, "make_callback") + + simulator_class.collect_diagnostics(...).using(legal_callback) + simulator_class.finalize() + # After finalize check if the created callback object is instance of the class we have given. + assert isinstance( + simulator_class._feature_group_callback._operator_collection[-1][ + -1 + ].func.__self__, + legal_callback, + ) + + simulator_class.apply_callbacks(time=0, current_step=0) + simulator_class.apply_callbacks(time=1, current_step=1) + + assert ( + spy.call_count == 3 + ) # Callback should be called twice: once during the finalize and once during the apply_callbacks + assert spy.call_args_list[1][1]["system"][0] == rod + assert spy.call_args_list[1][1]["system"][1] == rod + assert spy.call_args_list[1][1]["time"] == 0 + assert spy.call_args_list[1][1]["current_step"] == 0 + assert spy.call_args_list[2][1]["time"] == 1 + assert spy.call_args_list[2][1]["current_step"] == 1 diff --git a/tests/test_modules/test_callbacks.py b/tests/test_modules/test_callbacks.py index d5830229..6b79a74d 100644 --- a/tests/test_modules/test_callbacks.py +++ b/tests/test_modules/test_callbacks.py @@ -112,8 +112,9 @@ def test_callback_with_unregistered_system_throws(self, load_system_with_callbac def test_callback_with_illegal_system_throws(self, load_system_with_callbacks): scwc = load_system_with_callbacks - # Not a rod, but a list! - mock_rod = [1, 2, 3, 5] + # Not a rod, but a set! + # only ordered collections or single system are allowed + mock_rod = {1, 2, 3, 5} with pytest.raises(TypeError) as excinfo: scwc.collect_diagnostics(mock_rod) @@ -159,6 +160,60 @@ def mock_init(self, *args, **kwargs): return scwc, MockCallBack + @pytest.fixture + def load_multiple_rod_in_data_structure_with_callbacks( + self, load_system_with_callbacks + ): + scwc = load_system_with_callbacks + + mock_rod1 = self.MockRod(2, 3, 4, 5) + mock_rod2 = self.MockRod(2, 3, 4, 5) + mock_rod3 = self.MockRod(2, 3, 4, 5) + + scwc.append(mock_rod1) + scwc.append(mock_rod2) + scwc.append(mock_rod3) + + def mock_init(self, *args, **kwargs): + pass + + # in place class + MockCallBack = type( + "MockCallBack", (self.CallBackBaseClass, object), {"__init__": mock_init} + ) + + # Constrain any and all systems + scwc.collect_diagnostics([mock_rod1, mock_rod2, mock_rod3]).using( + MockCallBack, 2, 3 + ) # system based constraint + + return scwc, MockCallBack + + @pytest.fixture + def load_multiple_rod_in_ellipsis_with_callbacks(self, load_system_with_callbacks): + scwc = load_system_with_callbacks + + mock_rod1 = self.MockRod(2, 3, 4, 5) + mock_rod2 = self.MockRod(2, 3, 4, 5) + + scwc.append(mock_rod1) + scwc.append(mock_rod2) + + def mock_init(self, *args, **kwargs): + pass + + # in place class + MockCallBack = type( + "MockCallBack", (self.CallBackBaseClass, object), {"__init__": mock_init} + ) + + # Constrain any and all systems + scwc.collect_diagnostics(...).using( + MockCallBack, 2, 3 + ) # system based constraint + + return scwc, MockCallBack + def test_callback_finalize_correctness(self, load_rod_with_callbacks): scwc, callback_cls = load_rod_with_callbacks callback_features = [d for d in scwc._callback_list] @@ -184,3 +239,37 @@ def test_callback_finalize_sorted(self, load_rod_with_callbacks): for x, _ in scwc._callback_list: assert num < x num = x + + def test_callback_finalize_correctness_with_data_structure_of_systems( + self, load_multiple_rod_in_data_structure_with_callbacks + ): + scwc, callback_cls = load_multiple_rod_in_data_structure_with_callbacks + callback_features = [d for d in scwc._callback_list] + + scwc._finalize_callback() + + for _callback in callback_features: + x = _callback.id() + y = _callback.instantiate() + assert isinstance(x, list) + assert isinstance(x[0], int) + assert isinstance(y, callback_cls) + + assert not hasattr(scwc, "_callback_list") + + def test_callback_finalize_correctness_with_ellipsis( + self, load_multiple_rod_in_ellipsis_with_callbacks + ): + scwc, callback_cls = load_multiple_rod_in_ellipsis_with_callbacks + callback_features = [d for d in scwc._callback_list] + + scwc._finalize_callback() + + for _callback in callback_features: + x = _callback.id() + y = _callback.instantiate() + assert isinstance(x, tuple) + assert isinstance(x[0], int) + assert isinstance(y, callback_cls) + + assert not hasattr(scwc, "_callback_list") From ba8d0cdb6a40eb431517d9828366854a4297482d Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sat, 18 Oct 2025 02:16:27 -0500 Subject: [PATCH 11/85] docs: add description on different way to collect diagnostics --- docs/guide/workflow.md | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/docs/guide/workflow.md b/docs/guide/workflow.md index 0a823c90..9eaa8a07 100644 --- a/docs/guide/workflow.md +++ b/docs/guide/workflow.md @@ -38,8 +38,8 @@ Available components are: | [Constraints](../api/constraints.rst) | | | [Forcing](../api/external_forces.rst) | | | [Connections](../api/connections.rst) | | -| [CallBacks](../api/callback.rst) | | -| [Damping](../api/damping.rst) | | +| [CallBacks](../api/callback.rst) | | +| [Damping](../api/damping.rst) | | :::{Note} We adopted a composition and mixin design paradigm in building elastica. The detail of the implementation is not important in using the package, but we left some references to read [here](../advanced/PackageDesign.md). @@ -220,6 +220,32 @@ simulator.collect_diagnostics(rod2).using( You can define different callback functions for different rods and also have different data outputted at different time step intervals depending on your needs. See [this page](../api/callback.rst) for more in-depth documentation. +:::{note} +During setting up callbacks, `.collect_diagnostics` can python-collection (list, dict, tuple) of rods: +```python +... + def make_callback(self, system, time, current_step): + rod1 = system[key1] + rod2 = system[key2] +... +simulator.collect_diagnostics({key1: rod1, key2: rod2}).using( + # custom callback class +) +``` +to collect data from multiple rods at the same time. +Additionally, you can pass ellipsis (...) to collect data from all rods in the simulator. +```python +... + def make_callback(self, system, time, current_step): + for rod in system: + pass +... +simulator.collect_diagnostics(...).using( + # custom callback class +) +``` +::: +

5. Finalize Simulator

Now that we have finished defining our rods, the different boundary conditions and connections between them, and how often we want to save data, we have finished setting up the simulation. We now need to finalize the simulator by calling From 3b77e511dac181b328f66036f8626724067e85c6 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sat, 18 Oct 2025 16:06:34 -0500 Subject: [PATCH 12/85] Update documentation for collect_diagnostics usage Clarify the usage of `.collect_diagnostics` to specify that it can take collections of rods/systems and improve wording for better understanding. --- docs/guide/workflow.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guide/workflow.md b/docs/guide/workflow.md index 9eaa8a07..f98a705c 100644 --- a/docs/guide/workflow.md +++ b/docs/guide/workflow.md @@ -221,7 +221,7 @@ simulator.collect_diagnostics(rod2).using( You can define different callback functions for different rods and also have different data outputted at different time step intervals depending on your needs. See [this page](../api/callback.rst) for more in-depth documentation. :::{note} -During setting up callbacks, `.collect_diagnostics` can python-collection (list, dict, tuple) of rods: +During setting up callbacks, `.collect_diagnostics` can take python-collection (list, dict, tuple) of rods/systems: ```python ... def make_callback(self, system, time, current_step): @@ -232,8 +232,8 @@ simulator.collect_diagnostics({key1: rod1, key2: rod2}).using( # custom callback class ) ``` -to collect data from multiple rods at the same time. -Additionally, you can pass ellipsis (...) to collect data from all rods in the simulator. +to handle data from multiple rods at the same time. +Additionally, you can pass ellipsis (...) to collect data from all rods/systems in the simulator. ```python ... def make_callback(self, system, time, current_step): From 485c36fe59bac0fe0008c1ad374459ad523876b9 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sat, 18 Oct 2025 02:46:39 -0500 Subject: [PATCH 13/85] docs: fix numpydoc formats --- docs/api/constraints.rst | 7 +------ elastica/modules/base_system.py | 8 ++++---- elastica/typing.py | 2 +- elastica/utils.py | 34 ++++++++++++--------------------- 4 files changed, 18 insertions(+), 33 deletions(-) diff --git a/docs/api/constraints.rst b/docs/api/constraints.rst index 32308e94..a366c187 100644 --- a/docs/api/constraints.rst +++ b/docs/api/constraints.rst @@ -5,6 +5,7 @@ Constraints .. automodule:: elastica.boundary_conditions + Description ----------- @@ -21,8 +22,6 @@ Constraints are equivalent to displacement boundary condition. GeneralConstraint FixedConstraint HelicalBucklingBC - FreeRod - OneEndFixedRod Compatibility ~~~~~~~~~~~~~ @@ -72,7 +71,3 @@ Built-in Constraints .. autoclass:: HelicalBucklingBC :special-members: __init__ - -.. autoclass:: FreeRod - -.. autoclass:: OneEndFixedRod diff --git a/elastica/modules/base_system.py b/elastica/modules/base_system.py index 165d11db..1fe31415 100644 --- a/elastica/modules/base_system.py +++ b/elastica/modules/base_system.py @@ -46,8 +46,8 @@ class BaseSystemCollection(MutableSequence): blocks: Callable Returns block objects. Should be called after finalize. - Note - ---- + Notes + ----- We can directly subclass a list for the most part, but this is a bad idea, as List is non abstract https://stackoverflow.com/q/3945940 @@ -164,8 +164,8 @@ def get_system_index( Get the index of the system object in the system list. System list is private, so this is the only way to get the index of the system object. - Example - ------- + Examples + -------- >>> system_collection: SystemCollectionProtocol >>> system: SystemType ... diff --git a/elastica/typing.py b/elastica/typing.py index 31ef9917..03693f2d 100644 --- a/elastica/typing.py +++ b/elastica/typing.py @@ -62,7 +62,7 @@ SteppersOperatorsType: TypeAlias = tuple[tuple[StepType, ...], ...] -RodType: TypeAlias = "CosseratRodProtocol" +RodType: TypeAlias = CosseratRodProtocol RigidBodyType: TypeAlias = "RigidBodyProtocol" SurfaceType: TypeAlias = "SurfaceBase" diff --git a/elastica/utils.py b/elastica/utils.py index 39e23a65..aa7a605e 100644 --- a/elastica/utils.py +++ b/elastica/utils.py @@ -32,11 +32,9 @@ def isqrt(num: int) -> int: Notes ----- - - Doesn't handle edge-cases of negative numbers by design - - Doesn't type-check for integers by design, although it is hinted at + Doesn't handle edge-cases of negative numbers by design - Examples - -------- + Doesn't type-check for integers by design, although it is hinted at """ if num > 0: @@ -97,6 +95,8 @@ def perm_parity(lst: list[int]) -> int: """ Given a permutation of the digits 0..N in order as a list, returns its parity (or sign): +1 for even parity; -1 for odd. + Code obtained with thanks from https://code.activestate.com/recipes/578227-generate-the-parity-or-sign-of-a-permutation/ + licensed with a MIT License Parameters ---------- @@ -105,10 +105,6 @@ def perm_parity(lst: list[int]) -> int: Returns ------- - Credits - ------- - Code obtained with thanks from https://code.activestate.com/recipes/578227-generate-the-parity-or-sign-of-a-permutation/ - licensed with a MIT License """ parity = 1 for i in range(0, len(lst) - 1): @@ -123,7 +119,12 @@ def perm_parity(lst: list[int]) -> int: def grouper(iterable: Iterable[_T], n: int) -> Generator[tuple[_T, ...], None, None]: - """Collect data into fixed-length chunks or blocks" + """ + Collect data into fixed-length chunks or blocks" + https://docs.python.org/3/library/itertools.html#itertools-recipes + https://stackoverflow.com/a/10791887 + + grouper('ABCDEFG', 3) --> ABC DEF G" Parameters ---------- @@ -133,14 +134,6 @@ def grouper(iterable: Iterable[_T], n: int) -> Generator[tuple[_T, ...], None, N Returns ------- - Example - ------- - grouper('ABCDEFG', 3) --> ABC DEF G" - - Credits - ------- - https://docs.python.org/3/library/itertools.html#itertools-recipes - https://stackoverflow.com/a/10791887 """ it = iter(iterable) @@ -154,7 +147,8 @@ def grouper(iterable: Iterable[_T], n: int) -> Generator[tuple[_T, ...], None, N def extend_instance(obj: Any, cls: Any) -> None: """ - Apply mixins to a class instance after creation + Apply mixins to a class instance after creation. + https://stackoverflow.com/a/31075641 Parameters ---------- @@ -166,10 +160,6 @@ def extend_instance(obj: Any, cls: Any) -> None: ------- None - Credits - ------- - https://stackoverflow.com/a/31075641 - """ base_cls = obj.__class__ base_cls_name = obj.__class__.__name__ From 9288beb05020398687b970a63badcca7c7c31dcf Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sat, 25 Oct 2025 17:20:22 -0500 Subject: [PATCH 14/85] fix: forward referencing during documentation build --- elastica/contact_forces.py | 2 +- elastica/modules/base_system.py | 2 ++ elastica/modules/callbacks.py | 7 +++++- elastica/modules/connections.py | 4 ---- elastica/modules/damping.py | 2 +- elastica/modules/forcing.py | 2 +- elastica/rod/cosserat_rod.py | 4 ++-- elastica/rod/knot_theory.py | 38 ++++++++++++++++----------------- elastica/typing.py | 2 +- 9 files changed, 32 insertions(+), 31 deletions(-) diff --git a/elastica/contact_forces.py b/elastica/contact_forces.py index 93889b43..d8cb6b7f 100644 --- a/elastica/contact_forces.py +++ b/elastica/contact_forces.py @@ -1,7 +1,7 @@ __doc__ = """ Numba implementation module containing contact between rods and rigid bodies and other rods rigid bodies or surfaces.""" from typing import TypeVar, Generic, Type -from elastica.typing import RodType, SystemType, SurfaceType +from elastica.typing import RodType, SystemType, SurfaceType, CosseratRodProtocol from elastica.rod.rod_base import RodBase from elastica.rigidbody.cylinder import Cylinder diff --git a/elastica/modules/base_system.py b/elastica/modules/base_system.py index 1fe31415..dcd5890a 100644 --- a/elastica/modules/base_system.py +++ b/elastica/modules/base_system.py @@ -10,7 +10,9 @@ from elastica.typing import ( SystemType, StaticSystemType, + StaticSystemProtocol, BlockSystemType, + BlockSystemProtocol, SystemIdxType, OperatorType, OperatorCallbackType, diff --git a/elastica/modules/callbacks.py b/elastica/modules/callbacks.py index 055d5dc4..770b1f29 100644 --- a/elastica/modules/callbacks.py +++ b/elastica/modules/callbacks.py @@ -9,7 +9,12 @@ from types import EllipsisType from typing import Type, Any, TypeAlias, cast from typing_extensions import Self # 3.11: from typing import Self -from elastica.typing import SystemType, SystemIdxType, OperatorFinalizeType +from elastica.typing import ( + SystemType, + SystemIdxType, + OperatorFinalizeType, + SystemProtocol, +) from .protocol import ModuleProtocol import functools diff --git a/elastica/modules/connections.py b/elastica/modules/connections.py index 2b92377c..be10ec88 100644 --- a/elastica/modules/connections.py +++ b/elastica/modules/connections.py @@ -133,10 +133,6 @@ class _Connect: _connect_class: list first_sys_connection_idx: ConnectionIndex second_sys_connection_idx: ConnectionIndex - *args - Variable length argument list. - **kwargs - Arbitrary keyword arguments. """ def __init__( diff --git a/elastica/modules/damping.py b/elastica/modules/damping.py index b7abd754..503168b6 100644 --- a/elastica/modules/damping.py +++ b/elastica/modules/damping.py @@ -17,7 +17,7 @@ import numpy as np from elastica.dissipation import DamperBase -from elastica.typing import RodType, SystemType, SystemIdxType +from elastica.typing import RodType, SystemType, SystemIdxType, CosseratRodProtocol from .protocol import DampenedSystemCollectionProtocol, ModuleProtocol diff --git a/elastica/modules/forcing.py b/elastica/modules/forcing.py index 8b7f6282..b4c9fe9d 100644 --- a/elastica/modules/forcing.py +++ b/elastica/modules/forcing.py @@ -13,7 +13,7 @@ import numpy as np from elastica.external_forces import NoForces -from elastica.typing import SystemType, SystemIdxType +from elastica.typing import SystemType, SystemIdxType, SystemProtocol from .protocol import ForcedSystemCollectionProtocol, ModuleProtocol logger = logging.getLogger(__name__) diff --git a/elastica/rod/cosserat_rod.py b/elastica/rod/cosserat_rod.py index 932da09b..e47bcbad 100644 --- a/elastica/rod/cosserat_rod.py +++ b/elastica/rod/cosserat_rod.py @@ -295,7 +295,7 @@ def straight_rod( Damping coefficient for Rayleigh damping youngs_modulus : float Young's modulus - **kwargs : dict, optional + kwargs : dict, optional The "position" and/or "directors" can be overrided by passing "position" and "directors" argument. Remember, the shape of the "position" is (3,n_elements+1) and the shape of the "directors" is (3,3,n_elements). Returns @@ -442,7 +442,7 @@ def ring_rod( Damping coefficient for Rayleigh damping youngs_modulus : float Young's modulus - **kwargs : dict, optional + kwargs : dict, optional The "position" and/or "directors" can be overrided by passing "position" and "directors" argument. Remember, the shape of the "position" is (3,n_elements+1) and the shape of the "directors" is (3,3,n_elements). Returns diff --git a/elastica/rod/knot_theory.py b/elastica/rod/knot_theory.py index 44ab7688..f43cbabb 100644 --- a/elastica/rod/knot_theory.py +++ b/elastica/rod/knot_theory.py @@ -35,26 +35,24 @@ def __init__(self) -> None: total_twist = rod.compute_twist() total_link = rod.compute_link() - There are few alternative way of handling edge-condition in computing Link and Writhe. - Here, we provide three methods: "next_tangent", "end_to_end", and "net_tangent". - The default *type_of_additional_segment* is set to "next_tangent." - - ========================== ===================================== - type_of_additional_segment Description - ========================== ===================================== - next_tangent | Adds a two new point at the begining and end of the center line. - | Distance of these points are given in segment_length. - | Direction of these points are computed using the rod tangents at - | the begining and end. - end_to_end | Adds a two new point at the begining and end of the center line. - | Distance of these points are given in segment_length. - | Direction of these points are computed using the rod node end - | positions. - net_tangent | Adds a two new point at the begining and end of the center line. - | Distance of these points are given in segment_length. Direction of - | these points are point wise avarege of nodes at the first and - | second half of the rod. - ========================== ===================================== + There are a few alternative ways of handling edge-conditions in computing Link and Writhe. + The `type_of_additional_segment` parameter, which defaults to ``"next_tangent"``, can be set to one of the following: + + ``"next_tangent"`` + Adds two new points at the beginning and end of the center line. + The distance of these points is given by `segment_length`. + The direction of these points is computed using the rod tangents at + the beginning and end. + ``"end_to_end"`` + Adds two new points at the beginning and end of the center line. + The distance of these points is given by `segment_length`. + The direction of these points is computed using the rod node end + positions. + ``"net_tangent"`` + Adds two new points at the beginning and end of the center line. + The distance of these points is given by `segment_length`. The direction of + these points is the point-wise average of nodes in the first and + second half of the rod.= """ diff --git a/elastica/typing.py b/elastica/typing.py index 03693f2d..31ef9917 100644 --- a/elastica/typing.py +++ b/elastica/typing.py @@ -62,7 +62,7 @@ SteppersOperatorsType: TypeAlias = tuple[tuple[StepType, ...], ...] -RodType: TypeAlias = CosseratRodProtocol +RodType: TypeAlias = "CosseratRodProtocol" RigidBodyType: TypeAlias = "RigidBodyProtocol" SurfaceType: TypeAlias = "SurfaceBase" From a8a0937d147bdfcc1daf95ebec657c20e9c49f6c Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sat, 25 Oct 2025 18:11:13 -0500 Subject: [PATCH 15/85] fix: documentation no-end-of-line warning --- docs/api/callback.rst | 9 +++------ docs/api/damping.rst | 4 ++-- docs/api/utility.rst | 8 ++++++-- elastica/_rotations.py | 5 +---- elastica/boundary_conditions.py | 2 -- elastica/callback_functions.py | 2 +- elastica/dissipation.py | 1 - elastica/external_forces.py | 6 +----- elastica/interaction.py | 3 +-- elastica/joint.py | 5 ++--- elastica/rod/cosserat_rod.py | 2 +- elastica/utils.py | 24 ++++++++---------------- 12 files changed, 26 insertions(+), 45 deletions(-) diff --git a/docs/api/callback.rst b/docs/api/callback.rst index 59eaac27..7142d77a 100644 --- a/docs/api/callback.rst +++ b/docs/api/callback.rst @@ -1,14 +1,12 @@ Callback Functions =================== -.. _constraints: - .. automodule:: elastica.callback_functions Description ----------- -The frequency at which you have your callback function save data will depend on what information you need from the simulation. Excessive call backs can cause performance penalties, however, it is rarely necessary to make call backs at a frequency that this becomes a problem. We have found that making a call back roughly every 100 iterations has a negligible performance penalty. +The frequency at which you have your callback function save data will depend on what information you need from the simulation. Excessive call backs can cause performance penalties, however, it is rarely necessary to make call backs at a frequency that this becomes a problem. We have found that making a call back roughly every 100 iterations has a negligible performance penalty. Currently, all data saved from call back functions is saved in memory. If you have many rods or are running for a long time, you may want to consider editing the call back function to write the saved data to disk so you do not run out of memory during the simulation. @@ -19,8 +17,8 @@ Currently, all data saved from call back functions is saved in memory. If you ha ExportCallBack MyCallBack -Built-in Constraints --------------------- +Built-in Callbacks +------------------ .. autoclass:: CallBackBaseClass :special-members: __init__ @@ -30,4 +28,3 @@ Built-in Constraints .. autoclass:: MyCallBack :special-members: __init__ - diff --git a/docs/api/damping.rst b/docs/api/damping.rst index bd2b87a3..0940c257 100644 --- a/docs/api/damping.rst +++ b/docs/api/damping.rst @@ -28,8 +28,8 @@ LaplaceDissipationFilter ✅ ❌ =============================== ==== =========== -Built-in Constraints --------------------- +Built-in Dampers +---------------- .. autoclass:: DamperBase :inherited-members: diff --git a/docs/api/utility.rst b/docs/api/utility.rst index 53c03539..079a2b5d 100644 --- a/docs/api/utility.rst +++ b/docs/api/utility.rst @@ -1,16 +1,19 @@ Utility Functions -================== +================= Here, we provide some useful functions that we often use along with elastica. Transformations ------------------ +--------------- + .. automodule:: elastica.transformations :members: :exclude-members: __weakref__ + Math ---- + .. automodule:: elastica._calculus :members: :exclude-members: __weakref__ @@ -25,6 +28,7 @@ Math Miscellaneous ------------- + .. automodule:: elastica.utils :members: :exclude-members: __weakref__ diff --git a/elastica/_rotations.py b/elastica/_rotations.py index bbc2b336..b58cc356 100644 --- a/elastica/_rotations.py +++ b/elastica/_rotations.py @@ -4,10 +4,7 @@ from itertools import combinations import numpy as np -from numpy import sin -from numpy import cos -from numpy import sqrt -from numpy import arccos +from numpy import sin, cos, sqrt, arccos from numpy.typing import NDArray from numba import njit diff --git a/elastica/boundary_conditions.py b/elastica/boundary_conditions.py index 1075821c..ee825379 100644 --- a/elastica/boundary_conditions.py +++ b/elastica/boundary_conditions.py @@ -675,8 +675,6 @@ class HelicalBucklingBC(ConstraintBase): final_end_directors: numpy.ndarray 3D (dim, dim, 1) array containing data with 'float' type. Directors of last element of rod after twist completed. - - """ def __init__( diff --git a/elastica/callback_functions.py b/elastica/callback_functions.py index 42345586..7adca176 100644 --- a/elastica/callback_functions.py +++ b/elastica/callback_functions.py @@ -68,7 +68,7 @@ class MyCallBack(CallBackBaseClass): Collect data using make_callback method every sampling step. callback_params: dict Collected callback data is saved in this dictionary. - """ +""" def __init__(self, step_skip: int, callback_params: dict) -> None: """ diff --git a/elastica/dissipation.py b/elastica/dissipation.py index 22f618fc..e6c3f31f 100644 --- a/elastica/dissipation.py +++ b/elastica/dissipation.py @@ -29,7 +29,6 @@ class DamperBase(Generic[T], ABC): Attributes ---------- system : RodBase - """ _system: T diff --git a/elastica/external_forces.py b/elastica/external_forces.py index 340ba7cd..55dcea4b 100644 --- a/elastica/external_forces.py +++ b/elastica/external_forces.py @@ -73,7 +73,6 @@ class GravityForces(NoForces): ---------- acc_gravity: numpy.ndarray 1D (dim) array containing data with 'float' type. Gravitational acceleration vector. - """ def __init__( @@ -136,7 +135,6 @@ class EndpointForces(NoForces): 1D (dim) array containing data with 'float' type. Force applied to last node of the system. ramp_up_time: float Applied forces are ramped up until ramp up time. - """ def __init__( @@ -215,7 +213,6 @@ class UniformTorques(NoForces): ---------- torque: numpy.ndarray 2D (dim, 1) array containing data with 'float' type. Total torque applied to a rod-like object. - """ def __init__( @@ -258,7 +255,7 @@ class UniformForces(NoForces): ---------- force: numpy.ndarray 2D (dim, 1) array containing data with 'float' type. Total force applied to a rod-like object. - """ +""" def __init__( self, @@ -314,7 +311,6 @@ class MuscleTorques(NoForces): Applied muscle torques are ramped up until ramp up time. my_spline: numpy.ndarray 1D (blocksize) array containing data with 'float' type. Generated spline. - """ def __init__( diff --git a/elastica/interaction.py b/elastica/interaction.py index 1a72650d..8e4d9f87 100644 --- a/elastica/interaction.py +++ b/elastica/interaction.py @@ -41,7 +41,6 @@ class InteractionPlane(NoForces): The normal vector of the plane. surface_tol: float Penetration tolerance between the plane and the rod-like object. - """ def __init__( @@ -138,7 +137,7 @@ class AnisotropicFrictionalPlane(InteractionPlane): kinetic_mu_array: numpy.ndarray 1D (3,) array containing data with 'float' type. [forward, backward, sideways] kinetic friction coefficients. - """ +""" def __init__( self, diff --git a/elastica/joint.py b/elastica/joint.py index 62555cef..8bd14820 100644 --- a/elastica/joint.py +++ b/elastica/joint.py @@ -24,7 +24,6 @@ class FreeJoint: Stiffness coefficient of the joint. nu: float Damping coefficient of the joint. - """ # pass the k and nu for the forces @@ -137,7 +136,7 @@ class HingeJoint(FreeJoint): Rotational stiffness coefficient of the joint. normal_direction: numpy.ndarray 2D (dim, 1) array containing data with 'float' type. Constraint rotation direction. - """ +""" # TODO: IN WRAPPER COMPUTE THE NORMAL DIRECTION OR ASK USER TO GIVE INPUT, IF NOT THROW ERROR def __init__( @@ -234,7 +233,7 @@ class FixedJoint(FreeJoint): Rest 3x3 rotation matrix from system one to system two at the connected elements. Instead of aligning the directors of both systems directly, a desired rest rotational matrix labeled C_12* is enforced. - """ +""" def __init__( self, diff --git a/elastica/rod/cosserat_rod.py b/elastica/rod/cosserat_rod.py index e47bcbad..9521ed48 100644 --- a/elastica/rod/cosserat_rod.py +++ b/elastica/rod/cosserat_rod.py @@ -148,7 +148,7 @@ class CosseratRod(RodBase, KnotTheory): dilatation_rate: NDArray[np.float64] 1D (n_elems) array containing data with 'float' type. Rod element dilatation rates. - """ +""" REQUISITE_MODULES: list[Type] = [] diff --git a/elastica/utils.py b/elastica/utils.py index aa7a605e..553db0e1 100644 --- a/elastica/utils.py +++ b/elastica/utils.py @@ -1,4 +1,4 @@ -"""Handy utilities""" +__doc__ = """Handy utilities""" from typing import Generator, Iterable, Any, Literal, TypeVar import functools @@ -100,10 +100,11 @@ def perm_parity(lst: list[int]) -> int: Parameters ---------- - lst + lst : list[int] Returns ------- + int """ parity = 1 @@ -131,9 +132,6 @@ def grouper(iterable: Iterable[_T], n: int) -> Generator[tuple[_T, ...], None, N iterable : input collection n : size of chunk - Returns - ------- - """ it = iter(iterable) @@ -146,20 +144,16 @@ def grouper(iterable: Iterable[_T], n: int) -> Generator[tuple[_T, ...], None, N def extend_instance(obj: Any, cls: Any) -> None: """ - Apply mixins to a class instance after creation. https://stackoverflow.com/a/31075641 Parameters ---------- - obj : object (not class!) targeted for interface extension - Interface carries throughout its lifetime. - cls : class (not object!) to dynamically mixin - - Returns - ------- - None - + obj : + object (not class!) targeted for interface extension + Interface carries throughout its lifetime. + cls : + class (not object!) to dynamically mixin """ base_cls = obj.__class__ base_cls_name = obj.__class__.__name__ @@ -200,8 +194,6 @@ def _bspline( # type: ignore[no-any-unimported] def __bspline_impl__( # type: ignore[no-any-unimported] x_pts: NDArray, t_c: NDArray, degree: int ) -> tuple[BSpline, NDArray, NDArray]: - """""" - # Update the knots n_upd = t_c.shape[0] + (degree + 1) From 3d4e21d242b783dce899d0b99457e2bef2c2c6a5 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sat, 25 Oct 2025 18:36:17 -0500 Subject: [PATCH 16/85] remove POVray example --- README.md | 2 - examples/README.md | 3 - .../continuum_snake.py | 154 -------- .../continuum_snake_render.py | 184 --------- examples/Visualization/README.md | 14 - examples/Visualization/_povmacros.py | 353 ------------------ examples/Visualization/default.inc | 94 ----- 7 files changed, 804 deletions(-) delete mode 100644 examples/Visualization/ContinuumSnakeVisualization/continuum_snake.py delete mode 100644 examples/Visualization/ContinuumSnakeVisualization/continuum_snake_render.py delete mode 100644 examples/Visualization/README.md delete mode 100644 examples/Visualization/_povmacros.py delete mode 100644 examples/Visualization/default.inc diff --git a/README.md b/README.md index 8112480b..e9d17339 100644 --- a/README.md +++ b/README.md @@ -89,8 +89,6 @@ 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)). - ## Contribution If you would like to participate, please read our [contribution guideline](CONTRIBUTING.md). Private development branches are moved to [elastica-python](https://github.com/GazzolaLab/elastica-python) repository; access is limited to the core developers, collaborators, and maintainers. diff --git a/examples/README.md b/examples/README.md index ee70687a..548b0418 100644 --- a/examples/README.md +++ b/examples/README.md @@ -99,9 +99,6 @@ Examples can serve as a starting template for customized usages. * [RestartExample](./RestartExample) * __Purpose__: Demonstrate the usage of restart module. * __Features__: save_state, load_state -* [Visualization](./Visualization) - * __Purpose__: Include simple examples of raytrace rendering data. - * __Features__: POVray ## Advanced Cases diff --git a/examples/Visualization/ContinuumSnakeVisualization/continuum_snake.py b/examples/Visualization/ContinuumSnakeVisualization/continuum_snake.py deleted file mode 100644 index b46b647a..00000000 --- a/examples/Visualization/ContinuumSnakeVisualization/continuum_snake.py +++ /dev/null @@ -1,154 +0,0 @@ -import numpy as np -from collections import defaultdict -import elastica as ea - - -class SnakeSimulator( - ea.BaseSystemCollection, ea.Constraints, ea.Forcing, ea.Damping, ea.CallBacks -): - pass - - -def run_snake(b_coeff, SAVE_RESULTS=False): - - snake_sim = SnakeSimulator() - - # setting up test params - n_elem = 20 - start = np.zeros((3,)) - direction = np.array([0.0, 0.0, 1.0]) - normal = np.array([0.0, 1.0, 0.0]) - base_length = 1.0 - base_radius = 0.025 - density = 1000 - E = 1e7 - poisson_ratio = 0.5 - shear_modulus = E / (poisson_ratio + 1.0) - - shearable_rod = ea.CosseratRod.straight_rod( - n_elem, - start, - direction, - normal, - base_length, - base_radius, - density, - youngs_modulus=E, - shear_modulus=shear_modulus, - ) - - snake_sim.append(shearable_rod) - - # Add gravitational forces - gravitational_acc = -9.80665 - snake_sim.add_forcing_to(shearable_rod).using( - ea.GravityForces, acc_gravity=np.array([0.0, gravitational_acc, 0.0]) - ) - - period = 1.0 - wave_length = b_coeff[-1] - snake_sim.add_forcing_to(shearable_rod).using( - ea.MuscleTorques, - base_length=base_length, - b_coeff=b_coeff[:-1], - period=period, - wave_number=2.0 * np.pi / (wave_length), - phase_shift=0.0, - direction=normal, - rest_lengths=shearable_rod.rest_lengths, - ramp_up_time=period, - with_spline=True, - ) - - # Add friction forces - origin_plane = np.array([0.0, -base_radius, 0.0]) - normal_plane = normal - slip_velocity_tol = 1e-8 - froude = 0.1 - mu = base_length / (period * period * np.abs(gravitational_acc) * froude) - kinetic_mu_array = np.array( - [mu, 1.5 * mu, 2.0 * mu] - ) # [forward, backward, sideways] - static_mu_array = 2 * kinetic_mu_array - snake_sim.add_forcing_to(shearable_rod).using( - ea.AnisotropicFrictionalPlane, - k=1.0, - nu=1e-6, - plane_origin=origin_plane, - plane_normal=normal_plane, - slip_velocity_tol=slip_velocity_tol, - static_mu_array=static_mu_array, - kinetic_mu_array=kinetic_mu_array, - ) - - # add damping - damping_constant = 5.0 - dt = 5.0e-5 * period - snake_sim.dampen(shearable_rod).using( - ea.AnalyticalLinearDamper, - damping_constant=damping_constant, - time_step=dt, - ) - - # Add call backs - class ContinuumSnakeCallBack(ea.CallBackBaseClass): - """ - Call back function for continuum snake - """ - - def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) - self.every = step_skip - self.callback_params = callback_params - - def make_callback(self, system, time, current_step: int): - - if current_step % self.every == 0: - - self.callback_params["time"].append(time) - self.callback_params["position"].append( - system.position_collection.copy() - ) - - return - - pp_list = defaultdict(list) - snake_sim.collect_diagnostics(shearable_rod).using( - ContinuumSnakeCallBack, step_skip=200, callback_params=pp_list - ) - - snake_sim.finalize() - timestepper = ea.PositionVerlet() - # timestepper = PEFRL() - - final_time = (11.0 + 0.01) * period - total_steps = int(final_time / dt) - print("Total steps", total_steps) - dt = final_time / total_steps - time = 0.0 - for i in range(total_steps): - time = timestepper.step(snake_sim, time, dt) - - if SAVE_RESULTS: - import pickle - - filename = "continuum_snake.dat" - file = open(filename, "wb") - pickle.dump(pp_list, file) - file.close() - - return pp_list - - -if __name__ == "__main__": - - # Options - SAVE_RESULTS = True - - # Add muscle forces on the rod - t_coeff_optimized = np.array([17.4, 48.5, 5.4, 14.7, 0.97]) - - # run the simulation - pp_list = run_snake(t_coeff_optimized, SAVE_RESULTS) - - print("Datafile Created") diff --git a/examples/Visualization/ContinuumSnakeVisualization/continuum_snake_render.py b/examples/Visualization/ContinuumSnakeVisualization/continuum_snake_render.py deleted file mode 100644 index 6c740e26..00000000 --- a/examples/Visualization/ContinuumSnakeVisualization/continuum_snake_render.py +++ /dev/null @@ -1,184 +0,0 @@ -"""Rendering Script using POVray - -This script reads simulated data file to render POVray animation movie. -The data file should contain dictionary of positions vectors and times. - -The script supports multiple camera position where a video is generated -for each camera view. - -Notes ------ - The module requires POVray installed. -""" - -import multiprocessing -import os -from functools import partial -from multiprocessing import Pool - -import numpy as np -from scipy import interpolate -from tqdm import tqdm - -from examples.Visualization._povmacros import Stages, pyelastica_rod, render - -# Setup (USER DEFINE) -DATA_PATH = "continuum_snake.dat" # Path to the simulation data -SAVE_PICKLE = True - -# Rendering Configuration (USER DEFINE) -OUTPUT_FILENAME = "pov_snake" -OUTPUT_IMAGES_DIR = "frames" -FPS = 20.0 -WIDTH = 1920 # 400 -HEIGHT = 1080 # 250 -DISPLAY_FRAMES = "Off" # Display povray images during the rendering. ['On', 'Off'] - -# Camera/Light Configuration (USER DEFINE) -stages = Stages() -stages.add_camera( - # Add diagonal viewpoint - location=[15.0, 10.5, -15.0], - angle=30, - look_at=[4.0, 2.7, 2.0], - name="diag", -) -stages.add_camera( - # Add top viewpoint - location=[0, 15, 3], - angle=30, - look_at=[0.0, 0, 3], - sky=[-1, 0, 0], - name="top", -) -stages.add_light( - # Sun light - position=[1500, 2500, -1000], - color="White", - camera_id=-1, -) -stages.add_light( - # Flash light for camera 0 - position=[15.0, 10.5, -15.0], - color=[0.09, 0.09, 0.1], - camera_id=0, -) -stages.add_light( - # Flash light for camera 1 - position=[0.0, 8.0, 5.0], - color=[0.09, 0.09, 0.1], - camera_id=1, -) -stage_scripts = stages.generate_scripts() - -# Externally Including Files (USER DEFINE) -# If user wants to include other POVray objects such as grid or coordinate axes, -# objects can be defined externally and included separately. -included = ["../default.inc"] - -# Multiprocessing Configuration (USER DEFINE) -MULTIPROCESSING = True -THREAD_PER_AGENT = 4 # Number of thread use per rendering process. -NUM_AGENT = multiprocessing.cpu_count() // 2 # number of parallel rendering. - -# Execute -if __name__ == "__main__": - # Load Data - assert os.path.exists(DATA_PATH), "File does not exists" - try: - if SAVE_PICKLE: - import pickle as pk - - with open(DATA_PATH, "rb") as fptr: - data = pk.load(fptr) - else: - # (TODO) add importing npz file format - raise NotImplementedError("Only pickled data is supported") - except OSError as err: - print("Cannot open the datafile {}".format(DATA_PATH)) - print(str(err)) - raise - - # Convert data to numpy array - times = np.array(data["time"]) # shape: (timelength) - xs = np.array(data["position"]) # shape: (timelength, 3, num_element) - - # Interpolate Data - # Interpolation step serves two purposes. If simulated frame rate is lower than - # the video frame rate, the intermediate frames are linearly interpolated to - # produce smooth video. Otherwise if simulated frame rate is higher than - # the video frame rate, interpolation reduces the number of frame to reduce - # the rendering time. - runtime = times.max() # Physical run time - total_frame = int(runtime * FPS) # Number of frames for the video - recorded_frame = times.shape[0] # Number of simulated frames - times_true = np.linspace(0, runtime, total_frame) # Adjusted timescale - - xs = interpolate.interp1d(times, xs, axis=0)(times_true) - times = interpolate.interp1d(times, times, axis=0)(times_true) - base_radius = np.ones_like(xs[:, 0, :]) * 0.050 # (TODO) radius could change - - # Rendering - # For each frame, a 'pov' script file is generated in OUTPUT_IMAGE_DIR directory. - batch = [] - for view_name in stage_scripts.keys(): # Make Directory - output_path = os.path.join(OUTPUT_IMAGES_DIR, view_name) - os.makedirs(output_path, exist_ok=True) - for frame_number in tqdm(range(total_frame), desc="Scripting"): - for view_name, stage_script in stage_scripts.items(): - output_path = os.path.join(OUTPUT_IMAGES_DIR, view_name) - - # Colect povray scripts - script = [] - script.extend(['#include "{}"'.format(s) for s in included]) - script.append(stage_script) - - # If the data contains multiple rod, this part can be modified to include - # multiple rods. - rod_object = pyelastica_rod( - x=xs[frame_number], - r=base_radius[frame_number], - color="rgb<0.45,0.39,1>", - ) - script.append(rod_object) - pov_script = "\n".join(script) - - # Write .pov script file - file_path = os.path.join(output_path, "frame_{:04d}".format(frame_number)) - with open(file_path + ".pov", "w+") as f: - f.write(pov_script) - batch.append(file_path) - - # Process POVray - # For each frames, a 'png' image file is generated in OUTPUT_IMAGE_DIR directory. - pbar = tqdm(total=len(batch), desc="Rendering") # Progress Bar - if MULTIPROCESSING: - func = partial( - render, - width=WIDTH, - height=HEIGHT, - display=DISPLAY_FRAMES, - pov_thread=THREAD_PER_AGENT, - ) - with Pool(NUM_AGENT) as p: - for message in p.imap_unordered(func, batch): - # (TODO) POVray error within child process could be an issue - pbar.update() - else: - for filename in batch: - render( - filename, - width=WIDTH, - height=HEIGHT, - display=DISPLAY_FRAMES, - pov_thread=multiprocessing.cpu_count(), - ) - pbar.update() - - # Create Video using ffmpeg - for view_name in stage_scripts.keys(): - imageset_path = os.path.join(OUTPUT_IMAGES_DIR, view_name) - - filename = OUTPUT_FILENAME + "_" + view_name + ".mp4" - - os.system(f"ffmpeg -r {FPS} -i {imageset_path}/frame_%04d.png {filename}") diff --git a/examples/Visualization/README.md b/examples/Visualization/README.md deleted file mode 100644 index a9b6f6f4..00000000 --- a/examples/Visualization/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# POVray Visualization Example - -A simple example of rendering pyelastica using POVray. -The code [render](continuum_snake_render.py) generates POVray script (.pov) and image file (.png) to render POVray animation. - -### Bash Script to Run -``` bash -python continuum_snake.py # Creates continuum_snake.dat file -python continuum_snake_render.py # Creates pov_snake_diag.mp4 and pov_snake_top.mp4 file (3-5 minutes) -``` - -### Dependency -- povray -- ffmpeg diff --git a/examples/Visualization/_povmacros.py b/examples/Visualization/_povmacros.py deleted file mode 100644 index 10252445..00000000 --- a/examples/Visualization/_povmacros.py +++ /dev/null @@ -1,353 +0,0 @@ -"""POVray macros for pyelastica - -This module includes utility methods to support POVray rendering. - -""" - -import subprocess -from collections import defaultdict - - -def pyelastica_rod( - x, - r, - color="rgb<0.45,0.39,1>", - transmit=0.0, - interpolation="linear_spline", - deform=None, - tab=" ", -): - """pyelastica_rod POVray script generator - - Generates povray sphere_sweep object in string. - The rod is given with the element radius (r) and joint positions (x) - - Parameters - ---------- - x : numpy array - Position vector - Expected shape: [num_time_step, 3, num_element] - r : numpy array - Radius vector - Expected shape: [num_time_step, num_element] - color : str - Color of the rod (default: Purple <0.45,0.39,1>) - transmit : float - Transparency (0.0 to 1.0). - interpolation : str - Interpolation method for sphere_sweep - Supporting type: 'linear_spline', 'b_spline', 'cubic_spline' - (default: linear_spline) - deform : str - Additional object deformation - Example: "scale<4,4,4> rotate<0,90,90> translate<2,0,4>" - - Returns - ------- - cmd : string - Povray script - """ - - assert interpolation in ["linear_spline", "b_spline", "cubic_spline"] - tab = " " - - # Parameters - num_element = r.shape[0] - - lines = [] - lines.append("sphere_sweep {") - lines.append(tab + f"{interpolation} {num_element}") - for i in range(num_element): - lines.append(tab + f",<{x[0,i]},{x[1,i]},{x[2,i]}>,{r[i]}") - lines.append(tab + "texture{") - lines.append(tab + tab + "pigment{ color %s transmit %f }" % (color, transmit)) - lines.append(tab + tab + "finish{ phong 1 }") - lines.append(tab + "}") - if deform is not None: - lines.append(tab + deform) - lines.append(tab + "}\n") - - cmd = "\n".join(lines) - return cmd - - -def render( - filename, width, height, antialias="on", quality=11, display="Off", pov_thread=4 -): - """Rendering frame - - Generate the povray script file '.pov' and image file '.png' - The directory must be made before calling this method. - - Parameters - ---------- - filename : str - POV filename (without extension) - width : int - The width of the output image. - height : int - The height of the output image. - antialias : str ['on', 'off'] - Turns anti-aliasing on/off [default='on'] - quality : int - Image output quality. [default=11] - display : str - Turns display option on/off during POVray rendering. [default='off'] - pov_thread : int - Number of thread per povray process. [default=4] - Acceptable range is (4,512). - Refer 'Symmetric Multiprocessing (SMP)' for further details - https://www.povray.org/documentation/3.7.0/r3_2.html#r3_2_8_1 - - Raises - ------ - IOError - If the povray run causes unexpected error, such as parsing error, - this method will raise IOerror. - - """ - - # Define script path and image path - script_file = filename + ".pov" - image_file = filename + ".png" - - # Run Povray as subprocess - cmds = [ - "povray", - "+I" + script_file, - "+O" + image_file, - f"-H{height}", - f"-W{width}", - f"Work_Threads={pov_thread}", - f"Antialias={antialias}", - f"Quality={quality}", - f"Display={display}", - ] - process = subprocess.Popen( - cmds, stderr=subprocess.PIPE, stdin=subprocess.PIPE, stdout=subprocess.PIPE - ) - _, stderr = process.communicate() - - # Check execution error - if process.returncode: - print(type(stderr), stderr) - raise IOError( - "POVRay rendering failed with the following error: " - + stderr.decode("ascii") - ) - - -class Stages: - """Stage definition - - Collection of the camera and light sources. - Each camera added to the stage represent distinct viewpoints to render. - Lights can be assigned to multiple cameras. - The povray script can be generated for each viewpoints created using 'generate_scripts.' - - (TODO) Implement transform camera for dynamic camera moves - - Attributes - ---------- - pre_scripts : str - Prepending script for all viewpoints - post_scripts : str - Appending script for all viewpoints - cameras : list - List of camera setup - lights : list - List of lightings - _light_assign : dictionary[list] - Dictionary that pairs lighting to camera. - Example) _light_assign[2] is the list of light sources - assigned to the cameras[2] - - Methods - ------- - add_camera : Add new camera (viewpoint) to the stage. - add_light : Add new light source to the stage for a assigned camera. - generate_scripts : Generate list of povray script for each camera. - - Class Objects - ------------- - StageObject - Camera - Light - - Properties - ---------- - len : number of camera - The number of viewpoints - """ - - def __init__(self, pre_scripts="", post_scripts=""): - self.pre_scripts = pre_scripts - self.post_scripts = post_scripts - self.cameras = [] - self.lights = [] - self._light_assign = defaultdict(list) - - def add_camera(self, name, **kwargs): - """Add camera (viewpoint)""" - self.cameras.append(self.Camera(name=name, **kwargs)) - - def add_light(self, camera_id=-1, **kwargs): - """Add lighting and assign to camera - Parameters - ---------- - camera_id : int or list - Assigned camera. [default=-1] - If a list of camera_id is given, light is assigned for listed camera. - If camera_id==-1, the lighting is assigned for all camera. - """ - light_id = len(self.lights) - self.lights.append(self.Light(**kwargs)) - if isinstance(camera_id, list) or isinstance(camera_id, tuple): - camera_id = list(set(camera_id)) - for idx in camera_id: - self._light_assign[idx].append(light_id) - elif isinstance(camera_id, int): - self._light_assign[camera_id].append(light_id) - else: - raise NotImplementedError("camera_id can only be a list or int") - - def generate_scripts(self): - """Generate pov-ray script for all camera setup - Returns - ------- - scripts : list - Return list of pov-scripts (string) that includes camera and assigned lightings. - """ - scripts = {} - for idx, camera in enumerate(self.cameras): - light_ids = self._light_assign[idx] + self._light_assign[-1] - cmds = [] - cmds.append(self.pre_scripts) - cmds.append(str(camera)) # Script camera - for light_id in light_ids: # Script Lightings - cmds.append(str(self.lights[light_id])) - cmds.append(self.post_scripts) - scripts[camera.name] = "\n".join(cmds) - return scripts - - def transform_camera(self, dx, R, camera_id): - # (TODO) translate or rotate the assigned camera - raise NotImplementedError - - def __len_(self): - return len(self.cameras) - - # Stage Objects: Camera, Light - class StageObject: - """Template for stage objects - - Objects (camera and light) is defined as an object in order to - manipulate (translate or rotate) them during the rendering. - - Attributes - ---------- - str : str - String representation of object. - The placeholder exist to avoid rescripting. - - Methods - ------- - _color2str : str - Change triplet tuple (or list) of color into rgb string. - _position2str : str - Change triplet tuple (or list) of position vector into string. - """ - - def __init__(self): - self.str = "" - self.update_script() - - def update_script(self): - raise NotImplementedError - - def __str__(self): - return self.str - - def _color2str(self, color): - if isinstance(color, str): - return color - elif isinstance(color, list) and len(color) == 3: - # RGB - return "rgb<{},{},{}>".format(*color) - else: - raise NotImplementedError( - "Only string-type color or RGB input is implemented" - ) - - def _position2str(self, position): - assert len(position) == 3 - return "<{},{},{}>".format(*position) - - class Camera(StageObject): - """Camera object - - http://www.povray.org/documentation/view/3.7.0/246/ - - Attributes - ---------- - location : list or tuple - Position vector of camera location. (length=3) - angle : int - Camera angle - look_at : list or tuple - Position vector of the location where camera points to (length=3) - name : str - Name of the view-point. - sky : list or tuple - Tilt of the camera (length=3) [default=[0,1,0]] - """ - - def __init__(self, name, location, angle, look_at, sky=(0, 1, 0)): - self.name = name - self.location = location - self.angle = angle - self.look_at = look_at - self.sky = sky - super().__init__() - - def update_script(self): - location = self._position2str(self.location) - look_at = self._position2str(self.look_at) - sky = self._position2str(self.sky) - cmds = [] - cmds.append("camera{") - cmds.append(f" location {location}") - cmds.append(f" angle {self.angle}") - cmds.append(f" look_at {look_at}") - cmds.append(f" sky {sky}") - cmds.append(" right x*image_width/image_height") - cmds.append("}") - self.str = "\n".join(cmds) - - class Light(StageObject): - """Light object - - Attributes - ---------- - position : list or tuple - Position vector of light location. (length=3) - color : str or list - Color of the light. - Both string form of color or rgb (normalized) form is supported. - Example) color='White', color=[1,1,1] - """ - - def __init__(self, position, color): - self.position = position - self.color = color - super().__init__() - - def update_script(self): - position = self._position2str(self.position) - color = self._color2str(self.color) - cmds = [] - cmds.append("light_source{") - cmds.append(f" {position}") - cmds.append(f" color {color}") - cmds.append("}") - self.str = "\n".join(cmds) diff --git a/examples/Visualization/default.inc b/examples/Visualization/default.inc deleted file mode 100644 index 57f4d721..00000000 --- a/examples/Visualization/default.inc +++ /dev/null @@ -1,94 +0,0 @@ -// POV-Ray 3.6 / 3.7 Scene File "Ribbon_Cable_1.pov" -// author: Friedrich A. Lohmueller, Sept-2009/Jan-2011 -// email: Friedrich.Lohmueller_at_t-online.de -// homepage: http://www.f-lohmueller.de -//-------------------------------------------------------------------------- -#version 3.6; // 3.7; -global_settings{ assumed_gamma 1.0 } -#default{ finish{ ambient 0.1 diffuse 0.9 }} -#include "colors.inc" -#include "textures.inc" -#include "glass.inc" -#include "metals.inc" -#include "golds.inc" -#include "stones.inc" -#include "woods.inc" -#include "shapes.inc" -#include "shapes2.inc" -#include "functions.inc" -#include "math.inc" -#include "transforms.inc" - -background{ color White } - -//------------------------------ the Axes -------------------------------- -//------------------------------------------------------------------------ -#macro Axis_( AxisLen, Dark_Texture,Light_Texture) - union{ - cylinder { <0,-AxisLen,0>,<0,AxisLen,0>,0.05 - texture{checker texture{Dark_Texture } - texture{Light_Texture} - scale 0.5 - translate<0.1,0,0.1>} - } - cone{<0,AxisLen,0>,0.2,<0,AxisLen+0.7,0>,0 - texture{Dark_Texture} - } - } // end of union -#end // of macro "Axis()" -//------------------------------------------------------------------------ -#macro AxisXYZ( AxisLenX, AxisLenY, AxisLenZ, Tex_Dark, Tex_Light) -//--------------------- drawing of 3 Axes -------------------------------- -union{ -#if (AxisLenX != 0) - object { Axis_(AxisLenX, Tex_Dark, Tex_Light) rotate< 0,0,-90>}// x-Axis -#end // of #if -#if (AxisLenY != 0) - object { Axis_(AxisLenY, Tex_Dark, Tex_Light) rotate< 0,0, 0>}// y-Axis -#end // of #if -#if (AxisLenZ != 0) - object { Axis_(AxisLenZ, Tex_Dark, Tex_Light) rotate<90,0, 0>}// z-Axis -#end // of #if -} // end of union -#end// of macro "AxisXYZ( ... )" -//------------------------------------------------------------------------ - -#declare Texture_A_Dark = texture { - pigment{ color rgb<1,0.4,0>} - finish { phong 1} - } -#declare Texture_A_Light = texture { - pigment{ color rgb<1,1,1>} - finish { phong 1} - } - -object{ AxisXYZ( 2.70, 2.70, 2.70, Texture_A_Dark, Texture_A_Light) scale 0.25 } -//-------------------------------------------------- end of coordinate axes - - -// ground ----------------------------------------------------------------- -//---------------------------------<<< settings of squared plane dimensions -#declare RasterScale = 0.50; -#declare RasterHalfLine = 0.03; -#declare RasterHalfLineZ = 0.03; -//------------------------------------------------------------------------- -#macro Raster(RScale, HLine) - pigment{ gradient x scale RScale - color_map{[0.000 color rgbt<1,1,1,0>*0.6] - [0+HLine color rgbt<1,1,1,0>*0.6] - [0+HLine color rgbt<1,1,1,1>] - [1-HLine color rgbt<1,1,1,1>] - [1-HLine color rgbt<1,1,1,0>*0.6] - [1.000 color rgbt<1,1,1,0>*0.6]} } -#end// of Raster(RScale, HLine)-macro -//------------------------------------------------------------------------- - -plane { <0,1,0>, 0 // plane with layered textures - texture { pigment{color White*1.1} - finish {ambient 0.45 diffuse 0.85} - } - texture { Raster(RasterScale,RasterHalfLine ) rotate<0,0,0> } - texture { Raster(RasterScale,RasterHalfLineZ) rotate<0,90,0>} - rotate<0,0,0> - } -//------------------------------------------------ end of squared plane XZ From 7677fd0cae5f6bd064074169cf51772dc98dd708 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 26 Oct 2025 00:00:18 -0500 Subject: [PATCH 17/85] mypy: remove self return for .using --- elastica/modules/callbacks.py | 4 +--- elastica/modules/connections.py | 4 +--- elastica/modules/constraints.py | 4 +--- elastica/modules/contact.py | 4 +--- elastica/modules/damping.py | 4 +--- elastica/modules/forcing.py | 4 +--- elastica/modules/protocol.py | 3 +-- 7 files changed, 7 insertions(+), 20 deletions(-) diff --git a/elastica/modules/callbacks.py b/elastica/modules/callbacks.py index 770b1f29..f390e4a6 100644 --- a/elastica/modules/callbacks.py +++ b/elastica/modules/callbacks.py @@ -8,7 +8,6 @@ """ from types import EllipsisType from typing import Type, Any, TypeAlias, cast -from typing_extensions import Self # 3.11: from typing import Self from elastica.typing import ( SystemType, SystemIdxType, @@ -157,7 +156,7 @@ def using( cls: Type[CallBackBaseClass], *args: Any, **kwargs: Any, - ) -> Self: + ) -> None: """ This method is a module to set which callback class is used to collect data from user defined rod-like object. @@ -179,7 +178,6 @@ def using( self._callback_cls = cls self._args = args self._kwargs = kwargs - return self def id(self) -> SystemIdxDSType: return self._sys_idx diff --git a/elastica/modules/connections.py b/elastica/modules/connections.py index be10ec88..e4a8e2c1 100644 --- a/elastica/modules/connections.py +++ b/elastica/modules/connections.py @@ -6,7 +6,6 @@ rigid bodies) using joints (see `joints.py`). """ from typing import Type, cast, Any -from typing_extensions import Self from elastica.typing import ( SystemIdxType, OperatorFinalizeType, @@ -243,7 +242,7 @@ def using( cls: Type[FreeJoint], *args: Any, **kwargs: Any, - ) -> Self: + ) -> None: """ This method is a module to set which joint class is used to connect user defined rod-like objects. @@ -269,7 +268,6 @@ def using( self._connect_cls = cls self._args = args self._kwargs = kwargs - return self def id( self, diff --git a/elastica/modules/constraints.py b/elastica/modules/constraints.py index 3686a135..9664b757 100644 --- a/elastica/modules/constraints.py +++ b/elastica/modules/constraints.py @@ -5,7 +5,6 @@ Provides the constraints interface to enforce displacement boundary conditions (see `boundary_conditions.py`). """ from typing import Any, Type, cast -from typing_extensions import Self import functools @@ -166,7 +165,7 @@ def using( constrained_position_idx: ConstrainingIndex = (), constrained_director_idx: ConstrainingIndex = (), **kwargs: Any, - ) -> Self: + ) -> None: """ This method is a module to set which boundary condition class is used to enforce boundary condition from user defined rod-like objects. @@ -194,7 +193,6 @@ def using( self.constrained_director_idx = constrained_director_idx self._args = args self._kwargs = kwargs - return self def id(self) -> SystemIdxType: return self._sys_idx diff --git a/elastica/modules/contact.py b/elastica/modules/contact.py index 81fdd270..15f6735b 100644 --- a/elastica/modules/contact.py +++ b/elastica/modules/contact.py @@ -6,7 +6,6 @@ (rods, rigid bodies, surfaces). """ from typing import Type, Any -from typing_extensions import Self import functools from elastica.typing import ( @@ -137,7 +136,7 @@ def __init__( self._args: Any self._kwargs: Any - def using(self, cls: Type[NoContact], *args: Any, **kwargs: Any) -> Self: + def using(self, cls: Type[NoContact], *args: Any, **kwargs: Any) -> None: """ This method is a module to set which contact class is used to apply contact between user defined rod-like objects. @@ -163,7 +162,6 @@ def using(self, cls: Type[NoContact], *args: Any, **kwargs: Any) -> Self: self._contact_cls = cls self._args = args self._kwargs = kwargs - return self def id(self) -> Any: return ( diff --git a/elastica/modules/damping.py b/elastica/modules/damping.py index 503168b6..9d39c12b 100644 --- a/elastica/modules/damping.py +++ b/elastica/modules/damping.py @@ -10,7 +10,6 @@ """ from typing import Any, Type, List -from typing_extensions import Self import functools @@ -114,7 +113,7 @@ def __init__(self, sys_idx: SystemIdxType) -> None: self._args: Any self._kwargs: Any - def using(self, cls: Type[DamperBase], *args: Any, **kwargs: Any) -> Self: + def using(self, cls: Type[DamperBase], *args: Any, **kwargs: Any) -> None: """ This method is a module to set which damper class is used to enforce damping from user defined rod-like objects. @@ -140,7 +139,6 @@ def using(self, cls: Type[DamperBase], *args: Any, **kwargs: Any) -> Self: self._damper_cls = cls self._args = args self._kwargs = kwargs - return self def id(self) -> SystemIdxType: return self._sys_idx diff --git a/elastica/modules/forcing.py b/elastica/modules/forcing.py index b4c9fe9d..da7855af 100644 --- a/elastica/modules/forcing.py +++ b/elastica/modules/forcing.py @@ -8,7 +8,6 @@ import logging import functools from typing import Any, Type, List -from typing_extensions import Self import numpy as np @@ -112,7 +111,7 @@ def __init__(self, sys_idx: SystemIdxType) -> None: self._args: Any self._kwargs: Any - def using(self, cls: Type[NoForces], *args: Any, **kwargs: Any) -> Self: + def using(self, cls: Type[NoForces], *args: Any, **kwargs: Any) -> None: """ This method sets which forcing class is used to apply forcing to user defined rod-like objects. @@ -138,7 +137,6 @@ def using(self, cls: Type[NoForces], *args: Any, **kwargs: Any) -> Self: self._forcing_cls = cls self._args = args self._kwargs = kwargs - return self def id(self) -> SystemIdxType: return self._sys_idx diff --git a/elastica/modules/protocol.py b/elastica/modules/protocol.py index e17b23ca..116dd755 100644 --- a/elastica/modules/protocol.py +++ b/elastica/modules/protocol.py @@ -1,6 +1,5 @@ from typing import Protocol, Generator, TypeVar, Any, Type, overload, Iterator, Callable from typing import TYPE_CHECKING -from typing_extensions import Self # python 3.11: from typing import Self from elastica.typing import ( SystemIdxType, @@ -34,7 +33,7 @@ class MixinProtocol(Protocol): class ModuleProtocol(Protocol[M]): - def using(self, cls: Type[M], *args: Any, **kwargs: Any) -> Self: ... + def using(self, cls: Type[M], *args: Any, **kwargs: Any) -> None: ... def instantiate(self, *args: Any, **kwargs: Any) -> M: ... From c38752b18a6afe5bd98c104d0ea7b2e1900f3cc5 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 26 Oct 2025 00:02:16 -0500 Subject: [PATCH 18/85] example: timoshenko in gallery --- examples/TimoshenkoBeamCase/README.md | 0 examples/TimoshenkoBeamCase/run_timoshenko.py | 226 ++++++++++++++++++ examples/TimoshenkoBeamCase/timoshenko.py | 166 ------------- 3 files changed, 226 insertions(+), 166 deletions(-) create mode 100644 examples/TimoshenkoBeamCase/README.md create mode 100644 examples/TimoshenkoBeamCase/run_timoshenko.py delete mode 100644 examples/TimoshenkoBeamCase/timoshenko.py diff --git a/examples/TimoshenkoBeamCase/README.md b/examples/TimoshenkoBeamCase/README.md new file mode 100644 index 00000000..e69de29b diff --git a/examples/TimoshenkoBeamCase/run_timoshenko.py b/examples/TimoshenkoBeamCase/run_timoshenko.py new file mode 100644 index 00000000..bf8f080c --- /dev/null +++ b/examples/TimoshenkoBeamCase/run_timoshenko.py @@ -0,0 +1,226 @@ +""" +Timoshenko Beam +=============== + +Timoshenko beam validation case, for detailed explanation refer to +Gazzola et. al. R. Soc. 2018 section 3.4.3 + +This Elastica tutorial explains the basics of setting up and running a simple simulation of rods in Elastica. Elastica simulates Cosserat Rods, which are thin, 1-dimensional rods that undergo all possible modes of deformation. This example considers a Timoshenko beam, which is the deformation of a beam under a constant applied force while accounting for shear deforation and rotational bending. This is a good example of the capabilities of Elastica and Cosserat Rods as it requires accounting for the effects of shear deformation, something that the classical Euler-Bernoulli beam solution does not. + +.. image:: ../../../assets/timoshenko_beam_figure.png + +Getting Started +--------------- + +To set up the simulation, the first thing you need to do is import the necessary classes. Here we will only import the classes that we need. The `elastica.modules` classes make it easy to construct different simulation systems. Along with these modules, we need to import a rod class, classes for the boundary conditions, and time-stepping functions. As a note, this method of explicitly importing all classes can be a bit cumbersome. Future releases will simplify this step. +""" + +import numpy as np +import elastica as ea +from elastica.version import VERSION + +from timoshenko_postprocessing import plot_timoshenko + +# %% +# Now that we have imported all the necessary classes, we want to create our beam system. We do this by combining all the modules we need to represent the physics that we to include in the simulation. In this case, that is the `BaseSystemCollection`, `Constraint`, `Forcings` and `Damping` because the simulation will consider a rod that is fixed in place on one end, and subject to an applied force on the other end. + + +class TimoshenkoBeamSimulator( + ea.BaseSystemCollection, ea.Constraints, ea.Forcing, ea.CallBacks, ea.Damping +): + pass + + +timoshenko_sim = TimoshenkoBeamSimulator() + +# %% +# Creating Rods +# ------------- +# With our simulator set up, we can now define the numerical, material, and geometric properties. +# +# First we define the number of elements in the rod. Next, the material properties are defined for every rod. These are the Young's modulus, the Poisson ratio, the density and the viscous damping coefficient. Finally, the geometry of the rod also needs to be defined by specifying the location of the rod and its orientation, length and radius. +# +# All of the values defined here are done in SI units, though this is not strictly necessary. You can rescale properties however you want, as long as you use consistent units throughout the simulation. See [here](https://info.simuleon.com/blog/units-in-abaqus) for an example of consistent units. +# +# In order to make the difference between a shearable and unshearable rod more clear, we are using a Poisson ratio of 99. This is an unphysical value, as Poisson ratios can not exceed 0.5, however, it is used here for demonstration purposes. + +# setting up test params +simulation_time = 5 # 5000.0 # (sec) + +n_elem = 100 +start = np.zeros((3,)) +direction = np.array([0.0, 0.0, 1.0]) +normal = np.array([0.0, 1.0, 0.0]) +base_length = 3.0 +base_radius = 0.25 +base_area = np.pi * base_radius**2 +density = 5000 +nu = 0.1 / 7 / density / base_area +E = 1e6 +# For shear modulus of 1e4, nu is 99! +poisson_ratio = 99 +shear_modulus = E / (poisson_ratio + 1.0) + +# %% +# With all of the rod's parameters set, we can now create a rod with the specificed properties and add the rod to the simulator system. **Important:** Make sure that any rods you create get added to the simulator system (`timoshenko_sim`), otherwise they will not be included in your simulation. + +shearable_rod = ea.CosseratRod.straight_rod( + n_elem, + start, + direction, + normal, + base_length, + base_radius, + density, + youngs_modulus=E, + shear_modulus=shear_modulus, +) +timoshenko_sim.append(shearable_rod) + +# %% +# Adding Damping +# -------------- +# With the rod added to the simulator, we can add damping to the rod. We do this using the `.dampen()` option and the `AnalyticalLinearDamper`. We are modifying `timoshenko_sim` simulator to `dampen` the `shearable_rod` object using `AnalyticalLinearDamper` type of dissipation (damping) model. +# +# We also need to define `damping_constant` and simulation `time_step` and pass in `.using()` method. + +dl = base_length / n_elem +dt = 0.07 * dl +timoshenko_sim.dampen(shearable_rod).using( + ea.AnalyticalLinearDamper, + damping_constant=nu, + time_step=dt, +) + +# %% +# Adding Boundary Conditions +# -------------------------- +# With the rod added to the system, we need to apply boundary conditions. The first condition we will apply is fixing the location of one end of the rod. We do this using the `.constrain()` option and the `OneEndFixedRod` boundary condition. We are modifying the `timoshenko_sim` simulator to `constrain` the `shearable_rod` object using the `OneEndFixedRod` type of constraint. +# +# We also need to define which node of the rod is being constrained. We do this by passing the index of the nodes that we want to constain to `constrained_position_idx`. Here we are fixing the first node in the rod. In order to keep the rod from rotating around the fixed node, we also need to constrain an element between two nodes. This fixes the orientation of the rod. We do this by passing the index of the element that we want to fix to `constrained_director_idx`. Like with the position, we are fixing the first element of the rod. Together, this contrains the position and orientation of the rod at the origin. + +# One end of the rod is now fixed in place +timoshenko_sim.constrain(shearable_rod).using( + ea.OneEndFixedBC, constrained_position_idx=(0,), constrained_director_idx=(0,) +) + +# %% +# The next boundary condition that we want to apply is the endpoint force. Similarly to how we constrained one of the points, we want the `timoshenko_sim` simulator to `add_forcing_to` the `shearable_rod` object using the `EndpointForces` type of forcing. This `EndpointForces` applies forces to both ends of the rod. We want to apply a negative force in the $d_1$ direction, but only at the end of the rod. We do this by specifying the force vector to be applied at each end as `origin_force` and `end_force`. We also want to ramp up the force over time, so we make the force take some `ramp_up_time` to reach its steady-state value. This helps avoid numerical errors due to discontinuities in the applied force. + +# Forces added to the rod +end_force = np.array([-15.0, 0.0, 0.0]) +timoshenko_sim.add_forcing_to(shearable_rod).using( + ea.EndpointForces, 0.0 * end_force, end_force, ramp_up_time=simulation_time / 2.0 +) + +# %% +# Add Unshearable Rod +# ------------------- +# +# Along with the shearable rod, we also want to add an unshearable rod to be able to compare the difference between the two. We do this the same way we did for the first rod, however, because this rod is unsherable, we need to change the Poisson ratio to make the rod unsherable. For a truely unsheraable rod, you would need a Poisson ratio of -1.0, however, this causes the system to be numerically unstable, so instead we make the system nearly unshearable by using a Poisson ratio of -0.85. + +# Start into the plane +unshearable_start = np.array([0.0, -1.0, 0.0]) +shear_modulus = E / (-0.7 + 1.0) +unshearable_rod = ea.CosseratRod.straight_rod( + n_elem, + unshearable_start, + direction, + normal, + base_length, + base_radius, + density, + youngs_modulus=E, + # Unshearable rod needs G -> inf, which is achievable with -ve poisson ratio + shear_modulus=shear_modulus, +) + +timoshenko_sim.append(unshearable_rod) + +# add damping +timoshenko_sim.dampen(unshearable_rod).using( + ea.AnalyticalLinearDamper, + damping_constant=nu, + time_step=dt, +) +# add boundary conditions +timoshenko_sim.constrain(unshearable_rod).using( + ea.OneEndFixedBC, constrained_position_idx=(0,), constrained_director_idx=(0,) +) +timoshenko_sim.add_forcing_to(unshearable_rod).using( + ea.EndpointForces, 0.0 * end_force, end_force, ramp_up_time=simulation_time / 2.0 +) + +# %% +# Collect Data +# ------------ + + +# Add call backs +class VelocityCallBack(ea.CallBackBaseClass): + """ + Tracks the velocity norms of the rod + """ + + def __init__(self, step_skip: int, callback_params: dict): + ea.CallBackBaseClass.__init__(self) + self.every = step_skip + self.callback_params = callback_params + + def make_callback(self, system, time, current_step: int): + + if current_step % self.every == 0: + + self.callback_params["time"].append(time) + # Collect x + self.callback_params["velocity_norms"].append( + np.linalg.norm(system.velocity_collection.copy()) + ) + return + + +recorded_history = ea.defaultdict(list) +timoshenko_sim.collect_diagnostics(shearable_rod).using( + VelocityCallBack, step_skip=500, callback_params=recorded_history +) + + +# %% +# System Finalization +# ------------------- +# We have now added all the necessary rods and boundary conditions to our system. The last thing we need to do is finalize the system. This goes through the system, rearranges things, and precomputes useful quantities to prepare the system for simulation. +# +# As a note, if you make any changes to the rod after calling finalize, you will need to re-setup the system. This requires rerunning all cells above this point. + + +timoshenko_sim.finalize() + +# %% +# Define Simulation Time +# ---------------------- +# The last thing we need to do deceide how long we want the simulation to run for and what timestepping method to use. Currently, the PositionVerlet algorithim is suggested default method. +# +# In this example, we are trying to match a steady-state solution by temporally evolving our system to reach equillibrium. As such, there is a tradeoff between letting the simulation run long enough to each the equillibrium and waiting around for the simulation to be done. Here we are running the simulation for 10 seconds, this produces reasonable agreement with the analytical solution without taking to long to finish. If you run the simulation for longer, you will get better agreement with the analytical solution. + +timestepper = ea.PositionVerlet() +# timestepper = PEFRL() + +total_steps = int(simulation_time / dt) +print("Total steps", total_steps) + +# %% +# Run Simulation +# -------------- +# +# We are now ready to perform the simulation. To run the simulation, we `integrate` the `timoshenko_sim` system using the `timestepper` method until `final_time` by taking `total_steps`. As currently setup, the beam simulation takes about 1 minute to run. + +time = 0.0 +for i in range(total_steps): + time = timestepper.step(timoshenko_sim, time, dt) + +# %% +# Post Processing Results +# ----------------------- +# Now that we have finished the simulation, we want to post-process the results. Post processing script is separately provided in `timoshenko_postprocessing.py`. + +plot_timoshenko(shearable_rod, end_force, False, True) diff --git a/examples/TimoshenkoBeamCase/timoshenko.py b/examples/TimoshenkoBeamCase/timoshenko.py deleted file mode 100644 index b473e917..00000000 --- a/examples/TimoshenkoBeamCase/timoshenko.py +++ /dev/null @@ -1,166 +0,0 @@ -__doc__ = """Timoshenko beam validation case, for detailed explanation refer to -Gazzola et. al. R. Soc. 2018 section 3.4.3 """ - -import numpy as np -import elastica as ea -from examples.TimoshenkoBeamCase.timoshenko_postprocessing import plot_timoshenko - - -class TimoshenkoBeamSimulator( - ea.BaseSystemCollection, ea.Constraints, ea.Forcing, ea.CallBacks, ea.Damping -): - pass - - -timoshenko_sim = TimoshenkoBeamSimulator() -final_time = 5000.0 - -# Options -PLOT_FIGURE = True -SAVE_FIGURE = True -SAVE_RESULTS = False -ADD_UNSHEARABLE_ROD = False - -# setting up test params -n_elem = 100 -start = np.zeros((3,)) -direction = np.array([0.0, 0.0, 1.0]) -normal = np.array([0.0, 1.0, 0.0]) -base_length = 3.0 -base_radius = 0.25 -base_area = np.pi * base_radius**2 -density = 5000 -nu = 0.1 / 7 / density / base_area -E = 1e6 -# For shear modulus of 1e4, nu is 99! -poisson_ratio = 99 -shear_modulus = E / (poisson_ratio + 1.0) - -shearable_rod = ea.CosseratRod.straight_rod( - n_elem, - start, - direction, - normal, - base_length, - base_radius, - density, - youngs_modulus=E, - shear_modulus=shear_modulus, -) - -timoshenko_sim.append(shearable_rod) -# add damping -dl = base_length / n_elem -dt = 0.07 * dl -timoshenko_sim.dampen(shearable_rod).using( - ea.AnalyticalLinearDamper, - damping_constant=nu, - time_step=dt, -) - -timoshenko_sim.constrain(shearable_rod).using( - ea.OneEndFixedBC, constrained_position_idx=(0,), constrained_director_idx=(0,) -) - -end_force = np.array([-15.0, 0.0, 0.0]) -timoshenko_sim.add_forcing_to(shearable_rod).using( - ea.EndpointForces, 0.0 * end_force, end_force, ramp_up_time=final_time / 2.0 -) - - -if ADD_UNSHEARABLE_ROD: - # Start into the plane - unshearable_start = np.array([0.0, -1.0, 0.0]) - shear_modulus = E / (-0.7 + 1.0) - unshearable_rod = ea.CosseratRod.straight_rod( - n_elem, - unshearable_start, - direction, - normal, - base_length, - base_radius, - density, - youngs_modulus=E, - # Unshearable rod needs G -> inf, which is achievable with -ve poisson ratio - shear_modulus=shear_modulus, - ) - - timoshenko_sim.append(unshearable_rod) - - # add damping - timoshenko_sim.dampen(unshearable_rod).using( - ea.AnalyticalLinearDamper, - damping_constant=nu, - time_step=dt, - ) - timoshenko_sim.constrain(unshearable_rod).using( - ea.OneEndFixedBC, constrained_position_idx=(0,), constrained_director_idx=(0,) - ) - timoshenko_sim.add_forcing_to(unshearable_rod).using( - ea.EndpointForces, 0.0 * end_force, end_force, ramp_up_time=final_time / 2.0 - ) - - -# Add call backs -class VelocityCallBack(ea.CallBackBaseClass): - """ - Tracks the velocity norms of the rod - """ - - def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) - self.every = step_skip - self.callback_params = callback_params - - def make_callback(self, system, time, current_step: int): - - if current_step % self.every == 0: - - self.callback_params["time"].append(time) - # Collect x - self.callback_params["velocity_norms"].append( - np.linalg.norm(system.velocity_collection.copy()) - ) - return - - -recorded_history = ea.defaultdict(list) -timoshenko_sim.collect_diagnostics(shearable_rod).using( - VelocityCallBack, step_skip=500, callback_params=recorded_history -) - -timoshenko_sim.finalize() -timestepper = ea.PositionVerlet() -# timestepper = PEFRL() - -total_steps = int(final_time / dt) -print("Total steps", total_steps) -dt = final_time / total_steps -time = 0.0 -for i in range(total_steps): - time = timestepper.step(timoshenko_sim, time, dt) - -if PLOT_FIGURE: - plot_timoshenko(shearable_rod, end_force, SAVE_FIGURE, ADD_UNSHEARABLE_ROD) - -if SAVE_RESULTS: - import pickle - - filename = "Timoshenko_beam_data.dat" - file = open(filename, "wb") - pickle.dump(shearable_rod, file) - file.close() - - tv = ( - np.asarray(recorded_history["time"]), - np.asarray(recorded_history["velocity_norms"]), - ) - - def as_time_series(v): - return v.T - - np.savetxt( - "velocity_norms.csv", - as_time_series(np.stack(tv)), - delimiter=",", - ) From b892ca49b59c9324833e8b514af0a60761a7c2e3 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 26 Oct 2025 01:24:31 -0500 Subject: [PATCH 19/85] fix indentation --- examples/README.md | 40 +++++++++++++++++----------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/examples/README.md b/examples/README.md index 548b0418..97d2527a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,13 +4,6 @@ This directory contains number of examples of elastica. Each [example cases](#example-cases) are stored in separate subdirectories, containing case descriptions, run file, and all other data/script necessary to run. More [advanced cases](#advanced-cases) are stored in separate repository with its description. -## Installing Requirements -In order to run examples, you will need to install additional dependencies. - -```bash -make install_examples_dependencies -``` - ## Case Examples Some examples provide additional files or links to published paper for a complete description. @@ -56,8 +49,8 @@ Examples can serve as a starting template for customized usages. * __Purpose__: Demonstrate usage of rigid body on simulation. * __Features__: Cylinder, Sphere * [RodRigidBodyContact](./RigidBodyCases/RodRigidBodyContact) - * __Purpose__: Demonstrate contact between cylinder and rod, for different intial conditions. - * __Features__: Cylinder, CosseratRods, RodCylinderContact + * __Purpose__: Demonstrate contact between cylinder and rod, for different intial conditions. + * __Features__: Cylinder, CosseratRods, RodCylinderContact * [HelicalBucklingCase](./HelicalBucklingCase) * __Purpose__: Demonstrate helical buckling with extreme twisting boundary condition. * __Features__: HelicalBucklingBC @@ -68,16 +61,16 @@ Examples can serve as a starting template for customized usages. * __Purpose__: Example of customizing [Joint module](./MuscularFlagella/connection_flagella.py) and [Force module](./MuscularFlagella/muscle_forces_flagella.py) to implement muscular flagella. * __Features__: MuscleForces(custom implemented) * [RodContactCase](./RodContactCase) - * [RodRodContact](./RodContactCase/RodRodContact) - * __Purpose__: Demonstrates contact between two rods, for different initial conditions. - * __Features__: CosseratRod, RodRodContact - * [RodSelfContact](./RodContactCase/RodSelfContact) - * [PlectonemesCase](./RodContactCase/RodSelfContact/PlectonemesCase) - * __Purpose__: Demonstrates rod self contact with Plectoneme example, and how to use link-writhe-twist after simulation completed. - * __Features__: CosseratRod, SelonoidsBC, RodSelfContact, Link-Writhe-Twist - * [SolenoidsCase](./RodContactCase/RodSelfContact/SolenoidsCase) - * __Purpose__: Demonstrates rod self contact with Solenoid example, and how to use link-writhe-twist after simulation completed. - * __Features__: CosseratRod, SelonoidsBC, RodSelfContact, Link-Writhe-Twist + * [RodRodContact](./RodContactCase/RodRodContact) + * __Purpose__: Demonstrates contact between two rods, for different initial conditions. + * __Features__: CosseratRod, RodRodContact + * [RodSelfContact](./RodContactCase/RodSelfContact) + * [PlectonemesCase](./RodContactCase/RodSelfContact/PlectonemesCase) + * __Purpose__: Demonstrates rod self contact with Plectoneme example, and how to use link-writhe-twist after simulation completed. + * __Features__: CosseratRod, SelonoidsBC, RodSelfContact, Link-Writhe-Twist + * [SolenoidsCase](./RodContactCase/RodSelfContact/SolenoidsCase) + * __Purpose__: Demonstrates rod self contact with Solenoid example, and how to use link-writhe-twist after simulation completed. + * __Features__: CosseratRod, SelonoidsBC, RodSelfContact, Link-Writhe-Twist * [BoundaryConditionsCases](./BoundaryConditionsCases) * __Purpose__: Demonstrate the usage of boundary conditions for constraining the movement of the system. * __Features__: GeneralConstraint, CosseratRod @@ -97,8 +90,8 @@ Examples can serve as a starting template for customized usages. ## Functional Examples * [RestartExample](./RestartExample) - * __Purpose__: Demonstrate the usage of restart module. - * __Features__: save_state, load_state + * __Purpose__: Demonstrate the usage of restart module. + * __Features__: save_state, load_state ## Advanced Cases @@ -106,6 +99,7 @@ Examples can serve as a starting template for customized usages. * [Gym Softrobot](https://github.com/skim0119/gym-softrobot) - Soft-robot control environment developed in OpenAI-gym format to study slender body control with reinforcement learning. ## Experimental Cases + * [ParallelConnectionExample](./ExperimentalCases/ParallelConnectionExample) - * __Purpose__: Demonstrate the usage of parallel connection. - * __Features__: connect two parallel rods + * __Purpose__: Demonstrate the usage of parallel connection. + * __Features__: connect two parallel rods From 31162a686598ea2d957673992543fd7f3ece17d8 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 26 Oct 2025 01:36:47 -0500 Subject: [PATCH 20/85] docs: add sphinx gallery setup and configuration --- .gitignore | 3 ++ docs/conf.py | 80 ++++++++++++++++++++++++------------ docs/guide/binder.md | 16 -------- docs/guide/example_cases.rst | 31 -------------- docs/guide/visualization.md | 2 +- docs/index.rst | 12 +++--- pyproject.toml | 2 + uv.lock | 31 ++++++++++++++ 8 files changed, 97 insertions(+), 80 deletions(-) delete mode 100644 docs/guide/binder.md delete mode 100644 docs/guide/example_cases.rst diff --git a/.gitignore b/.gitignore index 91f44a17..61a79d21 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ __pycache__/ *.swp .vscode +docs/_gallery +docs/gen_modules +docs/sg_execution_times.rst # Distribution / packaging .Python diff --git a/docs/conf.py b/docs/conf.py index 0a3db538..3cde77d2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,19 +13,20 @@ import os import sys +import datetime -sys.path.insert(0, os.path.abspath('../')) +sys.path.insert(0, os.path.abspath("../")) from elastica.version import VERSION # -- Project information ----------------------------------------------------- -project = 'PyElastica' -copyright = '2025, Gazzola Lab' -author = 'Gazzola Lab' +YEAR = datetime.datetime.now().year -# The full version, including alpha/beta/rc tags +project = "PyElastica" +copyright = f"{YEAR}, Gazzola Lab" +author = "Gazzola Lab" release = VERSION @@ -35,17 +36,40 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosectionlabel', - 'sphinx_autodoc_typehints', + "sphinx.ext.autodoc", + "sphinx.ext.autosectionlabel", + "sphinx_autodoc_typehints", #'sphinx.ext.napoleon', - 'sphinx.ext.viewcode', - 'sphinx.ext.mathjax', + "sphinx.ext.viewcode", + "sphinx.ext.mathjax", + "sphinxcontrib.video", "sphinxcontrib.mermaid", - 'numpydoc', - 'myst_parser', + "numpydoc", + "myst_parser", + "sphinx_gallery.gen_gallery", ] +# To add example in gallery, +# 1. run script should start with GALLERY_KEY +# 2. Some README(.rst/.md/.txt) file should be in the directory +GALLERY_KEY = "run" +sphinx_gallery_conf = { + "examples_dirs": "../examples", + "subsection_order": [ + "../examples/TimoshenkoBeamCase", + "*", + "../examples/ContinuumSnakeCase", + ], + "gallery_dirs": "_gallery", + "backreferences_dir": "gen_modules/backreferences", + "example_extensions": ".py", + "ignore_pattern": rf"^(?!.*{GALLERY_KEY})[^/\\]+\.py$", + "filename_pattern": f"/{GALLERY_KEY}_.*", + # 'nested_sections': True, + "first_notebook_cell": ("# PyElastica installation\n" "# !pip install pyelastica"), + # "parallel": 2, +} + myst_enable_extensions = [ "amsmath", "colon_fence", @@ -57,48 +81,52 @@ ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [ - "README.md", # File reserved to explain how documentationing works. - ] + "README.md", # File reserved to explain how documentationing works. +] -autodoc_default_flags = ['members', 'private-members', 'special-members', 'show-inheritance'] +autodoc_default_flags = [ + "members", + "private-members", + "special-members", + "show-inheritance", +] autosectionlabel_prefix_document = True -source_parsers = { -} -source_suffix = ['.rst', '.md'] +source_parsers = {} +source_suffix = [".rst", ".md"] -master_doc = 'index' +master_doc = "index" # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinx_book_theme' +html_theme = "sphinx_book_theme" html_theme_options = { "repository_url": "https://github.com/GazzolaLab/PyElastica", "use_repository_button": True, } html_title = "PyElastica" html_logo = "_static/assets/Logo.png" -#pygments_style = "sphinx" +# pygments_style = "sphinx" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] -html_css_files = ['css/*', 'css/logo.css'] +html_static_path = ["_static"] +html_css_files = ["css/*", "css/logo.css"] # -- Options for autodoc --------------------------------------------------- -autodoc_member_order = 'bysource' +autodoc_member_order = "bysource" # -- Options for numpydoc --------------------------------------------------- numpydoc_show_class_members = False # -- Mermaid configuration --------------------------------------------------- -mermaid_params = ['--theme', 'neutral'] +mermaid_params = ["--theme", "neutral"] diff --git a/docs/guide/binder.md b/docs/guide/binder.md deleted file mode 100644 index 54393f0d..00000000 --- a/docs/guide/binder.md +++ /dev/null @@ -1,16 +0,0 @@ -# Binder Tutorials - - - -We have created several Jupyter notebooks and Python scripts to help get users started with using PyElastica. The Jupyter notebooks are available on Binder, allowing you to try out some of the tutorials without having to install PyElastica. - -[![](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/GazzolaLab/PyElastica/master?filepath=examples%2FBinder%2F0_PyElastica_Tutorials_Overview.ipynb) - -:::{note} -Additional examples are also available in the examples folder of PyElastica's [Github repo](https://github.com/GazzolaLab/PyElastica/tree/master/examples). -::: - diff --git a/docs/guide/example_cases.rst b/docs/guide/example_cases.rst deleted file mode 100644 index 4100a33f..00000000 --- a/docs/guide/example_cases.rst +++ /dev/null @@ -1,31 +0,0 @@ -************* -Example Cases -************* - -Example cases are the demonstration of physical example with known analytical solution or well-studied phenomenon. -Each cases follows the recommended workflow, shown :ref:`here `. Feel free to use them as an initial template to build your own case study. - -Axial Stretching -~~~~~~~~~~~~~~~~ -.. literalinclude:: ../../examples/AxialStretchingCase/axial_stretching.py - :linenos: - -Timoshenko -~~~~~~~~~~ -.. literalinclude:: ../../examples/TimoshenkoBeamCase/timoshenko.py - :linenos: - -Butterfly -~~~~~~~~~ -.. literalinclude:: ../../examples/ButterflyCase/butterfly.py - :linenos: - -Helical Buckling -~~~~~~~~~~~~~~~~ -.. literalinclude:: ../../examples/HelicalBucklingCase/helicalbuckling.py - :linenos: - -Continuum Snake -~~~~~~~~~~~~~~~ -.. literalinclude:: ../../examples/ContinuumSnakeCase/continuum_snake.py - :linenos: diff --git a/docs/guide/visualization.md b/docs/guide/visualization.md index 0710e6cc..af7b6871 100644 --- a/docs/guide/visualization.md +++ b/docs/guide/visualization.md @@ -6,7 +6,7 @@ If you wish to visualize your system, make sure you define your callback functio ## POVray -For high-quality visualization, we suggest [POVray](http://povray.com). See [this tutorial](https://github.com/GazzolaLab/PyElastica/tree/master/examples/Visualization) for examples of different ways of visualizing the system. +For high-quality visualization, we suggest [POVray](http://povray.com). ## Rhino diff --git a/docs/index.rst b/docs/index.rst index 59e639ad..27a20815 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -44,10 +44,14 @@ If you are interested to contribute, please read `contribution-guide`_ first. guide/workflow guide/discretization - guide/example_cases - guide/binder guide/visualization +.. toctree:: + :maxdepth: 3 + :caption: Case Examples + + _gallery/index + .. toctree:: :maxdepth: 2 :caption: API Documentation @@ -67,10 +71,6 @@ If you are interested to contribute, please read `contribution-guide`_ first. .. api/elastica++ -.. toctree:: - :maxdepth: 2 - :caption: Gallary - .. toctree:: :maxdepth: 2 :caption: Advanced Guide diff --git a/pyproject.toml b/pyproject.toml index 59fba5f2..bc467f98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,8 @@ docs = [ "myst-parser>=1.0", "numpydoc>=1.3.1", "docutils>=0.18", + "sphinx-gallery>=0.19.0", + "sphinxcontrib-video>=0.4.1", ] dev = [ "black", diff --git a/uv.lock b/uv.lock index 100ada42..6be9f1c9 100644 --- a/uv.lock +++ b/uv.lock @@ -1318,7 +1318,9 @@ docs = [ { name = "sphinx-autodoc-typehints", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx-autodoc-typehints", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "sphinx-book-theme" }, + { name = "sphinx-gallery" }, { name = "sphinxcontrib-mermaid" }, + { name = "sphinxcontrib-video" }, ] [package.metadata] @@ -1349,7 +1351,9 @@ requires-dist = [ { name = "sphinx", marker = "extra == 'docs'", specifier = ">=6.1" }, { name = "sphinx-autodoc-typehints", marker = "extra == 'docs'", specifier = ">=1.21" }, { name = "sphinx-book-theme", marker = "extra == 'docs'", specifier = ">=1.0" }, + { name = "sphinx-gallery", marker = "extra == 'docs'", specifier = ">=0.19.0" }, { name = "sphinxcontrib-mermaid", marker = "extra == 'docs'", specifier = ">=0.9.2" }, + { name = "sphinxcontrib-video", marker = "extra == 'docs'", specifier = ">=0.4.1" }, { name = "tqdm" }, ] @@ -1812,6 +1816,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/9e/c41d68be04eef5b6202b468e0f90faf0c469f3a03353f2a218fd78279710/sphinx_book_theme-1.1.4-py3-none-any.whl", hash = "sha256:843b3f5c8684640f4a2d01abd298beb66452d1b2394cd9ef5be5ebd5640ea0e1", size = 433952 }, ] +[[package]] +name = "sphinx-gallery" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pillow" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/e5/9ccd6ecd492043123adb465cba504217b9f0a82e2cb5b1d7249c648497c6/sphinx_gallery-0.19.0.tar.gz", hash = "sha256:8400cb5240ad642e28a612fdba0667f725d0505a9be0222d0243de60e8af2eb3", size = 471479 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c7/52b48aec16b26c52aba854d03a3a31e0681150301dac1bea2243645a69e7/sphinx_gallery-0.19.0-py3-none-any.whl", hash = "sha256:4c28751973f81769d5bbbf5e4ebaa0dc49dff8c48eb7f11131eb5f6e4aa25f0e", size = 455923 }, +] + [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" @@ -1880,6 +1898,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, ] +[[package]] +name = "sphinxcontrib-video" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/48/063e167b6e692bc84bbad74df30bcb27e460a7c620af7824729db8dba606/sphinxcontrib_video-0.4.1.tar.gz", hash = "sha256:75a033e71b7de124cc5902430b7ba818a1c6c377be6401d07e9f2329a95d5ca4", size = 11362 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/8b/a0271fe65357860ccc52168181891e9fc9d354bfdc9be273e6a77b84f905/sphinxcontrib_video-0.4.1-py3-none-any.whl", hash = "sha256:d63ec68983dac36960557973281a616b5d9e68838369763313fc80533b1ad774", size = 10066 }, +] + [[package]] name = "tomli" version = "2.2.1" From cbb955a2ff69f04664f87f805cebfc7906345094 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 26 Oct 2025 01:37:15 -0500 Subject: [PATCH 21/85] remove binder --- README.md | 10 +- .../0_PyElastica_Tutorials_Overview.ipynb | 68 -- examples/Binder/1_Timoshenko_Beam.ipynb | 767 ------------------ examples/Binder/2_Slithering_Snake.ipynb | 753 ----------------- 4 files changed, 1 insertion(+), 1597 deletions(-) delete mode 100644 examples/Binder/0_PyElastica_Tutorials_Overview.ipynb delete mode 100644 examples/Binder/1_Timoshenko_Beam.ipynb delete mode 100644 examples/Binder/2_Slithering_Snake.ipynb diff --git a/README.md b/README.md index e9d17339..6f8b68a8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

PyElastica

-[![CI][badge-CI]][link-CI] [![Documentation Status][badge-docs-status]][link-docs-status] [![codecov][badge-codecov]][link-codecov] [![Downloads][badge-pepy-download-count]][link-pepy-download-count] [![DOI][badge-doi]][link-doi] [![Binder][badge-binder]][link-binder] [![Gitter][badge-gitter]][link-gitter] +[![CI][badge-CI]][link-CI] [![Documentation Status][badge-docs-status]][link-docs-status] [![codecov][badge-codecov]][link-codecov] [![Downloads][badge-pepy-download-count]][link-pepy-download-count] [![DOI][badge-doi]][link-doi] [![Gitter][badge-gitter]][link-gitter]
PyElastica is the python implementation of **Elastica**: an *open-source* project for simulating assemblies of slender, one-dimensional structures using Cosserat Rod theory. @@ -84,11 +84,6 @@ We ask that any publications which use Elastica cite as following: - [Controlling a CyberOctopus soft arm with muscle-like actuation](https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=9683318) (UIUC, 2020) (IEEE CDC 2021) - [Energy shaping control of a CyberOctopus soft arm](https://ieeexplore.ieee.org/document/9304408) (UIUC, 2020) (IEEE CDC 2020) -## Tutorials -[![Binder][badge-binder-tutorial]][link-binder] - -We have created several Jupyter notebooks and Python scripts to help users get started with PyElastica. The Jupyter notebooks are available on Binder, allowing you to try out some of the tutorials without having to install PyElastica. - ## Contribution If you would like to participate, please read our [contribution guideline](CONTRIBUTING.md). Private development branches are moved to [elastica-python](https://github.com/GazzolaLab/elastica-python) repository; access is limited to the core developers, collaborators, and maintainers. @@ -120,7 +115,6 @@ _Names arranged alphabetically_ [badge-pypi]: https://badge.fury.io/py/pyelastica.svg [badge-CI]: https://github.com/GazzolaLab/PyElastica/workflows/CI/badge.svg [badge-docs-status]: https://readthedocs.org/projects/pyelastica/badge/?version=latest -[badge-binder]: https://mybinder.org/badge_logo.svg [badge-pepy-download-count]: https://pepy.tech/badge/pyelastica [badge-codecov]: https://codecov.io/gh/GazzolaLab/PyElastica/branch/master/graph/badge.svg [badge-gitter]: https://badges.gitter.im/PyElastica/community.svg @@ -131,7 +125,5 @@ _Names arranged alphabetically_ [link-pepy-download-count]: https://pepy.tech/project/pyelastica [link-codecov]: https://codecov.io/gh/GazzolaLab/PyElastica -[badge-binder-tutorial]: https://img.shields.io/badge/Launch-PyElastica%20Tutorials-579ACA.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFkAAABZCAMAAABi1XidAAAB8lBMVEX///9XmsrmZYH1olJXmsr1olJXmsrmZYH1olJXmsr1olJXmsrmZYH1olL1olJXmsr1olJXmsrmZYH1olL1olJXmsrmZYH1olJXmsr1olL1olJXmsrmZYH1olL1olJXmsrmZYH1olL1olL0nFf1olJXmsrmZYH1olJXmsq8dZb1olJXmsrmZYH1olJXmspXmspXmsr1olL1olJXmsrmZYH1olJXmsr1olL1olJXmsrmZYH1olL1olLeaIVXmsrmZYH1olL1olL1olJXmsrmZYH1olLna31Xmsr1olJXmsr1olJXmsrmZYH1olLqoVr1olJXmsr1olJXmsrmZYH1olL1olKkfaPobXvviGabgadXmsqThKuofKHmZ4Dobnr1olJXmsr1olJXmspXmsr1olJXmsrfZ4TuhWn1olL1olJXmsqBi7X1olJXmspZmslbmMhbmsdemsVfl8ZgmsNim8Jpk8F0m7R4m7F5nLB6jbh7jbiDirOEibOGnKaMhq+PnaCVg6qWg6qegKaff6WhnpKofKGtnomxeZy3noG6dZi+n3vCcpPDcpPGn3bLb4/Mb47UbIrVa4rYoGjdaIbeaIXhoWHmZYHobXvpcHjqdHXreHLroVrsfG/uhGnuh2bwj2Hxk17yl1vzmljzm1j0nlX1olL3AJXWAAAAbXRSTlMAEBAQHx8gICAuLjAwMDw9PUBAQEpQUFBXV1hgYGBkcHBwcXl8gICAgoiIkJCQlJicnJ2goKCmqK+wsLC4usDAwMjP0NDQ1NbW3Nzg4ODi5+3v8PDw8/T09PX29vb39/f5+fr7+/z8/Pz9/v7+zczCxgAABC5JREFUeAHN1ul3k0UUBvCb1CTVpmpaitAGSLSpSuKCLWpbTKNJFGlcSMAFF63iUmRccNG6gLbuxkXU66JAUef/9LSpmXnyLr3T5AO/rzl5zj137p136BISy44fKJXuGN/d19PUfYeO67Znqtf2KH33Id1psXoFdW30sPZ1sMvs2D060AHqws4FHeJojLZqnw53cmfvg+XR8mC0OEjuxrXEkX5ydeVJLVIlV0e10PXk5k7dYeHu7Cj1j+49uKg7uLU61tGLw1lq27ugQYlclHC4bgv7VQ+TAyj5Zc/UjsPvs1sd5cWryWObtvWT2EPa4rtnWW3JkpjggEpbOsPr7F7EyNewtpBIslA7p43HCsnwooXTEc3UmPmCNn5lrqTJxy6nRmcavGZVt/3Da2pD5NHvsOHJCrdc1G2r3DITpU7yic7w/7Rxnjc0kt5GC4djiv2Sz3Fb2iEZg41/ddsFDoyuYrIkmFehz0HR2thPgQqMyQYb2OtB0WxsZ3BeG3+wpRb1vzl2UYBog8FfGhttFKjtAclnZYrRo9ryG9uG/FZQU4AEg8ZE9LjGMzTmqKXPLnlWVnIlQQTvxJf8ip7VgjZjyVPrjw1te5otM7RmP7xm+sK2Gv9I8Gi++BRbEkR9EBw8zRUcKxwp73xkaLiqQb+kGduJTNHG72zcW9LoJgqQxpP3/Tj//c3yB0tqzaml05/+orHLksVO+95kX7/7qgJvnjlrfr2Ggsyx0eoy9uPzN5SPd86aXggOsEKW2Prz7du3VID3/tzs/sSRs2w7ovVHKtjrX2pd7ZMlTxAYfBAL9jiDwfLkq55Tm7ifhMlTGPyCAs7RFRhn47JnlcB9RM5T97ASuZXIcVNuUDIndpDbdsfrqsOppeXl5Y+XVKdjFCTh+zGaVuj0d9zy05PPK3QzBamxdwtTCrzyg/2Rvf2EstUjordGwa/kx9mSJLr8mLLtCW8HHGJc2R5hS219IiF6PnTusOqcMl57gm0Z8kanKMAQg0qSyuZfn7zItsbGyO9QlnxY0eCuD1XL2ys/MsrQhltE7Ug0uFOzufJFE2PxBo/YAx8XPPdDwWN0MrDRYIZF0mSMKCNHgaIVFoBbNoLJ7tEQDKxGF0kcLQimojCZopv0OkNOyWCCg9XMVAi7ARJzQdM2QUh0gmBozjc3Skg6dSBRqDGYSUOu66Zg+I2fNZs/M3/f/Grl/XnyF1Gw3VKCez0PN5IUfFLqvgUN4C0qNqYs5YhPL+aVZYDE4IpUk57oSFnJm4FyCqqOE0jhY2SMyLFoo56zyo6becOS5UVDdj7Vih0zp+tcMhwRpBeLyqtIjlJKAIZSbI8SGSF3k0pA3mR5tHuwPFoa7N7reoq2bqCsAk1HqCu5uvI1n6JuRXI+S1Mco54YmYTwcn6Aeic+kssXi8XpXC4V3t7/ADuTNKaQJdScAAAAAElFTkSuQmCC -[link-binder]: https://mybinder.org/v2/gh/GazzolaLab/PyElastica/master?filepath=examples%2FBinder%2F0_PyElastica_Tutorials_Overview.ipynb [link-gitter]: https://gitter.im/PyElastica/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge [link-doi]: https://zenodo.org/badge/latestdoi/254172891 diff --git a/examples/Binder/0_PyElastica_Tutorials_Overview.ipynb b/examples/Binder/0_PyElastica_Tutorials_Overview.ipynb deleted file mode 100644 index c7de2423..00000000 --- a/examples/Binder/0_PyElastica_Tutorials_Overview.ipynb +++ /dev/null @@ -1,68 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "# PyElastica Tutorials\n", - "\n", - "We have developed a number of different Jupyter notebook tutorials to explain how to use Elastica to simulate Cosserat rods in a number of different cases. Thanks to BinderHub, you can run these tutorials in directly in your web browser without needing to first download and install PyElastica. \n", - "\n", - "We suggest beginning with the Timoshenko beam tutorial available [here](./1_Timoshenko_Beam.ipynb). It walks through how to set up and simulate a very simple Cossert rod model and explains the basics of how to use Elastica. \n", - "\"timoshenko_beam_figure\"\n", - "\n", - "After this, for a tutorial covering more complicated use cases of a single Cosserat rods, check out the slithering snake tutorial, available [here](./2_Slithering_Snake.ipynb). This tutorial covers a possible use case of Cosserat rods and shows how to post-process the simulation to get quantitative data about the system as well as visualize the output. \n", - "\n", - "
\n", - " \n", - "
\n", - "\n", - "A list of all the available Jupyter notebook tutorials is [here](./). We are working to add more. If you think you have an interesting use case of Cosserat rods and Elastica and would like to showcase please make a pull request so we can add it! \n", - "\n", - "There are also a number of example Python scripts available [here](https://github.com/GazzolaLab/PyElastica/tree/master/examples) that cover convergence testing, parameter optimization and other more complex use cases. As a warning, these more complex cases take a much longer time to run. \n", - "\n", - "## More about PyElastica\n", - "If you want to learn more bout PyElastica and Cosserat rods, visit the [project website](https://cosseratrods.org). Or visit the [PyElastica GitHub repo](https://github.com/GazzolaLab/PyElastica).\n", - "\n", - "## PyElastica Documentation\n", - "Documentation of PyElastica is available online [here](https://docs.cosseratrods.org). There is also a getting started guide on the project website [here](https://cosseratrods.org/software/pyelastica).\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.7" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/Binder/1_Timoshenko_Beam.ipynb b/examples/Binder/1_Timoshenko_Beam.ipynb deleted file mode 100644 index d71d459a..00000000 --- a/examples/Binder/1_Timoshenko_Beam.ipynb +++ /dev/null @@ -1,767 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "# Timoshenko Beam Example\n", - "\n", - "This Elastica tutorial explains the basics of setting up and running a simple simulation of rods in Elastica. Elastica simulates Cosserat Rods, which are thin, 1-dimensional rods that undergo all possible modes of deformation. This example considers a Timoshenko beam, which is the deformation of a beam under a constant applied force while accounting for shear deforation and rotational bending. This is a good example of the capabilities of Elastica and Cosserat Rods as it requires accounting for the effects of shear deformation, something that the classical Euler-Bernoulli beam solution does not.\n", - "\n", - "![timoshenko_beam_figure.png](../../assets/timoshenko_beam_figure.png)\n", - "\n", - "## Getting Started\n", - "To set up the simulation, the first thing you need to do is import the necessary classes. Here we will only import the classes that we need. The `elastica.modules` classes make it easy to construct different simulation systems. Along with these modules, we need to import a rod class, classes for the boundary conditions, and time-stepping functions. As a note, this method of explicitly importing all classes can be a bit cumbersome. Future releases will simplify this step." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "!pip install pyelastica" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "# Import modules\n", - "from elastica.modules import BaseSystemCollection, Constraints, Forcing, Damping\n", - "\n", - "# Import Cosserat Rod Class\n", - "from elastica.rod.cosserat_rod import CosseratRod\n", - "\n", - "# Import Damping Class\n", - "from elastica.dissipation import AnalyticalLinearDamper\n", - "\n", - "# Import Boundary Condition Classes\n", - "from elastica.boundary_conditions import OneEndFixedRod, FreeRod\n", - "from elastica.external_forces import EndpointForces\n", - "\n", - "# Import Timestepping Functions\n", - "from elastica.timestepper.symplectic_steppers import PositionVerlet" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "Now that we have imported all the necessary classes, we want to create our beam system. We do this by combining all the modules we need to represent the physics that we to include in the simulation. In this case, that is the `BaseSystemCollection`, `Constraint`, `Forcings` and `Damping` because the simulation will consider a rod that is fixed in place on one end, and subject to an applied force on the other end." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "class TimoshenkoBeamSimulator(BaseSystemCollection, Constraints, Forcing, Damping):\n", - " pass\n", - "\n", - "\n", - "timoshenko_sim = TimoshenkoBeamSimulator()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Creating Rods\n", - "With our simulator set up, we can now define the numerical, material, and geometric properties. \n", - "\n", - "First we define the number of elements in the rod. Next, the material properties are defined for every rod. These are the Young's modulus, the Poisson ratio, the density and the viscous damping coefficient. Finally, the geometry of the rod also needs to be defined by specifying the location of the rod and its orientation, length and radius. \n", - "\n", - "All of the values defined here are done in SI units, though this is not strictly necessary. You can rescale properties however you want, as long as you use consistent units throughout the simulation. See [here](https://info.simuleon.com/blog/units-in-abaqus) for an example of consistent units.\n", - "\n", - "In order to make the difference between a shearable and unshearable rod more clear, we are using a Poisson ratio of 99. This is an unphysical value, as Poisson ratios can not exceed 0.5, however, it is used here for demonstration purposes. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# setting up test params\n", - "n_elem = 100\n", - "\n", - "density = 1000\n", - "nu = 1e-4\n", - "E = 1e6\n", - "# For shear modulus of 1e4, nu is 99!\n", - "poisson_ratio = 99\n", - "shear_modulus = E / (poisson_ratio + 1.0)\n", - "\n", - "start = np.zeros((3,))\n", - "direction = np.array([0.0, 0.0, 1.0])\n", - "normal = np.array([0.0, 1.0, 0.0])\n", - "base_length = 3.0\n", - "base_radius = 0.25\n", - "base_area = np.pi * base_radius ** 2" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "With all of the rod's parameters set, we can now create a rod with the specificed properties and add the rod to the simulator system. **Important:** Make sure that any rods you create get added to the simulator system (`timoshenko_sim`), otherwise they will not be included in your simulation. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "shearable_rod = CosseratRod.straight_rod(\n", - " n_elem,\n", - " start,\n", - " direction,\n", - " normal,\n", - " base_length,\n", - " base_radius,\n", - " density,\n", - " youngs_modulus=E,\n", - " shear_modulus=shear_modulus,\n", - ")\n", - "\n", - "timoshenko_sim.append(shearable_rod)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Adding Damping\n", - "With the rod added to the simulator, we can add damping to the rod. We do this using the `.dampen()` option and the `AnalyticalLinearDamper`. We are modifying `timoshenko_sim` simulator to `dampen` the `shearable_rod` object using `AnalyticalLinearDamper` type of dissipation (damping) model.\n", - "\n", - "We also need to define `damping_constant` and simulation `time_step` and pass in `.using()` method." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "\n", - "dl = base_length / n_elem\n", - "dt = 0.01 * dl\n", - "timoshenko_sim.dampen(shearable_rod).using(\n", - " AnalyticalLinearDamper,\n", - " damping_constant=nu,\n", - " time_step=dt,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Adding Boundary Conditions\n", - "With the rod added to the system, we need to apply boundary conditions. The first condition we will apply is fixing the location of one end of the rod. We do this using the `.constrain()` option and the `OneEndFixedRod` boundary condition. We are modifying the `timoshenko_sim` simulator to `constrain` the `shearable_rod` object using the `OneEndFixedRod` type of constraint. \n", - "\n", - "We also need to define which node of the rod is being constrained. We do this by passing the index of the nodes that we want to constain to `constrained_position_idx`. Here we are fixing the first node in the rod. In order to keep the rod from rotating around the fixed node, we also need to constrain an element between two nodes. This fixes the orientation of the rod. We do this by passing the index of the element that we want to fix to `constrained_director_idx`. Like with the position, we are fixing the first element of the rod. Together, this contrains the position and orientation of the rod at the origin. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "timoshenko_sim.constrain(shearable_rod).using(\n", - " OneEndFixedRod, constrained_position_idx=(0,), constrained_director_idx=(0,)\n", - ")\n", - "print(\"One end of the rod is now fixed in place\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "The next boundary condition that we want to apply is the endpoint force. Similarly to how we constrained one of the points, we want the `timoshenko_sim` simulator to `add_forcing_to` the `shearable_rod` object using the `EndpointForces` type of forcing. This `EndpointForces` applies forces to both ends of the rod. We want to apply a negative force in the $d_1$ direction, but only at the end of the rod. We do this by specifying the force vector to be applied at each end as `origin_force` and `end_force`. We also want to ramp up the force over time, so we make the force take some `ramp_up_time` to reach its steady-state value. This helps avoid numerical errors due to discontinuities in the applied force. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "origin_force = np.array([0.0, 0.0, 0.0])\n", - "end_force = np.array([-10.0, 0.0, 0.0])\n", - "ramp_up_time = 5.0\n", - "\n", - "timoshenko_sim.add_forcing_to(shearable_rod).using(\n", - " EndpointForces, origin_force, end_force, ramp_up_time=ramp_up_time\n", - ")\n", - "print(\"Forces added to the rod\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Add Unshearable Rod\n", - "\n", - "Along with the shearable rod, we also want to add an unshearable rod to be able to compare the difference between the two. We do this the same way we did for the first rod, however, because this rod is unsherable, we need to change the Poisson ratio to make the rod unsherable. For a truely unsheraable rod, you would need a Poisson ratio of -1.0, however, this causes the system to be numerically unstable, so instead we make the system nearly unshearable by using a Poisson ratio of -0.85. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Start into the plane\n", - "unshearable_start = np.array([0.0, -1.0, 0.0])\n", - "unshearable_rod = CosseratRod.straight_rod(\n", - " n_elem,\n", - " unshearable_start,\n", - " direction,\n", - " normal,\n", - " base_length,\n", - " base_radius,\n", - " density,\n", - " youngs_modulus=E,\n", - " # Unshearable rod needs G -> inf, which is achievable with a poisson ratio of -1.0\n", - " shear_modulus=E / (-0.85 + 1.0),\n", - ")\n", - "\n", - "timoshenko_sim.append(unshearable_rod)\n", - "\n", - "timoshenko_sim.dampen(unshearable_rod).using(\n", - " AnalyticalLinearDamper,\n", - " damping_constant=nu,\n", - " time_step=dt,\n", - ")\n", - "\n", - "timoshenko_sim.constrain(unshearable_rod).using(\n", - " OneEndFixedRod, constrained_position_idx=(0,), constrained_director_idx=(0,)\n", - ")\n", - "\n", - "timoshenko_sim.add_forcing_to(unshearable_rod).using(\n", - " EndpointForces, origin_force, end_force, ramp_up_time=ramp_up_time\n", - ")\n", - "print(\"Unshearable rod set up\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## System Finalization\n", - "\n", - "We have now added all the necessary rods and boundary conditions to our system. The last thing we need to do is finalize the system. This goes through the system, rearranges things, and precomputes useful quantities to prepare the system for simulation. \n", - "\n", - "As a note, if you make any changes to the rod after calling finalize, you will need to re-setup the system. This requires rerunning all cells above this point. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "timoshenko_sim.finalize()\n", - "print(\"System finalized\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Define Simulation Time\n", - "\n", - "The last thing we need to do deceide how long we want the simulation to run for and what timestepping method to use. Currently, the PositionVerlet algorithim is suggested default method. \n", - "\n", - "In this example, we are trying to match a steady-state solution by temporally evolving our system to reach equillibrium. As such, there is a tradeoff between letting the simulation run long enough to each the equillibrium and waiting around for the simulation to be done. Here we are running the simulation for 10 seconds, this produces reasonable agreement with the analytical solution without taking to long to finish. If you run the simulation for longer, you will get better agreement with the analytical solution. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "final_time = 10.0\n", - "total_steps = int(final_time / dt)\n", - "print(\"Total steps to take\", total_steps)\n", - "\n", - "timestepper = PositionVerlet()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Run Simulation\n", - "We are now ready to perform the simulation. To run the simulation, we integrate the `timoshenko_sim` system using the `timestepper` method until `final_time` by taking `total_steps`. As currently setup, the beam simulation takes about 1 minute to run. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "dt = final_time / total_steps\n", - "time = 0.0\n", - "for i in range(total_steps):\n", - " time = timestepper.step(timoshenko_sim, time, dt)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Post Processing Results\n", - "Now that we have finished the simulation, we want to post-process the results. We will do this by comparing the solutions for the shearable and unshearable beams with the analytical Timoshenko and Euler-Bernoulli beam results. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Compute beam position for sherable and unsherable beams.\n", - "def analytical_result(arg_rod, arg_end_force, shearing=True, n_elem=500):\n", - " base_length = np.sum(arg_rod.rest_lengths)\n", - " arg_s = np.linspace(0.0, base_length, n_elem)\n", - " if type(arg_end_force) is np.ndarray:\n", - " acting_force = arg_end_force[np.nonzero(arg_end_force)]\n", - " else:\n", - " acting_force = arg_end_force\n", - " acting_force = np.abs(acting_force)\n", - " linear_prefactor = -acting_force / arg_rod.shear_matrix[0, 0, 0]\n", - " quadratic_prefactor = (\n", - " -acting_force\n", - " / 2.0\n", - " * np.sum(arg_rod.rest_lengths / arg_rod.bend_matrix[0, 0, 0])\n", - " )\n", - " cubic_prefactor = (acting_force / 6.0) / arg_rod.bend_matrix[0, 0, 0]\n", - " if shearing:\n", - " return (\n", - " arg_s,\n", - " arg_s * linear_prefactor\n", - " + arg_s ** 2 * quadratic_prefactor\n", - " + arg_s ** 3 * cubic_prefactor,\n", - " )\n", - " else:\n", - " return arg_s, arg_s ** 2 * quadratic_prefactor + arg_s ** 3 * cubic_prefactor" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "Now we want to plot the results. The one thing to point out in this function is how to access the position of the rods. They are located in `rod.position_collection[dim, n_elem]`. In this case, we are plotting the x- and z-dimensions. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def plot_timoshenko(shearable_rod, unshearable_rod, end_force):\n", - " import matplotlib.pyplot as plt\n", - "\n", - " analytical_shearable_positon = analytical_result(\n", - " shearable_rod, end_force, shearing=True\n", - " )\n", - " analytical_unshearable_positon = analytical_result(\n", - " unshearable_rod, end_force, shearing=False\n", - " )\n", - "\n", - " fig = plt.figure(figsize=(5, 4), frameon=True, dpi=150)\n", - " ax = fig.add_subplot(111)\n", - " ax.grid(which=\"major\", color=\"grey\", linestyle=\"-\", linewidth=0.25)\n", - "\n", - " ax.plot(\n", - " analytical_shearable_positon[0],\n", - " analytical_shearable_positon[1],\n", - " \"k--\",\n", - " label=\"Timoshenko\",\n", - " )\n", - " ax.plot(\n", - " analytical_unshearable_positon[0],\n", - " analytical_unshearable_positon[1],\n", - " \"k-.\",\n", - " label=\"Euler-Bernoulli\",\n", - " )\n", - "\n", - " ax.plot(\n", - " shearable_rod.position_collection[2, :],\n", - " shearable_rod.position_collection[0, :],\n", - " \"b-\",\n", - " label=\"n=\" + str(shearable_rod.n_elems),\n", - " )\n", - " ax.plot(\n", - " unshearable_rod.position_collection[2, :],\n", - " unshearable_rod.position_collection[0, :],\n", - " \"r-\",\n", - " label=\"n=\" + str(unshearable_rod.n_elems),\n", - " )\n", - "\n", - " ax.legend(prop={\"size\": 12})\n", - " ax.set_ylabel(\"Y Position (m)\", fontsize=12)\n", - " ax.set_xlabel(\"X Position (m)\", fontsize=12)\n", - " plt.show()\n", - "\n", - "\n", - "plot_timoshenko(shearable_rod, unshearable_rod, end_force)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "For the sake of time, we are stopping this simulation early. This leads to some disagreement between the analytical solution and the Elastica solution as there are still some transient effects in the Elastica solution. Allowing the simulation to run longer will lead to a closer result between the analytical and Elastica solutions. \n", - "\n", - "## Dynamic Plotting \n", - "To illustrate how the system evolves over time, we can also plot the system in time. To do this, we need to recreate the system, which we now call `BeamSimulator`. It is the same as the previous system so we will just write everything very compactly to save space. We also slightly modify our plotting and integrating functions to allow the output to be plotting during the simulation. \n", - "\n", - "Since we will be plotting the system over time, we also need to initalize the time at 0.0" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "time = 0.0\n", - "\n", - "\n", - "class BeamSimulator(BaseSystemCollection, Constraints, Forcing, Damping):\n", - " pass\n", - "\n", - "\n", - "dynamic_update_sim = BeamSimulator()\n", - "\n", - "shearable_rod_new = CosseratRod.straight_rod(\n", - " n_elem,\n", - " start,\n", - " direction,\n", - " normal,\n", - " base_length,\n", - " base_radius,\n", - " density,\n", - " youngs_modulus=E,\n", - " shear_modulus=shear_modulus,\n", - ")\n", - "dynamic_update_sim.append(shearable_rod_new)\n", - "dynamic_update_sim.dampen(shearable_rod_new).using(\n", - " AnalyticalLinearDamper,\n", - " damping_constant=nu,\n", - " time_step=dt,\n", - ")\n", - "dynamic_update_sim.constrain(shearable_rod_new).using(\n", - " OneEndFixedRod, constrained_position_idx=(0,), constrained_director_idx=(0,)\n", - ")\n", - "dynamic_update_sim.add_forcing_to(shearable_rod_new).using(\n", - " EndpointForces, origin_force, end_force, ramp_up_time=ramp_up_time\n", - ")\n", - "\n", - "unshearable_rod_new = CosseratRod.straight_rod(\n", - " n_elem,\n", - " unshearable_start,\n", - " direction,\n", - " normal,\n", - " base_length,\n", - " base_radius,\n", - " density,\n", - " youngs_modulus=E,\n", - " # Unshearable rod needs G -> inf, which is achievable with a poisson ratio of -1.0\n", - " shear_modulus=E / (-0.85 + 1.0),\n", - ")\n", - "dynamic_update_sim.append(unshearable_rod_new)\n", - "dynamic_update_sim.dampen(unshearable_rod_new).using(\n", - " AnalyticalLinearDamper,\n", - " damping_constant=nu,\n", - " time_step=dt,\n", - ")\n", - "dynamic_update_sim.constrain(unshearable_rod_new).using(\n", - " OneEndFixedRod, constrained_position_idx=(0,), constrained_director_idx=(0,)\n", - ")\n", - "dynamic_update_sim.add_forcing_to(unshearable_rod_new).using(\n", - " EndpointForces, origin_force, end_force, ramp_up_time=ramp_up_time\n", - ")\n", - "\n", - "dynamic_update_sim.finalize()\n", - "\n", - "\n", - "def run_and_update_plot(simulator, dt, start_time, stop_time, ax):\n", - " from elastica.timestepper.symplectic_steppers import PositionVerlet\n", - "\n", - " timestepper = PositionVerlet()\n", - "\n", - " n_steps = int((stop_time - start_time) / dt)\n", - " time = start_time\n", - " for i in range(n_steps):\n", - " time = timestepper.step(simulator, time, dt)\n", - " plot_timoshenko_dynamic(shearable_rod_new, unshearable_rod_new, end_force, time, ax)\n", - " return time\n", - "\n", - "\n", - "def plot_timoshenko_dynamic(shearable_rod, unshearable_rod, end_force, time, ax):\n", - " import matplotlib.pyplot as plt\n", - " from IPython import display\n", - "\n", - " analytical_shearable_positon = analytical_result(\n", - " shearable_rod, end_force, shearing=True\n", - " )\n", - " analytical_unshearable_positon = analytical_result(\n", - " unshearable_rod, end_force, shearing=False\n", - " )\n", - "\n", - " ax.clear()\n", - " ax.grid(which=\"major\", color=\"grey\", linestyle=\"-\", linewidth=0.25)\n", - " ax.plot(\n", - " analytical_shearable_positon[0],\n", - " analytical_shearable_positon[1],\n", - " \"k--\",\n", - " label=\"Timoshenko\",\n", - " )\n", - " ax.plot(\n", - " analytical_unshearable_positon[0],\n", - " analytical_unshearable_positon[1],\n", - " \"k-.\",\n", - " label=\"Euler-Bernoulli\",\n", - " )\n", - "\n", - " ax.plot(\n", - " shearable_rod.position_collection[2, :],\n", - " shearable_rod.position_collection[0, :],\n", - " \"b-\",\n", - " label=\"shearable rod\",\n", - " )\n", - " ax.plot(\n", - " unshearable_rod.position_collection[2, :],\n", - " unshearable_rod.position_collection[0, :],\n", - " \"r-\",\n", - " label=\"unshearable rod\",\n", - " )\n", - "\n", - " ax.legend(prop={\"size\": 12}, loc=\"lower left\")\n", - " ax.set_ylabel(\"Y Position (m)\", fontsize=12)\n", - " ax.set_xlabel(\"X Position (m)\", fontsize=12)\n", - " ax.set_title(\"Simulation Time: %0.2f seconds\" % time)\n", - " ax.set_xlim([-0.1, 3.1])\n", - " ax.set_ylim([-0.045, 0.002])" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "Now we can run the simulation for a time interval `evolve_for_time` and have the system be plotted every `update_interval`. If you run the cell multiple times in a row, you will see that the that the system continues to evolve in time, this is because you are continually updating `dynamic_update_sim`. If you want to reset the system back to its original configuration, run the cell above this one. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "%matplotlib inline\n", - "import matplotlib.pyplot as plt\n", - "from IPython import display\n", - "\n", - "evolve_for_time = 10.0\n", - "update_interval = 1.0e-1\n", - "\n", - "# update the plot every 1 second\n", - "fig = plt.figure(figsize=(5, 4), frameon=True, dpi=150)\n", - "ax = fig.add_subplot(111)\n", - "first_interval_time = update_interval + time\n", - "last_interval_time = time + evolve_for_time\n", - "for stop_time in np.arange(\n", - " first_interval_time, last_interval_time + dt, update_interval\n", - "):\n", - " time = run_and_update_plot(dynamic_update_sim, dt, time, stop_time, ax)\n", - " display.clear_output(wait=True)\n", - " display.display(plt.gcf())\n", - "plt.close()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### Important note on saving data:\n", - "This current method of plotting data during the simulation is helpful for visualizing how the system evolves, but it is computationally inefficient as we are constantly pausing the simulation to plot. It also does not save data for additional post-processing later. A better method for saving data from a simulation is to use call-back functions. There is information on how to use these functions in the [snake tutorial](./2_Slithering_Snake.ipynb)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.4" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/Binder/2_Slithering_Snake.ipynb b/examples/Binder/2_Slithering_Snake.ipynb deleted file mode 100644 index 00d5cadc..00000000 --- a/examples/Binder/2_Slithering_Snake.ipynb +++ /dev/null @@ -1,753 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "# Slithering Snake Example\n", - "\n", - "This Elastica tutorial explains how to setup a Cosserat rod simulation to simulate a slithering snake. It is a more complex use case than the Timoshenko Beam example. If you have not done so, we strongly suggest you start with [this beam example](./1_Timoshenko_Beam.ipynb) as it covers many of the basics of setting up and running simulations with Elastica. \n", - "\n", - "This slithering snake example includes gravitational forces, friction forces, and internal muscle torques. It also introduces the use of call back functions to allow logging of simulations data for post-processing after the simulation is over. \n", - "\n", - "\n", - "## Getting Started\n", - "To set up the simulation, the first thing you need to do is import the necessary classes. As with the Timoshenko bean, we need to import modules which allow us to more easily construct different simulation systems. We also need to import a rod class, all the necessary forces to be applied, timestepping functions, and callback classes. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pip install pyelastica\n", - "!conda install -c conda-forge ffmpeg -y" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "# import modules\n", - "from elastica.modules import BaseSystemCollection, Constraints, Forcing, CallBacks, Damping\n", - "\n", - "# import rod class, damping and forces to be applied\n", - "from elastica.rod.cosserat_rod import CosseratRod\n", - "from elastica.dissipation import AnalyticalLinearDamper\n", - "from elastica.external_forces import GravityForces, MuscleTorques\n", - "from elastica.interaction import AnisotropicFrictionalPlane\n", - "\n", - "# import timestepping functions\n", - "from elastica.timestepper.symplectic_steppers import PositionVerlet\n", - "\n", - "# import call back functions\n", - "from elastica.callback_functions import CallBackBaseClass\n", - "from collections import defaultdict" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Initialize System and Add Rod\n", - "The first thing to do is initialize the simulator class by combining all the imported modules. After initializing, we will generate a rod and add it to the simulation. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "class SnakeSimulator(BaseSystemCollection, Constraints, Forcing, CallBacks, Damping):\n", - " pass\n", - "\n", - "\n", - "snake_sim = SnakeSimulator()\n", - "\n", - "# Define rod parameters\n", - "n_elem = 50\n", - "start = np.array([0.0, 0.0, 0.0])\n", - "direction = np.array([0.0, 0.0, 1.0])\n", - "normal = np.array([0.0, 1.0, 0.0])\n", - "base_length = 0.35\n", - "base_radius = base_length * 0.011\n", - "base_area = np.pi * base_radius ** 2\n", - "density = 1000\n", - "nu = 2e-3\n", - "E = 1e6\n", - "poisson_ratio = 0.5\n", - "shear_modulus = E / (poisson_ratio + 1.0)\n", - "\n", - "# Create rod\n", - "shearable_rod = CosseratRod.straight_rod(\n", - " n_elem,\n", - " start,\n", - " direction,\n", - " normal,\n", - " base_length,\n", - " base_radius,\n", - " density,\n", - " youngs_modulus=E,\n", - " shear_modulus=shear_modulus,\n", - ")\n", - "\n", - "# Add rod to the snake system\n", - "snake_sim.append(shearable_rod)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Adding Damping\n", - "With the rod added to the simulator, we can add damping to the rod. We do this using the `.dampen()` option and the `AnalyticalLinearDamper`. We are modifying `snake_sim` simulator to `dampen` the `shearable_rod` object using `AnalyticalLinearDamper` type of dissipation (damping) model.\n", - "\n", - "We also need to define `damping_constant` and simulation `time_step` and pass in `.using()` method." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "dt = 1e-4\n", - "snake_sim.dampen(shearable_rod).using(\n", - " AnalyticalLinearDamper,\n", - " damping_constant=nu,\n", - " time_step=dt,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Add Forces to Rod\n", - "With our rod added to the system, we need to specify the relevant forces that will be acting on the rod. For all the forces, the method of adding forces is `system_name.add_forcing_to(name_of_rod).using(type_of_force, *kwargs)` where `*kwargs` are the parameters specific to each type of force. \n", - "\n", - "### Gravity\n", - "The first force to add is gravity. We specify the strength of gravity and also the direction it is pointing. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Add gravitational forces\n", - "gravitational_acc = -9.80665\n", - "snake_sim.add_forcing_to(shearable_rod).using(\n", - " GravityForces, acc_gravity=np.array([0.0, gravitational_acc, 0.0])\n", - ")\n", - "print(\"Gravity now acting on shearable rod\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### Muscle Torques\n", - "A snake generates torque throughout its body through muscle activations. While these muscle activations are generated internally by the snake, it is simpler to treat them as applied external forces, allowing us to apply them to the rod in the same manner as the other external forces. \n", - "\n", - "You may notice that the muscle torque parameters appear to have special values. These are optimized coefficients for a snake gait. For information about how to do this optimization, see the [snake optimization example script](../ContinuumSnakeCase/continuum_snake.py)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Define muscle torque parameters\n", - "period = 2.0\n", - "wave_length = 1.0\n", - "b_coeff = np.array([3.4e-3, 3.3e-3, 4.2e-3, 2.6e-3, 3.6e-3, 3.5e-3])\n", - "\n", - "# Add muscle torques to the rod\n", - "snake_sim.add_forcing_to(shearable_rod).using(\n", - " MuscleTorques,\n", - " base_length=base_length,\n", - " b_coeff=b_coeff,\n", - " period=period,\n", - " wave_number=2.0 * np.pi / (wave_length),\n", - " phase_shift=0.0,\n", - " rest_lengths=shearable_rod.rest_lengths,\n", - " ramp_up_time=period,\n", - " direction=normal,\n", - " with_spline=True,\n", - ")\n", - "print(\"Muscle torques added to the rod\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### Anisotropic Friction Forces\n", - "The last force that needs to be added is the friction force between the snake and the ground. Snakes exhibits anisotropic friction where the friction coefficient is different in different directions. You can also define both static and kinematic friction coefficients. This is accomplished by defining some small velocity threshold `slip_velocity_tol` that defines the transitions between static and kinematic friction. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Define friction force parameters\n", - "origin_plane = np.array([0.0, -base_radius, 0.0])\n", - "normal_plane = normal\n", - "slip_velocity_tol = 1e-8\n", - "froude = 0.1\n", - "mu = base_length / (period * period * np.abs(gravitational_acc) * froude)\n", - "kinetic_mu_array = np.array(\n", - " [1.0 * mu, 1.5 * mu, 2.0 * mu]\n", - ") # [forward, backward, sideways]\n", - "static_mu_array = 2 * kinetic_mu_array\n", - "\n", - "# Add friction forces to the rod\n", - "snake_sim.add_forcing_to(shearable_rod).using(\n", - " AnisotropicFrictionalPlane,\n", - " k=1.0,\n", - " nu=1e-6,\n", - " plane_origin=origin_plane,\n", - " plane_normal=normal_plane,\n", - " slip_velocity_tol=slip_velocity_tol,\n", - " static_mu_array=static_mu_array,\n", - " kinetic_mu_array=kinetic_mu_array,\n", - ")\n", - "print(\"Friction forces added to the rod\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Add Callback Function\n", - "The simulation is now setup, but before it is run, we want to define a callback function. A callback function allows us to record time-series data throughout the simulation. If you do not define a callback function, you will only have access to the final configuration of the system. If you want to be able to analyze how the system evolves over time, it is critical that you record the appropriate quantities. \n", - "\n", - "To create a callback function, begin with the `CallBackBaseClass`. You can then define which state quantities you wish to record by having them appended to the `self.callback_params` dictionary as well as how often you wish to save the data by defining `skip_step`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "# Add call backs\n", - "class ContinuumSnakeCallBack(CallBackBaseClass):\n", - " \"\"\"\n", - " Call back function for continuum snake\n", - " \"\"\"\n", - "\n", - " def __init__(self, step_skip: int, callback_params: dict):\n", - " CallBackBaseClass.__init__(self)\n", - " self.every = step_skip\n", - " self.callback_params = callback_params\n", - "\n", - " def make_callback(self, system, time, current_step: int):\n", - "\n", - " if current_step % self.every == 0:\n", - "\n", - " self.callback_params[\"time\"].append(time)\n", - " self.callback_params[\"step\"].append(current_step)\n", - " self.callback_params[\"position\"].append(system.position_collection.copy())\n", - " self.callback_params[\"velocity\"].append(system.velocity_collection.copy())\n", - " self.callback_params[\"avg_velocity\"].append(\n", - " system.compute_velocity_center_of_mass()\n", - " )\n", - "\n", - " self.callback_params[\"center_of_mass\"].append(\n", - " system.compute_position_center_of_mass()\n", - " )\n", - " self.callback_params[\"curvature\"].append(system.kappa.copy())\n", - "\n", - " return\n", - "\n", - "\n", - "pp_list = defaultdict(list)\n", - "snake_sim.collect_diagnostics(shearable_rod).using(\n", - " ContinuumSnakeCallBack, step_skip=100, callback_params=pp_list\n", - ")\n", - "print(\"Callback function added to the simulator\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "With the callback function added, we can now finalize the system and also define the time stepping parameters of the simulation such as the time step, final time, and time stepping algorithm to use. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "snake_sim.finalize()\n", - "\n", - "final_time = 5.0 * period\n", - "total_steps = int(final_time / dt)\n", - "print(\"Total steps\", total_steps)\n", - "\n", - "timestepper = PositionVerlet()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "Now all that is left is to run the simulation. Using the default parameters the simulation takes about 2-3 minutes to complete. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "dt = final_time / total_steps\n", - "time = 0.0\n", - "for i in range(total_steps):\n", - " time = timestepper.step(snake_sim, time, dt)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "## Post-Process Data\n", - "With the simulation complete, we want to analyze the simulation. Because we added a callback function, we can analyze how the snake evolves over time. All of the data from the callback function is located in the `pp_list` dictionary. Here we will use this information to compute and plot the velocity of the snake in the forward, lateral, and normal directions. We do this by using a pre-written analysis function `compute_projected_velocity`.\n", - "\n", - "In the plotted graph, you can see that it takes about one period for the snake to begin moving before rapidly reaching a steady gait over just 2-3 periods. We also see that the normal velocity is zero since we are only actuating the snake in a 2D plane. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def compute_projected_velocity(plot_params: dict, period):\n", - " import numpy as np\n", - "\n", - " time_per_period = np.array(plot_params[\"time\"]) / period\n", - " avg_velocity = np.array(plot_params[\"avg_velocity\"])\n", - " center_of_mass = np.array(plot_params[\"center_of_mass\"])\n", - "\n", - " # Compute rod velocity in rod direction. We need to compute that because,\n", - " # after snake starts to move it chooses an arbitrary direction, which does not\n", - " # have to be initial tangent direction of the rod. Thus we need to project the\n", - " # snake velocity with respect to its new tangent and roll direction, after that\n", - " # we will get the correct forward and lateral speed. After this projection\n", - " # lateral velocity of the snake has to be oscillating between + and - values with\n", - " # zero mean.\n", - "\n", - " # Number of steps in one period.\n", - " period_step = int(1.0 / (time_per_period[-1] - time_per_period[-2]))\n", - " number_of_period = int(time_per_period[-1])\n", - "\n", - " # Center of mass position averaged in one period\n", - " center_of_mass_averaged_over_one_period = np.zeros((number_of_period - 2, 3))\n", - " for i in range(1, number_of_period - 2):\n", - " # position of center of mass averaged over one period\n", - " center_of_mass_averaged_over_one_period[i - 1] = np.mean(\n", - " center_of_mass[(i + 1) * period_step : (i + 2) * period_step]\n", - " - center_of_mass[(i + 0) * period_step : (i + 1) * period_step],\n", - " axis=0,\n", - " )\n", - " # Average the rod directions over multiple periods and get the direction of the rod.\n", - " direction_of_rod = np.mean(center_of_mass_averaged_over_one_period, axis=0)\n", - " direction_of_rod /= np.linalg.norm(direction_of_rod, ord=2)\n", - "\n", - " # Compute the projected rod velocity in the direction of the rod\n", - " velocity_mag_in_direction_of_rod = np.einsum(\n", - " \"ji,i->j\", avg_velocity, direction_of_rod\n", - " )\n", - " velocity_in_direction_of_rod = np.einsum(\n", - " \"j,i->ji\", velocity_mag_in_direction_of_rod, direction_of_rod\n", - " )\n", - "\n", - " # Get the lateral or roll velocity of the rod after subtracting its projected\n", - " # velocity in the direction of rod\n", - " velocity_in_rod_roll_dir = avg_velocity - velocity_in_direction_of_rod\n", - "\n", - " # Compute the average velocity over the simulation, this can be used for optimizing snake\n", - " # for fastest forward velocity. We start after first period, because of the ramping up happens\n", - " # in first period.\n", - " average_velocity_over_simulation = np.mean(\n", - " velocity_in_direction_of_rod[period_step * 2 :], axis=0\n", - " )\n", - "\n", - " return (\n", - " velocity_in_direction_of_rod,\n", - " velocity_in_rod_roll_dir,\n", - " average_velocity_over_simulation[2],\n", - " average_velocity_over_simulation[0],\n", - " )\n", - "\n", - "\n", - "def compute_and_plot_velocity(plot_params: dict, period):\n", - " from matplotlib import pyplot as plt\n", - " from matplotlib.colors import to_rgb\n", - "\n", - " time_per_period = np.array(plot_params[\"time\"]) / period\n", - " avg_velocity = np.array(plot_params[\"avg_velocity\"])\n", - "\n", - " [\n", - " velocity_in_direction_of_rod,\n", - " velocity_in_rod_roll_dir,\n", - " _,\n", - " _,\n", - " ] = compute_projected_velocity(plot_params, period)\n", - "\n", - " fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150)\n", - " plt.rcParams.update({\"font.size\": 16})\n", - " ax = fig.add_subplot(111)\n", - " ax.grid(which=\"minor\", color=\"k\", linestyle=\"--\")\n", - " ax.grid(which=\"major\", color=\"k\", linestyle=\"-\")\n", - " ax.plot(\n", - " time_per_period[:], velocity_in_direction_of_rod[:, 2], \"r-\", label=\"forward\"\n", - " )\n", - " ax.plot(\n", - " time_per_period[:],\n", - " velocity_in_rod_roll_dir[:, 0],\n", - " c=to_rgb(\"xkcd:bluish\"),\n", - " label=\"lateral\",\n", - " )\n", - " ax.plot(time_per_period[:], avg_velocity[:, 1], \"k-\", label=\"normal\")\n", - " ax.set_ylabel(\"Velocity [m/s]\", fontsize=16)\n", - " ax.set_xlabel(\"Time [s]\", fontsize=16)\n", - " fig.legend(prop={\"size\": 20})\n", - " plt.show()\n", - " plt.close(plt.gcf())\n", - "\n", - "\n", - "compute_and_plot_velocity(pp_list, period)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "We can plot the curvature along the snake at different time instance and compare it with the sterotypical snake curvature function $7cos(2 \\pi s)$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "def plot_curvature(\n", - " plot_params: dict,\n", - " rest_lengths,\n", - " period,\n", - "):\n", - " from matplotlib import pyplot as plt\n", - " from matplotlib.colors import to_rgb\n", - "\n", - " s = np.cumsum(rest_lengths)\n", - " L0 = s[-1]\n", - " s = s / L0\n", - " s = s[:-1].copy()\n", - " x = np.linspace(0, 1, 100)\n", - " curvature = np.array(plot_params[\"curvature\"])\n", - " time = np.array(plot_params[\"time\"])\n", - " peak_time = period * 0.125\n", - " dt = time[1] - time[0]\n", - " peak_idx = int(peak_time / (dt))\n", - " plt.rcParams.update({\"font.size\": 16})\n", - " fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150)\n", - " ax = fig.add_subplot(111)\n", - " try:\n", - " for i in range(peak_idx * 8, peak_idx * 8 * 2, peak_idx):\n", - " ax.plot(s, curvature[i, 0, :] * L0, \"k\")\n", - " except:\n", - " print(\"Simulation time not long enough to plot curvature\")\n", - " ax.plot(\n", - " x, 7 * np.cos(2 * np.pi * x - 0.80), \"--\", label=\"stereotypical snake curvature\"\n", - " )\n", - " ax.set_ylabel(r\"$\\kappa$\", fontsize=16)\n", - " ax.set_xlabel(\"s\", fontsize=16)\n", - " ax.set_xlim(0, 1)\n", - " ax.set_ylim(-10, 10)\n", - " fig.legend(prop={\"size\": 16})\n", - " plt.show()\n", - "\n", - " plt.close(plt.gcf())\n", - "\n", - "\n", - "plot_curvature(pp_list, shearable_rod.rest_lengths, period)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "### Make Video of Snake Gait\n", - "Because we saved data of the snake's behavior, we can make a video of its movement. The easiest way to do this is to do this is to plot the snake's position at each time that the data was recorded and then stitch these plots together to form a video. \n", - "\n", - "note: ffmpeg is required for matplotlib to be able to create a video. More info on ffmepg [here](https://www.ffmpeg.org/)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "from IPython.display import Video\n", - "from tqdm import tqdm\n", - "\n", - "\n", - "def plot_video_2D(plot_params: dict, video_name=\"video.mp4\", margin=0.2, fps=15):\n", - " from matplotlib import pyplot as plt\n", - " import matplotlib.animation as manimation\n", - "\n", - " t = np.array(plot_params[\"time\"])\n", - " positions_over_time = np.array(plot_params[\"position\"])\n", - " total_time = int(np.around(t[..., -1], 1))\n", - " total_frames = fps * total_time\n", - " step = round(len(t) / total_frames)\n", - "\n", - " print(\"creating video -- this can take a few minutes\")\n", - " FFMpegWriter = manimation.writers[\"ffmpeg\"]\n", - " metadata = dict(title=\"Movie Test\", artist=\"Matplotlib\", comment=\"Movie support!\")\n", - " writer = FFMpegWriter(fps=fps, metadata=metadata)\n", - "\n", - " fig = plt.figure()\n", - " ax = fig.add_subplot(111)\n", - " plt.axis(\"equal\")\n", - " rod_lines_2d = ax.plot(\n", - " positions_over_time[0][2], positions_over_time[0][0], linewidth=3\n", - " )[0]\n", - " ax.set_xlim([0 - margin, 3 + margin])\n", - " ax.set_ylim([-1.5 - margin, 1.5 + margin])\n", - " with writer.saving(fig, video_name, dpi=100):\n", - " for time in range(1, len(t), step):\n", - " rod_lines_2d.set_xdata([positions_over_time[time][2]])\n", - " rod_lines_2d.set_ydata([positions_over_time[time][0]])\n", - "\n", - " writer.grab_frame()\n", - " plt.close(fig)\n", - "\n", - "\n", - "filename_video = \"continuum_snake.mp4\"\n", - "plot_video_2D(pp_list, video_name=filename_video, margin=0.2, fps=125)\n", - "\n", - "Video(\"continuum_snake.mp4\")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "pycharm": { - "name": "#%% md\n" - } - }, - "source": [ - "Finally, you can also plot the position of the snake from a 3D perspective. This is most helpful is you have a simulation that consists of more than planar motion. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "from IPython.display import Video\n", - "\n", - "\n", - "def plot_video(plot_params: dict, video_name=\"video.mp4\", margin=0.2, fps=15):\n", - " from matplotlib import pyplot as plt\n", - " import matplotlib.animation as manimation\n", - " from mpl_toolkits import mplot3d\n", - "\n", - " t = np.array(plot_params[\"time\"])\n", - " positions_over_time = np.array(plot_params[\"position\"])\n", - " total_time = int(np.around(t[..., -1], 1))\n", - " total_frames = fps * total_time\n", - " step = round(len(t) / total_frames)\n", - " print(\"creating video -- this can take a few minutes\")\n", - " FFMpegWriter = manimation.writers[\"ffmpeg\"]\n", - " metadata = dict(title=\"Movie Test\", artist=\"Matplotlib\", comment=\"Movie support!\")\n", - " writer = FFMpegWriter(fps=fps, metadata=metadata)\n", - " fig = plt.figure()\n", - " ax = fig.add_subplot(111, projection=\"3d\")\n", - " ax.set_xlim(0 - margin, 3 + margin)\n", - " ax.set_ylim(-1.5 - margin, 1.5 + margin)\n", - " ax.set_zlim(0, 1)\n", - " ax.view_init(elev=20, azim=-80)\n", - " rod_lines_3d = ax.plot(\n", - " positions_over_time[0][2],\n", - " positions_over_time[0][0],\n", - " positions_over_time[0][1],\n", - " linewidth=3,\n", - " )[0]\n", - " with writer.saving(fig, video_name, dpi=100):\n", - " for time in range(1, len(t), step):\n", - " rod_lines_3d.set_xdata([positions_over_time[time][2]])\n", - " rod_lines_3d.set_ydata([positions_over_time[time][0]])\n", - " rod_lines_3d.set_3d_properties(positions_over_time[time][1])\n", - "\n", - " writer.grab_frame()\n", - " plt.close(fig)\n", - "\n", - "\n", - "filename_video = \"continuum_snake_3d.mp4\"\n", - "plot_video(pp_list, video_name=filename_video, margin=0.2, fps=60)\n", - "\n", - "Video(\"continuum_snake_3d.mp4\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3.9.12 ('base')", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.12" - }, - "vscode": { - "interpreter": { - "hash": "69b409ac0f88f35940b70a7cc6f81becc62e64d51a49713aaa6660602c575037" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} From 93616704b027ff51242b433e46084689ff90ec04 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 26 Oct 2025 01:37:38 -0500 Subject: [PATCH 22/85] formatting with documentation build --- elastica/callback_functions.py | 2 +- elastica/external_forces.py | 2 +- elastica/interaction.py | 2 +- elastica/joint.py | 4 ++-- elastica/rod/cosserat_rod.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/elastica/callback_functions.py b/elastica/callback_functions.py index 7adca176..42345586 100644 --- a/elastica/callback_functions.py +++ b/elastica/callback_functions.py @@ -68,7 +68,7 @@ class MyCallBack(CallBackBaseClass): Collect data using make_callback method every sampling step. callback_params: dict Collected callback data is saved in this dictionary. -""" + """ def __init__(self, step_skip: int, callback_params: dict) -> None: """ diff --git a/elastica/external_forces.py b/elastica/external_forces.py index 55dcea4b..6f10ea00 100644 --- a/elastica/external_forces.py +++ b/elastica/external_forces.py @@ -255,7 +255,7 @@ class UniformForces(NoForces): ---------- force: numpy.ndarray 2D (dim, 1) array containing data with 'float' type. Total force applied to a rod-like object. -""" + """ def __init__( self, diff --git a/elastica/interaction.py b/elastica/interaction.py index 8e4d9f87..3f372f19 100644 --- a/elastica/interaction.py +++ b/elastica/interaction.py @@ -137,7 +137,7 @@ class AnisotropicFrictionalPlane(InteractionPlane): kinetic_mu_array: numpy.ndarray 1D (3,) array containing data with 'float' type. [forward, backward, sideways] kinetic friction coefficients. -""" + """ def __init__( self, diff --git a/elastica/joint.py b/elastica/joint.py index 8bd14820..b95aedeb 100644 --- a/elastica/joint.py +++ b/elastica/joint.py @@ -136,7 +136,7 @@ class HingeJoint(FreeJoint): Rotational stiffness coefficient of the joint. normal_direction: numpy.ndarray 2D (dim, 1) array containing data with 'float' type. Constraint rotation direction. -""" + """ # TODO: IN WRAPPER COMPUTE THE NORMAL DIRECTION OR ASK USER TO GIVE INPUT, IF NOT THROW ERROR def __init__( @@ -233,7 +233,7 @@ class FixedJoint(FreeJoint): Rest 3x3 rotation matrix from system one to system two at the connected elements. Instead of aligning the directors of both systems directly, a desired rest rotational matrix labeled C_12* is enforced. -""" + """ def __init__( self, diff --git a/elastica/rod/cosserat_rod.py b/elastica/rod/cosserat_rod.py index 9521ed48..e47bcbad 100644 --- a/elastica/rod/cosserat_rod.py +++ b/elastica/rod/cosserat_rod.py @@ -148,7 +148,7 @@ class CosseratRod(RodBase, KnotTheory): dilatation_rate: NDArray[np.float64] 1D (n_elems) array containing data with 'float' type. Rod element dilatation rates. -""" + """ REQUISITE_MODULES: list[Type] = [] From 566afa5edbe1ee932f5f036cf2bddf58eb63d24d Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 26 Oct 2025 01:38:11 -0500 Subject: [PATCH 23/85] update example cases with run-flag for documentation gallery --- examples/AxialStretchingCase/README.md | 0 ..._stretching.py => run_axial_stretching.py} | 134 +++++---- examples/ButterflyCase/README.md | 0 .../{butterfly.py => run_butterfly.py} | 127 ++++---- examples/CatenaryCase/README.md | 0 .../{catenary.py => run_catenary.py} | 92 ++++-- examples/ContinuumSnakeCase/README.md | 0 ...tinuum_snake.py => run-continuum-snake.py} | 78 ++++- examples/KnotCase/README.md | 0 examples/KnotCase/knot_simulation.py | 208 ------------- examples/KnotCase/run_knot_simulation.py | 274 ++++++++++++++++++ examples/TimoshenkoBeamCase/run_timoshenko.py | 22 +- 12 files changed, 568 insertions(+), 367 deletions(-) create mode 100644 examples/AxialStretchingCase/README.md rename examples/AxialStretchingCase/{axial_stretching.py => run_axial_stretching.py} (53%) create mode 100644 examples/ButterflyCase/README.md rename examples/ButterflyCase/{butterfly.py => run_butterfly.py} (60%) create mode 100644 examples/CatenaryCase/README.md rename examples/CatenaryCase/{catenary.py => run_catenary.py} (62%) create mode 100644 examples/ContinuumSnakeCase/README.md rename examples/ContinuumSnakeCase/{continuum_snake.py => run-continuum-snake.py} (64%) create mode 100644 examples/KnotCase/README.md delete mode 100644 examples/KnotCase/knot_simulation.py create mode 100644 examples/KnotCase/run_knot_simulation.py diff --git a/examples/AxialStretchingCase/README.md b/examples/AxialStretchingCase/README.md new file mode 100644 index 00000000..e69de29b diff --git a/examples/AxialStretchingCase/axial_stretching.py b/examples/AxialStretchingCase/run_axial_stretching.py similarity index 53% rename from examples/AxialStretchingCase/axial_stretching.py rename to examples/AxialStretchingCase/run_axial_stretching.py index a4f54a81..098258b3 100644 --- a/examples/AxialStretchingCase/axial_stretching.py +++ b/examples/AxialStretchingCase/run_axial_stretching.py @@ -1,34 +1,23 @@ -"""Axial stretching test-case - -Assume we have a rod lying aligned in the x-direction, with high internal -damping. - -We fix one end (say, the left end) of the rod to a wall. On the right -end we apply a force directed axially pulling the rods tip. Linear -theory (assuming small displacements) predict that the net displacement -experienced by the rod tip is Δx = FL/AE where the symbols carry their -usual meaning (the rod is just a linear spring). We compare our results -with the above result. - -We can "improve" the theory by having a better estimate for the rod's -spring constant by assuming that it equilibriates under the new position, -with -Δx = F * (L + Δx)/ (A * E) -which results in Δx = (F*l)/(A*E - F). Our rod reaches equilibrium wrt to -this position. - -Note that if the damping is not high, the rod oscillates about the eventual -resting position (and this agrees with the theoretical predictions without -any damping : we should see the rod oscillating simple-harmonically in time). - -isort:skip_file """ +Axial Stretching +================ + +This case tests the axial stretching of a rod. A rod is fixed at one end and +a force is applied at the other end. The rod stretches and the displacement +of the tip is compared with the analytical solution. +""" + +# isort:skip_file import numpy as np from matplotlib import pyplot as plt import elastica as ea +# %% +# First, we define a simulator class that inherits from the necessary mixins. +# This makes it easy to add constraints, forces, and damping to the system. + class StretchingBeamSimulator( ea.BaseSystemCollection, ea.Constraints, ea.Forcing, ea.Damping, ea.CallBacks @@ -39,10 +28,10 @@ class StretchingBeamSimulator( stretch_sim = StretchingBeamSimulator() final_time = 200.0 -# Options -PLOT_FIGURE = True -SAVE_FIGURE = False -SAVE_RESULTS = False +# %% +# Next, we set up the test parameters for the simulation. This includes the +# number of elements, the start position, direction, normal, length, radius, +# density, and Young's modulus of the rod. # setting up test params n_elem = 19 @@ -58,6 +47,10 @@ class StretchingBeamSimulator( poisson_ratio = 0.5 shear_modulus = youngs_modulus / (poisson_ratio + 1.0) +# %% +# Now we can create the `CosseratRod` object. We use the `straight_rod` method +# to create a straight rod with the specified parameters. + stretchable_rod = ea.CosseratRod.straight_rod( n_elem, start, @@ -71,16 +64,29 @@ class StretchingBeamSimulator( ) stretch_sim.append(stretchable_rod) + +# %% +# We then apply a boundary condition to fix one end of the rod. We use the +# `OneEndFixedBC` constraint to fix the position and director of the first node. + stretch_sim.constrain(stretchable_rod).using( ea.OneEndFixedBC, constrained_position_idx=(0,), constrained_director_idx=(0,) ) +# %% +# A force is applied to the other end of the rod. We use the `EndpointForces` +# forcing to apply a force in the x-direction. + end_force_x = 1.0 end_force = np.array([end_force_x, 0.0, 0.0]) stretch_sim.add_forcing_to(stretchable_rod).using( ea.EndpointForces, 0.0 * end_force, end_force, ramp_up_time=1e-2 ) +# %% +# Damping is added to the system to help it reach a steady state. We use an +# `AnalyticalLinearDamper` to add damping to the rod. + # add damping dl = base_length / n_elem dt = 0.1 * dl @@ -92,6 +98,11 @@ class StretchingBeamSimulator( ) +# %% +# We define a callback class to record the position and velocity of the rod +# during the simulation. This is useful for post-processing the results. + + # Add call backs class AxialStretchingCallBack(ea.CallBackBaseClass): """ @@ -125,10 +136,17 @@ def make_callback( AxialStretchingCallBack, step_skip=200, callback_params=recorded_history ) +# %% +# We finalize the simulator and create the time-stepper. The `PositionVerlet` +# time-stepper is used to integrate the system. + stretch_sim.finalize() timestepper: ea.typing.StepperProtocol = ea.PositionVerlet() # timestepper = PEFRL() +# %% +# The simulation is run for the specified `final_time`. + total_steps = int(final_time / dt) print("Total steps", total_steps) dt = final_time / total_steps @@ -136,43 +154,23 @@ def make_callback( for i in range(total_steps): time = timestepper.step(stretch_sim, time, dt) -if PLOT_FIGURE: - # First-order theory with base-length - expected_tip_disp = end_force_x * base_length / base_area / youngs_modulus - # First-order theory with modified-length, gives better estimates - expected_tip_disp_improved = ( - end_force_x * base_length / (base_area * youngs_modulus - end_force_x) - ) - - fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150) - ax = fig.add_subplot(111) - ax.plot(recorded_history["time"], recorded_history["position"], lw=2.0) - ax.hlines(base_length + expected_tip_disp, 0.0, final_time, "k", "dashdot", lw=1.0) - ax.hlines( - base_length + expected_tip_disp_improved, 0.0, final_time, "k", "dashed", lw=2.0 - ) - if SAVE_FIGURE: - fig.savefig("axial_stretching.pdf") - plt.show() - -if SAVE_RESULTS: - import pickle - - filename = "axial_stretching_data.dat" - file = open(filename, "wb") - pickle.dump(stretchable_rod, file) - file.close() - - tv = ( - np.asarray(recorded_history["time"]), - np.asarray(recorded_history["velocity_norms"]), - ) - - def as_time_series(v: np.ndarray) -> np.ndarray: - return v.T - - np.savetxt( - "velocity_norms.csv", - as_time_series(np.stack(tv)), - delimiter=",", - ) +# %% +# Finally, we plot the results and compare them with the analytical solution. +# The analytical solution is calculated using the first-order theory with +# both the base length and the modified length. + +# First-order theory with base-length +expected_tip_disp = end_force_x * base_length / base_area / youngs_modulus +# First-order theory with modified-length, gives better estimates +expected_tip_disp_improved = ( + end_force_x * base_length / (base_area * youngs_modulus - end_force_x) +) + +fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150) +ax = fig.add_subplot(111) +ax.plot(recorded_history["time"], recorded_history["position"], lw=2.0) +ax.hlines(base_length + expected_tip_disp, 0.0, final_time, "k", "dashdot", lw=1.0) +ax.hlines( + base_length + expected_tip_disp_improved, 0.0, final_time, "k", "dashed", lw=2.0 +) +plt.show() diff --git a/examples/ButterflyCase/README.md b/examples/ButterflyCase/README.md new file mode 100644 index 00000000..e69de29b diff --git a/examples/ButterflyCase/butterfly.py b/examples/ButterflyCase/run_butterfly.py similarity index 60% rename from examples/ButterflyCase/butterfly.py rename to examples/ButterflyCase/run_butterfly.py index 5fd5d050..42561480 100644 --- a/examples/ButterflyCase/butterfly.py +++ b/examples/ButterflyCase/run_butterfly.py @@ -1,3 +1,12 @@ +""" +Butterfly +========= + +This case simulates the motion of a rod that is initially shaped like a +butterfly. The rod is released from rest and allowed to fall under gravity. +The simulation tracks the position and energy of the rod over time. +""" + import numpy as np from matplotlib import pyplot as plt from matplotlib.colors import to_rgb @@ -6,6 +15,9 @@ import elastica as ea from elastica.utils import MaxDimension +# %% +# First, we define a simulator class that inherits from the necessary mixins. + class ButterflySimulator(ea.BaseSystemCollection, ea.CallBacks): pass @@ -14,11 +26,10 @@ class ButterflySimulator(ea.BaseSystemCollection, ea.CallBacks): butterfly_sim = ButterflySimulator() final_time = 40.0 -# Options -PLOT_FIGURE = True -SAVE_FIGURE = True -SAVE_RESULTS = True -ADD_UNSHEARABLE_ROD = False +# %% +# Next, we set up the test parameters for the simulation. This includes the +# number of elements, the origin, the angle of inclination, the length, +# radius, density, and Young's modulus of the rod. # setting up test params # FIXME : Doesn't work with elements > 10 (the inverse rotate kernel fails) @@ -44,6 +55,10 @@ class ButterflySimulator(ea.BaseSystemCollection, ea.CallBacks): poisson_ratio = 0.5 shear_modulus = youngs_modulus / (poisson_ratio + 1.0) +# %% +# We then define the initial positions of the nodes of the rod to create the +# butterfly shape. + positions = np.empty((MaxDimension.value(), n_elem + 1)) dl = total_length / n_elem @@ -60,6 +75,9 @@ class ButterflySimulator(ea.BaseSystemCollection, ea.CallBacks): - np.sin(angle_of_inclination) * vertical_direction ) +# %% +# Now we can create the `CosseratRod` object with the specified positions. + butterfly_rod = ea.CosseratRod.straight_rod( n_elem, start=origin.reshape(3), @@ -76,6 +94,11 @@ class ButterflySimulator(ea.BaseSystemCollection, ea.CallBacks): butterfly_sim.append(butterfly_rod) +# %% +# We define a callback class to record the position and energy of the rod +# during the simulation. + + # Add call backs class VelocityCallBack(ea.CallBackBaseClass): """ @@ -117,12 +140,17 @@ def make_callback( VelocityCallBack, step_skip=100, callback_params=recorded_history ) +# %% +# We finalize the simulator and create the time-stepper. butterfly_sim.finalize() timestepper: ea.typing.StepperProtocol timestepper = ea.PositionVerlet() # timestepper = PEFRL() +# %% +# The simulation is run for the specified `final_time`. + dt = 0.01 * dl total_steps = int(final_time / dt) print("Total steps", total_steps) @@ -131,52 +159,43 @@ def make_callback( for i in range(total_steps): time = timestepper.step(butterfly_sim, time, dt) -if PLOT_FIGURE: - # Plot the histories - fig = plt.figure(figsize=(5, 4), frameon=True, dpi=150) - ax = fig.add_subplot(111) - positions_history = recorded_history["position"] - # record first position - first_position = positions_history.pop(0) - ax.plot(first_position[2, ...], first_position[0, ...], "r--", lw=2.0) - n_positions = len(positions_history) - for i, pos in enumerate(positions_history): - alpha = np.exp(i / n_positions - 1) - ax.plot(pos[2, ...], pos[0, ...], "b", lw=0.6, alpha=alpha) - # final position is also separate - last_position = positions_history.pop() - ax.plot(last_position[2, ...], last_position[0, ...], "k--", lw=2.0) - # don't block - fig.show() - - # Plot the energies - energy_fig = plt.figure(figsize=(5, 4), frameon=True, dpi=150) - energy_ax = energy_fig.add_subplot(111) - times = np.asarray(recorded_history["time"]) - te = np.asarray(recorded_history["te"]) - re = np.asarray(recorded_history["re"]) - be = np.asarray(recorded_history["be"]) - se = np.asarray(recorded_history["se"]) - - energy_ax.plot(times, te, c=to_rgb("xkcd:reddish"), lw=2.0, label="Translations") - energy_ax.plot(times, re, c=to_rgb("xkcd:bluish"), lw=2.0, label="Rotation") - energy_ax.plot(times, be, c=to_rgb("xkcd:burple"), lw=2.0, label="Bend") - energy_ax.plot(times, se, c=to_rgb("xkcd:goldenrod"), lw=2.0, label="Shear") - energy_ax.plot(times, te + re + be + se, c="k", lw=2.0, label="Total energy") - energy_ax.legend() - # don't block - energy_fig.show() - - if SAVE_FIGURE: - fig.savefig("butterfly.png") - energy_fig.savefig("energies.png") - - plt.show() - -if SAVE_RESULTS: - import pickle - - filename = "butterfly_data.dat" - file = open(filename, "wb") - pickle.dump(butterfly_rod, file) - file.close() +# %% +# Finally, we plot the results. The position of the rod is plotted at +# different time steps, and the energies are plotted as a function of time. + +# Plot the histories +fig = plt.figure(figsize=(5, 4), frameon=True, dpi=150) +ax = fig.add_subplot(111) +positions_history = recorded_history["position"] +# record first position +first_position = positions_history.pop(0) +ax.plot(first_position[2, ...], first_position[0, ...], "r--", lw=2.0) +n_positions = len(positions_history) +for i, pos in enumerate(positions_history): + alpha = np.exp(i / n_positions - 1) + ax.plot(pos[2, ...], pos[0, ...], "b", lw=0.6, alpha=alpha) +# final position is also separate +last_position = positions_history.pop() +ax.plot(last_position[2, ...], last_position[0, ...], "k--", lw=2.0) +# don't block +fig.show() + +# Plot the energies +energy_fig = plt.figure(figsize=(5, 4), frameon=True, dpi=150) +energy_ax = energy_fig.add_subplot(111) +times = np.asarray(recorded_history["time"]) +te = np.asarray(recorded_history["te"]) +re = np.asarray(recorded_history["re"]) +be = np.asarray(recorded_history["be"]) +se = np.asarray(recorded_history["se"]) + +energy_ax.plot(times, te, c=to_rgb("xkcd:reddish"), lw=2.0, label="Translations") +energy_ax.plot(times, re, c=to_rgb("xkcd:bluish"), lw=2.0, label="Rotation") +energy_ax.plot(times, be, c=to_rgb("xkcd:burple"), lw=2.0, label="Bend") +energy_ax.plot(times, se, c=to_rgb("xkcd:goldenrod"), lw=2.0, label="Shear") +energy_ax.plot(times, te + re + be + se, c="k", lw=2.0, label="Total energy") +energy_ax.legend() +# don't block +energy_fig.show() + +plt.show() diff --git a/examples/CatenaryCase/README.md b/examples/CatenaryCase/README.md new file mode 100644 index 00000000..e69de29b diff --git a/examples/CatenaryCase/catenary.py b/examples/CatenaryCase/run_catenary.py similarity index 62% rename from examples/CatenaryCase/catenary.py rename to examples/CatenaryCase/run_catenary.py index b228de01..9da23e53 100644 --- a/examples/CatenaryCase/catenary.py +++ b/examples/CatenaryCase/run_catenary.py @@ -1,3 +1,12 @@ +""" +Catenary +======== + +This case simulates a rod hanging under its own weight, forming a catenary +curve. The rod is fixed at both ends and is allowed to settle into its +equilibrium position. +""" + from collections import defaultdict import numpy as np @@ -8,6 +17,9 @@ plot_catenary, ) +# %% +# First, we define a simulator class that inherits from the necessary mixins. + class CatenarySimulator( ea.BaseSystemCollection, ea.Constraints, ea.Forcing, ea.Damping, ea.CallBacks @@ -16,6 +28,11 @@ class CatenarySimulator( catenary_sim = CatenarySimulator() + +# %% +# Next, we set up the simulation parameters. This includes the final time, +# damping constant, time step, and rendering frame rate. + final_time = 10 damping_constant = 0.3 time_step = 1e-4 @@ -23,6 +40,10 @@ class CatenarySimulator( rendering_fps = 20 step_skip = int(1.0 / (rendering_fps * time_step)) +# %% +# We then define the properties of the rod, such as the number of elements, +# start position, direction, normal, length, radius, and material properties. + n_elem = 500 start = np.zeros((3,)) @@ -41,6 +62,9 @@ class CatenarySimulator( poisson_ratio = 0.5 shear_modulus = E / (poisson_ratio + 1.0) +# %% +# Now we can create the `CosseratRod` object and add it to the simulator. + base_rod = ea.CosseratRod.straight_rod( n_elem, start, @@ -55,6 +79,9 @@ class CatenarySimulator( catenary_sim.append(base_rod) +# %% +# Damping is added to the system to help it reach a steady state. + # add damping catenary_sim.dampen(base_rod).using( ea.AnalyticalLinearDamper, @@ -62,11 +89,17 @@ class CatenarySimulator( time_step=time_step, ) +# %% +# We add gravity to the system to simulate the weight of the rod. + # Add gravity catenary_sim.add_forcing_to(base_rod).using( ea.GravityForces, acc_gravity=-9.80665 * normal ) +# %% +# We fix both ends of the rod using the `FixedConstraint`. + # fix catenary ends catenary_sim.constrain(base_rod).using( ea.FixedConstraint, @@ -75,6 +108,11 @@ class CatenarySimulator( ) +# %% +# We define a callback class to record the position, radius, and internal +# forces of the rod during the simulation. + + # Add call backs class CatenaryCallBack(ea.CallBackBaseClass): """ @@ -106,12 +144,15 @@ def make_callback( CatenaryCallBack, step_skip=step_skip, callback_params=recorded_history ) +# %% +# We finalize the simulator and create the time-stepper. catenary_sim.finalize() - - timestepper: ea.typing.StepperProtocol = ea.PositionVerlet() +# %% +# The simulation is run for the specified number of steps. + dt = final_time / total_steps time = 0.0 for i in range(total_steps): @@ -119,22 +160,31 @@ def make_callback( position = np.array(recorded_history["position"]) b = np.min(position[-1][2]) -SAVE_VIDEO = True -if SAVE_VIDEO: - # plotting the videos - filename_video = "catenary.mp4" - plot_video( - recorded_history, - video_name=filename_video, - fps=rendering_fps, - xlim=[0, base_length], - ylim=[-0.5 * base_length, 0.5 * base_length], - ) - -PLOT_RESULTS = True -if PLOT_RESULTS: - plot_catenary( - recorded_history, - xlim=(0, base_length), - ylim=(b, 0.0), - ) +# %% +# Finally, we can save a video of the simulation and plot the final +# shape of the catenary. + +# plotting the videos +filename_video = "catenary.mp4" +plot_video( + recorded_history, + video_name=filename_video, + fps=rendering_fps, + xlim=[0, base_length], + ylim=[-0.5 * base_length, 0.5 * base_length], +) + +# %% +# .. video:: ../../../examples/CatenaryCase/catenary.mp4 +# :width: 720 +# :autoplay: +# :muted: +# :loop: + +# %% +# plotting the catenary positions after simulation. +plot_catenary( + recorded_history, + xlim=(0, base_length), + ylim=(b, 0.0), +) diff --git a/examples/ContinuumSnakeCase/README.md b/examples/ContinuumSnakeCase/README.md new file mode 100644 index 00000000..e69de29b diff --git a/examples/ContinuumSnakeCase/continuum_snake.py b/examples/ContinuumSnakeCase/run-continuum-snake.py similarity index 64% rename from examples/ContinuumSnakeCase/continuum_snake.py rename to examples/ContinuumSnakeCase/run-continuum-snake.py index f773c0df..1368ba1a 100644 --- a/examples/ContinuumSnakeCase/continuum_snake.py +++ b/examples/ContinuumSnakeCase/run-continuum-snake.py @@ -1,4 +1,23 @@ -__doc__ = """Snake friction case from X. Zhang et. al. Nat. Comm. 2021""" +""" +Continuum Snake +=============== + +Snake friction case from X. Zhang et. al. Nat. Comm. 2021 + +This Elastica tutorial explains how to setup a Cosserat rod simulation to simulate a slithering snake. It covers many of the basics of setting up and running simulations with Elastica. + +This slithering snake example includes gravitational forces, friction forces, and internal muscle torques. It also introduces the use of call back functions to allow logging of simulations data for post-processing after the simulation is over. + +.. video:: ../../../assets/continuum_snake.mp4 + :width: 720 + :autoplay: + :muted: + :loop: + +Getting Started +--------------- +To set up the simulation, the first thing you need to do is import the necessary classes. As with the Timoshenko 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. +""" import os import numpy as np @@ -6,7 +25,12 @@ from numpy.typing import NDArray from elastica.typing import RodType -from examples.ContinuumSnakeCase.continuum_snake_postprocessing import ( +# %% +# Initialize System and Add Rod +# ----------------------------- +# The first thing to do is initialize the simulator class by combining all the imported modules. After initializing, we will generate a rod and add it to the simulation. + +from continuum_snake_postprocessing import ( plot_snake_velocity, plot_video, compute_projected_velocity, @@ -71,6 +95,13 @@ def run_snake( ea.GravityForces, acc_gravity=np.array([0.0, gravitational_acc, 0.0]) ) + # %% + # Muscle Torques + # -------------- + # A snake generates torque throughout its body through muscle activations. While these muscle activations are generated internally by the snake, it is simpler to treat them as applied external forces, allowing us to apply them to the rod in the same manner as the other external forces. + # + # You may notice that the muscle torque parameters appear to have special values. These are optimized coefficients for a snake gait. + # Add muscle torques wave_length = b_coeff[-1] snake_sim.add_forcing_to(shearable_rod).using( @@ -86,6 +117,10 @@ def run_snake( with_spline=True, ) + # Anisotropic Friction Forces + # --------------------------- + # The last force that needs to be added is the friction force between the snake and the ground. Snakes exhibits anisotropic friction where the friction coefficient is different in different directions. You can also define both static and kinematic friction coefficients. This is accomplished by defining some small velocity threshold `slip_velocity_tol` that defines the transitions between static and kinematic friction. + # Add friction forces ground_plane = ea.Plane( plane_origin=np.array([0.0, -base_radius, 0.0]), plane_normal=normal @@ -116,9 +151,11 @@ def run_snake( time_step=time_step, ) - total_steps = int(final_time / time_step) - rendering_fps = 60 - step_skip = int(1.0 / (rendering_fps * time_step)) + # Add Callback Function + # --------------------- + # The simulation is now setup, but before it is run, we want to define a callback function. A callback function allows us to record time-series data throughout the simulation. If you do not define a callback function, you will only have access to the final configuration of the system. If you want to be able to analyze how the system evolves over time, it is critical that you record the appropriate quantities. + # + # To create a callback function, begin with the `CallBackBaseClass`. You can then define which state quantities you wish to record by having them appended to the `self.callback_params` dictionary as well as how often you wish to save the data by defining `skip_step`. # Add call backs class ContinuumSnakeCallBack(ea.CallBackBaseClass): @@ -184,13 +221,21 @@ def get_slip_velocity(self, system: RodType) -> NDArray[np.float64]: ) return slip_function_along_axial_direction + total_steps = int(final_time / time_step) + rendering_fps = 60 + step_skip = int(1.0 / (rendering_fps * time_step)) + pp_list: dict[str, list] = ea.defaultdict(list) snake_sim.collect_diagnostics(shearable_rod).using( ContinuumSnakeCallBack, step_skip=step_skip, callback_params=pp_list ) + # With the callback function added, we can now finalize the system and also define the time stepping parameters of the simulation such as the time step, final time, and time stepping algorithm to use. + snake_sim.finalize() + # Now all that is left is to run the simulation. Using the default parameters the simulation takes about 2-3 minutes to complete. + timestepper = ea.PositionVerlet() dt = final_time / total_steps time = 0.0 @@ -226,6 +271,29 @@ def get_slip_velocity(self, system: RodType) -> NDArray[np.float64]: return avg_forward, avg_lateral, pp_list +# %% +# Post-Process Data +# ----------------- +# With the simulation complete, we want to analyze the simulation. Because we added a callback function, we can analyze how the snake evolves over time. All of the data from the callback function is located in the `pp_list` dictionary. Here we will use this information to compute and plot the velocity of the snake in the forward, lateral, and normal directions. We do this by using a pre-written analysis function `compute_projected_velocity`. +# +# In the plotted graph, you can see that it takes about one period for the snake to begin moving before rapidly reaching a steady gait over just 2-3 periods. We also see that the normal velocity is zero since we are only actuating the snake in a 2D plane. + + +# %% +# Gait Optimization with CMA +# -------------------------- +# The following block of code in the main script demonstrates how to use the +# Covariance Matrix Adaptation Evolution Strategy (CMA-ES) to optimize the +# snake's gait. CMA-ES is a stochastic, derivative-free method for numerical +# optimization of non-linear or non-convex continuous optimization problems. +# Here, we use it to find the optimal set of muscle torque coefficients +# (`b_coeff`) that maximize the snake's average forward velocity. The +# `optimize_snake` function serves as the objective function for the +# optimization, which takes the spline coefficients as input and returns the +# negative of the average forward velocity, as CMA-ES is a minimization +# algorithm. + + if __name__ == "__main__": # Options diff --git a/examples/KnotCase/README.md b/examples/KnotCase/README.md new file mode 100644 index 00000000..e69de29b diff --git a/examples/KnotCase/knot_simulation.py b/examples/KnotCase/knot_simulation.py deleted file mode 100644 index 9d506693..00000000 --- a/examples/KnotCase/knot_simulation.py +++ /dev/null @@ -1,208 +0,0 @@ -__doc__ = """Simulating overhand-knot, a degenerated case of Trefoil knot. -A demonstration includes how to create an arbitrary controller for a node in a rod, -resembling a proportional-controller of SO3 Pose. The same class can be used further -to mimic the MPC control or trajectory-tracing.""" - -from typing import Any, TypeAlias -from numpy.typing import NDArray -from elastica.typing import RodType - -import numpy as np -import matplotlib.pyplot as plt -from collections import defaultdict -import elastica as ea - -from knot_forcing import TargetPoseProportionalControl -from knot_visualization import plot_video3D - -Position: TypeAlias = NDArray[np.float64] # vector (3) -Orientation: TypeAlias = NDArray[np.float64] # SO3 matrix (3, 3) -Pose: TypeAlias = tuple[Position, Orientation] - - -class SoftRodSimulator( - ea.BaseSystemCollection, - ea.Constraints, - ea.Forcing, - ea.Damping, - ea.CallBacks, - ea.Contact, -): - pass - - -class AxialStretchingCallBack(ea.CallBackBaseClass): - """ - Records the position of the rod - """ - - def __init__(self, callback_params: dict) -> None: - ea.CallBackBaseClass.__init__(self) - self.every = 200 - self.callback_params = callback_params - - def make_callback(self, system: RodType, time: float, current_step: int) -> None: - if current_step % self.every == 0: - - self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) - self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["position"].append(system.position_collection.copy()) - self.callback_params["orientation"].append( - system.director_collection.copy() - ) - return - - -if __name__ == "__main__": - # Options - GENERATE_2D_VIDEO = False - GENERATE_3D_VIDEO = True - - simulator = SoftRodSimulator() - recorded_history: dict[str, list[Any]] = defaultdict(list) - final_time = 5 - dt = 0.0002 - - # setting up test params - n_elem = 50 - start = np.zeros((3,)) - direction = np.array([1.0, 0.0, 0.0]) - normal = np.array([0.0, 1.0, 0.0]) - base_length = 1.2 - base_radius = 0.025 - density = 2000 - youngs_modulus = 1e6 - poisson_ratio = 0.5 - shear_modulus = youngs_modulus / (2 * (poisson_ratio + 1.0)) - - stretchable_rod = ea.CosseratRod.straight_rod( - n_elem, - start, - direction, - normal, - base_length, - base_radius, - density, - youngs_modulus=youngs_modulus, - shear_modulus=shear_modulus, - ) - simulator.append(stretchable_rod) - - run_time = 4 - - def base_target(t: float, rod: RodType) -> Pose: - target_position = direction * base_length - 5 * base_radius * normal - if t <= run_time / 2: - ratio = min(2 * t / run_time, 1.0) - angular_ratio = ratio * np.pi * 2 - position = target_position * ratio - orientation_twist = np.array( - [ - [0, np.cos(angular_ratio), np.sin(angular_ratio)], - [0, -np.sin(angular_ratio), np.cos(angular_ratio)], - [1, 0, 0], - ], - dtype=float, - ) - else: - ratio = min(2 * (t - run_time / 2) / run_time, 1.0) - R = 8 - position = np.array( - [ - target_position[0] * (1 - ratio), - -R * base_radius * np.cos(2 * ratio * 12) * (1 - ratio), - -R * base_radius * np.sin(2 * ratio * 12) * (1 - ratio), - ] - ) - angular_ratio = (1 - ratio) * np.pi * 2 - orientation_twist = np.array( - [ - [0, np.cos(angular_ratio), -np.sin(angular_ratio)], - [0, np.sin(angular_ratio), np.cos(angular_ratio)], - [1, 0, 0], - ], - dtype=float, - ) - return position, orientation_twist - - # Control point - p = 3e3 - pt = 5e0 - simulator.add_forcing_to(stretchable_rod).using( - TargetPoseProportionalControl, - elem_index=0, - p_linear_value=p, - p_angular_value=pt, - target=base_target, - ramp_up_time=1e-6, - target_history=recorded_history["base_pose"], - ) - - # Boundary conditions - simulator.constrain(stretchable_rod).using( - ea.FixedConstraint, constrained_position_idx=(-1, -20) - ) - - # Self contact - simulator.detect_contact_between(stretchable_rod, stretchable_rod).using( - ea.RodSelfContact, k=1e4, nu=3 - ) - - # Gravity - simulator.add_forcing_to(stretchable_rod).using( - ea.GravityForces, acc_gravity=np.array([0.0, 0.0, -9.80665]) - ) - - # Damping - damping_constant = 5.0 - simulator.dampen(stretchable_rod).using( - ea.AnalyticalLinearDamper, - translational_damping_constant=damping_constant, - rotational_damping_constant=damping_constant * 0.01, - time_step=dt, - ) - simulator.dampen(stretchable_rod).using(ea.LaplaceDissipationFilter, filter_order=5) - - simulator.collect_diagnostics(stretchable_rod).using( - AxialStretchingCallBack, callback_params=recorded_history - ) - - # Finalize and run the simulation - simulator.finalize() - timestepper = ea.PositionVerlet() - total_steps = int(final_time / dt) - print("Total steps", total_steps) - dt = final_time / total_steps - time = 0.0 - for i in range(total_steps): - time = timestepper.step(simulator, time, dt) - - if GENERATE_3D_VIDEO: - filename_video = "knot3D.mp4" - plot_video3D(recorded_history, video_name=filename_video, margin=0.2, fps=10) - - # Plot knot topological quantities - timestep = np.asarray(recorded_history["time"]) - positions = np.asarray(recorded_history["position"]) - orientations = np.asarray(recorded_history["orientation"]) - radii = np.asarray(recorded_history["radius"]) - total_twist, _ = ea.compute_twist(positions, orientations[:, 0, ...]) - total_writhe = ea.compute_writhe(positions, np.float64(base_length), "next_tangent") - total_link = ea.compute_link( - positions, - orientations[:, 0, ...], - radii, - np.float64(base_length), - "next_tangent", - ) - - plt.figure() - plt.plot(timestep, total_twist, label="twist") - plt.plot(timestep, total_writhe, label="writhe") - plt.plot(timestep, total_link, label="link") - plt.legend() - plt.xlabel("time") - plt.ylabel("link-writhe-twist quantity") - plt.savefig("LWT.png", dpi=300) - plt.close("all") diff --git a/examples/KnotCase/run_knot_simulation.py b/examples/KnotCase/run_knot_simulation.py new file mode 100644 index 00000000..c4fb4fcf --- /dev/null +++ b/examples/KnotCase/run_knot_simulation.py @@ -0,0 +1,274 @@ +""" +Knot Simulation +=============== + +This script simulates the formation of an overhand knot in a soft rod. +It demonstrates how to create a controller to manipulate a node on the rod, +which can be used for tasks like trajectory tracing or model predictive control. +""" + +from typing import Any, TypeAlias +from numpy.typing import NDArray +from elastica.typing import RodType + +import numpy as np +import matplotlib.pyplot as plt +from collections import defaultdict +import elastica as ea + +from knot_forcing import TargetPoseProportionalControl +from knot_visualization import plot_video3D + +Position: TypeAlias = NDArray[np.float64] # vector (3) +Orientation: TypeAlias = NDArray[np.float64] # SO3 matrix (3, 3) +Pose: TypeAlias = tuple[Position, Orientation] + +# %% +# First, we define a simulator class that inherits from the necessary mixins +# for constraints, forcing, damping, callbacks, and contact. + + +class SoftRodSimulator( + ea.BaseSystemCollection, + ea.Constraints, + ea.Forcing, + ea.Damping, + ea.CallBacks, + ea.Contact, +): + pass + + +# %% +# We also define a callback class to record the position of the rod during the +# simulation. + + +class AxialStretchingCallBack(ea.CallBackBaseClass): + """ + Records the position of the rod + """ + + def __init__(self, callback_params: dict) -> None: + ea.CallBackBaseClass.__init__(self) + self.every = 200 + self.callback_params = callback_params + + def make_callback(self, system: RodType, time: float, current_step: int) -> None: + if current_step % self.every == 0: + + self.callback_params["time"].append(time) + self.callback_params["step"].append(current_step) + self.callback_params["radius"].append(system.radius.copy()) + self.callback_params["position"].append(system.position_collection.copy()) + self.callback_params["orientation"].append( + system.director_collection.copy() + ) + return + + +# %% +# The main part of the script is wrapped in an `if __name__ == "__main__"` block. +# We start by setting some options and initializing the simulator. + +simulator = SoftRodSimulator() +recorded_history: dict[str, list[Any]] = defaultdict(list) +final_time = 5 +dt = 0.0002 + +# %% +# Next, we set up the parameters for the rod, including the number of +# elements, start position, direction, normal, length, radius, density, +# and material properties. + +# setting up test params +n_elem = 50 +start = np.zeros((3,)) +direction = np.array([1.0, 0.0, 0.0]) +normal = np.array([0.0, 1.0, 0.0]) +base_length = 1.2 +base_radius = 0.025 +density = 2000 +youngs_modulus = 1e6 +poisson_ratio = 0.5 +shear_modulus = youngs_modulus / (2 * (poisson_ratio + 1.0)) + +# %% +# We create the `CosseratRod` object and add it to the simulator. + +stretchable_rod = ea.CosseratRod.straight_rod( + n_elem, + start, + direction, + normal, + base_length, + base_radius, + density, + youngs_modulus=youngs_modulus, + shear_modulus=shear_modulus, +) +simulator.append(stretchable_rod) + +# %% +# We define a function that returns the target pose (position and +# orientation) for the controller at a given time. This function creates +# the trajectory for the end of the rod to follow to tie the knot. + +run_time = 4 + + +def base_target(t: float, rod: RodType) -> Pose: + target_position = direction * base_length - 5 * base_radius * normal + if t <= run_time / 2: + ratio = min(2 * t / run_time, 1.0) + angular_ratio = ratio * np.pi * 2 + position = target_position * ratio + orientation_twist = np.array( + [ + [0, np.cos(angular_ratio), np.sin(angular_ratio)], + [0, -np.sin(angular_ratio), np.cos(angular_ratio)], + [1, 0, 0], + ], + dtype=float, + ) + else: + ratio = min(2 * (t - run_time / 2) / run_time, 1.0) + R = 8 + position = np.array( + [ + target_position[0] * (1 - ratio), + -R * base_radius * np.cos(2 * ratio * 12) * (1 - ratio), + -R * base_radius * np.sin(2 * ratio * 12) * (1 - ratio), + ] + ) + angular_ratio = (1 - ratio) * np.pi * 2 + orientation_twist = np.array( + [ + [0, np.cos(angular_ratio), -np.sin(angular_ratio)], + [0, np.sin(angular_ratio), np.cos(angular_ratio)], + [1, 0, 0], + ], + dtype=float, + ) + return position, orientation_twist + + +# %% +# We add a `TargetPoseProportionalControl` forcing to the rod. This +# controller applies forces and torques to drive a specific node of the +# rod to the target pose. + +# Control point +p = 3e3 +pt = 5e0 +simulator.add_forcing_to(stretchable_rod).using( + TargetPoseProportionalControl, + elem_index=0, + p_linear_value=p, + p_angular_value=pt, + target=base_target, + ramp_up_time=1e-6, + target_history=recorded_history["base_pose"], +) + +# %% +# We apply boundary conditions to fix the other end of the rod. + +# Boundary conditions +simulator.constrain(stretchable_rod).using( + ea.FixedConstraint, constrained_position_idx=(-1, -20) +) + +# %% +# We enable self-contact detection for the rod to prevent it from passing +# through itself. + +# Self contact +simulator.detect_contact_between(stretchable_rod, stretchable_rod).using( + ea.RodSelfContact, k=1e4, nu=3 +) + +# %% +# We add gravity and damping to the system. + +# Gravity +simulator.add_forcing_to(stretchable_rod).using( + ea.GravityForces, acc_gravity=np.array([0.0, 0.0, -9.80665]) +) + +# Damping +damping_constant = 5.0 +simulator.dampen(stretchable_rod).using( + ea.AnalyticalLinearDamper, + translational_damping_constant=damping_constant, + rotational_damping_constant=damping_constant * 0.01, + time_step=dt, +) +simulator.dampen(stretchable_rod).using(ea.LaplaceDissipationFilter, filter_order=5) + +# %% +# We set up the callback to record the simulation data. + +simulator.collect_diagnostics(stretchable_rod).using( + AxialStretchingCallBack, callback_params=recorded_history +) + +# %% +# We finalize the simulator and create the time-stepper. + +# Finalize and run the simulation +simulator.finalize() +timestepper = ea.PositionVerlet() + +# %% +# The simulation is run for the specified number of steps. + +total_steps = int(final_time / dt) +print("Total steps", total_steps) +dt = final_time / total_steps +time = 0.0 +for i in range(total_steps): + time = timestepper.step(simulator, time, dt) + +# %% +# After the simulation, we can generate a 3D video of the knot tying +# process. + +filename_video = "knot3D.mp4" +plot_video3D(recorded_history, video_name=filename_video, margin=0.2, fps=10) + +# %% +# .. video:: ../../../examples/KnotCase/knot3D.mp4 +# :width: 720 +# :autoplay: +# :muted: +# :loop: + + +# %% +# We can also plot the topological quantities of the knot, such as twist, +# writhe, and link, as a function of time. + +# Plot knot topological quantities +timestep = np.asarray(recorded_history["time"]) +positions = np.asarray(recorded_history["position"]) +orientations = np.asarray(recorded_history["orientation"]) +radii = np.asarray(recorded_history["radius"]) +total_twist, _ = ea.compute_twist(positions, orientations[:, 0, ...]) +total_writhe = ea.compute_writhe(positions, np.float64(base_length), "next_tangent") +total_link = ea.compute_link( + positions, + orientations[:, 0, ...], + radii, + np.float64(base_length), + "next_tangent", +) + +plt.figure() +plt.plot(timestep, total_twist, label="twist") +plt.plot(timestep, total_writhe, label="writhe") +plt.plot(timestep, total_link, label="link") +plt.legend() +plt.xlabel("time") +plt.ylabel("link-writhe-twist quantity") +plt.show() diff --git a/examples/TimoshenkoBeamCase/run_timoshenko.py b/examples/TimoshenkoBeamCase/run_timoshenko.py index bf8f080c..dc0d6d6e 100644 --- a/examples/TimoshenkoBeamCase/run_timoshenko.py +++ b/examples/TimoshenkoBeamCase/run_timoshenko.py @@ -22,7 +22,7 @@ from timoshenko_postprocessing import plot_timoshenko # %% -# Now that we have imported all the necessary classes, we want to create our beam system. We do this by combining all the modules we need to represent the physics that we to include in the simulation. In this case, that is the `BaseSystemCollection`, `Constraint`, `Forcings` and `Damping` because the simulation will consider a rod that is fixed in place on one end, and subject to an applied force on the other end. +# Now that we have imported all the necessary classes, we want to create our beam system. We do this by combining all the modules we need to represent the physics that we to include in the simulation. In this case, that is the ``BaseSystemCollection``, ``Constraint``, ``Forcings`` and ``Damping`` because the simulation will consider a rod that is fixed in place on one end, and subject to an applied force on the other end. class TimoshenkoBeamSimulator( @@ -40,12 +40,12 @@ class TimoshenkoBeamSimulator( # # First we define the number of elements in the rod. Next, the material properties are defined for every rod. These are the Young's modulus, the Poisson ratio, the density and the viscous damping coefficient. Finally, the geometry of the rod also needs to be defined by specifying the location of the rod and its orientation, length and radius. # -# All of the values defined here are done in SI units, though this is not strictly necessary. You can rescale properties however you want, as long as you use consistent units throughout the simulation. See [here](https://info.simuleon.com/blog/units-in-abaqus) for an example of consistent units. +# All of the values defined here are done in SI units, though this is not strictly necessary. You can rescale properties however you want, as long as you use consistent units throughout the simulation. See `here `_ for an example of consistent units. # # In order to make the difference between a shearable and unshearable rod more clear, we are using a Poisson ratio of 99. This is an unphysical value, as Poisson ratios can not exceed 0.5, however, it is used here for demonstration purposes. # setting up test params -simulation_time = 5 # 5000.0 # (sec) +simulation_time = 500 # 5000.0 # (sec) n_elem = 100 start = np.zeros((3,)) @@ -62,7 +62,7 @@ class TimoshenkoBeamSimulator( shear_modulus = E / (poisson_ratio + 1.0) # %% -# With all of the rod's parameters set, we can now create a rod with the specificed properties and add the rod to the simulator system. **Important:** Make sure that any rods you create get added to the simulator system (`timoshenko_sim`), otherwise they will not be included in your simulation. +# With all of the rod's parameters set, we can now create a rod with the specificed properties and add the rod to the simulator system. **Important:** Make sure that any rods you create get added to the simulator system (``timoshenko_sim``), otherwise they will not be included in your simulation. shearable_rod = ea.CosseratRod.straight_rod( n_elem, @@ -80,9 +80,9 @@ class TimoshenkoBeamSimulator( # %% # Adding Damping # -------------- -# With the rod added to the simulator, we can add damping to the rod. We do this using the `.dampen()` option and the `AnalyticalLinearDamper`. We are modifying `timoshenko_sim` simulator to `dampen` the `shearable_rod` object using `AnalyticalLinearDamper` type of dissipation (damping) model. +# With the rod added to the simulator, we can add damping to the rod. We do this using the ``.dampen()`` option and the ``AnalyticalLinearDamper``. We are modifying ``timoshenko_sim`` simulator to ``dampen`` the ``shearable_rod`` object using ``AnalyticalLinearDamper`` type of dissipation (damping) model. # -# We also need to define `damping_constant` and simulation `time_step` and pass in `.using()` method. +# We also need to define ``damping_constant`` and simulation ``time_step`` and pass in ``.using()`` method. dl = base_length / n_elem dt = 0.07 * dl @@ -95,9 +95,9 @@ class TimoshenkoBeamSimulator( # %% # Adding Boundary Conditions # -------------------------- -# With the rod added to the system, we need to apply boundary conditions. The first condition we will apply is fixing the location of one end of the rod. We do this using the `.constrain()` option and the `OneEndFixedRod` boundary condition. We are modifying the `timoshenko_sim` simulator to `constrain` the `shearable_rod` object using the `OneEndFixedRod` type of constraint. +# With the rod added to the system, we need to apply boundary conditions. The first condition we will apply is fixing the location of one end of the rod. We do this using the ``.constrain()`` option and the ``OneEndFixedRod`` boundary condition. We are modifying the ``timoshenko_sim`` simulator to ``constrain`` the ``shearable_rod`` object using the ``OneEndFixedRod`` type of constraint. # -# We also need to define which node of the rod is being constrained. We do this by passing the index of the nodes that we want to constain to `constrained_position_idx`. Here we are fixing the first node in the rod. In order to keep the rod from rotating around the fixed node, we also need to constrain an element between two nodes. This fixes the orientation of the rod. We do this by passing the index of the element that we want to fix to `constrained_director_idx`. Like with the position, we are fixing the first element of the rod. Together, this contrains the position and orientation of the rod at the origin. +# We also need to define which node of the rod is being constrained. We do this by passing the index of the nodes that we want to constrain to ``constrained_position_idx``. Here we are fixing the first node in the rod. In order to keep the rod from rotating around the fixed node, we also need to constrain an element between two nodes. This fixes the orientation of the rod. We do this by passing the index of the element that we want to fix to ``constrained_director_idx``. Like with the position, we are fixing the first element of the rod. Together, this constrains the position and orientation of the rod at the origin. # One end of the rod is now fixed in place timoshenko_sim.constrain(shearable_rod).using( @@ -105,7 +105,7 @@ class TimoshenkoBeamSimulator( ) # %% -# The next boundary condition that we want to apply is the endpoint force. Similarly to how we constrained one of the points, we want the `timoshenko_sim` simulator to `add_forcing_to` the `shearable_rod` object using the `EndpointForces` type of forcing. This `EndpointForces` applies forces to both ends of the rod. We want to apply a negative force in the $d_1$ direction, but only at the end of the rod. We do this by specifying the force vector to be applied at each end as `origin_force` and `end_force`. We also want to ramp up the force over time, so we make the force take some `ramp_up_time` to reach its steady-state value. This helps avoid numerical errors due to discontinuities in the applied force. +# The next boundary condition that we want to apply is the endpoint force. Similarly to how we constrained one of the points, we want the ``timoshenko_sim`` simulator to ``add_forcing_to`` the ``shearable_rod`` object using the ``EndpointForces`` type of forcing. This ``EndpointForces`` applies forces to both ends of the rod. We want to apply a negative force in the :math:`d_1` direction, but only at the end of the rod. We do this by specifying the force vector to be applied at each end as ``origin_force`` and ``end_force``. We also want to ramp up the force over time, so we make the force take some ``ramp_up_time`` to reach its steady-state value. This helps avoid numerical errors due to discontinuities in the applied force. # Forces added to the rod end_force = np.array([-15.0, 0.0, 0.0]) @@ -212,7 +212,7 @@ def make_callback(self, system, time, current_step: int): # Run Simulation # -------------- # -# We are now ready to perform the simulation. To run the simulation, we `integrate` the `timoshenko_sim` system using the `timestepper` method until `final_time` by taking `total_steps`. As currently setup, the beam simulation takes about 1 minute to run. +# We are now ready to perform the simulation. To run the simulation, we ``integrate`` the ``timoshenko_sim`` system using the ``timestepper`` method until ``final_time`` by taking ``total_steps``. As currently setup, the beam simulation takes about 1 minute to run. time = 0.0 for i in range(total_steps): @@ -221,6 +221,6 @@ def make_callback(self, system, time, current_step: int): # %% # Post Processing Results # ----------------------- -# Now that we have finished the simulation, we want to post-process the results. Post processing script is separately provided in `timoshenko_postprocessing.py`. +# Now that we have finished the simulation, we want to post-process the results. Post processing script is separately provided in ``timoshenko_postprocessing.py``. plot_timoshenko(shearable_rod, end_force, False, True) From 046df44cbac7fa984a42b2c13a4d40c394a595a2 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 26 Oct 2025 01:45:13 -0500 Subject: [PATCH 24/85] add ffmpeg in rtd --- .readthedocs.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index e0a0cc28..cb878e96 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -4,6 +4,8 @@ version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-24.04 + apt_packages: + - ffmpeg tools: { python: "3.10" } jobs: create_environment: @@ -14,6 +16,7 @@ build: install: - "true" + # Build documentation in the docs/ directory with Sphinx sphinx: builder: html From cac022952a68bfdc4bf487e862ebacea416a03e6 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 26 Oct 2025 15:13:56 -0500 Subject: [PATCH 25/85] docs: clean up tutorial for gallery --- Makefile | 6 +- .../run_axial_stretching.py | 43 ++++++------ examples/ButterflyCase/run_butterfly.py | 36 ++++++---- examples/CatenaryCase/run_catenary.py | 53 +++++++-------- examples/KnotCase/run_knot_simulation.py | 66 ++++++++++--------- 5 files changed, 111 insertions(+), 93 deletions(-) diff --git a/Makefile b/Makefile index b70f1c3b..341b5ece 100644 --- a/Makefile +++ b/Makefile @@ -112,8 +112,12 @@ pytestcache-remove: build-remove: rm -rf build/ dist/ +.PHONY: doc-remove +doc-remove: + rm -rf docs/_build docs/gen_modules/ docs/sg_execution_times.rst docs/_gallery/ + .PHONY: cleanup -cleanup: pycache-remove dsstore-remove ipynbcheckpoints-remove pytestcache-remove mypycache-remove build-remove +cleanup: pycache-remove dsstore-remove ipynbcheckpoints-remove pytestcache-remove mypycache-remove build-remove doc-remove all: format-codestyle cleanup test diff --git a/examples/AxialStretchingCase/run_axial_stretching.py b/examples/AxialStretchingCase/run_axial_stretching.py index 098258b3..61489b0e 100644 --- a/examples/AxialStretchingCase/run_axial_stretching.py +++ b/examples/AxialStretchingCase/run_axial_stretching.py @@ -2,9 +2,11 @@ Axial Stretching ================ -This case tests the axial stretching of a rod. A rod is fixed at one end and -a force is applied at the other end. The rod stretches and the displacement -of the tip is compared with the analytical solution. +This case tests the axial stretching of a rod. +The expected behavior is supposed to be like a spring-gravity motion, but +with a rod. A rod is fixed at one end and a force is applied at the other +end. The rod stretches and the displacement of the tip is compared with +the analytical solution. """ # isort:skip_file @@ -15,8 +17,10 @@ import elastica as ea # %% -# First, we define a simulator class that inherits from the necessary mixins. -# This makes it easy to add constraints, forces, and damping to the system. +# Simulation Setup +# ---------------- +# We define a simulator class that inherits from the necessary mixins. +# This makes constraints, forces, and damping evailable to the system. class StretchingBeamSimulator( @@ -29,9 +33,13 @@ class StretchingBeamSimulator( final_time = 200.0 # %% -# Next, we set up the test parameters for the simulation. This includes the +# Rod Setup +# --------- +# Next, we set up the test parameters for the simulating rods. This includes the # number of elements, the start position, direction, normal, length, radius, # density, and Young's modulus of the rod. +# For this case, we have fixed boundary condition at one end, and we apply external +# force at the other end. # setting up test params n_elem = 19 @@ -47,10 +55,6 @@ class StretchingBeamSimulator( poisson_ratio = 0.5 shear_modulus = youngs_modulus / (poisson_ratio + 1.0) -# %% -# Now we can create the `CosseratRod` object. We use the `straight_rod` method -# to create a straight rod with the specified parameters. - stretchable_rod = ea.CosseratRod.straight_rod( n_elem, start, @@ -65,18 +69,10 @@ class StretchingBeamSimulator( stretch_sim.append(stretchable_rod) -# %% -# We then apply a boundary condition to fix one end of the rod. We use the -# `OneEndFixedBC` constraint to fix the position and director of the first node. - stretch_sim.constrain(stretchable_rod).using( ea.OneEndFixedBC, constrained_position_idx=(0,), constrained_director_idx=(0,) ) -# %% -# A force is applied to the other end of the rod. We use the `EndpointForces` -# forcing to apply a force in the x-direction. - end_force_x = 1.0 end_force = np.array([end_force_x, 0.0, 0.0]) stretch_sim.add_forcing_to(stretchable_rod).using( @@ -99,7 +95,9 @@ class StretchingBeamSimulator( # %% -# We define a callback class to record the position and velocity of the rod +# Callbacks +# --------- +# A callback object is passed to the simulator to record states of the rod # during the simulation. This is useful for post-processing the results. @@ -137,6 +135,8 @@ def make_callback( ) # %% +# Finalize and Run +# ---------------- # We finalize the simulator and create the time-stepper. The `PositionVerlet` # time-stepper is used to integrate the system. @@ -144,9 +144,6 @@ def make_callback( timestepper: ea.typing.StepperProtocol = ea.PositionVerlet() # timestepper = PEFRL() -# %% -# The simulation is run for the specified `final_time`. - total_steps = int(final_time / dt) print("Total steps", total_steps) dt = final_time / total_steps @@ -155,6 +152,8 @@ def make_callback( time = timestepper.step(stretch_sim, time, dt) # %% +# Post-Processing +# --------------- # Finally, we plot the results and compare them with the analytical solution. # The analytical solution is calculated using the first-order theory with # both the base length and the modified length. diff --git a/examples/ButterflyCase/run_butterfly.py b/examples/ButterflyCase/run_butterfly.py index 42561480..85ca10a4 100644 --- a/examples/ButterflyCase/run_butterfly.py +++ b/examples/ButterflyCase/run_butterfly.py @@ -3,8 +3,13 @@ ========= This case simulates the motion of a rod that is initially shaped like a -butterfly. The rod is released from rest and allowed to fall under gravity. +butterfly. The rod is released from rest and allowed to deform freely. +The goal of the simulation is for sanity check: how does the timestepper +reliably preserve total energy of the system, when the system is simple Hamiltonian. The simulation tracks the position and energy of the rod over time. + +This example case also demonstrate how to setup rod with customized +positions and directors. """ import numpy as np @@ -16,7 +21,9 @@ from elastica.utils import MaxDimension # %% -# First, we define a simulator class that inherits from the necessary mixins. +# Simulation Setup +# ---------------- +# We define a simulator class that inherits from the necessary mixins. class ButterflySimulator(ea.BaseSystemCollection, ea.CallBacks): @@ -27,9 +34,9 @@ class ButterflySimulator(ea.BaseSystemCollection, ea.CallBacks): final_time = 40.0 # %% -# Next, we set up the test parameters for the simulation. This includes the -# number of elements, the origin, the angle of inclination, the length, -# radius, density, and Young's modulus of the rod. +# Rod Setup +# --------- +# Next, we set up the test parameters for the simulation. # setting up test params # FIXME : Doesn't work with elements > 10 (the inverse rotate kernel fails) @@ -95,7 +102,9 @@ class ButterflySimulator(ea.BaseSystemCollection, ea.CallBacks): # %% -# We define a callback class to record the position and energy of the rod +# Callback Setup +# -------------- +# A callback object is defined to record the position and energy of the rod # during the simulation. @@ -127,7 +136,9 @@ def make_callback( return +# database recorded_history: dict[str, list] = ea.defaultdict(list) + # initially record history recorded_history["time"].append(0.0) recorded_history["position"].append(butterfly_rod.position_collection.copy()) @@ -141,6 +152,8 @@ def make_callback( ) # %% +# Finalize and Run +# ---------------- # We finalize the simulator and create the time-stepper. butterfly_sim.finalize() @@ -148,9 +161,6 @@ def make_callback( timestepper = ea.PositionVerlet() # timestepper = PEFRL() -# %% -# The simulation is run for the specified `final_time`. - dt = 0.01 * dl total_steps = int(final_time / dt) print("Total steps", total_steps) @@ -160,8 +170,10 @@ def make_callback( time = timestepper.step(butterfly_sim, time, dt) # %% -# Finally, we plot the results. The position of the rod is plotted at -# different time steps, and the energies are plotted as a function of time. +# Post-Processing +# --------------- +# The position of the rod is plotted at different time steps, +# and the energies are plotted as a function of time. # Plot the histories fig = plt.figure(figsize=(5, 4), frameon=True, dpi=150) @@ -180,6 +192,8 @@ def make_callback( # don't block fig.show() +# %% + # Plot the energies energy_fig = plt.figure(figsize=(5, 4), frameon=True, dpi=150) energy_ax = energy_fig.add_subplot(111) diff --git a/examples/CatenaryCase/run_catenary.py b/examples/CatenaryCase/run_catenary.py index 9da23e53..721b7848 100644 --- a/examples/CatenaryCase/run_catenary.py +++ b/examples/CatenaryCase/run_catenary.py @@ -18,7 +18,9 @@ ) # %% -# First, we define a simulator class that inherits from the necessary mixins. +# Simulation Setup +# ---------------- +# We define a simulator class that inherits from the necessary mixins. class CatenarySimulator( @@ -28,24 +30,19 @@ class CatenarySimulator( catenary_sim = CatenarySimulator() +final_time = 30 # %% -# Next, we set up the simulation parameters. This includes the final time, -# damping constant, time step, and rendering frame rate. +# Rod Setup +# --------- +# We set up the rod parameters. This rod is affected by a gravity force. -final_time = 10 -damping_constant = 0.3 +n_elem = 500 time_step = 1e-4 total_steps = int(final_time / time_step) rendering_fps = 20 step_skip = int(1.0 / (rendering_fps * time_step)) -# %% -# We then define the properties of the rod, such as the number of elements, -# start position, direction, normal, length, radius, and material properties. - -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]) @@ -62,9 +59,6 @@ class CatenarySimulator( poisson_ratio = 0.5 shear_modulus = E / (poisson_ratio + 1.0) -# %% -# Now we can create the `CosseratRod` object and add it to the simulator. - base_rod = ea.CosseratRod.straight_rod( n_elem, start, @@ -79,10 +73,16 @@ class CatenarySimulator( catenary_sim.append(base_rod) +# Add gravity +catenary_sim.add_forcing_to(base_rod).using( + ea.GravityForces, acc_gravity=-9.80665 * normal +) + # %% # Damping is added to the system to help it reach a steady state. # add damping +damping_constant = 0.3 catenary_sim.dampen(base_rod).using( ea.AnalyticalLinearDamper, damping_constant=damping_constant, @@ -90,14 +90,8 @@ class CatenarySimulator( ) # %% -# We add gravity to the system to simulate the weight of the rod. - -# Add gravity -catenary_sim.add_forcing_to(base_rod).using( - ea.GravityForces, acc_gravity=-9.80665 * normal -) - -# %% +# Boundary Conditions +# ------------------- # We fix both ends of the rod using the `FixedConstraint`. # fix catenary ends @@ -107,10 +101,10 @@ class CatenarySimulator( constrained_director_idx=(0, -1), ) - # %% -# We define a callback class to record the position, radius, and internal -# forces of the rod during the simulation. +# Callback +# -------- +# We define a callback class to record the rod state during the simulation. # Add call backs @@ -145,14 +139,13 @@ def make_callback( ) # %% -# We finalize the simulator and create the time-stepper. +# Finalize and Run +# ---------------- +# We finalize the simulator, create the time-stepper, and run. catenary_sim.finalize() timestepper: ea.typing.StepperProtocol = ea.PositionVerlet() -# %% -# The simulation is run for the specified number of steps. - dt = final_time / total_steps time = 0.0 for i in range(total_steps): @@ -161,6 +154,8 @@ def make_callback( b = np.min(position[-1][2]) # %% +# Post-processing +# --------------- # Finally, we can save a video of the simulation and plot the final # shape of the catenary. diff --git a/examples/KnotCase/run_knot_simulation.py b/examples/KnotCase/run_knot_simulation.py index c4fb4fcf..9bc504f8 100644 --- a/examples/KnotCase/run_knot_simulation.py +++ b/examples/KnotCase/run_knot_simulation.py @@ -4,7 +4,7 @@ This script simulates the formation of an overhand knot in a soft rod. It demonstrates how to create a controller to manipulate a node on the rod, -which can be used for tasks like trajectory tracing or model predictive control. +which can be used for tasks like trajectory tracing or proportional control. """ from typing import Any, TypeAlias @@ -24,8 +24,9 @@ Pose: TypeAlias = tuple[Position, Orientation] # %% -# First, we define a simulator class that inherits from the necessary mixins -# for constraints, forcing, damping, callbacks, and contact. +# Simulation Setup +# ---------------- +# We define a simulator class that inherits from the necessary mixins. class SoftRodSimulator( @@ -39,12 +40,19 @@ class SoftRodSimulator( pass +simulator = SoftRodSimulator() +final_time = 5 +dt = 0.0002 + + # %% +# Callback Setup +# -------------- # We also define a callback class to record the position of the rod during the # simulation. -class AxialStretchingCallBack(ea.CallBackBaseClass): +class Callback(ea.CallBackBaseClass): """ Records the position of the rod """ @@ -67,19 +75,12 @@ def make_callback(self, system: RodType, time: float, current_step: int) -> None return -# %% -# The main part of the script is wrapped in an `if __name__ == "__main__"` block. -# We start by setting some options and initializing the simulator. - -simulator = SoftRodSimulator() recorded_history: dict[str, list[Any]] = defaultdict(list) -final_time = 5 -dt = 0.0002 # %% -# Next, we set up the parameters for the rod, including the number of -# elements, start position, direction, normal, length, radius, density, -# and material properties. +# Rod Setup +# --------- +# Next, we set up the parameters for the rod. # setting up test params n_elem = 50 @@ -93,9 +94,7 @@ def make_callback(self, system: RodType, time: float, current_step: int) -> None poisson_ratio = 0.5 shear_modulus = youngs_modulus / (2 * (poisson_ratio + 1.0)) -# %% # We create the `CosseratRod` object and add it to the simulator. - stretchable_rod = ea.CosseratRod.straight_rod( n_elem, start, @@ -109,18 +108,24 @@ def make_callback(self, system: RodType, time: float, current_step: int) -> None ) simulator.append(stretchable_rod) +simulator.collect_diagnostics(stretchable_rod).using( + Callback, callback_params=recorded_history +) + # %% +# Controller Setup +# ---------------- # We define a function that returns the target pose (position and # orientation) for the controller at a given time. This function creates # the trajectory for the end of the rod to follow to tie the knot. -run_time = 4 +activation_time = 4 def base_target(t: float, rod: RodType) -> Pose: target_position = direction * base_length - 5 * base_radius * normal - if t <= run_time / 2: - ratio = min(2 * t / run_time, 1.0) + if t <= activation_time / 2: + ratio = min(2 * t / activation_time, 1.0) angular_ratio = ratio * np.pi * 2 position = target_position * ratio orientation_twist = np.array( @@ -132,7 +137,7 @@ def base_target(t: float, rod: RodType) -> Pose: dtype=float, ) else: - ratio = min(2 * (t - run_time / 2) / run_time, 1.0) + ratio = min(2 * (t - activation_time / 2) / activation_time, 1.0) R = 8 position = np.array( [ @@ -156,7 +161,7 @@ def base_target(t: float, rod: RodType) -> Pose: # %% # We add a `TargetPoseProportionalControl` forcing to the rod. This # controller applies forces and torques to drive a specific node of the -# rod to the target pose. +# rod to the target pose. The class is defined in `knot_forcing.py`. # Control point p = 3e3 @@ -172,6 +177,8 @@ def base_target(t: float, rod: RodType) -> Pose: ) # %% +# Boundary Conditions +# ------------------- # We apply boundary conditions to fix the other end of the rod. # Boundary conditions @@ -180,6 +187,8 @@ def base_target(t: float, rod: RodType) -> Pose: ) # %% +# Contact Setup +# ------------- # We enable self-contact detection for the rod to prevent it from passing # through itself. @@ -189,6 +198,8 @@ def base_target(t: float, rod: RodType) -> Pose: ) # %% +# Environmental Forcing and Damping +# --------------------------------- # We add gravity and damping to the system. # Gravity @@ -206,23 +217,16 @@ def base_target(t: float, rod: RodType) -> Pose: ) simulator.dampen(stretchable_rod).using(ea.LaplaceDissipationFilter, filter_order=5) -# %% -# We set up the callback to record the simulation data. - -simulator.collect_diagnostics(stretchable_rod).using( - AxialStretchingCallBack, callback_params=recorded_history -) # %% +# Finalize and Run +# ---------------- # We finalize the simulator and create the time-stepper. # Finalize and run the simulation simulator.finalize() timestepper = ea.PositionVerlet() -# %% -# The simulation is run for the specified number of steps. - total_steps = int(final_time / dt) print("Total steps", total_steps) dt = final_time / total_steps @@ -231,6 +235,8 @@ def base_target(t: float, rod: RodType) -> Pose: time = timestepper.step(simulator, time, dt) # %% +# Post-Processing +# --------------- # After the simulation, we can generate a 3D video of the knot tying # process. From 798b8dd08f732c5c12002395684901607f6558ef Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Mon, 27 Oct 2025 21:27:38 -0500 Subject: [PATCH 26/85] fix: re-write sphere-rod contact; add detailed docstrings --- elastica/_contact_functions.py | 78 +++++++++++++++++++--------------- elastica/contact_forces.py | 12 +----- 2 files changed, 46 insertions(+), 44 deletions(-) diff --git a/elastica/_contact_functions.py b/elastica/_contact_functions.py index c8c9670c..c8462bbc 100644 --- a/elastica/_contact_functions.py +++ b/elastica/_contact_functions.py @@ -4,6 +4,7 @@ _dot_product, _norm, _find_min_dist, + _find_min_dist_cylinder_sphere, _find_slipping_elements, _node_to_element_mass_or_force, _elements_to_nodes_inplace, @@ -49,6 +50,13 @@ def _calculate_contact_forces_rod_cylinder( velocity_damping_coefficient: np.float64, friction_coefficient: np.float64, ) -> None: + """ + Calculates the contact forces between a rod and a cylinder. + + This function computes the linear and angular contact forces acting on both the rod and the cylinder + when they come into contact. It considers spring-damper-based contact forces as well as friction. + The forces are applied to the nodes of the rod and the center of mass of the cylinder. + """ # We already pass in only the first n_elem x n_points = x_collection_rod.shape[1] cylinder_total_contact_forces = np.zeros((3)) @@ -174,6 +182,9 @@ def _calculate_contact_forces_rod_rod( contact_k: np.float64, contact_nu: np.float64, ) -> None: + """ + Calculates the contact forces between two rods. + """ # We already pass in only the first n_elem x n_points_rod_one = x_collection_rod_one.shape[1] n_points_rod_two = x_collection_rod_two.shape[1] @@ -283,6 +294,12 @@ def _calculate_contact_forces_self_rod( contact_k: np.float64, contact_nu: np.float64, ) -> None: + """ + Calculates the self-contact forces within a single rod. + + This function prevents self-penetration of a rod by calculating contact forces between different elements + of the same rod. A skip parameter is used to avoid checking adjacent elements. + """ # We already pass in only the first n_elem x n_points_rod = x_collection_rod.shape[1] edge_collection_rod_one = _batch_product_k_ik_to_ik(length_rod, tangent_rod) @@ -364,16 +381,11 @@ def _calculate_contact_forces_self_rod( def _calculate_contact_forces_rod_sphere( x_collection_rod: NDArray[np.float64], edge_collection_rod: NDArray[np.float64], - x_sphere_center: NDArray[np.float64], - x_sphere_tip: NDArray[np.float64], - edge_sphere: NDArray[np.float64], + x_sphere: NDArray[np.float64], radii_sum: NDArray[np.float64], length_sum: NDArray[np.float64], - internal_forces_rod: NDArray[np.float64], external_forces_rod: NDArray[np.float64], external_forces_sphere: NDArray[np.float64], - external_torques_sphere: NDArray[np.float64], - sphere_director_collection: NDArray[np.float64], velocity_rod: NDArray[np.float64], velocity_sphere: NDArray[np.float64], contact_k: np.float64, @@ -381,15 +393,21 @@ def _calculate_contact_forces_rod_sphere( velocity_damping_coefficient: np.float64, friction_coefficient: np.float64, ) -> None: + """ + Calculates the contact forces between a rod and a sphere. + + This function computes the linear and angular contact forces acting on both the rod and the sphere + when they come into contact. It considers spring-damper-based contact forces as well as friction. + The forces are applied to the nodes of the rod and the center of mass of the sphere. + """ # We already pass in only the first n_elem x n_points = x_collection_rod.shape[1] sphere_total_contact_forces = np.zeros((3)) - sphere_total_contact_torques = np.zeros((3)) for i in range(n_points): # Element-wise bounding box x_selected = x_collection_rod[..., i] # x_sphere is already a (,) array from outside - del_x = x_selected - x_sphere_tip + del_x = x_selected - x_sphere norm_del_x = _norm(del_x) # If outside then don't process @@ -397,8 +415,10 @@ def _calculate_contact_forces_rod_sphere( continue # find the shortest line segment between the two centerline - distance_vector, x_sphere_contact_point, _ = _find_min_dist( - x_selected, edge_collection_rod[..., i], x_sphere_tip, edge_sphere + distance_vector, _, _ = _find_min_dist_cylinder_sphere( + x_selected, + edge_collection_rod[..., i], + x_sphere, ) distance_vector_length = _norm(distance_vector) distance_vector /= distance_vector_length @@ -453,37 +473,22 @@ def _calculate_contact_forces_rod_sphere( # Update contact force net_contact_force += friction_force - # Torques acting on the cylinder - moment_arm = x_sphere_contact_point - x_sphere_center - # Add it to the rods at the end of the day if i == 0: external_forces_rod[..., i] -= 2 / 3 * net_contact_force external_forces_rod[..., i + 1] -= 4 / 3 * net_contact_force sphere_total_contact_forces += 2.0 * net_contact_force - sphere_total_contact_torques += np.cross( - moment_arm, 2.0 * net_contact_force - ) elif i == n_points - 1: external_forces_rod[..., i] -= 4 / 3 * net_contact_force external_forces_rod[..., i + 1] -= 2 / 3 * net_contact_force sphere_total_contact_forces += 2.0 * net_contact_force - sphere_total_contact_torques += np.cross( - moment_arm, 2.0 * net_contact_force - ) else: external_forces_rod[..., i] -= net_contact_force external_forces_rod[..., i + 1] -= net_contact_force sphere_total_contact_forces += 2.0 * net_contact_force - sphere_total_contact_torques += np.cross( - moment_arm, 2.0 * net_contact_force - ) # Update the cylinder external forces and torques external_forces_sphere[..., 0] += sphere_total_contact_forces - external_torques_sphere[..., 0] += ( - sphere_director_collection @ sphere_total_contact_torques - ) @njit(cache=True) # type: ignore @@ -501,17 +506,16 @@ def _calculate_contact_forces_rod_plane( external_forces: NDArray[np.float64], ) -> tuple[NDArray[np.float64], NDArray[np.intp]]: """ - This function computes the plane force response on the element, in the - case of contact. Contact model given in Eqn 4.8 Gazzola et. al. RSoS 2018 paper + Calculates the contact forces between a rod and a plane. + + This function computes the contact force exerted by a flat plane on a rod. + It includes a linear spring-damper model for the normal force to handle penetration + and a response to prevent the rod from passing through the plane. + It returns the magnitude of the plane response force and indices of elements not in contact. + + 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 @@ -597,6 +601,9 @@ def _calculate_contact_forces_rod_plane_with_anisotropic_friction( internal_torques: NDArray[np.float64], external_torques: NDArray[np.float64], ) -> None: + """ + Calculates contact forces between a rod and a plane with anisotropic friction. + """ ( plane_response_force_mag, no_contact_point_idx, @@ -796,6 +803,9 @@ def _calculate_contact_forces_cylinder_plane( velocity_collection: NDArray[np.float64], external_forces: NDArray[np.float64], ) -> tuple[NDArray[np.float64], NDArray[np.intp]]: + """ + Calculates the contact forces between a cylinder and a plane. + """ # Compute plane response force # total_forces = system.internal_forces + system.external_forces diff --git a/elastica/contact_forces.py b/elastica/contact_forces.py index 93889b43..49b18c4a 100644 --- a/elastica/contact_forces.py +++ b/elastica/contact_forces.py @@ -433,10 +433,7 @@ def apply_contact( ): return - x_sph = ( - system_two.position_collection[..., 0] - - system_two.radius * system_two.director_collection[2, :, 0] - ) + sphere_position = system_two.position_collection[..., 0] rod_element_position = 0.5 * ( system_one.position_collection[..., 1:] @@ -445,16 +442,11 @@ def apply_contact( _calculate_contact_forces_rod_sphere( rod_element_position, system_one.lengths * system_one.tangents, - system_two.position_collection[..., 0], - x_sph, - system_two.radius * system_two.director_collection[2, :, 0], + sphere_position, system_one.radius + system_two.radius, system_one.lengths + 2 * system_two.radius, - system_one.internal_forces, system_one.external_forces, system_two.external_forces, - system_two.external_torques, - system_two.director_collection[:, :, 0], system_one.velocity_collection, system_two.velocity_collection, self.k, From c0b6e83538ec261f74d10e8ad2ee5eb1fcc5b634 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Mon, 27 Oct 2025 21:29:29 -0500 Subject: [PATCH 27/85] fix: bug for rod-sphere aabb contact detection --- elastica/contact_utils.py | 53 +++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/elastica/contact_utils.py b/elastica/contact_utils.py index bcb57701..d329db79 100644 --- a/elastica/contact_utils.py +++ b/elastica/contact_utils.py @@ -41,6 +41,9 @@ def _find_min_dist( x2: NDArray[np.float64], e2: NDArray[np.float64], ) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]: + """ + Find the minimum distance between two centerline segments: (x1, edge1) and (x2, edge2). + """ e1e1 = _dot_product(e1, e1) # type: ignore e1e2 = _dot_product(e1, e2) # type: ignore e2e2 = _dot_product(e2, e2) # type: ignore @@ -109,7 +112,7 @@ def _find_min_dist( def _aabbs_not_intersecting( aabb_one: NDArray[np.float64], aabb_two: NDArray[np.float64] ) -> Literal[1, 0]: - """Returns true if not intersecting else false""" + """Checks if two axis-aligned bounding boxes (AABBs) are not intersecting.""" if (aabb_one[0, 1] < aabb_two[0, 0]) | (aabb_one[0, 0] > aabb_two[0, 1]): return 1 if (aabb_one[1, 1] < aabb_two[1, 0]) | (aabb_one[1, 0] > aabb_two[1, 1]): @@ -130,7 +133,13 @@ def _prune_using_aabbs_rod_cylinder( cylinder_radius: NDArray[np.float64], cylinder_length: NDArray[np.float64], ) -> Literal[1, 0]: - max_possible_dimension = np.zeros((3,)) + """ + Prunes broad-phase collision detection between a rod and a cylinder using AABBs. + + This function checks for intersection between the axis-aligned bounding boxes (AABBs) + of a rod and a cylinder. It's a quick way to rule out collision without performing + a more detailed and expensive check. + """ aabb_rod = np.empty((3, 2)) aabb_cylinder = np.empty((3, 2)) max_possible_dimension[...] = np.max(rod_one_radius_collection) + np.max( @@ -209,33 +218,29 @@ def _prune_using_aabbs_rod_sphere( sphere_director: NDArray[np.float64], sphere_radius: NDArray[np.float64], ) -> Literal[1, 0]: - max_possible_dimension = np.zeros((3,)) + """ + Prunes broad-phase collision detection between a rod and a sphere using AABBs. + + This function checks for intersection between the axis-aligned bounding boxes (AABBs) + of a rod and a sphere. It's a quick way to rule out collision without performing + a more detailed and expensive check. + """ + # AABB for rod aabb_rod = np.empty((3, 2)) - aabb_sphere = np.empty((3, 2)) - max_possible_dimension[...] = np.max(rod_one_radius_collection) + np.max( - rod_one_length_collection - ) + # Taking max radius of rod + max_rod_radius = np.max(rod_one_radius_collection) for i in range(3): - aabb_rod[i, 0] = ( - np.min(rod_one_position_collection[i]) - max_possible_dimension[i] - ) - aabb_rod[i, 1] = ( - np.max(rod_one_position_collection[i]) + max_possible_dimension[i] - ) + aabb_rod[i, 0] = np.min(rod_one_position_collection[i]) - max_rod_radius + aabb_rod[i, 1] = np.max(rod_one_position_collection[i]) + max_rod_radius - sphere_dimensions_in_local_FOR = np.array( - [sphere_radius, sphere_radius, sphere_radius] - ) - sphere_dimensions_in_world_FOR = np.zeros_like(sphere_dimensions_in_local_FOR) + # AABB for sphere + aabb_sphere = np.empty((3, 2)) + # A sphere is symmetrical, so its AABB is easy to compute. + # The director is not needed. for i in range(3): - for j in range(3): - sphere_dimensions_in_world_FOR[i] += ( - sphere_director[j, i, 0] * sphere_dimensions_in_local_FOR[j] - ) + aabb_sphere[i, 0] = sphere_position[i, 0] - sphere_radius + aabb_sphere[i, 1] = sphere_position[i, 0] + sphere_radius - max_possible_dimension = np.abs(sphere_dimensions_in_world_FOR) - aabb_sphere[..., 0] = sphere_position[..., 0] - max_possible_dimension - aabb_sphere[..., 1] = sphere_position[..., 0] + max_possible_dimension return _aabbs_not_intersecting(aabb_sphere, aabb_rod) From 3b53b11acd9dc0fb9ce9c80c76760755b354fe6d Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Mon, 27 Oct 2025 21:32:44 -0500 Subject: [PATCH 28/85] feat: add function to find minimum distance between cylinder and sphere --- elastica/contact_utils.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/elastica/contact_utils.py b/elastica/contact_utils.py index d329db79..22000e00 100644 --- a/elastica/contact_utils.py +++ b/elastica/contact_utils.py @@ -108,6 +108,28 @@ def _find_min_dist( return x2 + s * e2 - x1 - t * e1, x2 + s * e2, x1 - t * e1 +@numba.njit(cache=True) # type: ignore +def _find_min_dist_cylinder_sphere( + x1: NDArray[np.float64], + e1: NDArray[np.float64], + x2: NDArray[np.float64], +) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]: + """ + Find the minimum distance between centerline segment and point: (x1, edge1) and (x2). + """ + e1e1 = _dot_product(e1, e1) # type: ignore + x1e1 = _dot_product(x1, e1) # type: ignore + x2e1 = _dot_product(e1, x2) # type: ignore + + # Parametrization + s = 0.0 + t = (x2e1 - x1e1) / e1e1 # Comes from taking dot of e1 with a normal + t = _clip(t, 0.0, 1.0) + + # Return distance, contact point of system 2, contact point of system 1 + return x2 - x1 - t * e1, x2, x1 - t * e1 + + @numba.njit(cache=True) # type: ignore def _aabbs_not_intersecting( aabb_one: NDArray[np.float64], aabb_two: NDArray[np.float64] From 510d4f4378040b6ffef82cdca2531f81316c99e6 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Mon, 27 Oct 2025 21:51:43 -0500 Subject: [PATCH 29/85] feat: add rod-sphere contact simulation example and improve documentation --- elastica/_contact_functions.py | 2 +- elastica/contact_utils.py | 8 + .../RodRigidBodyContact/rod_sphere_contact.py | 198 ++++++++++++++++++ 3 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 examples/RigidBodyCases/RodRigidBodyContact/rod_sphere_contact.py diff --git a/elastica/_contact_functions.py b/elastica/_contact_functions.py index c8462bbc..264861a6 100644 --- a/elastica/_contact_functions.py +++ b/elastica/_contact_functions.py @@ -512,7 +512,7 @@ def _calculate_contact_forces_rod_plane( It includes a linear spring-damper model for the normal force to handle penetration and a response to prevent the rod from passing through the plane. It returns the magnitude of the plane response force and indices of elements not in contact. - + Contact model given in Eqn 4.8 Gazzola et. al. RSoS 2018 paper is used. diff --git a/elastica/contact_utils.py b/elastica/contact_utils.py index 22000e00..f0a70df5 100644 --- a/elastica/contact_utils.py +++ b/elastica/contact_utils.py @@ -162,6 +162,7 @@ def _prune_using_aabbs_rod_cylinder( of a rod and a cylinder. It's a quick way to rule out collision without performing a more detailed and expensive check. """ + max_possible_dimension = np.zeros((3,)) aabb_rod = np.empty((3, 2)) aabb_cylinder = np.empty((3, 2)) max_possible_dimension[...] = np.max(rod_one_radius_collection) + np.max( @@ -202,6 +203,13 @@ def _prune_using_aabbs_rod_rod( rod_two_radius_collection: NDArray[np.float64], rod_two_length_collection: NDArray[np.float64], ) -> Literal[1, 0]: + """ + Prunes broad-phase collision detection between two rods using AABBs. + + This function checks for intersection between the axis-aligned bounding boxes (AABBs) + of two rods. It's a quick way to rule out collision without performing + a more detailed and expensive check. + """ max_possible_dimension = np.zeros((3,)) aabb_rod_one = np.empty((3, 2)) aabb_rod_two = np.empty((3, 2)) diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_sphere_contact.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_sphere_contact.py new file mode 100644 index 00000000..f0c7580f --- /dev/null +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_sphere_contact.py @@ -0,0 +1,198 @@ +import numpy as np +from tqdm import tqdm +import elastica as ea +from post_processing import plot_video_with_surface + +start = np.zeros((3,)) +direction = np.array([0.0, 1.0, 0.0]) +normal = np.array([0.0, 0.0, 1.0]) +base_length = 0.5 +base_radius = 0.1 + +sphere_radius = 0.10 +overlap_perc = 0.9 +sphere_center = np.array( + [(base_radius + sphere_radius) * overlap_perc, base_length / 2, 0.0] +) + + +def rotate_random_axis_and_angle(R): + # return R + from scipy.spatial.transform import Rotation + + axis = np.random.rand(3) + axis /= np.linalg.norm(axis) + angle = np.random.rand() * 2 * np.pi + return R @ Rotation.from_rotvec(angle * axis).as_matrix() + + +def main(): + class Simulator( + ea.BaseSystemCollection, + ea.Constraints, + ea.Contact, + ea.CallBacks, + ea.Forcing, + ea.Damping, + ): + pass + + simulator = Simulator() + + # time step etc + final_time = 1.0 + time_step = 5e-4 + total_steps = int(final_time / time_step) + 1 + rendering_fps = 30 # 20 * 1e1 + step_skip = 100 + + density = 1000 + E = 3e5 + n_elem = 50 + + rod = ea.CosseratRod.straight_rod( + n_elem, + start, + direction, + normal, + base_length, + base_radius, + density, + youngs_modulus=E, + ) + simulator.append(rod) + + simulator.constrain(rod).using( + ea.FixedConstraint, + constrained_position_idx=(0, -1), + constrained_director_idx=(0, -1), + ) + + density = 1000 + rr = rotate_random_axis_and_angle(np.eye(3)) + rigid_body = ea.Sphere(sphere_center, sphere_radius, density) + rigid_body.director_collection[0] = rr[0][:, None] + rigid_body.director_collection[1] = rr[1][:, None] + rigid_body.director_collection[2] = rr[2][:, None] + simulator.append(rigid_body) + + # Add contact between rigid body and rod + simulator.detect_contact_between(rod, rigid_body).using( + ea.RodSphereContact, k=5e4, nu=0.0 + ) + + # add damping + damping_constant = 1e-1 + simulator.dampen(rod).using( + ea.AnalyticalLinearDamper, + damping_constant=damping_constant, + time_step=time_step, + ) + # simulator.dampen(rigid_body).using( + # ea.AnalyticalLinearDamper, + # damping_constant=damping_constant, + # time_step=time_step, + # ) + + # Add callbacks + post_processing_dict_list = [] + + # For rod + class StraightRodCallBack(ea.CallBackBaseClass): + """ + Call back function for two arm octopus + """ + + def __init__(self, step_skip: int, callback_params: dict): + ea.CallBackBaseClass.__init__(self) + self.every = step_skip + self.callback_params = callback_params + + def make_callback(self, system, time, current_step: int): + if current_step % self.every == 0: + self.callback_params["time"].append(time) + self.callback_params["step"].append(current_step) + self.callback_params["position"].append( + system.position_collection.copy() + ) + self.callback_params["radius"].append(system.radius.copy()) + self.callback_params["com"].append( + system.compute_position_center_of_mass() + ) + total_energy = ( + system.compute_translational_energy() + + system.compute_rotational_energy() + + system.compute_bending_energy() + + system.compute_shear_energy() + ) + self.callback_params["total_energy"].append(total_energy) + return + + class RigidBodyCallback(ea.CallBackBaseClass): + """ + Call back function for two arm octopus + """ + + def __init__( + self, + step_skip: int, + callback_params: dict, + ): + ea.CallBackBaseClass.__init__(self) # TODO: Is this necessary? + 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["director"].append( + system.director_collection.copy() + ) + self.callback_params["radius"].append(np.array([system.radius.copy()])) + self.callback_params["com"].append( + system.compute_position_center_of_mass() + ) + + post_processing_dict_list.append(ea.defaultdict(list)) + simulator.collect_diagnostics(rod).using( + StraightRodCallBack, + step_skip=step_skip, + callback_params=post_processing_dict_list[0], + ) + # For rigid body + post_processing_dict_list.append(ea.defaultdict(list)) + simulator.collect_diagnostics(rigid_body).using( + RigidBodyCallback, + step_skip=step_skip, + callback_params=post_processing_dict_list[1], + ) + simulator.finalize() + + timestepper = ea.PositionVerlet() + + time = 0.0 + for i in tqdm(range(total_steps), disable=True): + time = timestepper.step(simulator, time, time_step) + + # Plot the rods + plot_video_with_surface( + post_processing_dict_list, + video_name="rod_sphere_contact.mp4", + fps=rendering_fps, + step=1, + # The following parameters are optional + x_limits=(-base_length * 5, base_length * 5), # Set bounds on x-axis + y_limits=(-base_length * 5, base_length * 5), # Set bounds on y-axis + z_limits=(-base_length * 5, base_length * 5), # Set bounds on z-axis + dpi=100, # Set the quality of the image + vis3D=True, # Turn on 3D visualization + vis2D=True, # Turn on projected (2D) visualization + ) + + +if __name__ == "__main__": + main() From 8fa4c92033027681f2799ef6a34a48674e992cee Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Mon, 27 Oct 2025 21:58:40 -0500 Subject: [PATCH 30/85] test: add rod-sphere contact tests for collision scenarios --- tests/test_contact_classes.py | 86 +++++++++++++++++++++++++++++++++ tests/test_contact_functions.py | 7 +-- tests/test_contact_utils.py | 1 + 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/tests/test_contact_classes.py b/tests/test_contact_classes.py index f99a6f3f..e69a0ee6 100644 --- a/tests/test_contact_classes.py +++ b/tests/test_contact_classes.py @@ -547,6 +547,92 @@ def test_contact_rod_sphere_with_collision_with_k_without_nu_and_friction( atol=1e-6, ) + def test_contact_rod_sphere_without_collision( + self, + ): + "Testing Rod Sphere Contact wrapper without Collision" + + mock_rod = MockRod() + mock_sphere = MockSphere() + rod_sphere_contact = RodSphereContact(k=1.0, nu=0.0) + + # Setting sphere position such that there is no collision + mock_sphere.position_collection = np.array([[100], [200], [300]]) + + mock_rod_external_forces_before_execution = mock_rod.external_forces.copy() + mock_sphere_external_forces_before_execution = ( + mock_sphere.external_forces.copy() + ) + mock_sphere_external_torques_before_execution = ( + mock_sphere.external_torques.copy() + ) + + rod_sphere_contact.apply_contact(mock_rod, mock_sphere) + + assert_allclose( + mock_rod.external_forces, mock_rod_external_forces_before_execution + ) + assert_allclose( + mock_sphere.external_forces, + mock_sphere_external_forces_before_execution, + ) + assert_allclose( + mock_sphere.external_torques, + mock_sphere_external_torques_before_execution, + ) + + def test_contact_rod_sphere_with_collision_with_nu_without_k( + self, + ): + "Testing Rod Sphere Contact wrapper with Collision with nu without k" + + mock_rod = MockRod() + mock_rod.velocity_collection = np.array([[-1, 0, 0], [-1, 0, 0], [-1, 0, 0]]) + + mock_sphere = MockSphere() + mock_sphere.velocity_collection = np.array([[1], [0], [0]]) + + rod_sphere_contact = RodSphereContact(k=0.0, nu=1.0) + rod_sphere_contact.apply_contact(mock_rod, mock_sphere) + + assert_allclose( + mock_sphere.external_forces, np.array([[-1.5], [0], [0]]), atol=1e-6 + ) + assert_allclose( + mock_sphere.external_torques, np.array([[0], [0], [0]]), atol=1e-6 + ) + assert_allclose( + mock_rod.external_forces, + np.array([[0.5, 1.0, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + + def test_contact_rod_sphere_with_collision_with_k_and_nu( + self, + ): + "Testing Rod Sphere Contact wrapper with Collision with k and nu" + + mock_rod = MockRod() + mock_rod.velocity_collection = np.array([[-1, 0, 0], [-1, 0, 0], [-1, 0, 0]]) + + mock_sphere = MockSphere() + mock_sphere.velocity_collection = np.array([[1], [0], [0]]) + + rod_sphere_contact = RodSphereContact(k=1.0, nu=1.0) + rod_sphere_contact.apply_contact(mock_rod, mock_sphere) + + assert_allclose( + mock_sphere.external_forces, np.array([[-2.0], [0], [0]]), atol=1e-6 + ) + assert_allclose( + mock_sphere.external_torques, np.array([[0], [0], [0]]), atol=1e-6 + ) + assert_allclose( + mock_rod.external_forces, + np.array([[0.666666, 1.333333, 0], [0, 0, 0], [0, 0, 0]]), + atol=1e-6, + ) + class TestRodPlaneContact: def initializer( diff --git a/tests/test_contact_functions.py b/tests/test_contact_functions.py index 354d57a1..abde68b0 100644 --- a/tests/test_contact_functions.py +++ b/tests/test_contact_functions.py @@ -624,7 +624,7 @@ def test_calculate_contact_forces_rod_sphere_with_k_without_nu_and_friction( "initializing sphere parameters" sphere = MockSphere() - x_sph = sphere.position[..., 0] - sphere.radius * sphere.director[2, :, 0] + x_sph = sphere.position[..., 0] "initializing constants" """ @@ -640,16 +640,11 @@ def test_calculate_contact_forces_rod_sphere_with_k_without_nu_and_friction( _calculate_contact_forces_rod_sphere( rod_element_position, rod.lengths * rod.tangents, - sphere.position[..., 0], x_sph, - sphere.radius * sphere.director[2, :, 0], rod.radius + sphere.radius, rod.lengths + sphere.radius * 2, - rod.internal_forces, rod.external_forces, sphere.external_forces, - sphere.external_torques, - sphere.director[:, :, 0], rod.velocity_collection, sphere.velocity_collection, k, diff --git a/tests/test_contact_utils.py b/tests/test_contact_utils.py index 2be96650..656797d9 100644 --- a/tests/test_contact_utils.py +++ b/tests/test_contact_utils.py @@ -12,6 +12,7 @@ _clip, _out_of_bounds, _find_min_dist, + _find_min_dist_cylinder_sphere, _aabbs_not_intersecting, _prune_using_aabbs_rod_cylinder, _prune_using_aabbs_rod_rod, From a6537459f3d8683a86b9f4bad303304972abbff1 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Mon, 27 Oct 2025 22:00:46 -0500 Subject: [PATCH 31/85] test: add unit tests for _find_min_dist_cylinder_sphere function with verified values --- elastica/contact_utils.py | 1 - tests/test_contact_utils.py | 45 +++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/elastica/contact_utils.py b/elastica/contact_utils.py index f0a70df5..bc898720 100644 --- a/elastica/contact_utils.py +++ b/elastica/contact_utils.py @@ -122,7 +122,6 @@ def _find_min_dist_cylinder_sphere( x2e1 = _dot_product(e1, x2) # type: ignore # Parametrization - s = 0.0 t = (x2e1 - x1e1) / e1e1 # Comes from taking dot of e1 with a normal t = _clip(t, 0.0, 1.0) diff --git a/tests/test_contact_utils.py b/tests/test_contact_utils.py index 656797d9..d6d5f60e 100644 --- a/tests/test_contact_utils.py +++ b/tests/test_contact_utils.py @@ -209,6 +209,51 @@ def test_find_min_dist(): assert_allclose(contact_point_of_system1, [0, 0, 0]) +def test_find_min_dist_cylinder_sphere(): + "Function to test the _find_min_dist_cylinder_sphere function" + + "testing function with analytically verified values" + + # Case 1: Closest point is on the segment (0 < t < 1). + x1 = np.array([0.0, 0.0, 0.0]) + e1 = np.array([1.0, 0.0, 0.0]) + x2 = np.array([0.5, 1.0, 0.0]) + ( + min_dist_vec, + contact_point_of_system2, + contact_point_of_system1, + ) = _find_min_dist_cylinder_sphere(x1, e1, x2) + assert_allclose(min_dist_vec, [0.0, 1.0, 0.0]) + assert_allclose(contact_point_of_system2, [0.5, 1.0, 0.0]) + assert_allclose(contact_point_of_system1, [-0.5, 0.0, 0.0]) + + # Case 2: Closest point is at the start of the segment (t < 0, clipped to t=0). + x1 = np.array([0.0, 0.0, 0.0]) + e1 = np.array([1.0, 0.0, 0.0]) + x2 = np.array([-0.5, 1.0, 0.0]) + ( + min_dist_vec, + contact_point_of_system2, + contact_point_of_system1, + ) = _find_min_dist_cylinder_sphere(x1, e1, x2) + assert_allclose(min_dist_vec, [-0.5, 1.0, 0.0]) + assert_allclose(contact_point_of_system2, [-0.5, 1.0, 0.0]) + assert_allclose(contact_point_of_system1, [0.0, 0.0, 0.0]) + + # Case 3: Closest point is at the end of the segment (t > 1, clipped to t=1). + x1 = np.array([0.0, 0.0, 0.0]) + e1 = np.array([1.0, 0.0, 0.0]) + x2 = np.array([1.5, 1.0, 0.0]) + ( + min_dist_vec, + contact_point_of_system2, + contact_point_of_system1, + ) = _find_min_dist_cylinder_sphere(x1, e1, x2) + assert_allclose(min_dist_vec, [0.5, 1.0, 0.0]) + assert_allclose(contact_point_of_system2, [1.5, 1.0, 0.0]) + assert_allclose(contact_point_of_system1, [-1.0, 0.0, 0.0]) + + def test_aabbs_not_intersecting(): "Function to test the _aabb_intersecting function" From 7c048e10e7f80c29c526754977640b6d5b3f674c Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Tue, 28 Oct 2025 16:39:02 -0500 Subject: [PATCH 32/85] remove dissipation for rigid body object --- .../RodRigidBodyContact/rod_sphere_contact.py | 60 ++++++++++--------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_sphere_contact.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_sphere_contact.py index f0c7580f..77e389a7 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_sphere_contact.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_sphere_contact.py @@ -10,14 +10,18 @@ base_radius = 0.1 sphere_radius = 0.10 -overlap_perc = 0.9 +# overlap_perc = 1.0 # Should be no contact +# overlap_perc = 1.0 + 1e-2 # Should be no contact +overlap_perc = 1.0 - 1e-2 # Contact sphere_center = np.array( [(base_radius + sphere_radius) * overlap_perc, base_length / 2, 0.0] ) def rotate_random_axis_and_angle(R): - # return R + """ + Randomly rotate the frame for testing purpose. + """ from scipy.spatial.transform import Rotation axis = np.random.rand(3) @@ -46,6 +50,7 @@ class Simulator( rendering_fps = 30 # 20 * 1e1 step_skip = 100 + # Add rod density = 1000 E = 3e5 n_elem = 50 @@ -68,31 +73,28 @@ class Simulator( constrained_director_idx=(0, -1), ) - density = 1000 - rr = rotate_random_axis_and_angle(np.eye(3)) - rigid_body = ea.Sphere(sphere_center, sphere_radius, density) - rigid_body.director_collection[0] = rr[0][:, None] - rigid_body.director_collection[1] = rr[1][:, None] - rigid_body.director_collection[2] = rr[2][:, None] - simulator.append(rigid_body) - - # Add contact between rigid body and rod - simulator.detect_contact_between(rod, rigid_body).using( - ea.RodSphereContact, k=5e4, nu=0.0 - ) - - # add damping damping_constant = 1e-1 simulator.dampen(rod).using( ea.AnalyticalLinearDamper, damping_constant=damping_constant, time_step=time_step, ) - # simulator.dampen(rigid_body).using( - # ea.AnalyticalLinearDamper, - # damping_constant=damping_constant, - # time_step=time_step, - # ) + + # Add sphere + density = 1000 + n_sphere = 1 + for _ in range(n_sphere): + rr = rotate_random_axis_and_angle(np.eye(3)) + rigid_body = ea.Sphere(sphere_center, sphere_radius, density) + rigid_body.director_collection[0] = rr[0][:, None] + rigid_body.director_collection[1] = rr[1][:, None] + rigid_body.director_collection[2] = rr[2][:, None] + simulator.append(rigid_body) + + # Add contact between rigid body and rod + simulator.detect_contact_between(rod, rigid_body).using( + ea.RodSphereContact, k=3e4, nu=0.0 + ) # Add callbacks post_processing_dict_list = [] @@ -163,13 +165,15 @@ def make_callback(self, system, time, current_step: int): step_skip=step_skip, callback_params=post_processing_dict_list[0], ) - # For rigid body - post_processing_dict_list.append(ea.defaultdict(list)) - simulator.collect_diagnostics(rigid_body).using( - RigidBodyCallback, - step_skip=step_skip, - callback_params=post_processing_dict_list[1], - ) + for _ in range(n_sphere): + # For rigid body + db = ea.defaultdict(list) + post_processing_dict_list.append(db) + simulator.collect_diagnostics(rigid_body).using( + RigidBodyCallback, + step_skip=step_skip, + callback_params=db, + ) simulator.finalize() timestepper = ea.PositionVerlet() From 58fa9478e65902285564303c569bdb6c5ff7462d Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Tue, 28 Oct 2025 16:41:58 -0500 Subject: [PATCH 33/85] remove redundant warning and move the note to documentation --- docs/api/contact.rst | 6 ++++++ elastica/modules/contact.py | 6 ++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/api/contact.rst b/docs/api/contact.rst index e3b2b009..039fc8e6 100644 --- a/docs/api/contact.rst +++ b/docs/api/contact.rst @@ -8,6 +8,12 @@ Contact Description ----------- +.. note:: + (CAUTION) + The contact is recommended to be added at last. This is because contact forces often includes + friction that may depend on other normal forces and contraints to be calculated accurately. + Be careful on the order of adding interactions. + .. rubric:: Available Contact Classes .. autosummary:: diff --git a/elastica/modules/contact.py b/elastica/modules/contact.py index 81fdd270..c34c57f3 100644 --- a/elastica/modules/contact.py +++ b/elastica/modules/contact.py @@ -26,8 +26,7 @@ logger = logging.getLogger(__name__) -def warnings() -> None: - logger.warning("Contact features should be instantiated lastly.") + class Contact: @@ -97,8 +96,7 @@ def _finalize_contact(self: ContactedSystemCollectionProtocol) -> None: self._feature_group_synchronize.add_operators(contact, [func]) - if not self._feature_group_synchronize.is_last(contact): - warnings() + self._contacts = [] del self._contacts From 689b8410ec0370ced2598c60e679afa2396c2d78 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Tue, 28 Oct 2025 16:47:17 -0500 Subject: [PATCH 34/85] remove outdated interaction plane --- elastica/__init__.py | 2 - elastica/interaction.py | 209 ------------ elastica/modules/contact.py | 5 - tests/test_interaction.py | 654 ------------------------------------ 4 files changed, 870 deletions(-) diff --git a/elastica/__init__.py b/elastica/__init__.py index 9297af16..a53f1e5b 100644 --- a/elastica/__init__.py +++ b/elastica/__init__.py @@ -28,8 +28,6 @@ EndpointForcesSinusoidal, ) from elastica.interaction import ( - AnisotropicFrictionalPlane, - InteractionPlane, SlenderBodyTheory, ) from elastica.joint import ( diff --git a/elastica/interaction.py b/elastica/interaction.py index 1a72650d..d279e3e1 100644 --- a/elastica/interaction.py +++ b/elastica/interaction.py @@ -9,8 +9,6 @@ _node_to_element_velocity, ) from elastica._contact_functions import ( - _calculate_contact_forces_rod_plane, - _calculate_contact_forces_rod_plane_with_anisotropic_friction, _calculate_contact_forces_cylinder_plane, ) @@ -19,213 +17,6 @@ from elastica.typing import SystemType, RodType, RigidBodyType -# base class for interaction -# only applies normal force no friction -class InteractionPlane(NoForces): - """ - The interaction plane class computes the plane reaction - force on a rod-like object. For more details regarding the contact module refer to - Eqn 4.8 of Gazzola et al. RSoS (2018). - - Attributes - ---------- - k: float - Stiffness coefficient between the plane and the rod-like object. - nu: float - Dissipation coefficient between the plane and the rod-like object. - plane_origin: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - Origin of the plane. - plane_normal: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - The normal vector of the plane. - surface_tol: float - Penetration tolerance between the plane and the rod-like object. - - """ - - def __init__( - self, - k: float, - nu: float, - plane_origin: NDArray[np.float64], - plane_normal: NDArray[np.float64], - ) -> None: - """ - - Parameters - ---------- - k: float - Stiffness coefficient between the plane and the rod-like object. - nu: float - Dissipation coefficient between the plane and the rod-like object. - plane_origin: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - Origin of the plane. - plane_normal: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - The normal vector of the plane. - """ - self.k = np.float64(k) - self.nu = np.float64(nu) - self.surface_tol = np.float64(1e-4) - self.plane_origin = plane_origin.reshape(3, 1) - self.plane_normal = plane_normal.reshape(3) - - def apply_forces(self, system: RodType, time: np.float64 = np.float64(0.0)) -> None: - """ - In the case of contact with the plane, this function computes the plane reaction force on the element. - - Parameters - ---------- - system: object - Rod-like object. - - Returns - ------- - plane_response_force_mag : numpy.ndarray - 1D (blocksize) array containing data with 'float' type. - Magnitude of plane response force acting on rod-like object. - no_contact_point_idx : numpy.ndarray - 1D (blocksize) array containing data with 'int' type. - Index of rod-like object elements that are not in contact with the plane. - """ - return _calculate_contact_forces_rod_plane( - self.plane_origin, - self.plane_normal, - self.surface_tol, - self.k, - self.nu, - system.radius, - system.mass, - system.position_collection, - system.velocity_collection, - system.internal_forces, - system.external_forces, - ) - - -# class for anisotropic frictional plane -# NOTE: friction coefficients are passed as arrays in the order -# mu_forward : mu_backward : mu_sideways -# head is at x[0] and forward means head to tail -# same convention for kinetic and static -# mu named as to which direction it opposes -class AnisotropicFrictionalPlane(InteractionPlane): - """ - This anisotropic friction plane class is for computing - anisotropic friction forces on rods. - A detailed explanation of the implemented equations - can be found in Gazzola et al. RSoS. (2018). - - Attributes - ---------- - k: float - Stiffness coefficient between the plane and the rod-like object. - nu: float - Dissipation coefficient between the plane and the rod-like object. - plane_origin: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - Origin of the plane. - plane_normal: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - The normal vector of the plane. - slip_velocity_tol: float - Velocity tolerance to determine if the element is slipping or not. - static_mu_array: numpy.ndarray - 1D (3,) array containing data with 'float' type. - [forward, backward, sideways] static friction coefficients. - kinetic_mu_array: numpy.ndarray - 1D (3,) array containing data with 'float' type. - [forward, backward, sideways] kinetic friction coefficients. - """ - - def __init__( - self, - k: float, - nu: float, - plane_origin: NDArray[np.float64], - plane_normal: NDArray[np.float64], - slip_velocity_tol: float, - static_mu_array: NDArray[np.float64], - kinetic_mu_array: NDArray[np.float64], - ) -> None: - """ - - Parameters - ---------- - k: float - Stiffness coefficient between the plane and the rod-like object. - nu: float - Dissipation coefficient between the plane and the rod-like object. - plane_origin: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - Origin of the plane. - plane_normal: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - The normal vector of the plane. - slip_velocity_tol: float - Velocity tolerance to determine if the element is slipping or not. - static_mu_array: numpy.ndarray - 1D (3,) array containing data with 'float' type. - [forward, backward, sideways] static friction coefficients. - kinetic_mu_array: numpy.ndarray - 1D (3,) array containing data with 'float' type. - [forward, backward, sideways] kinetic friction coefficients. - """ - InteractionPlane.__init__(self, k, nu, plane_origin, plane_normal) - self.slip_velocity_tol = np.float64(slip_velocity_tol) - ( - self.static_mu_forward, - self.static_mu_backward, - self.static_mu_sideways, - ) = static_mu_array - ( - self.kinetic_mu_forward, - self.kinetic_mu_backward, - self.kinetic_mu_sideways, - ) = kinetic_mu_array - - # kinetic and static friction should separate functions - # for now putting them together to figure out common variables - def apply_forces( - self, system: "RodType | RigidBodyType", time: np.float64 = np.float64(0.0) - ) -> None: - """ - Call numba implementation to apply friction forces - Parameters - ---------- - system : RodType | RigidBodyType - time - - """ - _calculate_contact_forces_rod_plane_with_anisotropic_friction( - self.plane_origin, - self.plane_normal, - self.surface_tol, - self.slip_velocity_tol, - self.k, - self.nu, - self.kinetic_mu_forward, - self.kinetic_mu_backward, - self.kinetic_mu_sideways, - self.static_mu_forward, - self.static_mu_backward, - self.static_mu_sideways, - system.radius, - system.mass, - system.tangents, - system.position_collection, - system.director_collection, - system.velocity_collection, - system.omega_collection, - system.internal_forces, - system.external_forces, - system.internal_torques, - system.external_torques, - ) - - # Slender body module @njit(cache=True) # type: ignore def sum_over_elements(input: NDArray[np.float64]) -> np.float64: diff --git a/elastica/modules/contact.py b/elastica/modules/contact.py index c34c57f3..bd042f98 100644 --- a/elastica/modules/contact.py +++ b/elastica/modules/contact.py @@ -26,9 +26,6 @@ logger = logging.getLogger(__name__) - - - class Contact: """ The Contact class is a module for applying contact between rod-like objects . To apply contact between rod-like objects, @@ -96,8 +93,6 @@ def _finalize_contact(self: ContactedSystemCollectionProtocol) -> None: self._feature_group_synchronize.add_operators(contact, [func]) - - self._contacts = [] del self._contacts diff --git a/tests/test_interaction.py b/tests/test_interaction.py index f6b8fe24..9bb00e62 100644 --- a/tests/test_interaction.py +++ b/tests/test_interaction.py @@ -7,8 +7,6 @@ from elastica.interaction import ( - InteractionPlane, - AnisotropicFrictionalPlane, SlenderBodyTheory, ) from elastica.contact_utils import ( @@ -60,658 +58,6 @@ def _compute_internal_torques(self): return np.zeros((MaxDimension.value(), self.n_elem)) -class TestInteractionPlane: - def initializer( - self, - rng, - n_elem, - shift=0.0, - k_w=0.0, - nu_w=0.0, - plane_normal=np.array([0.0, 1.0, 0.0]), - ): - rod = BaseRodClass(n_elem) - plane_origin = np.array([0.0, -rod.radius[0] + shift, 0.0]) - interaction_plane = InteractionPlane(k_w, nu_w, plane_origin, plane_normal) - fnormal = -10.0 * np.sign(plane_normal[1]) * rng.random(1).item() - external_forces = np.repeat( - np.array([0.0, fnormal, 0.0]).reshape(3, 1), n_elem + 1, axis=1 - ) - external_forces[..., 0] *= 0.5 - external_forces[..., -1] *= 0.5 - rod.external_forces = external_forces.copy() - - return rod, interaction_plane, external_forces - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - def test_interaction_without_contact(self, n_elem, rng): - """ - This test case tests the forces on rod, when there is no - contact between rod and the plane. - Parameters - ---------- - n_elem - - Returns - ------- - - """ - - shift = -( - (2.0 - 1.0) * rng.random(1) + 1.0 - ).item() # we move plane away from rod - - [rod, interaction_plane, external_forces] = self.initializer(rng, n_elem, shift) - - interaction_plane.apply_forces(rod) - correct_forces = external_forces # since no contact - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - def test_interaction_plane_without_k_and_nu(self, n_elem, rng): - """ - This function tests wall response on the rod. Here - wall stiffness coefficient and damping coefficient set - to zero to check only sum of all forces on the rod. - - Parameters - ---------- - n_elem - - Returns - ------- - - """ - - [rod, interaction_plane, external_forces] = self.initializer(rng, n_elem) - - interaction_plane.apply_forces(rod) - - correct_forces = np.zeros((3, n_elem + 1)) - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("k_w", [0.1, 0.5, 1.0, 2, 10]) - def test_interaction_plane_with_k_without_nu(self, n_elem, k_w, rng): - """ - Here wall stiffness coefficient changed parametrically - and damping coefficient set to zero . - Parameters - ---------- - n_elem - k_w - - Returns - ------- - - """ - - shift = rng.random(1).item() # we move plane towards to rod - [rod, interaction_plane, external_forces] = self.initializer( - rng, n_elem, shift=shift, k_w=k_w - ) - correct_forces = k_w * np.repeat( - np.array([0.0, shift, 0.0]).reshape(3, 1), n_elem + 1, axis=1 - ) - correct_forces[..., 0] *= 0.5 - correct_forces[..., -1] *= 0.5 - - interaction_plane.apply_forces(rod) - - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("nu_w", [0.5, 1.0, 5.0, 7.0, 12.0]) - def test_interaction_plane_without_k_with_nu(self, n_elem, nu_w, rng): - """ - Here wall damping coefficient are changed parametrically and - wall response functions tested. - Parameters - ---------- - n_elem - nu_w - - Returns - ------- - - """ - - [rod, interaction_plane, external_forces] = self.initializer( - rng, n_elem, nu_w=nu_w - ) - - normal_velocity = rng.random(1).item() - rod.velocity_collection[..., :] += np.array( - [0.0, -normal_velocity, 0.0] - ).reshape(3, 1) - - correct_forces = np.repeat( - (nu_w * np.array([0.0, normal_velocity, 0.0])).reshape(3, 1), - n_elem + 1, - axis=1, - ) - - correct_forces[..., 0] *= 0.5 - correct_forces[..., -1] *= 0.5 - - interaction_plane.apply_forces(rod) - - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - def test_interaction_when_rod_is_under_plane(self, n_elem, rng): - """ - This test case tests plane response forces on the rod - in the case rod is under the plane and pushed towards - the plane. - Parameters - ---------- - n_elem - - Returns - ------- - - """ - - # we move plane on top of the rod. Note that 0.25 is radius of the rod. - offset_of_plane_with_respect_to_rod = 2.0 * 0.25 - - # plane normal changed, it is towards the negative direction, because rod - # is under the rod. - plane_normal = np.array([0.0, -1.0, 0.0]) - - [rod, interaction_plane, external_forces] = self.initializer( - rng, - n_elem, - shift=offset_of_plane_with_respect_to_rod, - plane_normal=plane_normal, - ) - - interaction_plane.apply_forces(rod) - correct_forces = np.zeros((3, n_elem + 1)) - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("k_w", [0.1, 0.5, 1.0, 2, 10]) - def test_interaction_when_rod_is_under_plane_with_k_without_nu( - self, n_elem, k_w, rng - ): - """ - In this test case we move the rod under the plane. - Here wall stiffness coefficient changed parametrically - and damping coefficient set to zero . - Parameters - ---------- - n_elem - k_w - - Returns - ------- - - """ - # we move plane on top of the rod. Note that 0.25 is radius of the rod. - offset_of_plane_with_respect_to_rod = 2.0 * 0.25 - - # we move plane towards to rod by random distance - shift = offset_of_plane_with_respect_to_rod - rng.random(1).item() - - # plane normal changed, it is towards the negative direction, because rod - # is under the rod. - plane_normal = np.array([0.0, -1.0, 0.0]) - - [rod, interaction_plane, external_forces] = self.initializer( - rng, n_elem, shift=shift, k_w=k_w, plane_normal=plane_normal - ) - - # we have to substract rod offset because top part - correct_forces = k_w * np.repeat( - np.array([0.0, shift - offset_of_plane_with_respect_to_rod, 0.0]).reshape( - 3, 1 - ), - n_elem + 1, - axis=1, - ) - correct_forces[..., 0] *= 0.5 - correct_forces[..., -1] *= 0.5 - - interaction_plane.apply_forces(rod) - - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("nu_w", [0.5, 1.0, 5.0, 7.0, 12.0]) - def test_interaction_when_rod_is_under_plane_without_k_with_nu( - self, n_elem, nu_w, rng - ): - """ - In this test case we move under the plane and test damping force. - Here wall damping coefficient are changed parametrically and - wall response functions tested. - Parameters - ---------- - n_elem - nu_w - - Returns - ------- - - """ - # we move plane on top of the rod. Note that 0.25 is radius of the rod. - offset_of_plane_with_respect_to_rod = 2.0 * 0.25 - - # plane normal changed, it is towards the negative direction, because rod - # is under the rod. - plane_normal = np.array([0.0, -1.0, 0.0]) - - [rod, interaction_plane, external_forces] = self.initializer( - rng, - n_elem, - shift=offset_of_plane_with_respect_to_rod, - nu_w=nu_w, - plane_normal=plane_normal, - ) - - normal_velocity = rng.random(1).item() - rod.velocity_collection[..., :] += np.array( - [0.0, -normal_velocity, 0.0] - ).reshape(3, 1) - - correct_forces = np.repeat( - (nu_w * np.array([0.0, normal_velocity, 0.0])).reshape(3, 1), - n_elem + 1, - axis=1, - ) - - correct_forces[..., 0] *= 0.5 - correct_forces[..., -1] *= 0.5 - - interaction_plane.apply_forces(rod) - - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - -class TestAnisotropicFriction: - def initializer( - self, - rng, - n_elem, - static_mu_array=np.array([0.0, 0.0, 0.0]), - kinetic_mu_array=np.array([0.0, 0.0, 0.0]), - force_mag_long=0.0, # forces along the rod - force_mag_side=0.0, # side forces on the rod - ): - - rod = BaseRodClass(n_elem) - - origin_plane = np.array([0.0, -rod.radius[0], 0.0]) - normal_plane = np.array([0.0, 1.0, 0.0]) - slip_velocity_tol = 1e-2 - friction_plane = AnisotropicFrictionalPlane( - 0.0, - 0.0, - origin_plane, - normal_plane, - slip_velocity_tol, - static_mu_array, # forward, backward, sideways - kinetic_mu_array, # forward, backward, sideways - ) - fnormal = (10.0 - 5.0) * rng.random( - 1 - ).item() + 5.0 # generates random numbers [5.0,10) - external_forces = np.array([force_mag_side, -fnormal, force_mag_long]) - - external_forces_collection = np.repeat( - external_forces.reshape(3, 1), n_elem + 1, axis=1 - ) - external_forces_collection[..., 0] *= 0.5 - external_forces_collection[..., -1] *= 0.5 - rod.external_forces = external_forces_collection.copy() - - # Velocities has to be set to zero - assert_allclose( - np.zeros((3, n_elem)), rod.omega_collection, atol=Tolerance.atol() - ) - assert_allclose( - np.zeros((3, n_elem + 1)), rod.velocity_collection, atol=Tolerance.atol() - ) - - # We have not changed torques also, they have to be zero as well - assert_allclose( - np.zeros((3, n_elem)), rod.external_torques, atol=Tolerance.atol() - ) - assert_allclose( - np.zeros((3, n_elem)), - rod._compute_internal_torques(), - atol=Tolerance.atol(), - ) - - return rod, friction_plane, external_forces_collection - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("velocity", [-1.0, -3.0, 1.0, 5.0, 2.0]) - def test_axial_kinetic_friction(self, n_elem, velocity, rng): - """ - This function tests kinetic friction in forward and backward direction. - All other friction coefficients set to zero. - Parameters - ---------- - n_elem - velocity - - Returns - ------- - - """ - - [rod, friction_plane, external_forces_collection] = self.initializer( - rng, n_elem, kinetic_mu_array=np.array([1.0, 1.0, 0.0]) - ) - - rod.velocity_collection += np.array([0.0, 0.0, velocity]).reshape(3, 1) - - friction_plane.apply_forces(rod) - - direction_collection = np.repeat( - np.array([0.0, 0.0, 1.0]).reshape(3, 1), n_elem + 1, axis=1 - ) - correct_forces = ( - -1.0 - * np.sign(velocity) - * np.linalg.norm(external_forces_collection, axis=0) - * direction_collection - ) - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("force_mag", [-1.0, -3.0, 1.0, 5.0, 2.0]) - def test_axial_static_friction_total_force_smaller_than_static_friction_force( - self, n_elem, force_mag, rng - ): - """ - This test is for static friction when total forces applied - on the rod is smaller than the static friction force. - Fx < F_normal*mu_s - Parameters - ---------- - n_elem - force_mag - - Returns - ------- - - """ - [rod, frictionplane, external_forces_collection] = self.initializer( - rng, - n_elem, - static_mu_array=np.array([1.0, 1.0, 0.0]), - force_mag_long=force_mag, - ) - - frictionplane.apply_forces(rod) - correct_forces = np.zeros((3, n_elem + 1)) - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("force_mag", [-20.0, -15.0, 15.0, 20.0]) - def test_axial_static_friction_total_force_larger_than_static_friction_force( - self, n_elem, force_mag, rng - ): - """ - This test is for static friction when total forces applied - on the rod is larger than the static friction force. - Fx > F_normal*mu_s - Parameters - ---------- - n_elem - force_mag - - Returns - ------- - - """ - - [rod, frictionplane, external_forces_collection] = self.initializer( - rng, - n_elem, - static_mu_array=np.array([1.0, 1.0, 0.0]), - force_mag_long=force_mag, - ) - - frictionplane.apply_forces(rod) - correct_forces = np.zeros((3, n_elem + 1)) - if np.sign(force_mag) < 0: - correct_forces[2] = ( - external_forces_collection[2] - ) - 1.0 * external_forces_collection[1] - else: - correct_forces[2] = ( - external_forces_collection[2] - ) + 1.0 * external_forces_collection[1] - - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("velocity", [-1.0, -3.0, 1.0, 2.0, 5.0]) - @pytest.mark.parametrize("omega", [-5.0, -2.0, 0.0, 4.0, 6.0]) - def test_kinetic_rolling_friction(self, n_elem, velocity, omega, rng): - """ - This test is for testing kinetic rolling friction, - for different translational and angular velocities, - we compute the final external forces and torques on the rod - using apply friction function and compare results with - analytical solutions. - Parameters - ---------- - n_elem - velocity - omega - - Returns - ------- - - """ - [rod, frictionplane, external_forces_collection] = self.initializer( - rng, n_elem, kinetic_mu_array=np.array([0.0, 0.0, 1.0]) - ) - - rod.velocity_collection += np.array([velocity, 0.0, 0.0]).reshape(3, 1) - rod.omega_collection += np.array([0.0, 0.0, omega]).reshape(3, 1) - - frictionplane.apply_forces(rod) - - correct_forces = np.zeros((3, n_elem + 1)) - correct_forces[0] = ( - -1.0 - * np.sign(velocity + omega * rod.radius[0]) - * np.fabs(external_forces_collection[1]) - ) - - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - forces_on_elements = _node_to_element_mass_or_force(external_forces_collection) - correct_torques = np.zeros((3, n_elem)) - correct_torques[2] += ( - -1.0 - * np.sign(velocity + omega * rod.radius[0]) - * np.fabs(forces_on_elements[1]) - * rod.radius - ) - - assert_allclose(correct_torques, rod.external_torques, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("force_mag", [-20.0, -15.0, 15.0, 20.0]) - def test_static_rolling_friction_total_force_smaller_than_static_friction_force( - self, n_elem, force_mag, rng - ): - """ - In this test case static rolling friction force is tested. We set external and internal torques to - zero and only changed the force in rolling direction. In this test case, total force in rolling direction - is smaller than static friction force in rolling direction. Next test case will check what happens if - total forces in rolling direction larger than static friction force. - Parameters - ---------- - n_elem - force_mag - - Returns - ------- - - """ - - [rod, frictionplane, external_forces_collection] = self.initializer( - rng, - n_elem, - static_mu_array=np.array([0.0, 0.0, 10.0]), - force_mag_side=force_mag, - ) - - frictionplane.apply_forces(rod) - - correct_forces = np.zeros((3, n_elem + 1)) - correct_forces[0] = 2.0 / 3.0 * external_forces_collection[0] - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - forces_on_elements = _node_to_element_mass_or_force(external_forces_collection) - correct_torques = np.zeros((3, n_elem)) - correct_torques[2] += ( - -1.0 - * np.sign(forces_on_elements[0]) - * np.fabs(forces_on_elements[0]) - * rod.radius - / 3.0 - ) - - assert_allclose(correct_torques, rod.external_torques, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("force_mag", [-100.0, -80.0, 65.0, 95.0]) - def test_static_rolling_friction_total_force_larger_than_static_friction_force( - self, n_elem, force_mag, rng - ): - """ - In this test case static rolling friction force is tested. We set external and internal torques to - zero and only changed the force in rolling direction. In this test case, total force in rolling direction - is larger than static friction force in rolling direction. - Parameters - ---------- - n_elem - force_mag - - Returns - ------- - - """ - - [rod, frictionplane, external_forces_collection] = self.initializer( - rng, - n_elem, - static_mu_array=np.array([0.0, 0.0, 1.0]), - force_mag_side=force_mag, - ) - - frictionplane.apply_forces(rod) - - correct_forces = np.zeros((3, n_elem + 1)) - correct_forces[0] = external_forces_collection[0] - np.sign( - external_forces_collection[0] - ) * np.fabs(external_forces_collection[1]) - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - forces_on_elements = _node_to_element_mass_or_force(external_forces_collection) - correct_torques = np.zeros((3, n_elem)) - correct_torques[2] += ( - -1.0 - * np.sign(forces_on_elements[0]) - * np.fabs(forces_on_elements[1]) - * rod.radius - ) - - assert_allclose(correct_torques, rod.external_torques, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("torque_mag", [-3.0, -1.0, 2.0, 3.5]) - def test_static_rolling_friction_total_torque_smaller_than_static_friction_force( - self, n_elem, torque_mag, rng - ): - """ - In this test case, static rolling friction force tested with zero internal and external force and - with non-zero external torque. Here torque magnitude chosen such that total rolling force is - always smaller than the static friction force. - Parameters - ---------- - n_elem - torque_mag - - Returns - ------- - - """ - - [rod, frictionplane, external_forces_collection] = self.initializer( - rng, n_elem, static_mu_array=np.array([0.0, 0.0, 10.0]) - ) - - external_torques = np.zeros((3, n_elem)) - external_torques[2] = torque_mag - rod.external_torques = external_torques.copy() - - frictionplane.apply_forces(rod) - - correct_forces = np.zeros((3, n_elem + 1)) - correct_forces[0, :-1] -= external_torques[2] / (3.0 * rod.radius) - correct_forces[0, 1:] -= external_torques[2] / (3.0 * rod.radius) - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - correct_torques = np.zeros((3, n_elem)) - correct_torques[2] += external_torques[2] - 2.0 / 3.0 * external_torques[2] - - assert_allclose(correct_torques, rod.external_torques, atol=Tolerance.atol()) - - @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) - @pytest.mark.parametrize("torque_mag", [-10.0, -5.0, 6.0, 7.5]) - def test_static_rolling_friction_total_torque_larger_than_static_friction_force( - self, n_elem, torque_mag, rng - ): - """ - In this test case, static rolling friction force tested with zero internal and external force and - with non-zero external torque. Here torque magnitude chosen such that total rolling force is - always larger than the static friction force. Thus, lateral friction force will be equal to static - friction force. - Parameters - ---------- - n_elem - torque_mag - - Returns - ------- - - """ - - [rod, frictionplane, external_forces_collection] = self.initializer( - rng, n_elem, static_mu_array=np.array([0.0, 0.0, 1.0]) - ) - - external_torques = np.zeros((3, n_elem)) - external_torques[2] = torque_mag - rod.external_torques = external_torques.copy() - - frictionplane.apply_forces(rod) - - correct_forces = np.zeros((3, n_elem + 1)) - correct_forces[0] = ( - -1.0 * np.sign(torque_mag) * np.fabs(external_forces_collection[1]) - ) - assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) - - forces_on_elements = _node_to_element_mass_or_force(external_forces_collection) - correct_torques = external_torques - correct_torques[2] += -( - np.sign(torque_mag) * np.fabs(forces_on_elements[1]) * rod.radius - ) - - assert_allclose(correct_torques, rod.external_torques, atol=Tolerance.atol()) - - # Slender Body Theory Unit Tests from elastica.interaction import ( sum_over_elements, From efa4405ad0ac35e106a6f7ca652dd5b36c25919c Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Wed, 29 Oct 2025 11:50:44 -0500 Subject: [PATCH 35/85] test: remove outdated test - anisotropic plane forcing moved to contact --- tests/test_modules/test_forcing.py | 36 ------------------------------ 1 file changed, 36 deletions(-) diff --git a/tests/test_modules/test_forcing.py b/tests/test_modules/test_forcing.py index 4f621fc4..49c074ec 100644 --- a/tests/test_modules/test_forcing.py +++ b/tests/test_modules/test_forcing.py @@ -166,42 +166,6 @@ def mock_init(self, *args, **kwargs): return scwf, MockForcing - def test_friction_plane_forcing_class(self, load_system_with_forcings): - - scwf = load_system_with_forcings - - mock_rod = self.MockRod(2, 3, 4, 5) - scwf.append(mock_rod) - - from elastica.interaction import AnisotropicFrictionalPlane - - # Add friction plane - scwf.add_forcing_to(1).using( - AnisotropicFrictionalPlane, - k=0, - nu=0, - plane_origin=np.zeros((3,)), - plane_normal=np.zeros((3,)), - slip_velocity_tol=0, - static_mu_array=[0, 0, 0], - kinetic_mu_array=[0, 0, 0], - ) - # Add another forcing class - - def mock_init(self, *args, **kwargs): - pass - - MockForcing = type( - "MockForcing", (self.NoForces, object), {"__init__": mock_init} - ) - scwf.add_forcing_to(1).using(MockForcing, 2, 42) # index based forcing - - # Now check if the Anisotropic friction and the MockForcing are in the list - assert scwf._ext_forces_torques[-1]._forcing_cls == MockForcing - assert scwf._ext_forces_torques[-2]._forcing_cls == AnisotropicFrictionalPlane - scwf._finalize_forcing() - assert not hasattr(scwf, "_ext_forces_torques") - def test_constrain_finalize_correctness(self, load_rod_with_forcings): scwf, forcing_cls = load_rod_with_forcings forcing_features = [f for f in scwf._ext_forces_torques] From a8441cf4ea55a4c5da24bcfcbab29e1c5f39cff2 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Wed, 29 Oct 2025 13:07:50 -0500 Subject: [PATCH 36/85] docs: removed outdated forcing classes --- docs/api/external_forces.rst | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/docs/api/external_forces.rst b/docs/api/external_forces.rst index f34c0535..f67627c5 100644 --- a/docs/api/external_forces.rst +++ b/docs/api/external_forces.rst @@ -30,8 +30,6 @@ External force and environmental interaction are represented as force/torque bou .. autosummary:: :nosignatures: - AnisotropicFrictionalPlane - InteractionPlane SlenderBodyTheory Compatibility @@ -52,8 +50,6 @@ EndpointForcesSinusoidal ✅ ❌ ========================== ======= ============ Interaction Rod Rigid Body ========================== ======= ============ -AnisotropicFrictionalPlane ✅ ❌ -InteractionPlane ✅ ❌ SlenderBodyTheory ✅ ❌ ========================== ======= ============ @@ -88,11 +84,5 @@ Built-in Environment Interactions .. automodule:: elastica.interaction :noindex: -.. autoclass:: AnisotropicFrictionalPlane - :special-members: __init__ - -.. autoclass:: InteractionPlane - :special-members: __init__ - .. autoclass:: SlenderBodyTheory :special-members: __init__ From ad279e11d5456d8db215effc96d0fd8ac1717976 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Thu, 11 Dec 2025 06:09:39 -0600 Subject: [PATCH 37/85] update pre-commit outdated configuration --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 43ebcfe2..70d9c2c6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ default_language_version: python: python3 -default_stages: [commit, push] +default_stages: [pre-commit, pre-push] repos: - repo: https://github.com/pre-commit/pre-commit-hooks From cd7419787b77829a44f8dab537e8ed4b93642b79 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Thu, 11 Dec 2025 06:10:18 -0600 Subject: [PATCH 38/85] doc: fix typo and clarify constraining boundary conditions --- elastica/boundary_conditions.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/elastica/boundary_conditions.py b/elastica/boundary_conditions.py index ee825379..32fba9ed 100644 --- a/elastica/boundary_conditions.py +++ b/elastica/boundary_conditions.py @@ -22,14 +22,14 @@ class ConstraintBase(ABC, Generic[S]): Notes ----- - Constraint class must inherit BaseConstraint class. + Constraint class must inherit ConstraintBase class. Attributes ---------- - system : RodBase or RigidBodyBase - node_indices : None or numpy.ndarray - element_indices : None or numpy.ndarray + _system : RodType or RigidBodyType + _constrained_position_idx : NDArray[np.int32] + _constrained_director_idx : NDArray[np.int32] """ @@ -39,6 +39,7 @@ class ConstraintBase(ABC, Generic[S]): def __init__( self, + _system: "RodType | RigidBodyType", *args: Any, constrained_position_idx: ConstrainingIndex = (), constrained_director_idx: ConstrainingIndex = (), @@ -46,7 +47,7 @@ def __init__( ) -> None: """Initialize boundary condition""" try: - self._system = kwargs["_system"] + self._system = _system self._constrained_position_idx = np.array( constrained_position_idx, dtype=np.int32 ) @@ -85,7 +86,6 @@ def constrain_values(self, system: S, time: np.float64) -> None: time : float The time of simulation. """ - pass @abstractmethod def constrain_rates(self, system: S, time: np.float64) -> None: @@ -100,7 +100,6 @@ def constrain_rates(self, system: S, time: np.float64) -> None: The time of simulation. """ - pass class FreeBC(ConstraintBase): @@ -115,13 +114,11 @@ def constrain_values( self, system: "RodType | RigidBodyType", time: np.float64 ) -> None: """In FreeBC, this routine simply passes.""" - pass def constrain_rates( self, system: "RodType | RigidBodyType", time: np.float64 ) -> None: """In FreeBC, this routine simply passes.""" - pass class OneEndFixedBC(ConstraintBase): @@ -138,9 +135,13 @@ class OneEndFixedBC(ConstraintBase): >>> simulator.constrain(rod).using( ... OneEndFixedBC, - ... constrained_position_idx=(0,), - ... constrained_director_idx=(0,) + ... constrained_position_idx=(0,), # Specify node to fix + ... constrained_director_idx=(0,), # Specify element to fix ... ) + + See Also + -------- + :class:`GeneralConstraint`: For fixing multiple node/element with specific degrees-of-freedom. """ def __init__( From 0c2c903622577b358419b2c919167a7d5a5b8a86 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Thu, 11 Dec 2025 22:21:47 -0600 Subject: [PATCH 39/85] fix: rod -> system in rod-self contact cases --- .../PlectonemesCase/plectoneme_case.py | 34 +++++++++---------- .../SolenoidsCase/solenoid_case.py | 34 +++++++++---------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py b/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py index 6797f0d8..fa582b00 100644 --- a/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py +++ b/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py @@ -125,34 +125,34 @@ def __init__( def constrain_values(self, system, time): if time > self.twisting_time + self.time_twis_start: - rod.position_collection[..., 0] = self.position_start - rod.position_collection[0, -1] = 0.0 - rod.position_collection[2, -1] = 0.0 + system.position_collection[..., 0] = self.position_start + system.position_collection[0, -1] = 0.0 + system.position_collection[2, -1] = 0.0 - rod.director_collection[..., 0] = self.director_start - rod.director_collection[..., -1] = self.final_end_directors + system.director_collection[..., 0] = self.director_start + system.director_collection[..., -1] = self.final_end_directors def constrain_rates(self, system, time): if time > self.twisting_time + self.time_twis_start: - rod.velocity_collection[..., 0] = 0.0 - rod.omega_collection[..., 0] = 0.0 + system.velocity_collection[..., 0] = 0.0 + system.omega_collection[..., 0] = 0.0 - rod.velocity_collection[..., -1] = 0.0 - rod.omega_collection[..., -1] = 0.0 + system.velocity_collection[..., -1] = 0.0 + system.omega_collection[..., -1] = 0.0 elif time < self.time_twis_start: - rod.velocity_collection[..., 0] = 0.0 - rod.omega_collection[..., 0] = 0.0 + system.velocity_collection[..., 0] = 0.0 + system.omega_collection[..., 0] = 0.0 else: - rod.velocity_collection[..., 0] = 0.0 - rod.omega_collection[..., 0] = 0.0 + system.velocity_collection[..., 0] = 0.0 + system.omega_collection[..., 0] = 0.0 - rod.velocity_collection[0, -1] = 0.0 - rod.velocity_collection[2, -1] = 0.0 - rod.omega_collection[..., -1] = -self.ang_vel + system.velocity_collection[0, -1] = 0.0 + system.velocity_collection[2, -1] = 0.0 + system.omega_collection[..., -1] = -self.ang_vel - rod.velocity_collection[2, int(rod.n_elems / 2)] -= 1e-4 + system.velocity_collection[2, int(system.n_elems / 2)] -= 1e-4 plectonemes_sim.constrain(sherable_rod).using( diff --git a/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py b/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py index df8999b1..c0f683b7 100644 --- a/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py +++ b/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py @@ -127,34 +127,34 @@ def __init__( def constrain_values(self, system, time): if time > self.twisting_time + self.time_twis_start: - rod.position_collection[..., 0] = self.position_start - rod.position_collection[0, -1] = 0.0 - rod.position_collection[2, -1] = 0.0 + system.position_collection[..., 0] = self.position_start + system.position_collection[0, -1] = 0.0 + system.position_collection[2, -1] = 0.0 - rod.director_collection[..., 0] = self.director_start - rod.director_collection[..., -1] = self.final_end_directors + system.director_collection[..., 0] = self.director_start + system.director_collection[..., -1] = self.final_end_directors def constrain_rates(self, system, time): if time > self.twisting_time + self.time_twis_start: - rod.velocity_collection[..., 0] = 0.0 - rod.omega_collection[..., 0] = 0.0 + system.velocity_collection[..., 0] = 0.0 + system.omega_collection[..., 0] = 0.0 - rod.velocity_collection[..., -1] = 0.0 - rod.omega_collection[..., -1] = 0.0 + system.velocity_collection[..., -1] = 0.0 + system.omega_collection[..., -1] = 0.0 elif time < self.time_twis_start: - rod.velocity_collection[..., 0] = 0.0 - rod.omega_collection[..., 0] = 0.0 + system.velocity_collection[..., 0] = 0.0 + system.omega_collection[..., 0] = 0.0 else: - rod.velocity_collection[..., 0] = 0.0 - rod.omega_collection[..., 0] = 0.0 + system.velocity_collection[..., 0] = 0.0 + system.omega_collection[..., 0] = 0.0 - rod.velocity_collection[0, -1] = 0.0 - rod.velocity_collection[2, -1] = 0.0 - rod.omega_collection[..., -1] = -self.ang_vel + system.velocity_collection[0, -1] = 0.0 + system.velocity_collection[2, -1] = 0.0 + system.omega_collection[..., -1] = -self.ang_vel - rod.velocity_collection[2, int(rod.n_elems / 2)] -= 1e-4 + system.velocity_collection[2, int(system.n_elems / 2)] -= 1e-4 solenoid_sim.constrain(sherable_rod).using( From 6efe9e4549b4aa2638dff9b79cdf7f651d9a1e03 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Thu, 11 Dec 2025 22:24:56 -0600 Subject: [PATCH 40/85] test: fix issues with keyword arguments for boundary conditions --- elastica/boundary_conditions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elastica/boundary_conditions.py b/elastica/boundary_conditions.py index 32fba9ed..03bc090c 100644 --- a/elastica/boundary_conditions.py +++ b/elastica/boundary_conditions.py @@ -39,8 +39,8 @@ class ConstraintBase(ABC, Generic[S]): def __init__( self, - _system: "RodType | RigidBodyType", *args: Any, + _system: "RodType | RigidBodyType", constrained_position_idx: ConstrainingIndex = (), constrained_director_idx: ConstrainingIndex = (), **kwargs: Any, From 024194e0959f85a22710acffb8a69e7fbf009018 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 12 Dec 2025 03:26:02 -0600 Subject: [PATCH 41/85] deprecate ea.defaultdict. Use std lib --- elastica/__init__.py | 1 - .../run_axial_stretching.py | 3 +- .../general_constraint_allow_yaw.py | 8 +- examples/ButterflyCase/run_butterfly.py | 4 +- ...antilever_conservative_distributed_load.py | 3 +- ...ilever_nonconservative_distributed_load.py | 3 +- .../continuum_flagella.py | 7 +- .../ContinuumSnakeCase/run-continuum-snake.py | 5 +- .../continuum_snake_with_lifting_wave.py | 3 +- .../dynamic_cantilever.py | 7 +- .../generic_system_type_fixed_joint.py | 8 +- .../generic_system_type_spherical_joint.py | 8 +- .../parallel_connection_example.py | 17 +- .../flexible_swinging_pendulum.py | 5 +- examples/JointCases/fixed_joint.py | 32 +++- examples/JointCases/fixed_joint_torsion.py | 33 +++- examples/JointCases/hinge_joint.py | 32 +++- examples/JointCases/spherical_joint.py | 32 +++- .../MuscularFlagella/muscular_flagella.py | 25 +-- examples/MuscularSnake/muscular_snake.py | 11 +- .../rod_cylinder_contact.py | 5 +- .../rod_cylinder_contact_friction.py | 5 +- .../rod_cylinder_contact_validation.py | 7 +- .../rod_cylinder_contact_with_y_normal.py | 7 +- .../RodRigidBodyContact/rod_sphere_contact.py | 7 +- .../rigid_cylinder_rotational_motion_case.py | 19 +-- ...igid_cylinder_translational_motion_case.py | 19 +-- .../rigid_sphere_rotational_motion_case.py | 15 +- .../rigid_sphere_translational_motion_case.py | 15 +- examples/RingRodCase/ring_rod.py | 13 +- .../rod_rod_contact_inclined_validation.py | 8 +- .../rod_rod_contact_parallel_validation.py | 8 +- .../PlectonemesCase/plectoneme_case.py | 6 +- .../SolenoidsCase/solenoid_case.py | 6 +- examples/TimoshenkoBeamCase/run_timoshenko.py | 3 +- .../tumbling_unconstrained_rod.py | 152 +++++++++--------- 36 files changed, 313 insertions(+), 229 deletions(-) diff --git a/elastica/__init__.py b/elastica/__init__.py index a53f1e5b..96825850 100644 --- a/elastica/__init__.py +++ b/elastica/__init__.py @@ -1,4 +1,3 @@ -from collections import defaultdict from elastica.rod.knot_theory import ( compute_link, compute_twist, diff --git a/examples/AxialStretchingCase/run_axial_stretching.py b/examples/AxialStretchingCase/run_axial_stretching.py index 61489b0e..7cd38bdf 100644 --- a/examples/AxialStretchingCase/run_axial_stretching.py +++ b/examples/AxialStretchingCase/run_axial_stretching.py @@ -12,6 +12,7 @@ # isort:skip_file import numpy as np +from collections import defaultdict from matplotlib import pyplot as plt import elastica as ea @@ -129,7 +130,7 @@ def make_callback( return -recorded_history: dict[str, list] = ea.defaultdict(list) +recorded_history: dict[str, list] = defaultdict(list) stretch_sim.collect_diagnostics(stretchable_rod).using( AxialStretchingCallBack, step_skip=200, callback_params=recorded_history ) diff --git a/examples/BoundaryConditionsCases/general_constraint_allow_yaw.py b/examples/BoundaryConditionsCases/general_constraint_allow_yaw.py index b4fce8f5..a842571d 100644 --- a/examples/BoundaryConditionsCases/general_constraint_allow_yaw.py +++ b/examples/BoundaryConditionsCases/general_constraint_allow_yaw.py @@ -1,9 +1,11 @@ -__doc__ = """Fixed joint example, for detailed explanation refer to Zhang et. al. Nature Comm. methods section.""" +__doc__ = """General constraint example allowing yaw rotation, for detailed explanation refer to Zhang et. al. Nature Comm. methods section.""" import numpy as np +from collections import defaultdict + import elastica as ea -from examples.BoundaryConditionsCases.bc_cases_postprocessing import ( +from bc_cases_postprocessing import ( plot_position, plot_orientation, plot_video, @@ -92,7 +94,7 @@ class GeneralConstraintSimulator( ) -pp_list_rod1 = ea.defaultdict(list) +pp_list_rod1 = defaultdict(list) general_constraint_sim.collect_diagnostics(rod1).using( diff --git a/examples/ButterflyCase/run_butterfly.py b/examples/ButterflyCase/run_butterfly.py index 85ca10a4..00016120 100644 --- a/examples/ButterflyCase/run_butterfly.py +++ b/examples/ButterflyCase/run_butterfly.py @@ -15,7 +15,7 @@ import numpy as np from matplotlib import pyplot as plt from matplotlib.colors import to_rgb - +from collections import defaultdict import elastica as ea from elastica.utils import MaxDimension @@ -137,7 +137,7 @@ def make_callback( # database -recorded_history: dict[str, list] = ea.defaultdict(list) +recorded_history: dict[str, list] = defaultdict(list) # initially record history recorded_history["time"].append(0.0) diff --git a/examples/CantileverDistributedLoad/cantilever_conservative_distributed_load.py b/examples/CantileverDistributedLoad/cantilever_conservative_distributed_load.py index 1dca913e..932b1867 100644 --- a/examples/CantileverDistributedLoad/cantilever_conservative_distributed_load.py +++ b/examples/CantileverDistributedLoad/cantilever_conservative_distributed_load.py @@ -1,4 +1,5 @@ from matplotlib import pyplot as plt +from collections import defaultdict import numpy as np import elastica as ea import json @@ -113,7 +114,7 @@ def make_callback(self, system, time, current_step: int): ** 0.5 ) - recorded_history = ea.defaultdict(list) + recorded_history = defaultdict(list) square_rod_sim.collect_diagnostics(square_rod).using( CantileverDistributedLoadCallBack, step_skip=200, diff --git a/examples/CantileverDistributedLoad/cantilever_nonconservative_distributed_load.py b/examples/CantileverDistributedLoad/cantilever_nonconservative_distributed_load.py index 6e394cd3..c23b5b64 100644 --- a/examples/CantileverDistributedLoad/cantilever_nonconservative_distributed_load.py +++ b/examples/CantileverDistributedLoad/cantilever_nonconservative_distributed_load.py @@ -1,4 +1,5 @@ from matplotlib import pyplot as plt +from collections import defaultdict import numpy as np import elastica as ea import json @@ -107,7 +108,7 @@ def make_callback(self, system, time, current_step: int): ** 0.5 ) - recorded_history = ea.defaultdict(list) + recorded_history = defaultdict(list) square_rod_sim.collect_diagnostics(square_rod).using( NonConservativeDistributedLoadCallBack, diff --git a/examples/ContinuumFlagellaCase/continuum_flagella.py b/examples/ContinuumFlagellaCase/continuum_flagella.py index 2c2f42ca..efd24e47 100644 --- a/examples/ContinuumFlagellaCase/continuum_flagella.py +++ b/examples/ContinuumFlagellaCase/continuum_flagella.py @@ -1,10 +1,11 @@ -__doc__ = """Continuum flagella example, for detailed explanation refer to Gazzola et. al. R. Soc. 2018 +__doc__ = """Continuum flagella example, for detailed explanation refer to Gazzola et al. R. Soc. 2018 section 5.2.1 """ import numpy as np import os +from collections import defaultdict import elastica as ea -from examples.ContinuumFlagellaCase.continuum_flagella_postprocessing import ( +from continuum_flagella_postprocessing import ( plot_velocity, plot_video, compute_projected_velocity, @@ -117,7 +118,7 @@ def make_callback(self, system, time, current_step: int): return - pp_list = ea.defaultdict(list) + pp_list = defaultdict(list) flagella_sim.collect_diagnostics(shearable_rod).using( ContinuumFlagellaCallBack, step_skip=200, callback_params=pp_list ) diff --git a/examples/ContinuumSnakeCase/run-continuum-snake.py b/examples/ContinuumSnakeCase/run-continuum-snake.py index 1368ba1a..83d92bd3 100644 --- a/examples/ContinuumSnakeCase/run-continuum-snake.py +++ b/examples/ContinuumSnakeCase/run-continuum-snake.py @@ -16,10 +16,11 @@ Getting Started --------------- -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. +To set up the simulation, the first thing you need to do is import the necessary classes. As with the Timoshenko beam, we need to import modules which allow us to more easily construct different simulation systems. We also need to import a rod class, all the necessary forces to be applied, timestepping functions, and callback classes. """ import os +from collections import defaultdict import numpy as np import elastica as ea from numpy.typing import NDArray @@ -225,7 +226,7 @@ def get_slip_velocity(self, system: RodType) -> NDArray[np.float64]: rendering_fps = 60 step_skip = int(1.0 / (rendering_fps * time_step)) - pp_list: dict[str, list] = ea.defaultdict(list) + pp_list: dict[str, list] = defaultdict(list) snake_sim.collect_diagnostics(shearable_rod).using( ContinuumSnakeCallBack, step_skip=step_skip, callback_params=pp_list ) diff --git a/examples/ContinuumSnakeWithLiftingWaveCase/continuum_snake_with_lifting_wave.py b/examples/ContinuumSnakeWithLiftingWaveCase/continuum_snake_with_lifting_wave.py index f8585a91..05312be0 100755 --- a/examples/ContinuumSnakeWithLiftingWaveCase/continuum_snake_with_lifting_wave.py +++ b/examples/ContinuumSnakeWithLiftingWaveCase/continuum_snake_with_lifting_wave.py @@ -1,8 +1,9 @@ -__doc__ = """Snake friction case from X. Zhang et. al. Nat. Comm. 2021""" +__doc__ = """Snake friction case from X. Zhang et al. Nat. Comm. 2021""" import os import numpy as np import pickle +from collections import defaultdict from elastica import * diff --git a/examples/DynamicCantileverCase/dynamic_cantilever.py b/examples/DynamicCantileverCase/dynamic_cantilever.py index bc4a97bb..ea04f451 100644 --- a/examples/DynamicCantileverCase/dynamic_cantilever.py +++ b/examples/DynamicCantileverCase/dynamic_cantilever.py @@ -1,3 +1,5 @@ +from collections import defaultdict + import numpy as np from scipy.fft import fft, fftfreq from scipy.signal import find_peaks @@ -113,18 +115,15 @@ def make_callback(self, system, time, current_step: int): ) return - recorded_history = ea.defaultdict(list) + recorded_history = defaultdict(list) cantilever_sim.collect_diagnostics(cantilever_rod).using( CantileverCallBack, step_skip=step_skips, callback_params=recorded_history ) cantilever_sim.finalize() - total_steps = int(final_time / dt) print(f"Total steps: {total_steps}") timestepper = ea.PositionVerlet() - - dt = final_time / total_steps time = 0.0 for i in range(total_steps): time = timestepper.step(cantilever_sim, time, dt) diff --git a/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_fixed_joint.py b/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_fixed_joint.py index 14d66deb..d4b85e3c 100644 --- a/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_fixed_joint.py +++ b/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_fixed_joint.py @@ -1,6 +1,8 @@ __doc__ = """Fixed joint example, for detailed explanation refer to Zhang et. al. Nature Comm. methods section.""" import numpy as np +from collections import defaultdict + import elastica as ea from elastica.experimental.connection_contact_joint.generic_system_type_connection import ( GenericSystemTypeFixedJoint, @@ -135,9 +137,9 @@ class FixedJointSimulator( time_step=dt, ) -pp_list_rod1 = ea.defaultdict(list) -pp_list_rod2 = ea.defaultdict(list) -pp_list_cylinder = ea.defaultdict(list) +pp_list_rod1 = defaultdict(list) +pp_list_rod2 = defaultdict(list) +pp_list_cylinder = defaultdict(list) fixed_joint_sim.collect_diagnostics(rod1).using( ea.MyCallBack, step_skip=step_skip, callback_params=pp_list_rod1 diff --git a/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_spherical_joint.py b/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_spherical_joint.py index e31bc20b..01dd6415 100644 --- a/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_spherical_joint.py +++ b/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_spherical_joint.py @@ -2,6 +2,8 @@ methods section.""" import numpy as np +from collections import defaultdict + import elastica as ea from elastica.experimental.connection_contact_joint.generic_system_type_connection import ( GenericSystemTypeFreeJoint, @@ -131,9 +133,9 @@ class SphericalJointSimulator( time_step=dt, ) -pp_list_rod1 = ea.defaultdict(list) -pp_list_rod2 = ea.defaultdict(list) -pp_list_cylinder = ea.defaultdict(list) +pp_list_rod1 = defaultdict(list) +pp_list_rod2 = defaultdict(list) +pp_list_cylinder = defaultdict(list) spherical_joint_sim.collect_diagnostics(rod1).using( ea.MyCallBack, step_skip=step_skip, callback_params=pp_list_rod1 diff --git a/examples/ExperimentalCases/ParallelConnectionExample/parallel_connection_example.py b/examples/ExperimentalCases/ParallelConnectionExample/parallel_connection_example.py index acca8253..b3433010 100644 --- a/examples/ExperimentalCases/ParallelConnectionExample/parallel_connection_example.py +++ b/examples/ExperimentalCases/ParallelConnectionExample/parallel_connection_example.py @@ -1,19 +1,16 @@ __doc__ = """Parallel connection example""" import numpy as np +from collections import defaultdict + import elastica as ea from elastica.experimental.connection_contact_joint.parallel_connection import ( get_connection_vector_straight_straight_rod, SurfaceJointSideBySide, ) from elastica._calculus import difference_kernel -import sys - -sys.path.append("../") -sys.path.append("../../") -sys.path.append("../../../") -from examples.JointCases.joint_cases_postprocessing import ( +from joint_cases_postprocessing import ( plot_position, plot_video, plot_video_xy, @@ -171,15 +168,15 @@ def make_callback(self, system, time, current_step: int): return -pp_list_rod1 = ea.defaultdict(list) -pp_list_rod2 = ea.defaultdict(list) +pp_list_rod1 = defaultdict(list) +pp_list_rod2 = defaultdict(list) parallel_connection_sim.collect_diagnostics(rod_one).using( - ParallelConnecitonCallback, step_skip=40, callback_params=pp_list_rod1 + ParallelConnectionCallback, step_skip=40, callback_params=pp_list_rod1 ) parallel_connection_sim.collect_diagnostics(rod_two).using( - ParallelConnecitonCallback, step_skip=40, callback_params=pp_list_rod2 + ParallelConnectionCallback, step_skip=40, callback_params=pp_list_rod2 ) diff --git a/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum.py b/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum.py index cdfadb39..bb24a84b 100644 --- a/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum.py +++ b/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum.py @@ -2,6 +2,9 @@ isort:skip_file """ +import pickle +from collections import defaultdict + import numpy as np from matplotlib import pyplot as plt @@ -112,7 +115,7 @@ def make_callback(self, system, time, current_step: int): total_steps = int(final_time / dt) print("Total steps", total_steps) -recorded_history = ea.defaultdict(list) +recorded_history = defaultdict(list) step_skip = ( 60 if PLOT_VIDEO diff --git a/examples/JointCases/fixed_joint.py b/examples/JointCases/fixed_joint.py index 8a856659..9a1c1180 100644 --- a/examples/JointCases/fixed_joint.py +++ b/examples/JointCases/fixed_joint.py @@ -1,8 +1,9 @@ -__doc__ = """Fixed joint example, for detailed explanation refer to Zhang et. al. Nature Comm. methods section.""" +__doc__ = """Fixed joint example, for detailed explanation refer to Zhang et al. Nature Comm. methods section.""" import numpy as np +from collections import defaultdict import elastica as ea -from examples.JointCases.joint_cases_postprocessing import ( +from joint_cases_postprocessing import ( plot_position, plot_video, plot_video_xy, @@ -100,14 +101,33 @@ class FixedJointSimulator( time_step=dt, ) -pp_list_rod1 = ea.defaultdict(list) -pp_list_rod2 = ea.defaultdict(list) + +class JointCasesCallBack(ea.CallBackBaseClass): + """ + Callback function for joint cases. + """ + + def __init__(self, step_skip: int, callback_params: dict): + super().__init__() + self.every = step_skip + self.callback_params = callback_params + + def make_callback(self, system, time, current_step: int): + if current_step % self.every == 0: + self.callback_params["time"].append(time) + self.callback_params["position"].append(system.position_collection.copy()) + self.callback_params["directors"].append(system.director_collection.copy()) + return + + +pp_list_rod1 = defaultdict(list) +pp_list_rod2 = defaultdict(list) fixed_joint_sim.collect_diagnostics(rod1).using( - ea.MyCallBack, step_skip=1000, callback_params=pp_list_rod1 + JointCasesCallBack, step_skip=1000, callback_params=pp_list_rod1 ) fixed_joint_sim.collect_diagnostics(rod2).using( - ea.MyCallBack, step_skip=1000, callback_params=pp_list_rod2 + JointCasesCallBack, step_skip=1000, callback_params=pp_list_rod2 ) fixed_joint_sim.finalize() diff --git a/examples/JointCases/fixed_joint_torsion.py b/examples/JointCases/fixed_joint_torsion.py index e616fb41..4d44a4db 100644 --- a/examples/JointCases/fixed_joint_torsion.py +++ b/examples/JointCases/fixed_joint_torsion.py @@ -1,9 +1,10 @@ -__doc__ = """Fixed joint example, for detailed explanation refer to Zhang et. al. Nature Comm. methods section.""" +__doc__ = """Fixed joint example, for detailed explanation refer to Zhang et al. Nature Comm. methods section.""" import numpy as np +from collections import defaultdict import elastica as ea from elastica.joint import get_relative_rotation_two_systems -from examples.JointCases.joint_cases_postprocessing import ( +from joint_cases_postprocessing import ( plot_position, plot_orientation, plot_video, @@ -106,25 +107,41 @@ class FixedJointSimulator( ) -pp_list_rod1 = ea.defaultdict(list) -pp_list_rod2 = ea.defaultdict(list) +class JointCasesCallBack(ea.CallBackBaseClass): + """ + Callback function for joint cases. + """ + + def __init__(self, step_skip: int, callback_params: dict): + super().__init__() + self.every = step_skip + self.callback_params = callback_params + + def make_callback(self, system, time, current_step: int): + if current_step % self.every == 0: + self.callback_params["time"].append(time) + self.callback_params["position"].append(system.position_collection.copy()) + self.callback_params["directors"].append(system.director_collection.copy()) + return + + +pp_list_rod1 = defaultdict(list) +pp_list_rod2 = defaultdict(list) fixed_joint_sim.collect_diagnostics(rod1).using( - ea.MyCallBack, step_skip=1000, callback_params=pp_list_rod1 + JointCasesCallBack, step_skip=1000, callback_params=pp_list_rod1 ) fixed_joint_sim.collect_diagnostics(rod2).using( - ea.MyCallBack, step_skip=1000, callback_params=pp_list_rod2 + JointCasesCallBack, step_skip=1000, callback_params=pp_list_rod2 ) fixed_joint_sim.finalize() timestepper = ea.PositionVerlet() final_time = 10 -dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) -dt = final_time / total_steps time = 0.0 for i in range(total_steps): time = timestepper.step(fixed_joint_sim, time, dt) diff --git a/examples/JointCases/hinge_joint.py b/examples/JointCases/hinge_joint.py index f5084869..d7ff076f 100644 --- a/examples/JointCases/hinge_joint.py +++ b/examples/JointCases/hinge_joint.py @@ -1,8 +1,9 @@ -__doc__ = """Hinge joint example, for detailed explanation refer to Zhang et. al. Nature Comm. methods section.""" +__doc__ = """Hinge joint example, for detailed explanation refer to Zhang et al. Nature Comm. methods section.""" import numpy as np +from collections import defaultdict import elastica as ea -from examples.JointCases.joint_cases_postprocessing import ( +from joint_cases_postprocessing import ( plot_position, plot_video, plot_video_xy, @@ -102,14 +103,33 @@ class HingeJointSimulator( time_step=dt, ) -pp_list_rod1 = ea.defaultdict(list) -pp_list_rod2 = ea.defaultdict(list) + +class JointCasesCallBack(ea.CallBackBaseClass): + """ + Callback function for joint cases. + """ + + def __init__(self, step_skip: int, callback_params: dict): + super().__init__() + self.every = step_skip + self.callback_params = callback_params + + def make_callback(self, system, time, current_step: int): + if current_step % self.every == 0: + self.callback_params["time"].append(time) + self.callback_params["position"].append(system.position_collection.copy()) + self.callback_params["directors"].append(system.director_collection.copy()) + return + + +pp_list_rod1 = defaultdict(list) +pp_list_rod2 = defaultdict(list) hinge_joint_sim.collect_diagnostics(rod1).using( - ea.MyCallBack, step_skip=1000, callback_params=pp_list_rod1 + JointCasesCallBack, step_skip=1000, callback_params=pp_list_rod1 ) hinge_joint_sim.collect_diagnostics(rod2).using( - ea.MyCallBack, step_skip=1000, callback_params=pp_list_rod2 + JointCasesCallBack, step_skip=1000, callback_params=pp_list_rod2 ) hinge_joint_sim.finalize() diff --git a/examples/JointCases/spherical_joint.py b/examples/JointCases/spherical_joint.py index e9b78433..ebcfa5ac 100644 --- a/examples/JointCases/spherical_joint.py +++ b/examples/JointCases/spherical_joint.py @@ -1,9 +1,10 @@ -__doc__ = """Spherical(Free) joint example, for detailed explanation refer to Zhang et. al. Nature Comm. +__doc__ = """Spherical(Free) joint example, for detailed explanation refer to Zhang et al. Nature Comm. methods section.""" import numpy as np +from collections import defaultdict import elastica as ea -from examples.JointCases.joint_cases_postprocessing import ( +from joint_cases_postprocessing import ( plot_position, plot_video, plot_video_xy, @@ -103,14 +104,33 @@ class SphericalJointSimulator( time_step=dt, ) -pp_list_rod1 = ea.defaultdict(list) -pp_list_rod2 = ea.defaultdict(list) + +class JointCasesCallBack(ea.CallBackBaseClass): + """ + Callback function for joint cases. + """ + + def __init__(self, step_skip: int, callback_params: dict): + super().__init__() + self.every = step_skip + self.callback_params = callback_params + + def make_callback(self, system, time, current_step: int): + if current_step % self.every == 0: + self.callback_params["time"].append(time) + self.callback_params["position"].append(system.position_collection.copy()) + self.callback_params["directors"].append(system.director_collection.copy()) + return + + +pp_list_rod1 = defaultdict(list) +pp_list_rod2 = defaultdict(list) spherical_joint_sim.collect_diagnostics(rod1).using( - ea.MyCallBack, step_skip=1000, callback_params=pp_list_rod1 + JointCasesCallBack, step_skip=1000, callback_params=pp_list_rod1 ) spherical_joint_sim.collect_diagnostics(rod2).using( - ea.MyCallBack, step_skip=1000, callback_params=pp_list_rod2 + JointCasesCallBack, step_skip=1000, callback_params=pp_list_rod2 ) spherical_joint_sim.finalize() diff --git a/examples/MuscularFlagella/muscular_flagella.py b/examples/MuscularFlagella/muscular_flagella.py index 12764659..d51bee02 100644 --- a/examples/MuscularFlagella/muscular_flagella.py +++ b/examples/MuscularFlagella/muscular_flagella.py @@ -1,17 +1,20 @@ __doc__ = """Muscular flagella example from Zhang et. al. Nature Comm 2019 paper.""" +import os +from collections import defaultdict + import numpy as np import elastica as ea -from examples.MuscularFlagella.post_processing import ( +from post_processing import ( plot_video_2D, plot_video, plot_com_position_vs_time, plot_position_vs_time_comparison_cpp, ) -from examples.MuscularFlagella.connection_flagella import ( +from connection_flagella import ( MuscularFlagellaConnection, ) -from examples.MuscularFlagella.muscle_forces_flagella import MuscleForces +from muscle_forces_flagella import MuscleForces class MuscularFlagellaSimulator( @@ -200,32 +203,36 @@ class MuscularFlagellaSimulator( ) -# Add call backs +# Add callbacks class MuscularFlagellaCallBack(ea.CallBackBaseClass): + """ + Callback function for collecting data from Muscular Flagella simulation. + Records time, position, center of mass, radius, velocity, and tangents. + """ + def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) self.callback_params["position"].append(system.position_collection.copy()) - self.callback_params["com"].append(system.compute_position_center_of_mass()) + self.callback_params["center_of_mass"].append(system.compute_position_center_of_mass()) self.callback_params["radius"].append(system.radius.copy()) self.callback_params["velocity"].append(system.velocity_collection.copy()) self.callback_params["tangents"].append(system.tangents.copy()) -post_processing_dict_body = ea.defaultdict(list) +post_processing_dict_body = defaultdict(list) muscular_flagella_sim.collect_diagnostics(flagella_body).using( MuscularFlagellaCallBack, step_skip=step_skip, callback_params=post_processing_dict_body, ) -post_processing_dict_muscle = ea.defaultdict(list) +post_processing_dict_muscle = defaultdict(list) muscular_flagella_sim.collect_diagnostics(flagella_muscle).using( MuscularFlagellaCallBack, step_skip=step_skip, diff --git a/examples/MuscularSnake/muscular_snake.py b/examples/MuscularSnake/muscular_snake.py index ce265f7c..d87d0639 100644 --- a/examples/MuscularSnake/muscular_snake.py +++ b/examples/MuscularSnake/muscular_snake.py @@ -1,11 +1,12 @@ __doc__ = """Muscular snake example from Zhang et. al. Nature Comm 2019 paper.""" import numpy as np +from collections import defaultdict import elastica as ea -from examples.MuscularSnake.post_processing import ( +from post_processing import ( plot_video_with_surface, plot_snake_velocity, ) -from examples.MuscularSnake.muscle_forces import MuscleForces +from muscle_forces import MuscleForces from elastica.experimental.connection_contact_joint.parallel_connection import ( SurfaceJointSideBySide, get_connection_vector_straight_straight_rod, @@ -248,7 +249,7 @@ class MuscularSnakeSimulator( post_processing_forces_dict_list = [] for i in range(n_muscle_fibers): - post_processing_forces_dict_list.append(ea.defaultdict(list)) + post_processing_forces_dict_list.append(defaultdict(list)) muscle_rod = muscle_rod_list[i] side_of_body = 1 if i % 2 == 0 else -1 @@ -268,7 +269,7 @@ class MuscularSnakeSimulator( straight_straight_rod_connection_list = [] -straight_straight_rod_connection_post_processing_dict = ea.defaultdict(list) +straight_straight_rod_connection_post_processing_dict = defaultdict(list) for idx, rod_two in enumerate(muscle_rod_list): rod_one = snake_body ( @@ -385,7 +386,7 @@ def make_callback(self, system, time, current_step: int): post_processing_dict_list = [] for idx, rod in enumerate(rod_list): - post_processing_dict_list.append(ea.defaultdict(list)) + post_processing_dict_list.append(defaultdict(list)) muscular_snake_simulator.collect_diagnostics(rod).using( MuscularSnakeCallBack, step_skip=step_skip, diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact.py index a25f3af6..aecc5e7c 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact.py @@ -1,4 +1,5 @@ import numpy as np +from collections import defaultdict import elastica as ea from post_processing import plot_velocity, plot_video_with_surface @@ -198,14 +199,14 @@ def make_callback(self, system, time, current_step: int): return - post_processing_dict_list.append(ea.defaultdict(list)) + post_processing_dict_list.append(defaultdict(list)) rod_cylinder_parallel_contact_simulator.collect_diagnostics(rod).using( StraightRodCallBack, step_skip=step_skip, callback_params=post_processing_dict_list[0], ) # For rigid body - post_processing_dict_list.append(ea.defaultdict(list)) + post_processing_dict_list.append(defaultdict(list)) rod_cylinder_parallel_contact_simulator.collect_diagnostics(rigid_body).using( RigidCylinderCallBack, step_skip=step_skip, diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction.py index f4f58a39..d09d4eea 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction.py @@ -1,4 +1,5 @@ import numpy as np +from collections import defaultdict import elastica as ea from post_processing import plot_velocity, plot_video_with_surface @@ -219,14 +220,14 @@ def make_callback(self, system, time, current_step: int): return if POST_PROCESSING: - post_processing_dict_list.append(ea.defaultdict(list)) + post_processing_dict_list.append(defaultdict(list)) rod_cylinder_parallel_contact_simulator.collect_diagnostics(rod).using( StraightRodCallBack, step_skip=step_skip, callback_params=post_processing_dict_list[0], ) # For rigid body - post_processing_dict_list.append(ea.defaultdict(list)) + post_processing_dict_list.append(defaultdict(list)) rod_cylinder_parallel_contact_simulator.collect_diagnostics(rigid_body).using( RigidCylinderCallBack, step_skip=step_skip, diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_validation.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_validation.py index b7dcfc0b..0dce9566 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_validation.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_validation.py @@ -1,4 +1,5 @@ import numpy as np +from collections import defaultdict import elastica as ea from post_processing import plot_video, plot_cylinder_rod_position @@ -130,15 +131,15 @@ def make_callback(self, system, time, current_step: int): self.callback_params["time"].append(time) # Collect only x self.callback_params["position"].append(system.position_collection.copy()) - self.callback_params["com"].append(system.compute_position_center_of_mass()) + self.callback_params["center_of_mass"].append(system.compute_position_center_of_mass()) return -recorded_rod_history = ea.defaultdict(list) +recorded_rod_history = defaultdict(list) single_rod_sim.collect_diagnostics(rod1).using( PositionCollector, step_skip=200, callback_params=recorded_rod_history ) -recorded_cyl_history = ea.defaultdict(list) +recorded_cyl_history = defaultdict(list) single_rod_sim.collect_diagnostics(cylinder).using( PositionCollector, step_skip=200, callback_params=recorded_cyl_history ) 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 5a6b3680..f379e5b2 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_with_y_normal.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_with_y_normal.py @@ -1,4 +1,5 @@ import numpy as np +from collections import defaultdict import elastica as ea from post_processing import plot_video, plot_cylinder_rod_position @@ -108,15 +109,15 @@ def make_callback(self, system, time, current_step: int): self.callback_params["time"].append(time) # Collect only x self.callback_params["position"].append(system.position_collection.copy()) - self.callback_params["com"].append(system.compute_position_center_of_mass()) + self.callback_params["center_of_mass"].append(system.compute_position_center_of_mass()) return -recorded_rod_history = ea.defaultdict(list) +recorded_rod_history = defaultdict(list) single_rod_sim.collect_diagnostics(rod1).using( PositionCollector, step_skip=200, callback_params=recorded_rod_history ) -recorded_cyl_history = ea.defaultdict(list) +recorded_cyl_history = defaultdict(list) single_rod_sim.collect_diagnostics(cylinder).using( PositionCollector, step_skip=200, callback_params=recorded_cyl_history ) diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_sphere_contact.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_sphere_contact.py index 77e389a7..a00d7386 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_sphere_contact.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_sphere_contact.py @@ -1,4 +1,5 @@ import numpy as np +from collections import defaultdict from tqdm import tqdm import elastica as ea from post_processing import plot_video_with_surface @@ -155,11 +156,11 @@ def make_callback(self, system, time, current_step: int): system.director_collection.copy() ) self.callback_params["radius"].append(np.array([system.radius.copy()])) - self.callback_params["com"].append( + self.callback_params["center_of_mass"].append( system.compute_position_center_of_mass() ) - post_processing_dict_list.append(ea.defaultdict(list)) + post_processing_dict_list.append(defaultdict(list)) simulator.collect_diagnostics(rod).using( StraightRodCallBack, step_skip=step_skip, @@ -167,7 +168,7 @@ def make_callback(self, system, time, current_step: int): ) for _ in range(n_sphere): # For rigid body - db = ea.defaultdict(list) + db = defaultdict(list) post_processing_dict_list.append(db) simulator.collect_diagnostics(rigid_body).using( RigidBodyCallback, diff --git a/examples/RigidBodyCases/rigid_cylinder_rotational_motion_case.py b/examples/RigidBodyCases/rigid_cylinder_rotational_motion_case.py index 71522006..282f1b79 100644 --- a/examples/RigidBodyCases/rigid_cylinder_rotational_motion_case.py +++ b/examples/RigidBodyCases/rigid_cylinder_rotational_motion_case.py @@ -1,6 +1,7 @@ import numpy as np +from collections import defaultdict import elastica as ea -from examples.FrictionValidationCases.friction_validation_postprocessing import ( +from friction_validation_postprocessing import ( plot_friction_validation, ) @@ -62,34 +63,30 @@ def apply_forces(self, system, time: np.float64 = np.float64(0.0)): PointCoupleToCenter, torque=torque, direction=direction ) - # Add call backs - class RigidSphereCallBack(ea.CallBackBaseClass): + # Add callbacks + class RigidCylinderCallBack(ea.CallBackBaseClass): """ - Call back function for continuum snake + Callback function for rigid cylinder """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) self.callback_params["position"].append( system.position_collection.copy() ) - self.callback_params["velocity"].append( - system.velocity_collection.copy() - ) return step_skip = 200 - pp_list = ea.defaultdict(list) + pp_list = defaultdict(list) rigid_cylinder_sim.collect_diagnostics(cylinder).using( - RigidSphereCallBack, step_skip=step_skip, callback_params=pp_list + RigidCylinderCallBack, step_skip=step_skip, callback_params=pp_list ) rigid_cylinder_sim.finalize() diff --git a/examples/RigidBodyCases/rigid_cylinder_translational_motion_case.py b/examples/RigidBodyCases/rigid_cylinder_translational_motion_case.py index b86c6e33..64d7b86e 100644 --- a/examples/RigidBodyCases/rigid_cylinder_translational_motion_case.py +++ b/examples/RigidBodyCases/rigid_cylinder_translational_motion_case.py @@ -1,6 +1,7 @@ import numpy as np +from collections import defaultdict import elastica as ea -from examples.FrictionValidationCases.friction_validation_postprocessing import ( +from friction_validation_postprocessing import ( plot_friction_validation, ) @@ -60,34 +61,30 @@ def apply_forces(self, system, time: np.float64 = np.float64(0.0)): PointForceToCenter, force=force, direction=normal.reshape(3, 1) ) - # Add call backs - class RigidSphereCallBack(ea.CallBackBaseClass): + # Add callbacks + class RigidCylinderCallBack(ea.CallBackBaseClass): """ - Call back function + Callback function """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) self.callback_params["position"].append( system.position_collection.copy() ) - self.callback_params["velocity"].append( - system.velocity_collection.copy() - ) return step_skip = 200 - pp_list = ea.defaultdict(list) + pp_list = defaultdict(list) rigid_cylinder_sim.collect_diagnostics(cylinder).using( - RigidSphereCallBack, step_skip=step_skip, callback_params=pp_list + RigidCylinderCallBack, step_skip=step_skip, callback_params=pp_list ) rigid_cylinder_sim.finalize() diff --git a/examples/RigidBodyCases/rigid_sphere_rotational_motion_case.py b/examples/RigidBodyCases/rigid_sphere_rotational_motion_case.py index cc1ed157..1a4f2e8a 100644 --- a/examples/RigidBodyCases/rigid_sphere_rotational_motion_case.py +++ b/examples/RigidBodyCases/rigid_sphere_rotational_motion_case.py @@ -1,6 +1,7 @@ import numpy as np +from collections import defaultdict import elastica as ea -from examples.FrictionValidationCases.friction_validation_postprocessing import ( +from friction_validation_postprocessing import ( plot_friction_validation, ) @@ -53,32 +54,28 @@ def apply_forces(self, system, time: np.float64 = np.float64(0.0)): PointCoupleToCenter, torque=torque, direction=np.array([0.0, -1.0, 0.0]) ) - # Add call backs + # Add callbacks class RigidSphereCallBack(ea.CallBackBaseClass): """ - Call back function + Callback function """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) self.callback_params["position"].append( system.position_collection.copy() ) - self.callback_params["velocity"].append( - system.velocity_collection.copy() - ) return step_skip = 200 - pp_list = ea.defaultdict(list) + pp_list = defaultdict(list) rigid_sphere_sim.collect_diagnostics(sphere).using( RigidSphereCallBack, step_skip=step_skip, callback_params=pp_list ) diff --git a/examples/RigidBodyCases/rigid_sphere_translational_motion_case.py b/examples/RigidBodyCases/rigid_sphere_translational_motion_case.py index 858b393f..3aee16e7 100644 --- a/examples/RigidBodyCases/rigid_sphere_translational_motion_case.py +++ b/examples/RigidBodyCases/rigid_sphere_translational_motion_case.py @@ -1,6 +1,7 @@ import numpy as np +from collections import defaultdict import elastica as ea -from examples.FrictionValidationCases.friction_validation_postprocessing import ( +from friction_validation_postprocessing import ( plot_friction_validation, ) @@ -54,32 +55,28 @@ def apply_forces(self, system, time: np.float64 = np.float64(0.0)): direction=np.array([0.0, -1.0, 0.0]).reshape(3, 1), ) - # Add call backs + # Add callbacks class RigidSphereCallBack(ea.CallBackBaseClass): """ - Call back function + Callback function """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) self.callback_params["position"].append( system.position_collection.copy() ) - self.callback_params["velocity"].append( - system.velocity_collection.copy() - ) return step_skip = 200 - pp_list = ea.defaultdict(list) + pp_list = defaultdict(list) rigid_sphere_sim.collect_diagnostics(sphere).using( RigidSphereCallBack, step_skip=step_skip, callback_params=pp_list ) diff --git a/examples/RingRodCase/ring_rod.py b/examples/RingRodCase/ring_rod.py index ff203b53..5ef6a1ab 100644 --- a/examples/RingRodCase/ring_rod.py +++ b/examples/RingRodCase/ring_rod.py @@ -1,7 +1,8 @@ import numpy as np +from collections import defaultdict import elastica as ea -from examples.RingRodCase.ring_rod_post_processing import plot_video +from ring_rod_post_processing import plot_video class RingSimulator( @@ -85,20 +86,14 @@ def make_callback(self, system, time, current_step: int): self.callback_params["time"].append(time) self.callback_params["step"].append(current_step) self.callback_params["position"].append(system.position_collection.copy()) - self.callback_params["length"].append(system.rest_lengths.copy()) self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["velocity"].append(system.velocity_collection.copy()) - self.callback_params["avg_velocity"].append( - system.compute_velocity_center_of_mass() - ) - self.callback_params["com"].append(system.compute_position_center_of_mass()) - self.callback_params["curvature"].append(system.kappa.copy()) + self.callback_params["center_of_mass"].append(system.compute_position_center_of_mass()) return -pp_list = ea.defaultdict(list) +pp_list = defaultdict(list) ring_sim.collect_diagnostics(ring_rod).using( RingRodCallBack, step_skip=step_skip, callback_params=pp_list ) diff --git a/examples/RodContactCase/RodRodContact/rod_rod_contact_inclined_validation.py b/examples/RodContactCase/RodRodContact/rod_rod_contact_inclined_validation.py index 2f4132ee..7b5fb042 100644 --- a/examples/RodContactCase/RodRodContact/rod_rod_contact_inclined_validation.py +++ b/examples/RodContactCase/RodRodContact/rod_rod_contact_inclined_validation.py @@ -1,6 +1,8 @@ import numpy as np +from collections import defaultdict + import elastica as ea -from examples.RodContactCase.post_processing import ( +from post_processing import ( plot_video_with_surface, plot_velocity, ) @@ -138,7 +140,7 @@ def make_callback(self, system, time, current_step: int): return -post_processing_dict_rod1 = ea.defaultdict( +post_processing_dict_rod1 = defaultdict( list ) # list which collected data will be append # set the diagnostics for rod and collect data @@ -148,7 +150,7 @@ def make_callback(self, system, time, current_step: int): callback_params=post_processing_dict_rod1, ) -post_processing_dict_rod2 = ea.defaultdict( +post_processing_dict_rod2 = defaultdict( list ) # list which collected data will be append # set the diagnostics for rod and collect data diff --git a/examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py b/examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py index e8ebfd8e..2f5012a2 100644 --- a/examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py +++ b/examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py @@ -1,6 +1,8 @@ import numpy as np +from collections import defaultdict + import elastica as ea -from examples.RodContactCase.post_processing import ( +from post_processing import ( plot_video_with_surface, plot_velocity, ) @@ -135,7 +137,7 @@ def make_callback(self, system, time, current_step: int): return -post_processing_dict_rod1 = ea.defaultdict( +post_processing_dict_rod1 = defaultdict( list ) # list which collected data will be append # set the diagnostics for rod and collect data @@ -145,7 +147,7 @@ def make_callback(self, system, time, current_step: int): callback_params=post_processing_dict_rod1, ) -post_processing_dict_rod2 = ea.defaultdict( +post_processing_dict_rod2 = defaultdict( list ) # list which collected data will be append # set the diagnostics for rod and collect data diff --git a/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py b/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py index fa582b00..16d44835 100644 --- a/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py +++ b/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py @@ -1,6 +1,8 @@ import numpy as np +from collections import defaultdict + import elastica as ea -from examples.RodContactCase.post_processing import ( +from post_processing import ( plot_video_with_surface, plot_link_writhe_twist, ) @@ -202,7 +204,7 @@ def make_callback(self, system, time, current_step: int): return -post_processing_dict = ea.defaultdict(list) # list which collected data will be append +post_processing_dict = defaultdict(list) # list which collected data will be append # set the diagnostics for rod and collect data plectonemes_sim.collect_diagnostics(sherable_rod).using( RodCallBack, diff --git a/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py b/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py index c0f683b7..94962b6f 100644 --- a/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py +++ b/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py @@ -1,6 +1,8 @@ import numpy as np +from collections import defaultdict + import elastica as ea -from examples.RodContactCase.post_processing import ( +from post_processing import ( plot_video_with_surface, plot_link_writhe_twist, ) @@ -213,7 +215,7 @@ def make_callback(self, system, time, current_step: int): return -post_processing_dict = ea.defaultdict(list) # list which collected data will be append +post_processing_dict = defaultdict(list) # list which collected data will be append # set the diagnostics for rod and collect data solenoid_sim.collect_diagnostics(sherable_rod).using( RodCallBack, diff --git a/examples/TimoshenkoBeamCase/run_timoshenko.py b/examples/TimoshenkoBeamCase/run_timoshenko.py index dc0d6d6e..ca03c027 100644 --- a/examples/TimoshenkoBeamCase/run_timoshenko.py +++ b/examples/TimoshenkoBeamCase/run_timoshenko.py @@ -16,6 +16,7 @@ """ import numpy as np +from collections import defaultdict import elastica as ea from elastica.version import VERSION @@ -179,7 +180,7 @@ def make_callback(self, system, time, current_step: int): return -recorded_history = ea.defaultdict(list) +recorded_history = defaultdict(list) timoshenko_sim.collect_diagnostics(shearable_rod).using( VelocityCallBack, step_skip=500, callback_params=recorded_history ) diff --git a/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod.py b/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod.py index 7c3fd8bc..14bf735f 100644 --- a/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod.py +++ b/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod.py @@ -1,17 +1,17 @@ import json import numpy as np +from collections import defaultdict from matplotlib import pyplot as plt import elastica as ea from elastica.timestepper.symplectic_steppers import PositionVerlet from tqdm import tqdm -from elastica.external_forces import UniformTorques from forces import ( EndpointforcesWithTimeFactor, EndpointtorqueWithTimeFactor, - lamda_t_function, + time_factor_function, ) from tumbling_unconstrained_rod_postprocessing import ( plot_video_with_surface, @@ -100,94 +100,86 @@ class NonConstrainRodSimulator( class TumblingUnconstrainedRodCallBack(ea.CallBackBaseClass): def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) self.callback_params["position"].append(system.position_collection.copy()) self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["velocity"].append(system.velocity_collection.copy()) - self.callback_params["avg_velocity"].append( - system.compute_velocity_center_of_mass() - ) self.callback_params["center_of_mass"].append( system.compute_position_center_of_mass() ) -if __name__ == "__main__": - - recorded_history = ea.defaultdict(list) - square_rod_sim.collect_diagnostics(square_rod).using( - TumblingUnconstrainedRodCallBack, - step_skip=step_skip, - callback_params=recorded_history, - ) - - square_rod_sim.finalize() - print("System finalized") - - timestepper = PositionVerlet() - - dt = final_time / total_steps - time = 0.0 - for i in tqdm(range(total_steps)): - time = timestepper.step(square_rod_sim, time, dt) - - with open("TumblingUnconstrainedRod.json", "r") as file: - analytic_data = json.load(file) - - time_analytic = analytic_data["time_analytic"] - mass_center_analytic = analytic_data["mass_center_analytic"] - - plt.plot( - time_analytic, - mass_center_analytic[0], - marker="*", - color="black", - label="x_analytic", - ) - plt.plot( - time_analytic, - mass_center_analytic[1], - marker="*", - color="black", - label="y_analytic", - ) - plt.plot( - time_analytic, - mass_center_analytic[2], - marker="*", - color="black", - label="z_analytic", - ) - - mass_center = np.array(recorded_history["center_of_mass"]) - - plt.plot(recorded_history["time"][0:240], mass_center[:, 0][0:240], label="x") - plt.plot(recorded_history["time"][0:240], mass_center[:, 1][0:240], label="y") - plt.plot(recorded_history["time"][0:240], mass_center[:, 2][0:240], label="z") - - plt.xlabel("Time/(second)") # X-axis label - plt.ylabel("Center of mass") # Y-axis label - plt.grid() - plt.legend() # Optional: Add a grid - plt.show() - - plot_video_with_surface( - [recorded_history], - video_name="Tumbling_Unconstrained_Rod.mp4", - fps=rendering_fps, - step=1, - # The following parameters are optional - x_limits=(0, 200), # Set bounds on x-axis - y_limits=(-4, 4), # Set bounds on y-axis - z_limits=(0.0, 8), # Set bounds on z-axis - dpi=100, # Set the quality of the image - vis3D=True, # Turn on 3D visualization - vis2D=False, # Turn on projected (2D) visualization - ) +recorded_history = defaultdict(list) +square_rod_sim.collect_diagnostics(square_rod).using( + TumblingUnconstrainedRodCallBack, + step_skip=step_skip, + callback_params=recorded_history, +) + +square_rod_sim.finalize() +print("System finalized") + +timestepper = PositionVerlet() + +time = 0.0 +for i in tqdm(range(total_steps)): + time = timestepper.step(square_rod_sim, time, dt) + +with open("TumblingUnconstrainedRod.json", "r") as file: + analytic_data = json.load(file) + +time_analytic = analytic_data["time_analytic"] +mass_center_analytic = analytic_data["mass_center_analytic"] + +plt.plot( + time_analytic, + mass_center_analytic[0], + marker="*", + color="black", + label="x_analytic", +) +plt.plot( + time_analytic, + mass_center_analytic[1], + marker="*", + color="black", + label="y_analytic", +) +plt.plot( + time_analytic, + mass_center_analytic[2], + marker="*", + color="black", + label="z_analytic", +) + +mass_center = np.array(recorded_history["center_of_mass"]) + +plt.plot(recorded_history["time"], mass_center[:, 0], label="x") +plt.plot(recorded_history["time"], mass_center[:, 1], label="y") +plt.plot(recorded_history["time"], mass_center[:, 2], label="z") + +plt.xlabel("Time (seconds)") +plt.ylabel("Center of mass") +plt.grid() +plt.legend() +plt.show() + +plot_video_with_surface( + [recorded_history], + video_name="Tumbling_Unconstrained_Rod.mp4", + fps=rendering_fps, + step=1, + # The following parameters are optional + x_limits=(0, 200), # Set bounds on x-axis + y_limits=(-4, 4), # Set bounds on y-axis + z_limits=(0.0, 8), # Set bounds on z-axis + dpi=100, # Set the quality of the image + vis3D=True, # Turn on 3D visualization + vis2D=False, # Turn on projected (2D) visualization +) From c61d0835907ebdb75746bd40b6981d7d9ca0fb21 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 12 Dec 2025 03:28:27 -0600 Subject: [PATCH 42/85] move convergen_function into use case directories --- .../convergence_functions.py | 3 + .../convergence_functions.py | 61 +++++++++++++++++++ .../convergence_functions.py | 61 +++++++++++++++++++ 3 files changed, 125 insertions(+) rename examples/{ => CantileverTransversalLoadCase}/convergence_functions.py (93%) create mode 100644 examples/HelicalBucklingCase/convergence_functions.py create mode 100644 examples/TimoshenkoBeamCase/convergence_functions.py diff --git a/examples/convergence_functions.py b/examples/CantileverTransversalLoadCase/convergence_functions.py similarity index 93% rename from examples/convergence_functions.py rename to examples/CantileverTransversalLoadCase/convergence_functions.py index 743eb0cd..ca81417f 100644 --- a/examples/convergence_functions.py +++ b/examples/CantileverTransversalLoadCase/convergence_functions.py @@ -51,6 +51,9 @@ def plot_convergence(results, SAVE_FIGURE, filename): label="l2", ) ax.loglog(convergence_elements, linf, marker="o", ms=10, c="k", lw=2, label="linf") + ax.set_xlabel("N_element") + ax.set_ylabel("Error") + ax.set_title("Error Convergence Analysis") fig.legend(prop={"size": 20}) if SAVE_FIGURE: assert filename != "", "provide a file name for figure" diff --git a/examples/HelicalBucklingCase/convergence_functions.py b/examples/HelicalBucklingCase/convergence_functions.py new file mode 100644 index 00000000..ca81417f --- /dev/null +++ b/examples/HelicalBucklingCase/convergence_functions.py @@ -0,0 +1,61 @@ +import numpy as np +from matplotlib import pyplot as plt +from matplotlib.colors import to_rgb +from scipy.linalg import norm + + +def calculate_error_norm(true_solution, computed_solution, n_elem): + assert ( + true_solution.shape == computed_solution.shape + ), "Shape of computed and true solution does not match" + error = true_solution - computed_solution + l1 = norm(error, 1) / n_elem + l2 = norm(error, 2) / n_elem + linf = norm(error, np.inf) + + return error, l1, l2, linf + + +def plot_convergence(results, SAVE_FIGURE, filename): + convergence_elements = [] + l1 = [] + l2 = [] + linf = [] + + for result in results: + convergence_elements.append(result["rod"].n_elems) + l1.append(result["l1"]) + l2.append(result["l2"]) + linf.append(result["linf"]) + + fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150) + ax = fig.add_subplot(111) + ax.grid(which="minor", color="k", linestyle="--") + ax.grid(which="major", color="k", linestyle="-") + ax.loglog( + convergence_elements, + l1, + marker="o", + ms=10, + c=to_rgb("xkcd:bluish"), + lw=2, + label="l1", + ) + ax.loglog( + convergence_elements, + l2, + marker="o", + ms=10, + c=to_rgb("xkcd:reddish"), + lw=2, + label="l2", + ) + ax.loglog(convergence_elements, linf, marker="o", ms=10, c="k", lw=2, label="linf") + ax.set_xlabel("N_element") + ax.set_ylabel("Error") + ax.set_title("Error Convergence Analysis") + fig.legend(prop={"size": 20}) + if SAVE_FIGURE: + assert filename != "", "provide a file name for figure" + fig.savefig(filename) + fig.show() diff --git a/examples/TimoshenkoBeamCase/convergence_functions.py b/examples/TimoshenkoBeamCase/convergence_functions.py new file mode 100644 index 00000000..ca81417f --- /dev/null +++ b/examples/TimoshenkoBeamCase/convergence_functions.py @@ -0,0 +1,61 @@ +import numpy as np +from matplotlib import pyplot as plt +from matplotlib.colors import to_rgb +from scipy.linalg import norm + + +def calculate_error_norm(true_solution, computed_solution, n_elem): + assert ( + true_solution.shape == computed_solution.shape + ), "Shape of computed and true solution does not match" + error = true_solution - computed_solution + l1 = norm(error, 1) / n_elem + l2 = norm(error, 2) / n_elem + linf = norm(error, np.inf) + + return error, l1, l2, linf + + +def plot_convergence(results, SAVE_FIGURE, filename): + convergence_elements = [] + l1 = [] + l2 = [] + linf = [] + + for result in results: + convergence_elements.append(result["rod"].n_elems) + l1.append(result["l1"]) + l2.append(result["l2"]) + linf.append(result["linf"]) + + fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150) + ax = fig.add_subplot(111) + ax.grid(which="minor", color="k", linestyle="--") + ax.grid(which="major", color="k", linestyle="-") + ax.loglog( + convergence_elements, + l1, + marker="o", + ms=10, + c=to_rgb("xkcd:bluish"), + lw=2, + label="l1", + ) + ax.loglog( + convergence_elements, + l2, + marker="o", + ms=10, + c=to_rgb("xkcd:reddish"), + lw=2, + label="l2", + ) + ax.loglog(convergence_elements, linf, marker="o", ms=10, c="k", lw=2, label="linf") + ax.set_xlabel("N_element") + ax.set_ylabel("Error") + ax.set_title("Error Convergence Analysis") + fig.legend(prop={"size": 20}) + if SAVE_FIGURE: + assert filename != "", "provide a file name for figure" + fig.savefig(filename) + fig.show() From a7df9f64cd2296562b53553e1825ef2b6a39d42a Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 12 Dec 2025 03:37:03 -0600 Subject: [PATCH 43/85] remove outdated gitignores --- examples/.gitignore | 1 - examples/AxialStretchingCase/.gitignore | 2 -- examples/ButterflyCase/.gitignore | 5 ----- 3 files changed, 8 deletions(-) delete mode 100644 examples/.gitignore delete mode 100644 examples/AxialStretchingCase/.gitignore delete mode 100644 examples/ButterflyCase/.gitignore diff --git a/examples/.gitignore b/examples/.gitignore deleted file mode 100644 index ed6a5437..00000000 --- a/examples/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*local* diff --git a/examples/AxialStretchingCase/.gitignore b/examples/AxialStretchingCase/.gitignore deleted file mode 100644 index 13833d4b..00000000 --- a/examples/AxialStretchingCase/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -axial_stretching_data.dat -axial_stretching.pdf diff --git a/examples/ButterflyCase/.gitignore b/examples/ButterflyCase/.gitignore deleted file mode 100644 index 62b9c346..00000000 --- a/examples/ButterflyCase/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -butterfly_data.dat -butterfly.pdf -energies.pdf -butterfly.png -energies.png From 229ad301ccb6be9d13d520253e84d4072a7ccdac Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 12 Dec 2025 03:58:41 -0600 Subject: [PATCH 44/85] examples: naming consistency - com --> center_of_mass --- .../cantilever_conservative_distributed_load.py | 5 +---- .../cantilever_distrubuted_load_postprecessing.py | 4 +++- .../cantilever_nonconservative_distributed_load.py | 5 +---- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/examples/CantileverDistributedLoad/cantilever_conservative_distributed_load.py b/examples/CantileverDistributedLoad/cantilever_conservative_distributed_load.py index 932b1867..a1d759a1 100644 --- a/examples/CantileverDistributedLoad/cantilever_conservative_distributed_load.py +++ b/examples/CantileverDistributedLoad/cantilever_conservative_distributed_load.py @@ -80,7 +80,7 @@ class SquareRodSimulator( # Add call backs class CantileverDistributedLoadCallBack(ea.CallBackBaseClass): def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -91,9 +91,6 @@ def make_callback(self, system, time, current_step: int): self.callback_params["position"].append( system.position_collection.copy() ) - self.callback_params["com"].append( - system.compute_position_center_of_mass() - ) self.callback_params["radius"].append(system.radius.copy()) self.callback_params["velocity"].append( system.velocity_collection.copy() diff --git a/examples/CantileverDistributedLoad/cantilever_distrubuted_load_postprecessing.py b/examples/CantileverDistributedLoad/cantilever_distrubuted_load_postprecessing.py index e0fb8abb..990943c9 100644 --- a/examples/CantileverDistributedLoad/cantilever_distrubuted_load_postprecessing.py +++ b/examples/CantileverDistributedLoad/cantilever_distrubuted_load_postprecessing.py @@ -158,7 +158,9 @@ def plot_video_with_surface( rods_history[rod_idx]["radius"][t_idx], ) # Rod center of mass - com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx]["com"][time_idx] + com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx][ + "center_of_mass" + ][t_idx] # Generate target sphere data sphere_flag = False diff --git a/examples/CantileverDistributedLoad/cantilever_nonconservative_distributed_load.py b/examples/CantileverDistributedLoad/cantilever_nonconservative_distributed_load.py index c23b5b64..50f58bea 100644 --- a/examples/CantileverDistributedLoad/cantilever_nonconservative_distributed_load.py +++ b/examples/CantileverDistributedLoad/cantilever_nonconservative_distributed_load.py @@ -74,7 +74,7 @@ class NonConservativeDistributedLoadCallBack(ea.CallBackBaseClass): """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -85,9 +85,6 @@ def make_callback(self, system, time, current_step: int): self.callback_params["position"].append( system.position_collection.copy() ) - self.callback_params["com"].append( - system.compute_position_center_of_mass() - ) self.callback_params["radius"].append(system.radius.copy()) self.callback_params["velocity"].append( system.velocity_collection.copy() From c8fe3cc2c1277885591289bc263ca09689919baf Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 12 Dec 2025 04:17:57 -0600 Subject: [PATCH 45/85] fix: correct typos and improve naming consistency across various examples --- .../run_axial_stretching.py | 2 +- .../bc_cases_postprocessing.py | 28 +- .../general_constraint_allow_yaw.py | 4 - examples/ButterflyCase/run_butterfly.py | 16 +- ...antilever_conservative_distributed_load.py | 8 +- ...tilever_distrubuted_load_postprecessing.py | 8 +- ...ilever_nonconservative_distributed_load.py | 65 ++-- .../cantilever_transversal_load.py | 8 +- ...tilever_transversal_load_postprocessing.py | 252 ------------- ...convergence_cantilever_transversal_load.py | 34 +- .../setup_helper.py | 100 ++++++ examples/CatenaryCase/post_processing.py | 4 +- examples/CatenaryCase/run_catenary.py | 3 +- .../continuum_flagella.py | 10 +- .../continuum_flagella_postprocessing.py | 8 +- .../ContinuumSnakeCase/run-continuum-snake.py | 4 +- .../snake_contact.py | 4 +- .../snake_forcing.py | 4 +- .../analytical_dynamic_cantilever.py | 2 +- .../dynamic_cantilever.py | 12 +- .../dynamic_cantilever_phase_space.py | 2 +- .../dynamic_cantilever_post_processing.py | 5 +- .../dynamic_cantilever_visualization.py | 9 - .../generic_system_type_fixed_joint.py | 3 - .../generic_system_type_spherical_joint.py | 3 - .../joint_cases_postprocessing.py | 340 ++++++++++++++++++ .../parallel_connection_example.py | 6 +- .../flexible_swinging_pendulum.py | 15 +- ...exible_swinging_pendulum_visualize_data.py | 49 +-- .../FrictionValidationCases/axial_friction.py | 3 +- .../rolling_friction_initial_velocity.py | 3 +- .../rolling_friction_on_inclined_plane.py | 3 +- .../rolling_friction_torque.py | 3 +- .../convergence_helicalbuckling.py | 6 +- .../HelicalBucklingCase/helicalbuckling.py | 11 +- .../helicalbuckling_postprocessing.py | 2 +- examples/JointCases/fixed_joint.py | 5 +- examples/JointCases/fixed_joint_torsion.py | 1 - examples/JointCases/hinge_joint.py | 3 - examples/JointCases/spherical_joint.py | 5 +- examples/KnotCase/run_knot_simulation.py | 2 +- .../MuscularFlagella/connection_flagella.py | 2 +- .../muscle_forces_flagella.py | 4 - .../MuscularFlagella/muscular_flagella.py | 14 +- examples/MuscularFlagella/post_processing.py | 10 +- examples/MuscularSnake/muscular_snake.py | 14 +- examples/MuscularSnake/post_processing.py | 12 +- examples/RestartExample/restart_example.py | 7 +- .../RodRigidBodyContact/post_processing.py | 18 +- .../rod_cylinder_contact.py | 18 +- .../rod_cylinder_contact_friction.py | 14 +- .../rod_cylinder_contact_validation.py | 10 +- .../rod_cylinder_contact_with_y_normal.py | 11 +- .../RodRigidBodyContact/rod_sphere_contact.py | 12 +- .../rigid_cylinder_rotational_motion_case.py | 10 +- ...igid_cylinder_translational_motion_case.py | 4 - .../rigid_sphere_rotational_motion_case.py | 17 +- .../rigid_sphere_translational_motion_case.py | 1 - examples/RingRodCase/ring_rod.py | 13 +- .../RingRodCase/ring_rod_post_processing.py | 4 +- .../rod_rod_contact_inclined_validation.py | 12 +- .../rod_rod_contact_parallel_validation.py | 12 +- .../PlectonemesCase/plectoneme_case.py | 18 +- .../SolenoidsCase/solenoid_case.py | 19 +- examples/RodContactCase/post_processing.py | 4 +- examples/TimoshenkoBeamCase/run_timoshenko.py | 6 +- .../timoshenko_postprocessing.py | 12 +- examples/TumblingUnconstrainedRod/forces.py | 6 +- .../tumbling_unconstrained_rod.py | 5 +- ...mbling_unconstrained_rod_postprocessing.py | 6 +- 70 files changed, 714 insertions(+), 636 deletions(-) delete mode 100644 examples/CantileverTransversalLoadCase/cantilever_transversal_load_postprocessing.py create mode 100644 examples/CantileverTransversalLoadCase/setup_helper.py create mode 100644 examples/ExperimentalCases/ParallelConnectionExample/joint_cases_postprocessing.py diff --git a/examples/AxialStretchingCase/run_axial_stretching.py b/examples/AxialStretchingCase/run_axial_stretching.py index 7cd38bdf..18ecae23 100644 --- a/examples/AxialStretchingCase/run_axial_stretching.py +++ b/examples/AxialStretchingCase/run_axial_stretching.py @@ -21,7 +21,7 @@ # Simulation Setup # ---------------- # We define a simulator class that inherits from the necessary mixins. -# This makes constraints, forces, and damping evailable to the system. +# This makes constraints, forces, and damping available to the system. class StretchingBeamSimulator( diff --git a/examples/BoundaryConditionsCases/bc_cases_postprocessing.py b/examples/BoundaryConditionsCases/bc_cases_postprocessing.py index 954dd4e8..1a7196dd 100644 --- a/examples/BoundaryConditionsCases/bc_cases_postprocessing.py +++ b/examples/BoundaryConditionsCases/bc_cases_postprocessing.py @@ -53,7 +53,7 @@ def plot_video( ): # (time step, x/y/z, node) import matplotlib.animation as manimation - time = plot_params_rod1["time"] + time_list = plot_params_rod1["time"] position_of_rod1 = np.array(plot_params_rod1["position"]) print("plot video") @@ -62,15 +62,15 @@ def plot_video( writer = FFMpegWriter(fps=fps, metadata=metadata) fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150) with writer.saving(fig, video_name, 100): - for time in range(1, len(time)): + for time_idx in range(1, len(time_list)): fig.clf() ax = plt.axes(projection="3d") # fig.add_subplot(111) ax.grid(which="minor", color="k", linestyle="--") ax.grid(which="major", color="k", linestyle="-") ax.plot( - position_of_rod1[time, 0], - position_of_rod1[time, 1], - position_of_rod1[time, 2], + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 1], + position_of_rod1[time_idx, 2], "or", label="rod1", ) @@ -91,7 +91,7 @@ def plot_video_xy( ): # (time step, x/y/z, node) import matplotlib.animation as manimation - time = plot_params_rod1["time"] + time_list = plot_params_rod1["time"] position_of_rod1 = np.array(plot_params_rod1["position"]) print("plot video xy") @@ -101,10 +101,13 @@ def plot_video_xy( fig = plt.figure() plt.axis("equal") with writer.saving(fig, video_name, 100): - for time in range(1, len(time)): + for time_idx in range(1, len(time_list)): fig.clf() plt.plot( - position_of_rod1[time, 0], position_of_rod1[time, 1], "or", label="rod1" + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 1], + "or", + label="rod1", ) plt.xlim([-0.25, 0.25]) @@ -121,7 +124,7 @@ def plot_video_xz( ): # (time step, x/y/z, node) import matplotlib.animation as manimation - time = plot_params_rod1["time"] + time_list = plot_params_rod1["time"] position_of_rod1 = np.array(plot_params_rod1["position"]) print("plot video xz") @@ -131,10 +134,13 @@ def plot_video_xz( fig = plt.figure() plt.axis("equal") with writer.saving(fig, video_name, 100): - for time in range(1, len(time)): + for time_idx in range(1, len(time_list)): fig.clf() plt.plot( - position_of_rod1[time, 0], position_of_rod1[time, 2], "or", label="rod1" + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 2], + "or", + label="rod1", ) plt.xlim([-0.25, 0.25]) diff --git a/examples/BoundaryConditionsCases/general_constraint_allow_yaw.py b/examples/BoundaryConditionsCases/general_constraint_allow_yaw.py index a842571d..08d2505f 100644 --- a/examples/BoundaryConditionsCases/general_constraint_allow_yaw.py +++ b/examples/BoundaryConditionsCases/general_constraint_allow_yaw.py @@ -17,7 +17,6 @@ class GeneralConstraintSimulator( ea.BaseSystemCollection, ea.Constraints, - ea.Connections, ea.Forcing, ea.Damping, ea.CallBacks, @@ -33,7 +32,6 @@ class GeneralConstraintSimulator( normal = np.array([0.0, 1.0, 0.0]) base_length = 0.2 base_radius = 0.007 -base_area = np.pi * base_radius**2 density = 1750 E = 3e7 poisson_ratio = 0.5 @@ -41,14 +39,12 @@ class GeneralConstraintSimulator( # setting up timestepper and video final_time = 10 -dl = base_length / n_elem dt = 1e-5 total_steps = int(final_time / dt) fps = 100 # frames per second of the video diagnostic_step_skip = 1 / (fps * dt) start_rod_1 = np.zeros((3,)) -start_rod_2 = start_rod_1 + direction * base_length # Create rod 1 rod1 = ea.CosseratRod.straight_rod( diff --git a/examples/ButterflyCase/run_butterfly.py b/examples/ButterflyCase/run_butterfly.py index 00016120..87f33854 100644 --- a/examples/ButterflyCase/run_butterfly.py +++ b/examples/ButterflyCase/run_butterfly.py @@ -4,7 +4,7 @@ This case simulates the motion of a rod that is initially shaped like a butterfly. The rod is released from rest and allowed to deform freely. -The goal of the simulation is for sanity check: how does the timestepper +The goal of the simulation is as a sanity check: how does the timestepper reliably preserve total energy of the system, when the system is simple Hamiltonian. The simulation tracks the position and energy of the rod over time. @@ -39,7 +39,8 @@ class ButterflySimulator(ea.BaseSystemCollection, ea.CallBacks): # Next, we set up the test parameters for the simulation. # setting up test params -# FIXME : Doesn't work with elements > 10 (the inverse rotate kernel fails) +# Note: This example has a limitation where n_elem > 10 may fail due to +# inverse rotation kernel issues. For reliable results, keep n_elem <= 10. n_elem = 4 # Change based on requirements, but be careful n_elem += n_elem % 2 half_n_elem = n_elem // 2 @@ -56,7 +57,6 @@ class ButterflySimulator(ea.BaseSystemCollection, ea.CallBacks): total_length = 3.0 base_radius = 0.25 -base_area = np.pi * base_radius**2 density = 5000 youngs_modulus = 1e4 poisson_ratio = 0.5 @@ -109,13 +109,13 @@ class ButterflySimulator(ea.BaseSystemCollection, ea.CallBacks): # Add call backs -class VelocityCallBack(ea.CallBackBaseClass): +class ButterflyCallBack(ea.CallBackBaseClass): """ - Call back function for continuum snake + Call back function for butterfly case to track position and energy """ def __init__(self, step_skip: int, callback_params: dict) -> None: - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -148,7 +148,7 @@ def make_callback( recorded_history["be"].append(butterfly_rod.compute_bending_energy()) butterfly_sim.collect_diagnostics(butterfly_rod).using( - VelocityCallBack, step_skip=100, callback_params=recorded_history + ButterflyCallBack, step_skip=100, callback_params=recorded_history ) # %% @@ -203,7 +203,7 @@ def make_callback( be = np.asarray(recorded_history["be"]) se = np.asarray(recorded_history["se"]) -energy_ax.plot(times, te, c=to_rgb("xkcd:reddish"), lw=2.0, label="Translations") +energy_ax.plot(times, te, c=to_rgb("xkcd:reddish"), lw=2.0, label="Translational") energy_ax.plot(times, re, c=to_rgb("xkcd:bluish"), lw=2.0, label="Rotation") energy_ax.plot(times, be, c=to_rgb("xkcd:burple"), lw=2.0, label="Bend") energy_ax.plot(times, se, c=to_rgb("xkcd:goldenrod"), lw=2.0, label="Shear") diff --git a/examples/CantileverDistributedLoad/cantilever_conservative_distributed_load.py b/examples/CantileverDistributedLoad/cantilever_conservative_distributed_load.py index a1d759a1..730c313d 100644 --- a/examples/CantileverDistributedLoad/cantilever_conservative_distributed_load.py +++ b/examples/CantileverDistributedLoad/cantilever_conservative_distributed_load.py @@ -30,7 +30,7 @@ class SquareRodSimulator( np.pi ** (1 / 2) ) # The Cross-sectional area is 1e-4(we assume its equivalent to a square cross-sectional surface with same area) base_area = np.pi * base_radius**2 - density = 1000 # nomilized with conservative case F=15 + density = 1000 # normalized with conservative case F=15 youngs_modulus = 1.2e7 dl = base_length / n_elem dt = 0.1 * dl / 50 @@ -127,12 +127,6 @@ def make_callback(self, system, time, current_step: int): for i in range(total_steps): time = timestepper.step(square_rod_sim, time, dt) - relative_tip_position = np.zeros( - 2, - ) - relative_tip_position[0] = find_tip_position(square_rod, n_elem)[0] / base_length - relative_tip_position[1] = -find_tip_position(square_rod, n_elem)[1] / base_length - if animation: plot_video_with_surface( [recorded_history], diff --git a/examples/CantileverDistributedLoad/cantilever_distrubuted_load_postprecessing.py b/examples/CantileverDistributedLoad/cantilever_distrubuted_load_postprecessing.py index 990943c9..3a5aa807 100644 --- a/examples/CantileverDistributedLoad/cantilever_distrubuted_load_postprecessing.py +++ b/examples/CantileverDistributedLoad/cantilever_distrubuted_load_postprecessing.py @@ -8,17 +8,17 @@ from elastica.external_forces import NoForces, inplace_addition, SystemType -class NonconserativeForce(NoForces): +class NonConservativeForce(NoForces): def __init__(self, load=1): - super(NonconserativeForce, self).__init__() + super().__init__() self.load = load def apply_forces(self, system: SystemType, time=0.0): - self.compute_nonconserative_forces( + self.compute_nonconservative_forces( self.load, system.mass, system.director_collection, system.external_forces ) - def compute_nonconserative_forces(self, load, mass, direction, external_forces): + def compute_nonconservative_forces(self, load, mass, direction, external_forces): NCforce_direction = direction[0] NCforce_direction = NCforce_direction / np.linalg.norm( NCforce_direction, axis=0 diff --git a/examples/CantileverDistributedLoad/cantilever_nonconservative_distributed_load.py b/examples/CantileverDistributedLoad/cantilever_nonconservative_distributed_load.py index 50f58bea..d03d729e 100644 --- a/examples/CantileverDistributedLoad/cantilever_nonconservative_distributed_load.py +++ b/examples/CantileverDistributedLoad/cantilever_nonconservative_distributed_load.py @@ -8,7 +8,7 @@ plot_video_with_surface, find_tip_position, adjust_square_cross_section, - NonconserativeForce, + NonConservativeForce, ) @@ -19,15 +19,27 @@ class SquareRodSimulator( def cantilever_subjected_to_a_nonconservative_load( - n_elem, - base_length, - side_length, - base_radius, - youngs_modulus, - dimentionless_varible, + dimensionless_variable, animation=False, plot_figure_equilibrium=False, ): + # Setting up test params + final_time = 10 + n_elem = 100 + start = np.zeros((3,)) + direction = np.array([1.0, 0.0, 0.0]) + normal = np.array([0.0, 1.0, 0.0]) + base_length = 0.5 + side_length = 0.01 + base_radius = 0.01 / (np.pi ** (1 / 2)) + base_area = np.pi * base_radius**2 + density = 1000 + youngs_modulus = 1.2e7 + # For shear modulus of 1e4, nu is 99! + poisson_ratio = 0 + shear_modulus = youngs_modulus / (2 * (poisson_ratio + 1.0)) + I = (0.01**4) / 12 + square_rod_sim = SquareRodSimulator() square_rod = ea.CosseratRod.straight_rod( @@ -50,11 +62,11 @@ def cantilever_subjected_to_a_nonconservative_load( ea.OneEndFixedBC, constrained_position_idx=(0,), constrained_director_idx=(0,) ) - load = (youngs_modulus * I * dimentionless_varible) / ( + load = (youngs_modulus * I * dimensionless_variable) / ( density * base_area * (base_length**3) ) - square_rod_sim.add_forcing_to(square_rod).using(NonconserativeForce, load) + square_rod_sim.add_forcing_to(square_rod).using(NonConservativeForce, load) # add damping dl = base_length / n_elem @@ -167,27 +179,7 @@ def make_callback(self, system, time, current_step: int): if __name__ == "__main__": - final_time = 10 - # setting up test params - n_elem = 100 - start = np.zeros((3,)) - direction = np.array([1.0, 0.0, 0.0]) - normal = np.array([0.0, 1.0, 0.0]) - base_length = 0.5 - side_length = 0.01 - base_radius = 0.01 / (np.pi ** (1 / 2)) - base_area = np.pi * base_radius**2 - density = 1000 - dimentionless_varible = 15 - youngs_modulus = 1.2e7 - # For shear modulus of 1e4, nu is 99! - poisson_ratio = 0 - shear_modulus = youngs_modulus / (2 * (poisson_ratio + 1.0)) - I = (0.01**4) / 12 - - cantilever_subjected_to_a_nonconservative_load( - n_elem, base_length, side_length, base_radius, youngs_modulus, -15, True, False - ) + cantilever_subjected_to_a_nonconservative_load(-15, True, False) with open("cantilever_distributed_load_data.json", "r") as file: tip_position_paper = json.load(file) @@ -199,16 +191,11 @@ def make_callback(self, system, time, current_step: int): load_on_rod = np.arange(1, 26, 2) for i in load_on_rod: - x_tip_experiment.append( - cantilever_subjected_to_a_nonconservative_load( - n_elem, base_length, base_radius, youngs_modulus, i, False, False - )[0] - ) - y_tip_experiment.append( - -cantilever_subjected_to_a_nonconservative_load( - n_elem, base_length, base_radius, youngs_modulus, i, False, False - )[1] + relative_tip_position = cantilever_subjected_to_a_nonconservative_load( + i, False, False ) + x_tip_experiment.append(relative_tip_position[0]) + y_tip_experiment.append(-relative_tip_position[1]) plt.plot( load_on_rod, diff --git a/examples/CantileverTransversalLoadCase/cantilever_transversal_load.py b/examples/CantileverTransversalLoadCase/cantilever_transversal_load.py index 56f9b7d4..442de4a0 100644 --- a/examples/CantileverTransversalLoadCase/cantilever_transversal_load.py +++ b/examples/CantileverTransversalLoadCase/cantilever_transversal_load.py @@ -26,7 +26,6 @@ def cantilever_subjected_to_a_transversal_load(n_elem=19): base_radius = 0.01 / ( np.pi ** (1 / 2) ) # The Cross-sectional area is 1e-4(we assume its equivalent to a square cross-sectional surface with same area) - base_area = 1e-4 density = 1000 youngs_modulus = 1e9 poisson_ratio = 0 @@ -39,7 +38,6 @@ class SquareRodSimulator( square_rod_sim = SquareRodSimulator() - density = 1000 t = np.linspace(0, 0.25 * np.pi, n_elem + 1) tmp = np.zeros((3, n_elem + 1), dtype=np.float64) tmp[0, :] = -radius * np.cos(t) + 1 @@ -76,7 +74,6 @@ class SquareRodSimulator( square_rod_sim.append(square_rod) - # square_rod_sim.finalize() square_rod.rest_kappa[...] = square_rod.kappa dl = base_length / n_elem @@ -112,9 +109,8 @@ class SquareRodSimulator( square_rod_sim.finalize() print("System finalized") - # The simulation result from Project3.3.2 with 400 elements/ Tip position Z - - # generate analytical solution array from [400] + # The simulation result from Project3.3.2 with 400 elements (tip position Z) + # Generate analytical solution array by interpolating from the 400-element reference solution analytical_results_sub = np.zeros(n_elem + 1) diff --git a/examples/CantileverTransversalLoadCase/cantilever_transversal_load_postprocessing.py b/examples/CantileverTransversalLoadCase/cantilever_transversal_load_postprocessing.py deleted file mode 100644 index fc0dce06..00000000 --- a/examples/CantileverTransversalLoadCase/cantilever_transversal_load_postprocessing.py +++ /dev/null @@ -1,252 +0,0 @@ -import numpy as np -from matplotlib import pyplot as plt -from matplotlib import cm -from tqdm import tqdm -from typing import Dict, Sequence -import logging -from elastica.utils import MaxDimension, Tolerance - - -def find_tip_position(rod, n_elem): - x_tip = rod.position_collection[0][n_elem] - y_tip = rod.position_collection[1][n_elem] - z_tip = rod.position_collection[2][n_elem] - - return x_tip, y_tip, z_tip - - -def plot_video_with_surface( - rods_history: Sequence[Dict], - video_name="video.mp4", - fps=60, - step=1, - **kwargs, -): - plt.rcParams.update({"font.size": 22}) - - folder_name = kwargs.get("folder_name", "") - - # 2d case - import matplotlib.animation as animation - - # simulation time - sim_time = np.array(rods_history[0]["time"]) - - # Rod - n_visualized_rods = len(rods_history) # should be one for now - # Rod info - rod_history_unpacker = lambda rod_idx, t_idx: ( - rods_history[rod_idx]["position"][t_idx], - rods_history[rod_idx]["radius"][t_idx], - ) - # Rod center of mass - com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx]["com"][time_idx] - - # Generate target sphere data - sphere_flag = False - if kwargs.__contains__("sphere_history"): - sphere_flag = True - sphere_history = kwargs.get("sphere_history") - n_visualized_spheres = len(sphere_history) # should be one for now - sphere_history_unpacker = lambda sph_idx, t_idx: ( - sphere_history[sph_idx]["position"][t_idx], - sphere_history[sph_idx]["radius"][t_idx], - ) - # color mapping - sphere_cmap = cm.get_cmap("Spectral", n_visualized_spheres) - - # video pre-processing - print("plot scene visualization video") - FFMpegWriter = animation.writers["ffmpeg"] - metadata = dict(title="Movie Test", artist="Matplotlib", comment="Movie support!") - writer = FFMpegWriter(fps=fps, metadata=metadata) - dpi = kwargs.get("dpi", 100) - - xlim = kwargs.get("x_limits", (-1.0, 1.0)) - ylim = kwargs.get("y_limits", (-1.0, 1.0)) - zlim = kwargs.get("z_limits", (-0.05, 1.0)) - - difference = lambda x: x[1] - x[0] - max_axis_length = max(difference(xlim), difference(ylim)) - # The scaling factor from physical space to matplotlib space - scaling_factor = (2 * 0.1) / max_axis_length # Octopus head dimension - scaling_factor *= 2.6e3 # Along one-axis - - if kwargs.get("vis3D", True): - fig = plt.figure(1, figsize=(10, 8), frameon=True, dpi=dpi) - ax = plt.axes(projection="3d") - - ax.set_xlabel("x") - ax.set_ylabel("y") - ax.set_zlabel("z") - - ax.set_xlim(*xlim) - ax.set_ylim(*ylim) - ax.set_zlim(*zlim) - - ax.view_init(elev=20, azim=20) - - time_idx = 0 - rod_scatters = [None for _ in range(n_visualized_rods)] - - for rod_idx in range(n_visualized_rods): - inst_position, inst_radius = rod_history_unpacker(rod_idx, time_idx) - if not inst_position.shape[1] == inst_radius.shape[0]: - inst_position = 0.5 * (inst_position[..., 1:] + inst_position[..., :-1]) - - rod_scatters[rod_idx] = ax.scatter( - inst_position[0], - inst_position[1], - inst_position[2], - s=np.pi * (scaling_factor * inst_radius) ** 2, - ) - - if sphere_flag: - sphere_artists = [None for _ in range(n_visualized_spheres)] - for sphere_idx in range(n_visualized_spheres): - sphere_position, sphere_radius = sphere_history_unpacker( - sphere_idx, time_idx - ) - sphere_artists[sphere_idx] = ax.scatter( - sphere_position[0], - sphere_position[1], - sphere_position[2], - s=np.pi * (scaling_factor * sphere_radius) ** 2, - ) - # sphere_radius, - # color=sphere_cmap(sphere_idx),) - ax.add_artist(sphere_artists[sphere_idx]) - - # ax.set_aspect("equal") - video_name_3D = folder_name + "3D_" + video_name - - with writer.saving(fig, video_name_3D, dpi): - for time_idx in tqdm(range(0, sim_time.shape[0], int(step))): - - for rod_idx in range(n_visualized_rods): - inst_position, inst_radius = rod_history_unpacker(rod_idx, time_idx) - if not inst_position.shape[1] == inst_radius.shape[0]: - inst_position = 0.5 * ( - inst_position[..., 1:] + inst_position[..., :-1] - ) - - rod_scatters[rod_idx]._offsets3d = ( - inst_position[0], - inst_position[1], - inst_position[2], - ) - - rod_scatters[rod_idx].set_sizes( - np.pi * (scaling_factor * inst_radius) ** 2 - ) - - if sphere_flag: - for sphere_idx in range(n_visualized_spheres): - sphere_position, _ = sphere_history_unpacker( - sphere_idx, time_idx - ) - sphere_artists[sphere_idx]._offsets3d = ( - sphere_position[0], - sphere_position[1], - sphere_position[2], - ) - - writer.grab_frame() - - # Be a good boy and close figures - # https://stackoverflow.com/a/37451036 - # plt.close(fig) alone does not suffice - # See https://github.com/matplotlib/matplotlib/issues/8560/ - # plt.close(plt.gcf()) - - -def adjust_square_cross_section( - rod, youngs_modulus: float, length: float, ring_rod_flag: bool = False -): - n_elements = rod.n_elems - n_voronoi_elements = n_elements if ring_rod_flag else n_elements - 1 - - log = logging.getLogger() - - side_length = np.zeros(n_elements) - side_length.fill(length) - - new_area = np.pi * rod.radius * rod.radius - - new_moi_1 = (side_length**4) / 12 - new_moi_2 = (side_length**4) / 12 - new_moi_3 = new_moi_2 * 2 - - new_moi = np.array([new_moi_1, new_moi_2, new_moi_3]).transpose() - - mass_second_moment_of_inertia_temp = np.einsum( - "ij,i->ij", new_moi, rod.density * rod.rest_lengths - ) - - for i in range(n_elements): - np.fill_diagonal( - rod.mass_second_moment_of_inertia[..., i], - mass_second_moment_of_inertia_temp[i, :], - ) - # sanity check of mass second moment of inertia - if (rod.mass_second_moment_of_inertia < Tolerance.atol()).all(): - message = "Mass moment of inertia matrix smaller than tolerance, please check provided radius, density and length." - log.warning(message) - - for i in range(n_elements): - # Check rank of mass moment of inertia matrix to see if it is invertible - assert ( - np.linalg.matrix_rank(rod.mass_second_moment_of_inertia[..., i]) - == MaxDimension.value() - ) - rod.inv_mass_second_moment_of_inertia[..., i] = np.linalg.inv( - rod.mass_second_moment_of_inertia[..., i] - ) - - # Shear/Stretch matrix - shear_modulus = youngs_modulus / (2.0 * (1.0 + 0.5)) - - # Value taken based on best correlation for Poisson ratio = 0.5, from - # "On Timoshenko's correction for shear in vibrating beams" by Kaneko, 1975 - alpha_c = 27.0 / 28.0 - rod.shear_matrix *= 0.0 - for i in range(n_elements): - np.fill_diagonal( - rod.shear_matrix[..., i], - [ - alpha_c * shear_modulus * new_area[i], - alpha_c * shear_modulus * new_area[i], - youngs_modulus * new_area[i], - ], - ) - - # Bend/Twist matrix - bend_matrix = np.zeros( - (MaxDimension.value(), MaxDimension.value(), n_voronoi_elements + 1), np.float64 - ) - for i in range(n_elements): - np.fill_diagonal( - bend_matrix[..., i], - [ - youngs_modulus * new_moi_1[i], - youngs_modulus * new_moi_2[i], - shear_modulus * new_moi_3[i], - ], - ) - if ring_rod_flag: # wrap around the value in the last element - bend_matrix[..., -1] = bend_matrix[..., 0] - for i in range(0, MaxDimension.value()): - assert np.all( - bend_matrix[i, i, :] > Tolerance.atol() - ), " Bend matrix has to be greater than 0." - - # Compute bend matrix in Voronoi Domain - rest_lengths_temp_for_voronoi = ( - np.hstack((rod.rest_lengths, rod.rest_lengths[0])) - if ring_rod_flag - else rod.rest_lengths - ) - rod.bend_matrix = ( - bend_matrix[..., 1:] * rest_lengths_temp_for_voronoi[1:] - + bend_matrix[..., :-1] * rest_lengths_temp_for_voronoi[0:-1] - ) / (rest_lengths_temp_for_voronoi[1:] + rest_lengths_temp_for_voronoi[:-1]) diff --git a/examples/CantileverTransversalLoadCase/convergence_cantilever_transversal_load.py b/examples/CantileverTransversalLoadCase/convergence_cantilever_transversal_load.py index c37c0db9..442de4a0 100644 --- a/examples/CantileverTransversalLoadCase/convergence_cantilever_transversal_load.py +++ b/examples/CantileverTransversalLoadCase/convergence_cantilever_transversal_load.py @@ -26,7 +26,6 @@ def cantilever_subjected_to_a_transversal_load(n_elem=19): base_radius = 0.01 / ( np.pi ** (1 / 2) ) # The Cross-sectional area is 1e-4(we assume its equivalent to a square cross-sectional surface with same area) - base_area = 1e-4 density = 1000 youngs_modulus = 1e9 poisson_ratio = 0 @@ -37,9 +36,8 @@ class SquareRodSimulator( ): pass - squarerod_sim = SquareRodSimulator() + square_rod_sim = SquareRodSimulator() - density = 1000 t = np.linspace(0, 0.25 * np.pi, n_elem + 1) tmp = np.zeros((3, n_elem + 1), dtype=np.float64) tmp[0, :] = -radius * np.cos(t) + 1 @@ -57,7 +55,7 @@ class SquareRodSimulator( director[1, :, :] = d2 director[2, :, :] = tan - rod = ea.CosseratRod.straight_rod( + square_rod = ea.CosseratRod.straight_rod( n_elem, start, direction, @@ -72,23 +70,22 @@ class SquareRodSimulator( ) # Adjust the Cross Section - adjust_square_cross_section(rod, youngs_modulus, side_length) + adjust_square_cross_section(square_rod, youngs_modulus, side_length) - squarerod_sim.append(rod) + square_rod_sim.append(square_rod) - # squarerod_sim.finalize() - rod.rest_kappa[...] = rod.kappa + square_rod.rest_kappa[...] = square_rod.kappa dl = base_length / n_elem dt = 0.01 * dl / 100 - squarerod_sim.constrain(rod).using( + square_rod_sim.constrain(square_rod).using( OneEndFixedBC, constrained_position_idx=(0,), constrained_director_idx=(0,) ) print("One end of the rod is now fixed in place") - squarerod_sim.dampen(rod).using( + square_rod_sim.dampen(square_rod).using( ea.AnalyticalLinearDamper, damping_constant=0.3, time_step=dt, @@ -99,7 +96,7 @@ class SquareRodSimulator( origin_force = np.array([0.0, 0.0, 0.0]) end_force = np.array([0.0, 0.0, 6.0]) - squarerod_sim.add_forcing_to(rod).using( + square_rod_sim.add_forcing_to(square_rod).using( EndpointForces, origin_force, end_force, ramp_up_time=ramp_up_time ) print("Forces added to the rod") @@ -109,12 +106,11 @@ class SquareRodSimulator( total_steps = int(final_time / dt) print("Total steps to take", total_steps) - squarerod_sim.finalize() + square_rod_sim.finalize() print("System finalized") - # The simulation result from Project3.3.2 with 400 elements/ Tip position Z - - # generate analytical solution array from [400] + # The simulation result from Project3.3.2 with 400 elements (tip position Z) + # Generate analytical solution array by interpolating from the 400-element reference solution analytical_results_sub = np.zeros(n_elem + 1) @@ -128,16 +124,16 @@ class SquareRodSimulator( dt = final_time / total_steps time = 0.0 for i in range(total_steps): - time = timestepper.step(squarerod_sim, time, dt) - print(rod.position_collection[2, ...]) + time = timestepper.step(square_rod_sim, time, dt) + print(square_rod.position_collection[2, ...]) error, l1, l2, linf = calculate_error_norm( analytical_results_sub, - rod.position_collection[2, ...], + square_rod.position_collection[2, ...], n_elem, ) - return {"rod": rod, "error": error, "l1": l1, "l2": l2, "linf": linf} + return {"rod": square_rod, "error": error, "l1": l1, "l2": l2, "linf": linf} if __name__ == "__main__": diff --git a/examples/CantileverTransversalLoadCase/setup_helper.py b/examples/CantileverTransversalLoadCase/setup_helper.py new file mode 100644 index 00000000..972b3847 --- /dev/null +++ b/examples/CantileverTransversalLoadCase/setup_helper.py @@ -0,0 +1,100 @@ +import numpy as np +from matplotlib import pyplot as plt +from matplotlib import cm +from tqdm import tqdm + +pass +import logging +from elastica.utils import MaxDimension, Tolerance + + +def adjust_square_cross_section( + rod, youngs_modulus: float, length: float, ring_rod_flag: bool = False +): + n_elements = rod.n_elems + n_voronoi_elements = n_elements if ring_rod_flag else n_elements - 1 + + log = logging.getLogger() + + side_length = np.zeros(n_elements) + side_length.fill(length) + + new_area = np.pi * rod.radius * rod.radius + + new_moi_1 = (side_length**4) / 12 + new_moi_2 = (side_length**4) / 12 + new_moi_3 = new_moi_2 * 2 + + new_moi = np.array([new_moi_1, new_moi_2, new_moi_3]).transpose() + + mass_second_moment_of_inertia_temp = np.einsum( + "ij,i->ij", new_moi, rod.density * rod.rest_lengths + ) + + for i in range(n_elements): + np.fill_diagonal( + rod.mass_second_moment_of_inertia[..., i], + mass_second_moment_of_inertia_temp[i, :], + ) + # sanity check of mass second moment of inertia + if (rod.mass_second_moment_of_inertia < Tolerance.atol()).all(): + message = "Mass moment of inertia matrix smaller than tolerance, please check provided radius, density and length." + log.warning(message) + + for i in range(n_elements): + # Check rank of mass moment of inertia matrix to see if it is invertible + assert ( + np.linalg.matrix_rank(rod.mass_second_moment_of_inertia[..., i]) + == MaxDimension.value() + ) + rod.inv_mass_second_moment_of_inertia[..., i] = np.linalg.inv( + rod.mass_second_moment_of_inertia[..., i] + ) + + # Shear/Stretch matrix + shear_modulus = youngs_modulus / (2.0 * (1.0 + 0.5)) + + # Value taken based on best correlation for Poisson ratio = 0.5, from + # "On Timoshenko's correction for shear in vibrating beams" by Kaneko, 1975 + alpha_c = 27.0 / 28.0 + rod.shear_matrix *= 0.0 + for i in range(n_elements): + np.fill_diagonal( + rod.shear_matrix[..., i], + [ + alpha_c * shear_modulus * new_area[i], + alpha_c * shear_modulus * new_area[i], + youngs_modulus * new_area[i], + ], + ) + + # Bend/Twist matrix + bend_matrix = np.zeros( + (MaxDimension.value(), MaxDimension.value(), n_voronoi_elements + 1), np.float64 + ) + for i in range(n_elements): + np.fill_diagonal( + bend_matrix[..., i], + [ + youngs_modulus * new_moi_1[i], + youngs_modulus * new_moi_2[i], + shear_modulus * new_moi_3[i], + ], + ) + if ring_rod_flag: # wrap around the value in the last element + bend_matrix[..., -1] = bend_matrix[..., 0] + for i in range(0, MaxDimension.value()): + assert np.all( + bend_matrix[i, i, :] > Tolerance.atol() + ), " Bend matrix has to be greater than 0." + + # Compute bend matrix in Voronoi Domain + rest_lengths_temp_for_voronoi = ( + np.hstack((rod.rest_lengths, rod.rest_lengths[0])) + if ring_rod_flag + else rod.rest_lengths + ) + rod.bend_matrix = ( + bend_matrix[..., 1:] * rest_lengths_temp_for_voronoi[1:] + + bend_matrix[..., :-1] * rest_lengths_temp_for_voronoi[0:-1] + ) / (rest_lengths_temp_for_voronoi[1:] + rest_lengths_temp_for_voronoi[:-1]) diff --git a/examples/CatenaryCase/post_processing.py b/examples/CatenaryCase/post_processing.py index 69e7293b..402764a6 100644 --- a/examples/CatenaryCase/post_processing.py +++ b/examples/CatenaryCase/post_processing.py @@ -4,7 +4,7 @@ from matplotlib import pyplot as plt import matplotlib.animation as manimation from tqdm import tqdm -import scipy as sci +from scipy import optimize def plot_video( @@ -57,7 +57,7 @@ def plot_catenary( def f_non_elastic_catenary(x: float) -> float: return x * (1 - np.cosh(1 / (2 * x))) - lowest_point - a = sci.optimize.fsolve(f_non_elastic_catenary, x0=1.0) # solve for a + a = optimize.fsolve(f_non_elastic_catenary, x0=1.0) # solve for a y_catenary = a * np.cosh((x_catenary - 0.5) / a) - a * np.cosh(1 / (2 * a)) plt.plot(position[-1][0], position[-1][2], label="Simulation", linewidth=3) plt.plot( diff --git a/examples/CatenaryCase/run_catenary.py b/examples/CatenaryCase/run_catenary.py index 721b7848..836a55d3 100644 --- a/examples/CatenaryCase/run_catenary.py +++ b/examples/CatenaryCase/run_catenary.py @@ -46,7 +46,6 @@ class CatenarySimulator( 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 @@ -110,7 +109,7 @@ class CatenarySimulator( # Add call backs class CatenaryCallBack(ea.CallBackBaseClass): """ - Call back function for continuum snake + Call back function for catenary case """ def __init__(self, step_skip: int, callback_params: dict) -> None: diff --git a/examples/ContinuumFlagellaCase/continuum_flagella.py b/examples/ContinuumFlagellaCase/continuum_flagella.py index efd24e47..ecb6f13a 100644 --- a/examples/ContinuumFlagellaCase/continuum_flagella.py +++ b/examples/ContinuumFlagellaCase/continuum_flagella.py @@ -89,11 +89,11 @@ def run_flagella( # Add call backs class ContinuumFlagellaCallBack(ea.CallBackBaseClass): """ - Call back function for continuum snake + Call back function for continuum flagella """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -171,7 +171,7 @@ def make_callback(self, system, time, current_step: int): SAVE_OPTIMIZED_COEFFICIENTS = False - def optimize_snake(spline_coefficient): + def optimize_flagella(spline_coefficient): [avg_forward, _, _] = run_flagella( spline_coefficient, PLOT_FIGURE=False, @@ -181,10 +181,10 @@ def optimize_snake(spline_coefficient): ) return -avg_forward - # Optimize snake for forward velocity. In cma.fmin first input is function + # Optimize flagella for forward velocity. In cma.fmin first input is function # to be optimized, second input is initial guess for coefficients you are optimizing # for and third input is standard deviation you initially set. - optimized_spline_coefficients = cma.fmin(optimize_snake, 5 * [0], 0.5) + optimized_spline_coefficients = cma.fmin(optimize_flagella, 5 * [0], 0.5) # Save the optimized coefficients to a file filename_data = "optimized_coefficients.txt" diff --git a/examples/ContinuumFlagellaCase/continuum_flagella_postprocessing.py b/examples/ContinuumFlagellaCase/continuum_flagella_postprocessing.py index fcbd44ee..a0945986 100644 --- a/examples/ContinuumFlagellaCase/continuum_flagella_postprocessing.py +++ b/examples/ContinuumFlagellaCase/continuum_flagella_postprocessing.py @@ -72,11 +72,11 @@ def compute_projected_velocity(plot_params: dict, period): center_of_mass = np.array(plot_params["center_of_mass"]) # Compute rod velocity in rod direction. We need to compute that because, - # after snake starts to move it chooses an arbitrary direction, which does not + # after flagella starts to move it chooses an arbitrary direction, which does not # have to be initial tangent direction of the rod. Thus we need to project the - # snake velocity with respect to its new tangent and roll direction, after that + # flagella velocity with respect to its new tangent and roll direction, after that # we will get the correct forward and lateral speed. After this projection - # lateral velocity of the snake has to be oscillating between + and - values with + # lateral velocity of the flagella has to be oscillating between + and - values with # zero mean. # Number of steps in one period. @@ -108,7 +108,7 @@ def compute_projected_velocity(plot_params: dict, period): # velocity in the direction of rod velocity_in_rod_roll_dir = avg_velocity - velocity_in_direction_of_rod - # Compute the average velocity over the simulation, this can be used for optimizing snake + # Compute the average velocity over the simulation, this can be used for optimizing flagella # for fastest forward velocity. We start after first period, because of the ramping up happens # in first period. average_velocity_over_simulation = np.mean( diff --git a/examples/ContinuumSnakeCase/run-continuum-snake.py b/examples/ContinuumSnakeCase/run-continuum-snake.py index 83d92bd3..7b339831 100644 --- a/examples/ContinuumSnakeCase/run-continuum-snake.py +++ b/examples/ContinuumSnakeCase/run-continuum-snake.py @@ -2,7 +2,7 @@ Continuum Snake =============== -Snake friction case from X. Zhang et. al. Nat. Comm. 2021 +Snake friction case from X. Zhang et al. Nat. Comm. 2021 This Elastica tutorial explains how to setup a Cosserat rod simulation to simulate a slithering snake. It covers many of the basics of setting up and running simulations with Elastica. @@ -165,7 +165,7 @@ class ContinuumSnakeCallBack(ea.CallBackBaseClass): """ def __init__(self, step_skip: int, callback_params: dict) -> None: - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params diff --git a/examples/ContinuumSnakeWithLiftingWaveCase/snake_contact.py b/examples/ContinuumSnakeWithLiftingWaveCase/snake_contact.py index 36fcf313..4fe160ff 100755 --- a/examples/ContinuumSnakeWithLiftingWaveCase/snake_contact.py +++ b/examples/ContinuumSnakeWithLiftingWaveCase/snake_contact.py @@ -1,4 +1,4 @@ -__doc__ = """Rod plane contact with anistropic friction (no static friction)""" +__doc__ = """Rod plane contact with anisotropic friction (no static friction)""" from typing import Type import numpy as np @@ -46,7 +46,7 @@ def apply_normal_force_numba( ): """ This function computes the plane force response on the element, in the - case of contact. Contact model given in Eqn 4.8 Gazzola et. al. RSoS 2018 paper + case of contact. Contact model given in Eqn 4.8 Gazzola et al. RSoS 2018 paper is used. Parameters diff --git a/examples/ContinuumSnakeWithLiftingWaveCase/snake_forcing.py b/examples/ContinuumSnakeWithLiftingWaveCase/snake_forcing.py index 3b471aca..e12216a5 100755 --- a/examples/ContinuumSnakeWithLiftingWaveCase/snake_forcing.py +++ b/examples/ContinuumSnakeWithLiftingWaveCase/snake_forcing.py @@ -21,7 +21,7 @@ class MuscleTorquesLifting(NoForces): This class applies muscle torques along the body. The applied muscle torques are treated as applied external forces. This class can apply lifting muscle torques as a traveling wave with a beta spline or only - as a traveling wave. For implementation details refer to X. Zhang et. al. Nat. Comm. 2021 + as a traveling wave. For implementation details refer to X. Zhang et al. Nat. Comm. 2021 Attributes ---------- @@ -57,7 +57,7 @@ def __init__( Parameters ---------- - b_coeff: nump.ndarray + b_coeff: numpy.ndarray 1D array containing data with 'float' type. Beta coefficients for beta-spline. period: float diff --git a/examples/DynamicCantileverCase/analytical_dynamic_cantilever.py b/examples/DynamicCantileverCase/analytical_dynamic_cantilever.py index 3ce2d04c..011f1c41 100644 --- a/examples/DynamicCantileverCase/analytical_dynamic_cantilever.py +++ b/examples/DynamicCantileverCase/analytical_dynamic_cantilever.py @@ -41,7 +41,7 @@ class AnalyticalDynamicCantilever: Cross-sectional area of the rod moment_of_inertia: float Second moment of area of the rod's cross-section - young's_modulus: float + youngs_modulus: float Young's modulus of the rod density: float Density of the rod diff --git a/examples/DynamicCantileverCase/dynamic_cantilever.py b/examples/DynamicCantileverCase/dynamic_cantilever.py index ea04f451..ee037499 100644 --- a/examples/DynamicCantileverCase/dynamic_cantilever.py +++ b/examples/DynamicCantileverCase/dynamic_cantilever.py @@ -7,6 +7,10 @@ from analytical_dynamic_cantilever import AnalyticalDynamicCantilever +class DynamicCantileverSimulator(ea.BaseSystemCollection, ea.Constraints, ea.CallBacks): + pass + + def simulate_dynamic_cantilever_with( density=2000.0, n_elem=100, @@ -39,11 +43,6 @@ def simulate_dynamic_cantilever_with( """ - class DynamicCantileverSimulator( - ea.BaseSystemCollection, ea.Constraints, ea.CallBacks - ): - pass - cantilever_sim = DynamicCantileverSimulator() # Add test parameters @@ -59,6 +58,7 @@ class DynamicCantileverSimulator( dl = base_length / n_elem dt = dl * 0.05 + total_steps = int(final_time / dt) step_skips = int(1.0 / (rendering_fps * dt)) # Add Cosserat rod @@ -98,7 +98,7 @@ class DynamicCantileverSimulator( # Add call backs class CantileverCallBack(ea.CallBackBaseClass): def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params diff --git a/examples/DynamicCantileverCase/dynamic_cantilever_phase_space.py b/examples/DynamicCantileverCase/dynamic_cantilever_phase_space.py index bdbec084..52b5aed1 100644 --- a/examples/DynamicCantileverCase/dynamic_cantilever_phase_space.py +++ b/examples/DynamicCantileverCase/dynamic_cantilever_phase_space.py @@ -1,4 +1,4 @@ -__doc__ = """ Validating phase space of dynamic cantilever beam analytical_cantilever_soln with respect to varying densities. +__doc__ = """Validating phase space of dynamic cantilever beam analytical solution with respect to varying densities. The theoretical dynamic response is obtained via Euler-Bernoulli beam theory.""" from dynamic_cantilever_post_processing import plot_phase_space_with diff --git a/examples/DynamicCantileverCase/dynamic_cantilever_post_processing.py b/examples/DynamicCantileverCase/dynamic_cantilever_post_processing.py index d6387fd5..4923dfdd 100644 --- a/examples/DynamicCantileverCase/dynamic_cantilever_post_processing.py +++ b/examples/DynamicCantileverCase/dynamic_cantilever_post_processing.py @@ -1,5 +1,7 @@ +import matplotlib.animation as manimation import matplotlib.pyplot as plt import numpy as np +from tqdm import tqdm # Plotting frequency and amplitudes against densities @@ -124,9 +126,6 @@ def plot_dynamic_cantilever_video_with( print("Plotting video ...") video_name = f"Dynamic_cantilever_mode_{mode + 1}.mp4" - import matplotlib.animation as manimation - from tqdm import tqdm - positions_over_time = np.array(recorded_history["position"]) FFMpegWriter = manimation.writers["ffmpeg"] diff --git a/examples/DynamicCantileverCase/dynamic_cantilever_visualization.py b/examples/DynamicCantileverCase/dynamic_cantilever_visualization.py index bcb182cb..f2e04a89 100644 --- a/examples/DynamicCantileverCase/dynamic_cantilever_visualization.py +++ b/examples/DynamicCantileverCase/dynamic_cantilever_visualization.py @@ -1,19 +1,11 @@ __doc__ = """Visualization of simulated dynamic cantilever beam""" -import elastica as ea from dynamic_cantilever import simulate_dynamic_cantilever_with from dynamic_cantilever_post_processing import ( plot_end_position_with, plot_dynamic_cantilever_video_with, ) - -class DynamicCantileverSimulator(ea.BaseSystemCollection, ea.Constraints, ea.CallBacks): - pass - - -cantilever_sim = DynamicCantileverSimulator() - # Options PLOT_FIGURE = True SAVE_FIGURE = True @@ -33,7 +25,6 @@ class DynamicCantileverSimulator(ea.BaseSystemCollection, ea.Constraints, ea.Cal rendering_fps=rendering_fps, ) -cantilever = sim_result["rod"] recorded_history = sim_result["recorded_history"] omegas = sim_result["fft_frequencies"] amplitudes = sim_result["fft_amplitudes"] diff --git a/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_fixed_joint.py b/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_fixed_joint.py index d4b85e3c..bd3e38fc 100644 --- a/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_fixed_joint.py +++ b/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_fixed_joint.py @@ -32,10 +32,8 @@ class FixedJointSimulator( n_elem = 10 direction = np.array([0.0, 0.0, 1.0]) normal = np.array([0.0, 1.0, 0.0]) -roll_direction = np.cross(direction, normal) base_length = 0.2 base_radius = 0.007 -base_area = np.pi * base_radius**2 density = 1750 E = 3e7 poisson_ratio = 0.5 @@ -155,7 +153,6 @@ class FixedJointSimulator( timestepper = ea.PositionVerlet() # timestepper = PEFRL() -dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) dt = final_time / total_steps diff --git a/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_spherical_joint.py b/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_spherical_joint.py index 01dd6415..09ec4c68 100644 --- a/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_spherical_joint.py +++ b/examples/ExperimentalCases/GenericSystemConnectionCases/generic_system_type_spherical_joint.py @@ -33,10 +33,8 @@ class SphericalJointSimulator( n_elem = 10 direction = np.array([0.0, 0.0, 1.0]) normal = np.array([0.0, 1.0, 0.0]) -roll_direction = np.cross(direction, normal) base_length = 0.2 base_radius = 0.007 -base_area = np.pi * base_radius**2 density = 1750 E = 3e7 poisson_ratio = 0.5 @@ -151,7 +149,6 @@ class SphericalJointSimulator( timestepper = ea.PositionVerlet() # timestepper = PEFRL() -dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) dt = final_time / total_steps diff --git a/examples/ExperimentalCases/ParallelConnectionExample/joint_cases_postprocessing.py b/examples/ExperimentalCases/ParallelConnectionExample/joint_cases_postprocessing.py new file mode 100644 index 00000000..71c68c93 --- /dev/null +++ b/examples/ExperimentalCases/ParallelConnectionExample/joint_cases_postprocessing.py @@ -0,0 +1,340 @@ +import numpy as np +from matplotlib import pyplot as plt +from matplotlib.colors import to_rgb +from scipy.spatial.transform import Rotation + +from elastica.rigidbody import Cylinder +from elastica._linalg import _batch_matvec + + +def plot_position( + plot_params_rod1: dict, + plot_params_rod2: dict, + plot_params_cylinder: dict = None, + filename="joint_cases_last_node_pos_xy.png", + SAVE_FIGURE=False, +): + position_of_rod1 = np.array(plot_params_rod1["position"]) + position_of_rod2 = np.array(plot_params_rod2["position"]) + position_of_cylinder = ( + np.array(plot_params_cylinder["position"]) + if plot_params_cylinder is not None + else None + ) + + fig = plt.figure(figsize=(10, 10), frameon=True, dpi=150) + ax = fig.add_subplot(111) + + ax.grid(which="minor", color="k", linestyle="--") + ax.grid(which="major", color="k", linestyle="-") + ax.plot(position_of_rod1[:, 0, -1], position_of_rod1[:, 1, -1], "r-", label="rod1") + ax.plot( + position_of_rod2[:, 0, -1], + position_of_rod2[:, 1, -1], + c=to_rgb("xkcd:bluish"), + label="rod2", + ) + if position_of_cylinder is not None: + ax.plot( + position_of_cylinder[:, 0, -1], + position_of_cylinder[:, 1, -1], + c=to_rgb("xkcd:greenish"), + label="cylinder", + ) + + fig.legend(prop={"size": 20}) + plt.xlabel("x") + plt.ylabel("y") + + plt.show() + + if SAVE_FIGURE: + fig.savefig(filename) + + +def plot_orientation(title, time, directors): + """ + Plot the orientation of one node + """ + quat = [] + for t in range(len(time)): + quat_t = Rotation.from_matrix(directors[t].T).as_quat() + quat.append(quat_t) + quat = np.array(quat) + + plt.figure(num=title) + plt.plot(time, quat[:, 0], label="x") + plt.plot(time, quat[:, 1], label="y") + plt.plot(time, quat[:, 2], label="z") + plt.plot(time, quat[:, 3], label="w") + plt.title(title) + plt.legend() + plt.xlabel("Time [s]") + plt.ylabel("Quaternion") + plt.show() + + +def plot_video( + plot_params_rod1: dict, + plot_params_rod2: dict, + plot_params_cylinder: dict = None, + video_name="joint_cases_video.mp4", + fps=100, + cylinder: Cylinder = None, +): # (time step, x/y/z, node) + import matplotlib.animation as manimation + + time_list = plot_params_rod1["time"] + position_of_rod1 = np.array(plot_params_rod1["position"]) + position_of_rod2 = np.array(plot_params_rod2["position"]) + position_of_cylinder = ( + np.array(plot_params_cylinder["position"]) + if plot_params_cylinder is not None + else None + ) + director_of_cylinder = ( + np.array(plot_params_cylinder["directors"]) + if plot_params_cylinder is not None + else None + ) + + print("plot video") + FFMpegWriter = manimation.writers["ffmpeg"] + metadata = dict(title="Movie Test", artist="Matplotlib", comment="Movie support!") + writer = FFMpegWriter(fps=fps, metadata=metadata) + fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150) + with writer.saving(fig, video_name, 100): + for time_idx in range(1, len(time_list)): + fig.clf() + ax = plt.axes(projection="3d") # fig.add_subplot(111) + ax.grid(which="minor", color="k", linestyle="--") + ax.grid(which="major", color="k", linestyle="-") + ax.plot( + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 1], + position_of_rod1[time_idx, 2], + "or", + label="rod1", + ) + ax.plot( + position_of_rod2[time_idx, 0], + position_of_rod2[time_idx, 1], + position_of_rod2[time_idx, 2], + "o", + c=to_rgb("xkcd:bluish"), + label="rod2", + ) + if position_of_cylinder is not None: + ax.plot( + position_of_cylinder[time_idx, 0], + position_of_cylinder[time_idx, 1], + position_of_cylinder[time_idx, 2], + "o", + c=to_rgb("xkcd:greenish"), + label="Cylinder CoM", + ) + if cylinder is not None: + cylinder_axis_points = np.zeros((3, 10)) + cylinder_axis_points[2, :] = np.linspace( + start=-cylinder.length.squeeze() / 2, + stop=cylinder.length.squeeze() / 2, + num=cylinder_axis_points.shape[1], + ) + cylinder_director = director_of_cylinder[time_idx, ...] + cylinder_director_batched = np.repeat( + cylinder_director, repeats=cylinder_axis_points.shape[1], axis=2 + ) + # rotate points into inertial frame + cylinder_axis_points = _batch_matvec( + cylinder_director_batched.transpose((1, 0, 2)), + cylinder_axis_points, + ) + # add offset position of CoM + cylinder_axis_points += position_of_cylinder[time_idx, ...] + ax.plot( + cylinder_axis_points[0, :], + cylinder_axis_points[1, :], + cylinder_axis_points[2, :], + c=to_rgb("xkcd:greenish"), + label="Cylinder axis", + ) + + ax.set_xlim(-0.25, 0.25) + ax.set_ylim(-0.25, 0.25) + ax.set_zlim(0, 0.61) + ax.set_xlabel("x [m]") + ax.set_ylabel("y [m]") + ax.set_zlabel("z [m]") + writer.grab_frame() + + +def plot_video_xy( + plot_params_rod1: dict, + plot_params_rod2: dict, + plot_params_cylinder: dict = None, + video_name="joint_cases_video_xy.mp4", + fps=100, + cylinder: Cylinder = None, +): # (time step, x/y/z, node) + import matplotlib.animation as manimation + + time_list = plot_params_rod1["time"] + position_of_rod1 = np.array(plot_params_rod1["position"]) + position_of_rod2 = np.array(plot_params_rod2["position"]) + position_of_cylinder = ( + np.array(plot_params_cylinder["position"]) + if plot_params_cylinder is not None + else None + ) + director_of_cylinder = ( + np.array(plot_params_cylinder["directors"]) + if plot_params_cylinder is not None + else None + ) + + print("plot video xy") + FFMpegWriter = manimation.writers["ffmpeg"] + metadata = dict(title="Movie Test", artist="Matplotlib", comment="Movie support!") + writer = FFMpegWriter(fps=fps, metadata=metadata) + fig = plt.figure() + plt.axis("equal") + with writer.saving(fig, video_name, 100): + for time_idx in range(1, len(time_list)): + fig.clf() + plt.plot( + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 1], + "or", + label="rod1", + ) + plt.plot( + position_of_rod2[time_idx, 0], + position_of_rod2[time_idx, 1], + "o", + c=to_rgb("xkcd:bluish"), + label="rod2", + ) + if position_of_cylinder is not None: + plt.plot( + position_of_cylinder[time_idx, 0], + position_of_cylinder[time_idx, 1], + "o", + c=to_rgb("xkcd:greenish"), + label="cylinder", + ) + if cylinder is not None: + cylinder_axis_points = np.zeros((3, 10)) + cylinder_axis_points[2, :] = np.linspace( + start=-cylinder.length.squeeze() / 2, + stop=cylinder.length.squeeze() / 2, + num=cylinder_axis_points.shape[1], + ) + cylinder_director = director_of_cylinder[time_idx, ...] + cylinder_director_batched = np.repeat( + cylinder_director, repeats=cylinder_axis_points.shape[1], axis=2 + ) + # rotate points into inertial frame + cylinder_axis_points = _batch_matvec( + cylinder_director_batched.transpose((1, 0, 2)), + cylinder_axis_points, + ) + # add offset position of CoM + cylinder_axis_points += position_of_cylinder[time_idx, ...] + plt.plot( + cylinder_axis_points[0, :], + cylinder_axis_points[1, :], + c=to_rgb("xkcd:greenish"), + label="Cylinder axis", + ) + + plt.xlim([-0.25, 0.25]) + plt.ylim([-0.25, 0.25]) + plt.xlabel("x [m]") + plt.ylabel("y [m]") + writer.grab_frame() + + +def plot_video_xz( + plot_params_rod1: dict, + plot_params_rod2: dict, + plot_params_cylinder: dict = None, + video_name="joint_cases_video_xz.mp4", + fps=100, + cylinder: Cylinder = None, +): # (time step, x/y/z, node) + import matplotlib.animation as manimation + + time_list = plot_params_rod1["time"] + position_of_rod1 = np.array(plot_params_rod1["position"]) + position_of_rod2 = np.array(plot_params_rod2["position"]) + position_of_cylinder = ( + np.array(plot_params_cylinder["position"]) + if plot_params_cylinder is not None + else None + ) + director_of_cylinder = ( + np.array(plot_params_cylinder["directors"]) + if plot_params_cylinder is not None + else None + ) + + print("plot video xz") + FFMpegWriter = manimation.writers["ffmpeg"] + metadata = dict(title="Movie Test", artist="Matplotlib", comment="Movie support!") + writer = FFMpegWriter(fps=fps, metadata=metadata) + fig = plt.figure() + plt.axis("equal") + with writer.saving(fig, video_name, 100): + for time_idx in range(1, len(time_list)): + fig.clf() + plt.plot( + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 2], + "or", + label="rod1", + ) + plt.plot( + position_of_rod2[time_idx, 0], + position_of_rod2[time_idx, 2], + "o", + c=to_rgb("xkcd:bluish"), + label="rod2", + ) + if position_of_cylinder is not None: + plt.plot( + position_of_cylinder[time_idx, 0], + position_of_cylinder[time_idx, 2], + "o", + c=to_rgb("xkcd:greenish"), + label="cylinder", + ) + if cylinder is not None: + cylinder_axis_points = np.zeros((3, 10)) + cylinder_axis_points[2, :] = np.linspace( + start=-cylinder.length.squeeze() / 2, + stop=cylinder.length.squeeze() / 2, + num=cylinder_axis_points.shape[1], + ) + cylinder_director = director_of_cylinder[time_idx, ...] + cylinder_director_batched = np.repeat( + cylinder_director, repeats=cylinder_axis_points.shape[1], axis=2 + ) + # rotate points into inertial frame + cylinder_axis_points = _batch_matvec( + cylinder_director_batched.transpose((1, 0, 2)), + cylinder_axis_points, + ) + # add offset position of CoM + cylinder_axis_points += position_of_cylinder[time_idx, ...] + plt.plot( + cylinder_axis_points[0, :], + cylinder_axis_points[2, :], + c=to_rgb("xkcd:greenish"), + label="Cylinder axis", + ) + + plt.xlim([-0.25, 0.25]) + plt.ylim([0, 0.61]) + plt.xlabel("x [m]") + plt.ylabel("z [m]") + writer.grab_frame() diff --git a/examples/ExperimentalCases/ParallelConnectionExample/parallel_connection_example.py b/examples/ExperimentalCases/ParallelConnectionExample/parallel_connection_example.py index b3433010..1e45b77d 100644 --- a/examples/ExperimentalCases/ParallelConnectionExample/parallel_connection_example.py +++ b/examples/ExperimentalCases/ParallelConnectionExample/parallel_connection_example.py @@ -38,7 +38,6 @@ class ParallelConnection( binormal = np.cross(direction, normal) base_length = 0.2 base_radius = 0.007 -base_area = np.pi * base_radius**2 density = 1750 E = 3e4 poisson_ratio = 0.5 @@ -149,13 +148,13 @@ def apply_forces(self, system, time: np.float64 = 0.0): ) -class ParallelConnecitonCallback(ea.CallBackBaseClass): +class ParallelConnectionCallback(ea.CallBackBaseClass): """ Call back function for parallel connection """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -184,7 +183,6 @@ def make_callback(self, system, time, current_step: int): timestepper = ea.PositionVerlet() final_time = 20.0 -dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) dt = final_time / total_steps diff --git a/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum.py b/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum.py index bb24a84b..d2b4b59b 100644 --- a/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum.py +++ b/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum.py @@ -23,7 +23,7 @@ class SwingingFlexiblePendulumSimulator( SAVE_FIGURE = False SAVE_RESULTS = True -# For 10 elements, the prefac is 0.0007 +# For 10 elements, the prefactor is 0.0007 pendulum_sim = SwingingFlexiblePendulumSimulator() final_time = 1.0 if SAVE_RESULTS else 5.0 @@ -34,7 +34,6 @@ class SwingingFlexiblePendulumSimulator( normal = np.array([1.0, 0.0, 0.0]) base_length = 1.0 base_radius = 0.005 -base_area = np.pi * base_radius**2 density = 1100.0 youngs_modulus = 5e6 # For shear modulus of 1e4, nu is 99! @@ -55,10 +54,9 @@ class SwingingFlexiblePendulumSimulator( pendulum_sim.append(pendulum_rod) -# Bad name : whats a FreeRod anyway? class HingeBC(ea.ConstraintBase): """ - the end of the rod fixed x[0] + Hinge boundary condition that fixes the position of the first node """ def __init__(self, fixed_position, fixed_directors, **kwargs): @@ -84,14 +82,14 @@ def constrain_rates(self, system, time): ) -# Add call backs +# Add callbacks class PendulumCallBack(ea.CallBackBaseClass): """ - Call back function for continuum snake + Callback function for flexible swinging pendulum """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -129,7 +127,6 @@ def make_callback(self, system, time, current_step: int): timestepper = ea.PositionVerlet() # timestepper = PEFRL() -dt = final_time / total_steps time = 0.0 for i in range(total_steps): time = timestepper.step(pendulum_sim, time, dt) @@ -191,8 +188,6 @@ def plot_video( plt.show() if SAVE_RESULTS: - import pickle as pickle - filename = "flexible_swinging_pendulum.dat" with open(filename, "wb") as file: pickle.dump(recorded_history, file) diff --git a/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum_visualize_data.py b/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum_visualize_data.py index 28705816..4ec6d5fc 100644 --- a/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum_visualize_data.py +++ b/examples/FlexibleSwingingPendulumCase/flexible_swinging_pendulum_visualize_data.py @@ -1,67 +1,30 @@ import numpy as np from matplotlib import pyplot as plt import os +import pickle -# def main(): data_file_name = "flexible_swinging_pendulum.dat" if os.path.exists(data_file_name): - import pickle - with open(data_file_name, "rb") as file_handle: recorded_history = pickle.load(file_handle) -# Generate data in six separate figures and not in one subplot -NODAL_SELECTION = np.arange(0, 10 + 2, 2) -ELEMENT_SELECTION = list(range(0, 8 + 2, 2)) + [9] -VORONOI_SELECTION = range(0, 9) +# Generate data in separate figures FORCE_SELECTION = range(0, 10, 3) -# 1. Centroid positions in vertical plane +# Extract time and positions time = np.array(recorded_history["time"]) -positions = np.array(recorded_history["position"]) - -if False: - fig = plt.figure(1, figsize=(8, 5)) - ax = fig.add_subplot(111) - for node in NODAL_SELECTION: - ax.plot(time, positions[:, 2, node]) - - fig = plt.figure(2, figsize=(8, 5)) - ax = fig.add_subplot(111) - for node in NODAL_SELECTION: - ax.plot(time, positions[:, 0, node]) - - fig = plt.figure(3, figsize=(8, 5)) - ax = fig.add_subplot(111) - # (time, 3, 3, n_elem) array - directors = np.array(recorded_history["directors"]) - # Plot d1 . e1 - projected_director = np.einsum( - "ijk,j->ik", directors[:, 0, :, :], np.array([1.0, 0.0, 0.0]) - ) - for elem in ELEMENT_SELECTION: - ax.plot(time, projected_director[:, elem]) - - fig = plt.figure(4, figsize=(8, 5)) - ax = fig.add_subplot(111) - # (n_time, 3, n_elem) - internal_couple = np.array(recorded_history["internal_couple"]) - for voronoi in VORONOI_SELECTION: - ax.plot(time[1:], internal_couple[:, 1, voronoi]) - +internal_stress = np.array(recorded_history["internal_stress"]) +# Plot internal stress in x-direction fig = plt.figure(5, figsize=(8, 5)) ax = fig.add_subplot(111) -internal_stress = np.array(recorded_history["internal_stress"]) for elem in FORCE_SELECTION: ax.plot(time[1:], internal_stress[:, 0, elem]) - +# Plot internal stress in z-direction fig = plt.figure(6, figsize=(8, 5)) ax = fig.add_subplot(111) for elem in FORCE_SELECTION: ax.plot(time[1:], internal_stress[:, 2, elem]) plt.show() -# if __name__ == "__main__": -# main() diff --git a/examples/FrictionValidationCases/axial_friction.py b/examples/FrictionValidationCases/axial_friction.py index 3e34ca9f..3435fbf3 100644 --- a/examples/FrictionValidationCases/axial_friction.py +++ b/examples/FrictionValidationCases/axial_friction.py @@ -54,7 +54,7 @@ def simulate_axial_friction_with(force=0.0): ) # TODO: CosseratRod has to be able to take shear matrix as input, we should change it as done below - shearable_rod.shear_matrix = shear_matrix + shearable_rod.shear_matrix[:] = shear_matrix axial_friction_sim.append(shearable_rod) axial_friction_sim.constrain(shearable_rod).using(ea.FreeBC) @@ -95,7 +95,6 @@ def simulate_axial_friction_with(force=0.0): dt = 1e-5 total_steps = int(final_time / dt) print("Total steps", total_steps) - dt = final_time / total_steps time = 0.0 for i in range(total_steps): time = timestepper.step(axial_friction_sim, time, dt) diff --git a/examples/FrictionValidationCases/rolling_friction_initial_velocity.py b/examples/FrictionValidationCases/rolling_friction_initial_velocity.py index 3162015e..dcdc5e3d 100644 --- a/examples/FrictionValidationCases/rolling_friction_initial_velocity.py +++ b/examples/FrictionValidationCases/rolling_friction_initial_velocity.py @@ -56,7 +56,7 @@ def simulate_rolling_friction_initial_velocity_with(IFactor=0.0): ) # TODO: CosseratRod has to be able to take shear matrix as input, we should change it as done below - shearable_rod.shear_matrix = shear_matrix + shearable_rod.shear_matrix[:] = shear_matrix # change the mass moment of inertia matrix and its inverse shearable_rod.mass_second_moment_of_inertia *= IFactor shearable_rod.inv_mass_second_moment_of_inertia /= IFactor @@ -108,7 +108,6 @@ def simulate_rolling_friction_initial_velocity_with(IFactor=0.0): final_time = 2.0 total_steps = int(final_time / dt) print("Total steps", total_steps) - dt = final_time / total_steps time = 0.0 for i in range(total_steps): time = timestepper.step(rolling_friction_initial_velocity_sim, time, dt) diff --git a/examples/FrictionValidationCases/rolling_friction_on_inclined_plane.py b/examples/FrictionValidationCases/rolling_friction_on_inclined_plane.py index fd352308..20aae5e5 100644 --- a/examples/FrictionValidationCases/rolling_friction_on_inclined_plane.py +++ b/examples/FrictionValidationCases/rolling_friction_on_inclined_plane.py @@ -55,7 +55,7 @@ def simulate_rolling_friction_on_inclined_plane_with(alpha_s=0.0): ) # TODO: CosseratRod has to be able to take shear matrix as input, we should change it as done below - shearable_rod.shear_matrix = shear_matrix + shearable_rod.shear_matrix[:] = shear_matrix rolling_friction_on_inclined_plane_sim.append(shearable_rod) rolling_friction_on_inclined_plane_sim.constrain(shearable_rod).using(ea.FreeBC) @@ -95,7 +95,6 @@ def simulate_rolling_friction_on_inclined_plane_with(alpha_s=0.0): dt = 1e-6 total_steps = int(final_time / dt) print("Total steps", total_steps) - dt = final_time / total_steps time = 0.0 for i in range(total_steps): time = timestepper.step(rolling_friction_on_inclined_plane_sim, time, dt) diff --git a/examples/FrictionValidationCases/rolling_friction_torque.py b/examples/FrictionValidationCases/rolling_friction_torque.py index b462c0ec..57634f4b 100644 --- a/examples/FrictionValidationCases/rolling_friction_torque.py +++ b/examples/FrictionValidationCases/rolling_friction_torque.py @@ -55,7 +55,7 @@ def simulate_rolling_friction_torque_with(C_s=0.0): ) # TODO: CosseratRod has to be able to take shear matrix as input, we should change it as done below - shearable_rod.shear_matrix = shear_matrix + shearable_rod.shear_matrix[:] = shear_matrix rolling_friction_torque_sim.append(shearable_rod) rolling_friction_torque_sim.constrain(shearable_rod).using(ea.FreeBC) @@ -99,7 +99,6 @@ def simulate_rolling_friction_torque_with(C_s=0.0): dt = 1e-6 total_steps = int(final_time / dt) print("Total steps", total_steps) - dt = final_time / total_steps time = 0.0 for i in range(total_steps): time = timestepper.step(rolling_friction_torque_sim, time, dt) diff --git a/examples/HelicalBucklingCase/convergence_helicalbuckling.py b/examples/HelicalBucklingCase/convergence_helicalbuckling.py index 55737f55..d803c4bd 100644 --- a/examples/HelicalBucklingCase/convergence_helicalbuckling.py +++ b/examples/HelicalBucklingCase/convergence_helicalbuckling.py @@ -23,7 +23,7 @@ class HelicalBucklingSimulator( SAVE_RESULTS = False -def simulate_helicalbucklin_beam_with( +def simulate_helicalbuckling_beam_with( elements=10, SAVE_FIGURE=False, PLOT_FIGURE=False ): helicalbuckling_sim = HelicalBucklingSimulator() @@ -57,6 +57,7 @@ def simulate_helicalbucklin_beam_with( base_radius, density, youngs_modulus=E, + shear_modulus=shear_modulus, ) # TODO: CosseratRod has to be able to take shear matrix as input, we should change it as done below @@ -92,7 +93,6 @@ def simulate_helicalbucklin_beam_with( final_time = 10500 total_steps = int(final_time / dt) print("Total steps", total_steps) - dt = final_time / total_steps time = 0.0 for i in range(total_steps): time = timestepper.step(helicalbuckling_sim, time, dt) @@ -120,7 +120,7 @@ def simulate_helicalbucklin_beam_with( # Convergence study # for n_elem in [5, 6, 7, 8, 9, 10] with mp.Pool(mp.cpu_count()) as pool: - results = pool.map(simulate_helicalbucklin_beam_with, convergence_elements) + results = pool.map(simulate_helicalbuckling_beam_with, convergence_elements) if PLOT_FIGURE: filename = "HelicalBuckling_convergence_test.png" diff --git a/examples/HelicalBucklingCase/helicalbuckling.py b/examples/HelicalBucklingCase/helicalbuckling.py index 019232dd..2da6b0e6 100644 --- a/examples/HelicalBucklingCase/helicalbuckling.py +++ b/examples/HelicalBucklingCase/helicalbuckling.py @@ -1,9 +1,9 @@ __doc__ = """Helical buckling validation case, for detailed explanation refer to -Gazzola et. al. R. Soc. 2018 section 3.4.1 """ +Gazzola et al. R. Soc. 2018 section 3.4.1 """ import numpy as np import elastica as ea -from examples.HelicalBucklingCase.helicalbuckling_postprocessing import ( +from helicalbuckling_postprocessing import ( plot_helicalbuckling, ) @@ -34,7 +34,7 @@ class HelicalBucklingSimulator( E = 1e6 slack = 3 number_of_rotations = 27 -# For shear modulus of 1e5, nu is 99! +# For shear modulus of 1e5, poisson_ratio should be 9 poisson_ratio = 9 shear_modulus = E / (poisson_ratio + 1.0) shear_matrix = np.repeat( @@ -57,8 +57,8 @@ class HelicalBucklingSimulator( ) # TODO: CosseratRod has to be able to take shear matrix as input, we should change it as done below -shearable_rod.shear_matrix = shear_matrix -shearable_rod.bend_matrix = bend_matrix +shearable_rod.shear_matrix[:] = shear_matrix +shearable_rod.bend_matrix[:] = bend_matrix helicalbuckling_sim.append(shearable_rod) @@ -88,7 +88,6 @@ class HelicalBucklingSimulator( final_time = 10500.0 total_steps = int(final_time / dt) print("Total steps", total_steps) -dt = final_time / total_steps time = 0.0 for i in range(total_steps): time = timestepper.step(helicalbuckling_sim, time, dt) diff --git a/examples/HelicalBucklingCase/helicalbuckling_postprocessing.py b/examples/HelicalBucklingCase/helicalbuckling_postprocessing.py index c0645135..39eab447 100644 --- a/examples/HelicalBucklingCase/helicalbuckling_postprocessing.py +++ b/examples/HelicalBucklingCase/helicalbuckling_postprocessing.py @@ -45,7 +45,7 @@ def analytical_solution(L, n_elem=10000): # nu = 1.0 / gamma - 1.0 # These are magic constants, but you can obtain them by solving - # this equation (accoring to matlab syntax) + # this equation (according to matlab syntax) # syms x y # S = vpasolve([d == sqrt(16/y*(1-x*x/(4*y))), R == x/gamma+4*acos(x/(2*sqrt(y)))], [x, y]); # moment = double(S.x); # dimensionless end moment diff --git a/examples/JointCases/fixed_joint.py b/examples/JointCases/fixed_joint.py index 9a1c1180..013db64c 100644 --- a/examples/JointCases/fixed_joint.py +++ b/examples/JointCases/fixed_joint.py @@ -28,10 +28,9 @@ class FixedJointSimulator( n_elem = 10 direction = np.array([0.0, 0.0, 1.0]) normal = np.array([0.0, 1.0, 0.0]) -roll_direction = np.cross(direction, normal) + base_length = 0.2 base_radius = 0.007 -base_area = np.pi * base_radius**2 density = 1750 E = 3e7 poisson_ratio = 0.5 @@ -135,10 +134,8 @@ def make_callback(self, system, time, current_step: int): # timestepper = PEFRL() final_time = 10 -dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) -dt = final_time / total_steps time = 0.0 for i in range(total_steps): time = timestepper.step(fixed_joint_sim, time, dt) diff --git a/examples/JointCases/fixed_joint_torsion.py b/examples/JointCases/fixed_joint_torsion.py index 4d44a4db..4331d218 100644 --- a/examples/JointCases/fixed_joint_torsion.py +++ b/examples/JointCases/fixed_joint_torsion.py @@ -34,7 +34,6 @@ class FixedJointSimulator( normal_rod2 = np.array([0.0, 0.0, 1.0]) base_length = 0.2 base_radius = 0.007 -base_area = np.pi * base_radius**2 density = 1750 E = 3e7 poisson_ratio = 0.5 diff --git a/examples/JointCases/hinge_joint.py b/examples/JointCases/hinge_joint.py index d7ff076f..50835b61 100644 --- a/examples/JointCases/hinge_joint.py +++ b/examples/JointCases/hinge_joint.py @@ -31,7 +31,6 @@ class HingeJointSimulator( roll_direction = np.cross(direction, normal) base_length = 0.2 base_radius = 0.007 -base_area = np.pi * base_radius**2 density = 1750 E = 3e7 poisson_ratio = 0.5 @@ -137,10 +136,8 @@ def make_callback(self, system, time, current_step: int): # timestepper = PEFRL() final_time = 10 -dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) -dt = final_time / total_steps time = 0.0 for i in range(total_steps): time = timestepper.step(hinge_joint_sim, time, dt) diff --git a/examples/JointCases/spherical_joint.py b/examples/JointCases/spherical_joint.py index ebcfa5ac..af914f2e 100644 --- a/examples/JointCases/spherical_joint.py +++ b/examples/JointCases/spherical_joint.py @@ -29,10 +29,9 @@ class SphericalJointSimulator( n_elem = 10 direction = np.array([0.0, 0.0, 1.0]) normal = np.array([0.0, 1.0, 0.0]) -roll_direction = np.cross(direction, normal) + base_length = 0.2 base_radius = 0.007 -base_area = np.pi * base_radius**2 density = 1750 E = 3e7 poisson_ratio = 0.5 @@ -138,10 +137,8 @@ def make_callback(self, system, time, current_step: int): # timestepper = PEFRL() final_time = 10 -dl = base_length / n_elem total_steps = int(final_time / dt) print("Total steps", total_steps) -dt = final_time / total_steps time = 0.0 for i in range(total_steps): time = timestepper.step(spherical_joint_sim, time, dt) diff --git a/examples/KnotCase/run_knot_simulation.py b/examples/KnotCase/run_knot_simulation.py index 9bc504f8..01ede4c0 100644 --- a/examples/KnotCase/run_knot_simulation.py +++ b/examples/KnotCase/run_knot_simulation.py @@ -58,7 +58,7 @@ class Callback(ea.CallBackBaseClass): """ def __init__(self, callback_params: dict) -> None: - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = 200 self.callback_params = callback_params diff --git a/examples/MuscularFlagella/connection_flagella.py b/examples/MuscularFlagella/connection_flagella.py index 52ec9415..eb2d0ead 100644 --- a/examples/MuscularFlagella/connection_flagella.py +++ b/examples/MuscularFlagella/connection_flagella.py @@ -56,7 +56,7 @@ def _apply_forces( system_two_external_forces, ): # This connection routine is not generalizable. Our goal here is to replicate the experiment data. - # Thus below code is hard codded. Torques are computed along the centerline of the muscle + # Thus below code is hard coded. Torques are computed along the centerline of the muscle # and transfered to the body. start_idx = index_one[0] end_idx = index_one[-1] diff --git a/examples/MuscularFlagella/muscle_forces_flagella.py b/examples/MuscularFlagella/muscle_forces_flagella.py index 1fcd23fb..ecd34244 100644 --- a/examples/MuscularFlagella/muscle_forces_flagella.py +++ b/examples/MuscularFlagella/muscle_forces_flagella.py @@ -31,10 +31,6 @@ def __init__(self, amplitude, frequency): self.wave_number = 2 * np.pi * frequency def apply_forces(self, system, time: np.float64 = 0.0): - # muscle_force = ( - # system.tangents * self.amplitude * np.abs(np.sin(self.wave_number * time)) - # ) - # system.external_forces += difference_kernel(muscle_force) self._apply_forces( self.amplitude, diff --git a/examples/MuscularFlagella/muscular_flagella.py b/examples/MuscularFlagella/muscular_flagella.py index d51bee02..94174e43 100644 --- a/examples/MuscularFlagella/muscular_flagella.py +++ b/examples/MuscularFlagella/muscular_flagella.py @@ -59,7 +59,7 @@ class MuscularFlagellaSimulator( start[2] = 0.1 direction = np.array([1.0, 0.0, 0.0]) normal = np.array([0.0, 0.0, 1.0]) -binormal = np.cross(direction, normal) + nu_body = 0 flagella_body = ea.CosseratRod.straight_rod( @@ -219,7 +219,9 @@ def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) self.callback_params["position"].append(system.position_collection.copy()) - self.callback_params["center_of_mass"].append(system.compute_position_center_of_mass()) + self.callback_params["center_of_mass"].append( + system.compute_position_center_of_mass() + ) self.callback_params["radius"].append(system.radius.copy()) self.callback_params["velocity"].append(system.velocity_collection.copy()) self.callback_params["tangents"].append(system.tangents.copy()) @@ -244,10 +246,9 @@ def make_callback(self, system, time, current_step: int): timestepper = ea.PositionVerlet() print("Total steps", total_steps) -dt = final_time / total_steps time = 0.0 for i in range(total_steps): - time = timestepper.step(muscular_flagella_sim, time, dt) + time = timestepper.step(muscular_flagella_sim, time, time_step) # Plot the videos @@ -285,7 +286,6 @@ def make_callback(self, system, time, current_step: int): ) # Store the data for later use and plotting -import os save_folder = os.path.join(os.getcwd(), "data") os.makedirs(save_folder, exist_ok=True) @@ -293,8 +293,8 @@ def make_callback(self, system, time, current_step: int): position_history_body = np.array(post_processing_dict_body["position"]) position_history_muscle = np.array(post_processing_dict_muscle["position"]) -com_history_body = np.array(post_processing_dict_body["com"]) -com_history_muscle = np.array(post_processing_dict_muscle["com"]) +com_history_body = np.array(post_processing_dict_body["center_of_mass"]) +com_history_muscle = np.array(post_processing_dict_muscle["center_of_mass"]) radius_history_body = np.array(post_processing_dict_body["radius"]) radius_history_muscle = np.array(post_processing_dict_muscle["radius"]) diff --git a/examples/MuscularFlagella/post_processing.py b/examples/MuscularFlagella/post_processing.py index 8602c8bd..b71eeaf3 100644 --- a/examples/MuscularFlagella/post_processing.py +++ b/examples/MuscularFlagella/post_processing.py @@ -28,7 +28,9 @@ def plot_video( rods_history[rod_idx]["radius"][t_idx], ) # Rod center of mass - com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx]["com"][time_idx] + com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx][ + "center_of_mass" + ][t_idx] # video pre-processing print("plot scene visualization video") @@ -135,7 +137,9 @@ def plot_video_2D( rods_history[rod_idx]["radius"][t_idx], ) # Rod center of mass - com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx]["com"][time_idx] + com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx][ + "center_of_mass" + ][t_idx] # video pre-processing print("plot scene visualization video") @@ -270,7 +274,7 @@ def plot_com_position_vs_time( ): time = rods_history["time"] - # rod_com_position = np.array(rods_history["com"]) * -1e3 + # rod_com_position = np.array(rods_history["center_of_mass"]) * -1e3 rod_com_position = np.array(rods_history["position"])[:, :, 9] * -1e3 # We are interested in dx, subtract initial position. diff --git a/examples/MuscularSnake/muscular_snake.py b/examples/MuscularSnake/muscular_snake.py index d87d0639..bde9852f 100644 --- a/examples/MuscularSnake/muscular_snake.py +++ b/examples/MuscularSnake/muscular_snake.py @@ -360,8 +360,13 @@ class MuscularSnakeSimulator( class MuscularSnakeCallBack(ea.CallBackBaseClass): + """ + Callback function for collecting data from Muscular Snake simulation. + Records time, position, center of mass, radius, and average velocity. + """ + def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -369,15 +374,11 @@ def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) self.callback_params["position"].append(system.position_collection.copy()) - self.callback_params["com"].append(system.compute_position_center_of_mass()) self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["velocity"].append(system.velocity_collection.copy()) self.callback_params["avg_velocity"].append( system.compute_velocity_center_of_mass() ) - self.callback_params["center_of_mass"].append( system.compute_position_center_of_mass() ) @@ -395,10 +396,9 @@ def make_callback(self, system, time, current_step: int): muscular_snake_simulator.finalize() timestepper = ea.PositionVerlet() -dt = final_time / total_steps time = 0.0 for i in range(total_steps): - time = timestepper.step(muscular_snake_simulator, time, dt) + time = timestepper.step(muscular_snake_simulator, time, time_step) plot_video_with_surface( diff --git a/examples/MuscularSnake/post_processing.py b/examples/MuscularSnake/post_processing.py index 5157c490..56c4a9de 100644 --- a/examples/MuscularSnake/post_processing.py +++ b/examples/MuscularSnake/post_processing.py @@ -1,10 +1,11 @@ import numpy as np import matplotlib -matplotlib.use("Agg") # Must be before importing matplotlib.pyplot or pylab! from matplotlib import pyplot as plt from matplotlib.colors import to_rgb from matplotlib import cm +from matplotlib.patches import Circle +import matplotlib.animation as manimation from tqdm import tqdm from typing import Dict, Sequence @@ -22,9 +23,6 @@ def plot_video_with_surface( folder_name = kwargs.get("folder_name", "") - # 2d case - import matplotlib.animation as animation - # simulation time sim_time = np.array(rods_history[0]["time"]) @@ -36,7 +34,9 @@ def plot_video_with_surface( rods_history[rod_idx]["radius"][t_idx], ) # Rod center of mass - com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx]["com"][time_idx] + com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx][ + "center_of_mass" + ][t_idx] # Generate target sphere data sphere_flag = False @@ -53,7 +53,7 @@ def plot_video_with_surface( # video pre-processing print("plot scene visualization video") - FFMpegWriter = animation.writers["ffmpeg"] + FFMpegWriter = manimation.writers["ffmpeg"] metadata = dict(title="Movie Test", artist="Matplotlib", comment="Movie support!") writer = FFMpegWriter(fps=fps, metadata=metadata) dpi = kwargs.get("dpi", 100) diff --git a/examples/RestartExample/restart_example.py b/examples/RestartExample/restart_example.py index b34ce1df..72a066cd 100644 --- a/examples/RestartExample/restart_example.py +++ b/examples/RestartExample/restart_example.py @@ -1,7 +1,8 @@ """ -This script is an example to how to use Pyelastica restart functionality. +This script is an example of how to use PyElastica restart functionality. """ +import os import numpy as np import elastica as ea @@ -22,7 +23,6 @@ class RestartExampleSimulator( normal = np.array([0.0, 1.0, 0.0]) base_length = 3.0 base_radius = 0.25 -base_area = np.pi * base_radius**2 density = 5000 E = 1e6 # For shear modulus of 1e4, nu is 99! @@ -83,8 +83,6 @@ class RestartExampleSimulator( timestepper = ea.PositionVerlet() total_steps = int(final_time / dt) - -dt = final_time / total_steps time = restart_time for i in range(total_steps): time = timestepper.step(restart_example_simulator, time, dt) @@ -93,4 +91,5 @@ class RestartExampleSimulator( # `restart_file_location` directory there is one file called system_0.npz . For each system appended on the simulator # separate system_#.npz file will be created. if SAVE_DATA_RESTART: + os.makedirs(restart_file_location, exist_ok=True) ea.save_state(restart_example_simulator, restart_file_location, time, True) diff --git a/examples/RigidBodyCases/RodRigidBodyContact/post_processing.py b/examples/RigidBodyCases/RodRigidBodyContact/post_processing.py index 066a0c50..a79286b1 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/post_processing.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/post_processing.py @@ -1,6 +1,7 @@ import numpy as np from matplotlib import pyplot as plt from matplotlib import cm +import matplotlib.animation as manimation from typing import Dict, Sequence from tqdm import tqdm @@ -36,16 +37,14 @@ def plot_video( cylinder_start, cylinder_radius, cylinder_height ) - import matplotlib.animation as manimation - plt.rcParams.update({"font.size": 22}) # Should give a (n_time, 3, n_elem) array positions = np.array(rod_history["position"]) # (n_time, 3) array - com = np.array(rod_history["com"]) + com = np.array(rod_history["center_of_mass"]) - cylinder_com = np.array(cylinder_history["com"]) + cylinder_com = np.array(cylinder_history["center_of_mass"]) cylinder_origin = cylinder_com - 0.5 * cylinder_height * cylinder_direction print("plot video") @@ -54,8 +53,6 @@ def plot_video( writer = FFMpegWriter(fps=fps, metadata=metadata) dpi = 50 - # min_limits = np.roll(np.array([0.0, -0.5 * cylinder_height, 0.0]), _roll_key) - fig = plt.figure(1, figsize=(10, 8), frameon=True, dpi=dpi) ax = plt.axes(projection="3d") # fig.add_subplot(111) ax.grid(which="minor", color="k", linestyle="--") @@ -267,9 +264,6 @@ def plot_video_with_surface( folder_name = kwargs.get("folder_name", "") - # 2d case - import matplotlib.animation as animation - # simulation time sim_time = np.array(rods_history[0]["time"]) @@ -281,7 +275,9 @@ def plot_video_with_surface( rods_history[rod_idx]["radius"][t_idx], ) # Rod center of mass - com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx]["com"][time_idx] + com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx][ + "center_of_mass" + ][time_idx] # Generate target sphere data sphere_flag = False @@ -298,7 +294,7 @@ def plot_video_with_surface( # video pre-processing print("plot scene visualization video") - FFMpegWriter = animation.writers["ffmpeg"] + FFMpegWriter = manimation.writers["ffmpeg"] metadata = dict(title="Movie Test", artist="Matplotlib", comment="Movie support!") writer = FFMpegWriter(fps=fps, metadata=metadata) dpi = kwargs.get("dpi", 100) diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact.py index aecc5e7c..abcc4813 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact.py @@ -90,23 +90,22 @@ class RodCylinderParallelContact( # For rod class StraightRodCallBack(ea.CallBackBaseClass): """ - Call back function for two arm octopus + Callback function for a straight rod in contact with a cylinder. """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) self.callback_params["position"].append( system.position_collection.copy() ) self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["com"].append( + self.callback_params["center_of_mass"].append( system.compute_position_center_of_mass() ) if current_step == 0: @@ -130,13 +129,13 @@ def make_callback(self, system, time, current_step: int): class RigidCylinderCallBack(ea.CallBackBaseClass): """ - Call back function for two arm octopus + Callback function for a rigid cylinder. """ def __init__( self, step_skip: int, callback_params: dict, resize_cylinder_elems: int ): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params self.n_elem_cylinder = resize_cylinder_elems @@ -145,7 +144,6 @@ def __init__( def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) cylinder_center_position = system.position_collection cylinder_length = system.length @@ -180,7 +178,7 @@ def make_callback(self, system, time, current_step: int): cylinder_velocity_collection.copy() ) self.callback_params["radius"].append(cylinder_radius_collection.copy()) - self.callback_params["com"].append( + self.callback_params["center_of_mass"].append( system.compute_position_center_of_mass() ) @@ -244,3 +242,7 @@ def make_callback(self, system, time, current_step: int): filename=filaname, SAVE_FIGURE=True, ) + + +if __name__ == "__main__": + rod_cylinder_contact_case() diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction.py index d09d4eea..e08a6716 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction.py @@ -110,23 +110,22 @@ class RodCylinderParallelContact( # For rod class StraightRodCallBack(ea.CallBackBaseClass): """ - Call back function for two arm octopus + Callback function for a straight rod in contact with a cylinder. """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) self.callback_params["position"].append( system.position_collection.copy() ) self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["com"].append( + self.callback_params["center_of_mass"].append( system.compute_position_center_of_mass() ) if current_step == 0: @@ -150,13 +149,13 @@ def make_callback(self, system, time, current_step: int): class RigidCylinderCallBack(ea.CallBackBaseClass): """ - Call back function for two arm octopus + Callback function for a rigid cylinder """ def __init__( self, step_skip: int, callback_params: dict, resize_cylinder_elems: int ): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params self.n_elem_cylinder = resize_cylinder_elems @@ -165,7 +164,6 @@ def __init__( def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) cylinder_center_position = system.position_collection cylinder_length = system.length @@ -200,7 +198,7 @@ def make_callback(self, system, time, current_step: int): cylinder_velocity_collection.copy() ) self.callback_params["radius"].append(cylinder_radius_collection.copy()) - self.callback_params["com"].append( + self.callback_params["center_of_mass"].append( system.compute_position_center_of_mass() ) diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_validation.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_validation.py index 0dce9566..84804792 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_validation.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_validation.py @@ -115,14 +115,14 @@ class SingleRodSingleCylinderInteractionSimulator( ) -# Add call backs +# Add callbacks class PositionCollector(ea.CallBackBaseClass): """ - Call back function for continuum snake + Callback function for collecting position data of a rod and cylinder. """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -131,7 +131,9 @@ def make_callback(self, system, time, current_step: int): self.callback_params["time"].append(time) # Collect only x self.callback_params["position"].append(system.position_collection.copy()) - self.callback_params["center_of_mass"].append(system.compute_position_center_of_mass()) + self.callback_params["center_of_mass"].append( + system.compute_position_center_of_mass() + ) return 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 f379e5b2..96ef770c 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_with_y_normal.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_with_y_normal.py @@ -93,14 +93,14 @@ class SingleRodSingleCylinderInteractionSimulator( ) -# Add call backs +# Add callbacks class PositionCollector(ea.CallBackBaseClass): """ - Call back function for continuum snake + Callback function for collecting position data of a rod and cylinder. """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -109,7 +109,9 @@ def make_callback(self, system, time, current_step: int): self.callback_params["time"].append(time) # Collect only x self.callback_params["position"].append(system.position_collection.copy()) - self.callback_params["center_of_mass"].append(system.compute_position_center_of_mass()) + self.callback_params["center_of_mass"].append( + system.compute_position_center_of_mass() + ) return @@ -160,4 +162,5 @@ def make_callback(self, system, time, current_step: int): rod_base_radius=base_radius, TIP_COLLISION=TIP_COLLISION, TIP_CHOICE=TIP_CHOICE, + _roll_key=1, # For y-normal case, we are interested in y-direction. ) diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_sphere_contact.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_sphere_contact.py index a00d7386..b28a2470 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_sphere_contact.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_sphere_contact.py @@ -103,23 +103,22 @@ class Simulator( # For rod class StraightRodCallBack(ea.CallBackBaseClass): """ - Call back function for two arm octopus + Callback function for a straight rod in contact with a sphere. """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) self.callback_params["position"].append( system.position_collection.copy() ) self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["com"].append( + self.callback_params["center_of_mass"].append( system.compute_position_center_of_mass() ) total_energy = ( @@ -133,7 +132,7 @@ def make_callback(self, system, time, current_step: int): class RigidBodyCallback(ea.CallBackBaseClass): """ - Call back function for two arm octopus + Callback function for a rigid sphere. """ def __init__( @@ -141,14 +140,13 @@ def __init__( step_skip: int, callback_params: dict, ): - ea.CallBackBaseClass.__init__(self) # TODO: Is this necessary? + super().__init__() self.every = step_skip self.callback_params = callback_params def make_callback(self, system, time, current_step: int): if current_step % self.every == 0: self.callback_params["time"].append(time) - self.callback_params["step"].append(current_step) self.callback_params["position"].append( system.position_collection.copy() ) diff --git a/examples/RigidBodyCases/rigid_cylinder_rotational_motion_case.py b/examples/RigidBodyCases/rigid_cylinder_rotational_motion_case.py index 282f1b79..22b8ef68 100644 --- a/examples/RigidBodyCases/rigid_cylinder_rotational_motion_case.py +++ b/examples/RigidBodyCases/rigid_cylinder_rotational_motion_case.py @@ -21,23 +21,20 @@ class RigidCylinderSimulator( def rigid_cylinder_rotational_motion_verification(torque=0.0): """ This test case is for validating rotational motion of - rigid cylinder. Here we are applying point for on the cylinder + rigid cylinder. Here we are applying point torque on the cylinder and compare the kinetic energy of the cylinder after T=0.25s with the analytical calculation. - :param force: + :param torque: :return: """ rigid_cylinder_sim = RigidCylinderSimulator() - # setting up test params # setting up test params start = np.zeros((3,)) direction = np.array([0.0, 1.0, 0.0]) normal = np.array([0.0, 0.0, 1.0]) - binormal = np.cross(direction, normal) base_length = 1.0 base_radius = 0.05 - base_area = np.pi * base_radius**2 density = 1000 cylinder = ea.Cylinder(start, direction, normal, base_length, base_radius, density) @@ -53,7 +50,7 @@ def __init__(self, torque, direction=np.array([0.0, 0.0, 0.0])): super(PointCoupleToCenter, self).__init__() self.torque = (torque * direction).reshape(3, 1) - def apply_forces(self, system, time: np.float64 = np.float64(0.0)): + def apply_torques(self, system, time: np.float64 = np.float64(0.0)): system.external_torques += np.einsum( "ijk, jk->ik", system.director_collection, self.torque ) @@ -96,7 +93,6 @@ def make_callback(self, system, time, current_step: int): dt = 4.0e-5 total_steps = int(final_time / dt) print("Total steps", total_steps) - dt = final_time / total_steps time = 0.0 for i in range(total_steps): time = timestepper.step(rigid_cylinder_sim, time, dt) diff --git a/examples/RigidBodyCases/rigid_cylinder_translational_motion_case.py b/examples/RigidBodyCases/rigid_cylinder_translational_motion_case.py index 64d7b86e..255ea5f1 100644 --- a/examples/RigidBodyCases/rigid_cylinder_translational_motion_case.py +++ b/examples/RigidBodyCases/rigid_cylinder_translational_motion_case.py @@ -29,15 +29,12 @@ def rigid_cylinder_translational_motion_verification(force=0.0): """ rigid_cylinder_sim = RigidCylinderSimulator() - # setting up test params # setting up test params start = np.zeros((3,)) direction = np.array([0.0, 1.0, 0.0]) normal = np.array([0.0, 0.0, 1.0]) - binormal = np.cross(direction, normal) base_length = 1.0 base_radius = 0.05 - base_area = np.pi * base_radius**2 density = 1000 cylinder = ea.Cylinder(start, direction, normal, base_length, base_radius, density) @@ -94,7 +91,6 @@ def make_callback(self, system, time, current_step: int): dt = 4.0e-5 total_steps = int(final_time / dt) print("Total steps", total_steps) - dt = final_time / total_steps time = 0.0 for i in range(total_steps): time = timestepper.step(rigid_cylinder_sim, time, dt) diff --git a/examples/RigidBodyCases/rigid_sphere_rotational_motion_case.py b/examples/RigidBodyCases/rigid_sphere_rotational_motion_case.py index 1a4f2e8a..b0c66617 100644 --- a/examples/RigidBodyCases/rigid_sphere_rotational_motion_case.py +++ b/examples/RigidBodyCases/rigid_sphere_rotational_motion_case.py @@ -20,10 +20,10 @@ class RigidSphereSimulator( def rigid_sphere_rolling_verification(torque=0.0): """ - This test case is for validating friction calculation for rigid body. Here cylinder direction - and normal directions are parallel and base of the cylinder is touching the ground. We are validating - our friction model for different forces. - :param force: + This test case is for validating rotational motion of a rigid sphere. + Here, a torque is applied to the sphere, and its kinetic energy + is compared with analytical calculations after a given time. + :param torque: :return: """ rigid_sphere_sim = RigidSphereSimulator() @@ -44,7 +44,7 @@ def __init__(self, torque, direction=np.array([0.0, 0.0, 0.0])): super(PointCoupleToCenter, self).__init__() self.torque = (torque * direction).reshape(3, 1) - def apply_forces(self, system, time: np.float64 = np.float64(0.0)): + def apply_torques(self, system, time: np.float64 = np.float64(0.0)): system.external_torques += np.einsum( "ijk, jk->ik", system.director_collection, self.torque ) @@ -83,11 +83,10 @@ def make_callback(self, system, time, current_step: int): rigid_sphere_sim.finalize() timestepper = ea.PositionVerlet() - final_time = 0.25 # 11.0 + 0.01) + final_time = 0.25 dt = 4.0e-5 total_steps = int(final_time / dt) print("Total steps", total_steps) - dt = final_time / total_steps time = 0.0 for i in range(total_steps): time = timestepper.step(rigid_sphere_sim, time, dt) @@ -127,13 +126,13 @@ def make_callback(self, system, time, current_step: int): results = pool.map(rigid_sphere_rolling_verification, torque) if PLOT_FIGURE: - filename = "rotationa_energy_test_for_sphere.png" + filename = "rotational_energy_test_for_sphere.png" plot_friction_validation(results, SAVE_FIGURE, filename) if SAVE_RESULTS: import pickle - filename = "rotationa_energy_test_for_sphere.dat" + filename = "rotational_energy_test_for_sphere.dat" file = open(filename, "wb") pickle.dump([results], file) file.close() diff --git a/examples/RigidBodyCases/rigid_sphere_translational_motion_case.py b/examples/RigidBodyCases/rigid_sphere_translational_motion_case.py index 3aee16e7..30f27db2 100644 --- a/examples/RigidBodyCases/rigid_sphere_translational_motion_case.py +++ b/examples/RigidBodyCases/rigid_sphere_translational_motion_case.py @@ -88,7 +88,6 @@ def make_callback(self, system, time, current_step: int): dt = 4.0e-5 total_steps = int(final_time / dt) print("Total steps", total_steps) - dt = final_time / total_steps time = 0.0 for i in range(total_steps): time = timestepper.step(rigid_sphere_sim, time, dt) diff --git a/examples/RingRodCase/ring_rod.py b/examples/RingRodCase/ring_rod.py index 5ef6a1ab..8579d242 100644 --- a/examples/RingRodCase/ring_rod.py +++ b/examples/RingRodCase/ring_rod.py @@ -68,14 +68,14 @@ class RingSimulator( step_skip = int(1.0 / (rendering_fps * time_step)) -# Add call backs +# Add callbacks class RingRodCallBack(ea.CallBackBaseClass): """ - Call back function for ring rod + Callback function for ring rod """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -88,7 +88,9 @@ def make_callback(self, system, time, current_step: int): self.callback_params["position"].append(system.position_collection.copy()) self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["center_of_mass"].append(system.compute_position_center_of_mass()) + self.callback_params["center_of_mass"].append( + system.compute_position_center_of_mass() + ) return @@ -101,10 +103,9 @@ def make_callback(self, system, time, current_step: int): ring_sim.finalize() timestepper = ea.PositionVerlet() -dt = final_time / total_steps time = 0.0 for i in range(total_steps): - time = timestepper.step(ring_sim, time, dt) + time = timestepper.step(ring_sim, time, time_step) filename_video = "ring_rod.mp4" diff --git a/examples/RingRodCase/ring_rod_post_processing.py b/examples/RingRodCase/ring_rod_post_processing.py index 933b0787..e1656772 100644 --- a/examples/RingRodCase/ring_rod_post_processing.py +++ b/examples/RingRodCase/ring_rod_post_processing.py @@ -32,7 +32,9 @@ def plot_video( rods_history[rod_idx]["radius"][t_idx], ) # Rod center of mass - com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx]["com"][time_idx] + com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx][ + "center_of_mass" + ][t_idx] # Generate target sphere data sphere_flag = False diff --git a/examples/RodContactCase/RodRodContact/rod_rod_contact_inclined_validation.py b/examples/RodContactCase/RodRodContact/rod_rod_contact_inclined_validation.py index 7b5fb042..76c4d721 100644 --- a/examples/RodContactCase/RodRodContact/rod_rod_contact_inclined_validation.py +++ b/examples/RodContactCase/RodRodContact/rod_rod_contact_inclined_validation.py @@ -32,9 +32,7 @@ class InclinedRodRodContact( # Rod parameters base_length = 0.5 base_radius = 0.01 -base_area = np.pi * base_radius**2 density = 1750 -nu = 0.0 E = 3e5 poisson_ratio = 0.5 shear_modulus = E / (poisson_ratio + 1.0) @@ -114,7 +112,7 @@ class RodCallBack(ea.CallBackBaseClass): """ """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -124,7 +122,9 @@ def make_callback(self, system, time, current_step: int): self.callback_params["step"].append(current_step) self.callback_params["position"].append(system.position_collection.copy()) self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["com"].append(system.compute_position_center_of_mass()) + self.callback_params["center_of_mass"].append( + system.compute_position_center_of_mass() + ) self.callback_params["com_velocity"].append( system.compute_velocity_center_of_mass() ) @@ -180,10 +180,10 @@ def make_callback(self, system, time, current_step: int): vis2D=True, ) -filaname = "inclined_rods_velocity.png" +filename = "inclined_rods_velocity.png" plot_velocity( post_processing_dict_rod1, post_processing_dict_rod2, - filename=filaname, + filename=filename, SAVE_FIGURE=True, ) diff --git a/examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py b/examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py index 2f5012a2..359650b6 100644 --- a/examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py +++ b/examples/RodContactCase/RodRodContact/rod_rod_contact_parallel_validation.py @@ -32,9 +32,7 @@ class ParallelRodRodContact( # Rod parameters base_length = 0.5 base_radius = 0.01 -base_area = np.pi * base_radius**2 density = 1750 -nu = 0.0 E = 3e5 poisson_ratio = 0.5 shear_modulus = E / (poisson_ratio + 1.0) @@ -111,7 +109,7 @@ class RodCallBack(ea.CallBackBaseClass): """ """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -121,7 +119,9 @@ def make_callback(self, system, time, current_step: int): self.callback_params["step"].append(current_step) self.callback_params["position"].append(system.position_collection.copy()) self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["com"].append(system.compute_position_center_of_mass()) + self.callback_params["center_of_mass"].append( + system.compute_position_center_of_mass() + ) self.callback_params["com_velocity"].append( system.compute_velocity_center_of_mass() ) @@ -177,10 +177,10 @@ def make_callback(self, system, time, current_step: int): vis2D=True, ) -filaname = "parallel_rods_velocity.png" +filename = "parallel_rods_velocity.png" plot_velocity( post_processing_dict_rod1, post_processing_dict_rod2, - filename=filaname, + filename=filename, SAVE_FIGURE=True, ) diff --git a/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py b/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py index 16d44835..e85db918 100644 --- a/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py +++ b/examples/RodContactCase/RodSelfContact/PlectonemesCase/plectoneme_case.py @@ -82,7 +82,7 @@ class PlectonemesCase( from elastica._rotations import _get_rotation_matrix -class SelonoidsBC(ea.ConstraintBase): +class SolenoidsBC(ea.ConstraintBase): """ """ def __init__( @@ -102,7 +102,7 @@ def __init__( theta = 2.0 * number_of_rotations * np.pi - angel_vel_scalar = theta / self.twisting_time + angle_vel_scalar = theta / self.twisting_time direction = -(position_end - position_start) / np.linalg.norm( position_end - position_start @@ -120,7 +120,7 @@ def __init__( @ director_end ) # rotation_matrix wants vectors 3,1 - self.ang_vel = angel_vel_scalar * axis_of_rotation_in_material_frame + self.ang_vel = angle_vel_scalar * axis_of_rotation_in_material_frame self.position_start = position_start self.director_start = director_start @@ -158,7 +158,7 @@ def constrain_rates(self, system, time): plectonemes_sim.constrain(sherable_rod).using( - SelonoidsBC, + SolenoidsBC, constrained_position_idx=(0, -1), constrained_director_idx=(0, -1), time_twis_start=time_start_twist, @@ -177,7 +177,7 @@ class RodCallBack(ea.CallBackBaseClass): """ """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -187,7 +187,9 @@ def make_callback(self, system, time, current_step: int): self.callback_params["step"].append(current_step) self.callback_params["position"].append(system.position_collection.copy()) self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["com"].append(system.compute_position_center_of_mass()) + self.callback_params["center_of_mass"].append( + system.compute_position_center_of_mass() + ) self.callback_params["com_velocity"].append( system.compute_velocity_center_of_mass() ) @@ -244,14 +246,14 @@ def make_callback(self, system, time, current_step: int): # Compute twist density theta = 2.0 * number_of_rotations * np.pi -angel_vel_scalar = theta / time_twist +angle_vel_scalar = theta / time_twist twist_time_interval_start_idx = np.where(time > time_start_twist)[0][0] twist_time_interval_end_idx = np.where(time < (time_relax + time_twist))[0][-1] twist_density = ( (time[twist_time_interval_start_idx:twist_time_interval_end_idx] - time_start_twist) - * angel_vel_scalar + * angle_vel_scalar * base_radius ) diff --git a/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py b/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py index 94962b6f..afd34def 100644 --- a/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py +++ b/examples/RodContactCase/RodSelfContact/SolenoidsCase/solenoid_case.py @@ -40,7 +40,6 @@ class SolenoidCase( # Rest of the rod parameters and construct rod base_radius = 0.025 base_area = np.pi * base_radius**2 -I = np.pi / 4 * base_radius**4 volume = base_area * base_length mass = 1.0 density = mass / volume @@ -84,7 +83,7 @@ class SolenoidCase( from elastica._rotations import _get_rotation_matrix -class SelonoidsBC(ea.ConstraintBase): +class SolenoidsBC(ea.ConstraintBase): """ """ def __init__( @@ -104,7 +103,7 @@ def __init__( theta = 2.0 * number_of_rotations * np.pi - angel_vel_scalar = theta / self.twisting_time + angle_vel_scalar = theta / self.twisting_time direction = -(position_end - position_start) / np.linalg.norm( position_end - position_start @@ -122,7 +121,7 @@ def __init__( @ director_end ) # rotation_matrix wants vectors 3,1 - self.ang_vel = angel_vel_scalar * axis_of_rotation_in_material_frame + self.ang_vel = angle_vel_scalar * axis_of_rotation_in_material_frame self.position_start = position_start self.director_start = director_start @@ -160,7 +159,7 @@ def constrain_rates(self, system, time): solenoid_sim.constrain(sherable_rod).using( - SelonoidsBC, + SolenoidsBC, constrained_position_idx=(0, -1), constrained_director_idx=(0, -1), time_twis_start=time_start_twist, @@ -188,7 +187,7 @@ class RodCallBack(ea.CallBackBaseClass): """ """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -198,7 +197,9 @@ def make_callback(self, system, time, current_step: int): self.callback_params["step"].append(current_step) self.callback_params["position"].append(system.position_collection.copy()) self.callback_params["radius"].append(system.radius.copy()) - self.callback_params["com"].append(system.compute_position_center_of_mass()) + self.callback_params["center_of_mass"].append( + system.compute_position_center_of_mass() + ) self.callback_params["com_velocity"].append( system.compute_velocity_center_of_mass() ) @@ -255,14 +256,14 @@ def make_callback(self, system, time, current_step: int): # Compute twist density theta = 2.0 * number_of_rotations * np.pi -angel_vel_scalar = theta / time_twist +angle_vel_scalar = theta / time_twist twist_time_interval_start_idx = np.where(time > time_start_twist)[0][0] twist_time_interval_end_idx = np.where(time < (time_relax + time_twist))[0][-1] twist_density = ( (time[twist_time_interval_start_idx:twist_time_interval_end_idx] - time_start_twist) - * angel_vel_scalar + * angle_vel_scalar * base_radius ) diff --git a/examples/RodContactCase/post_processing.py b/examples/RodContactCase/post_processing.py index 43f0764b..c1192880 100644 --- a/examples/RodContactCase/post_processing.py +++ b/examples/RodContactCase/post_processing.py @@ -28,7 +28,9 @@ def plot_video_with_surface( rods_history[rod_idx]["radius"][t_idx], ) # Rod center of mass - com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx]["com"][time_idx] + com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx][ + "center_of_mass" + ][t_idx] # video pre-processing print("plot scene visualization video") diff --git a/examples/TimoshenkoBeamCase/run_timoshenko.py b/examples/TimoshenkoBeamCase/run_timoshenko.py index ca03c027..6a9aa29f 100644 --- a/examples/TimoshenkoBeamCase/run_timoshenko.py +++ b/examples/TimoshenkoBeamCase/run_timoshenko.py @@ -164,7 +164,7 @@ class VelocityCallBack(ea.CallBackBaseClass): """ def __init__(self, step_skip: int, callback_params: dict): - ea.CallBackBaseClass.__init__(self) + super().__init__() self.every = step_skip self.callback_params = callback_params @@ -199,9 +199,9 @@ def make_callback(self, system, time, current_step: int): # %% # Define Simulation Time # ---------------------- -# The last thing we need to do deceide how long we want the simulation to run for and what timestepping method to use. Currently, the PositionVerlet algorithim is suggested default method. +# The last thing we need to do decide how long we want the simulation to run for and what timestepping method to use. Currently, the PositionVerlet algorithim is suggested default method. # -# In this example, we are trying to match a steady-state solution by temporally evolving our system to reach equillibrium. As such, there is a tradeoff between letting the simulation run long enough to each the equillibrium and waiting around for the simulation to be done. Here we are running the simulation for 10 seconds, this produces reasonable agreement with the analytical solution without taking to long to finish. If you run the simulation for longer, you will get better agreement with the analytical solution. +# In this example, we are trying to match a steady-state solution by temporally evolving our system to reach equilibrium. As such, there is a tradeoff between letting the simulation run long enough to reach the equilibrium and waiting around for the simulation to be done. Here we are running the simulation for 10 seconds, this produces reasonable agreement with the analytical solution without taking to long to finish. If you run the simulation for longer, you will get better agreement with the analytical solution. timestepper = ea.PositionVerlet() # timestepper = PEFRL() diff --git a/examples/TimoshenkoBeamCase/timoshenko_postprocessing.py b/examples/TimoshenkoBeamCase/timoshenko_postprocessing.py index b222d508..ebf593c0 100644 --- a/examples/TimoshenkoBeamCase/timoshenko_postprocessing.py +++ b/examples/TimoshenkoBeamCase/timoshenko_postprocessing.py @@ -51,10 +51,10 @@ def plot_timoshenko(rod, end_force, SAVE_FIGURE, ADD_UNSHEARABLE_ROD=False): ax = fig.add_subplot(111) ax.grid(which="minor", color="k", linestyle="--") ax.grid(which="major", color="k", linestyle="-") - analytical_shearable_positon = analytical_shearable(rod, end_force) + analytical_shearable_position = analytical_shearable(rod, end_force) ax.plot( - analytical_shearable_positon[0], - analytical_shearable_positon[1], + analytical_shearable_position[0], + analytical_shearable_position[1], "k--", label="Timoshenko", ) @@ -65,10 +65,10 @@ def plot_timoshenko(rod, end_force, SAVE_FIGURE, ADD_UNSHEARABLE_ROD=False): label="n=" + str(rod.n_elems), ) if ADD_UNSHEARABLE_ROD: - analytical_unshearable_positon = analytical_unshearable(rod, end_force) + analytical_unshearable_position = analytical_unshearable(rod, end_force) ax.plot( - analytical_unshearable_positon[0], - analytical_unshearable_positon[1], + analytical_unshearable_position[0], + analytical_unshearable_position[1], "r-.", label="Euler-Bernoulli", ) diff --git a/examples/TumblingUnconstrainedRod/forces.py b/examples/TumblingUnconstrainedRod/forces.py index 58330154..0155437c 100644 --- a/examples/TumblingUnconstrainedRod/forces.py +++ b/examples/TumblingUnconstrainedRod/forces.py @@ -2,8 +2,6 @@ from elastica.external_forces import NoForces from elastica.typing import SystemType -from tqdm import tqdm - class EndpointforcesWithTimeFactor(NoForces): @@ -29,14 +27,12 @@ def __init__(self, torque, time_factor, direction=np.array([0.0, 0.0, 0.0])): self.time_factor = time_factor def apply_torques(self, system: SystemType, time: np.float64 = 0.0): - n_elems = system.n_elems - factor = self.time_factor(time) system.external_torques[..., -1] += self.torque * factor -def lamda_t_function(time): +def time_factor_function(time): if time < 2.5: factor = time * (1 / 2.5) elif time > 2.5 and time < 5.0: diff --git a/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod.py b/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod.py index 14bf735f..b250243c 100644 --- a/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod.py +++ b/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod.py @@ -20,7 +20,6 @@ n_elem = 256 start = np.array([0.0, 0.0, 8.0]) -end = np.array([6.0, 0.0, 0.0]) direction = np.array([0.6, 0.0, -0.8]) normal = np.array([0.0, 1.0, 0.0]) base_length = 10 @@ -70,14 +69,14 @@ class NonConstrainRodSimulator( end_force = np.array([20.0, 0.0, 0.0]) square_rod_sim.add_forcing_to(square_rod).using( - EndpointforcesWithTimeFactor, origin_force, end_force, lamda_t_function + EndpointforcesWithTimeFactor, origin_force, end_force, time_factor_function ) square_rod_sim.add_forcing_to(square_rod).using( EndpointtorqueWithTimeFactor, 1, - lamda_t_function, + time_factor_function, direction=np.array([0.0, 200.0, -100.0]), ) diff --git a/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod_postprocessing.py b/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod_postprocessing.py index 5ce18bf4..ceff240c 100644 --- a/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod_postprocessing.py +++ b/examples/TumblingUnconstrainedRod/tumbling_unconstrained_rod_postprocessing.py @@ -36,7 +36,9 @@ def plot_video_with_surface( rods_history[rod_idx]["radius"][t_idx], ) # Rod center of mass - com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx]["com"][time_idx] + com_history_unpacker = lambda rod_idx, t_idx: rods_history[rod_idx][ + "center_of_mass" + ][t_idx] # Generate target sphere data sphere_flag = False @@ -83,8 +85,6 @@ def plot_video_with_surface( ax.view_init(elev=0, azim=0) time_idx = 0 - rod_lines = [None for _ in range(n_visualized_rods)] - rod_com_lines = [None for _ in range(n_visualized_rods)] rod_scatters = [None for _ in range(n_visualized_rods)] for rod_idx in range(n_visualized_rods): From 16b59b82b326d4cc1262d1683f378e1e9054638c Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 12 Dec 2025 11:57:50 -0600 Subject: [PATCH 46/85] refactor: update import paths for consistency and clarity across examples --- .../cantilever_transversal_load.py | 5 ++- ...convergence_cantilever_transversal_load.py | 45 ++----------------- .../FrictionValidationCases/axial_friction.py | 5 ++- .../rolling_friction_initial_velocity.py | 5 ++- .../rolling_friction_on_inclined_plane.py | 4 +- .../rolling_friction_torque.py | 4 +- .../convergence_helicalbuckling.py | 7 +-- .../rod_cylinder_contact_friction_case.py | 2 +- ...d_cylinder_contact_friction_phase_space.py | 4 +- .../convergence_timoshenko.py | 5 ++- 10 files changed, 27 insertions(+), 59 deletions(-) diff --git a/examples/CantileverTransversalLoadCase/cantilever_transversal_load.py b/examples/CantileverTransversalLoadCase/cantilever_transversal_load.py index 442de4a0..e78161c0 100644 --- a/examples/CantileverTransversalLoadCase/cantilever_transversal_load.py +++ b/examples/CantileverTransversalLoadCase/cantilever_transversal_load.py @@ -3,12 +3,13 @@ from elastica.external_forces import EndpointForces from elastica.timestepper.symplectic_steppers import PositionVerlet import elastica as ea -from examples.convergence_functions import calculate_error_norm -from cantilever_transversal_load_postprocessing import adjust_square_cross_section from matplotlib import pyplot as plt from matplotlib.colors import to_rgb import json +from convergence_functions import calculate_error_norm +from setup_helper import adjust_square_cross_section + def analytical_results(index): with open("cantilever_transversal_load_data.json", "r") as file: diff --git a/examples/CantileverTransversalLoadCase/convergence_cantilever_transversal_load.py b/examples/CantileverTransversalLoadCase/convergence_cantilever_transversal_load.py index 442de4a0..8bee5734 100644 --- a/examples/CantileverTransversalLoadCase/convergence_cantilever_transversal_load.py +++ b/examples/CantileverTransversalLoadCase/convergence_cantilever_transversal_load.py @@ -3,12 +3,13 @@ from elastica.external_forces import EndpointForces from elastica.timestepper.symplectic_steppers import PositionVerlet import elastica as ea -from examples.convergence_functions import calculate_error_norm -from cantilever_transversal_load_postprocessing import adjust_square_cross_section from matplotlib import pyplot as plt from matplotlib.colors import to_rgb import json +from convergence_functions import calculate_error_norm, plot_convergence +from setup_helper import adjust_square_cross_section + def analytical_results(index): with open("cantilever_transversal_load_data.json", "r") as file: @@ -160,42 +161,4 @@ class SquareRodSimulator( for i in convergence_elements: results.append(cantilever_subjected_to_a_transversal_load(i)) - l1 = [] - l2 = [] - linf = [] - - for result in results: - l1.append(result["l1"]) - l2.append(result["l2"]) - linf.append(result["linf"]) - - fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150) - ax = fig.add_subplot(111) - ax.grid(which="minor", color="k", linestyle="--") - ax.grid(which="major", color="k", linestyle="-") - ax.set_xlabel("N_element") # X-axis label - ax.set_ylabel("Error") # Y-axis label - ax.set_title("Error Convergence Analysis") - - ax.loglog( - convergence_elements, - l1, - marker="o", - ms=10, - c=to_rgb("xkcd:bluish"), - lw=2, - label="l1", - ) - ax.loglog( - convergence_elements, - l2, - marker="o", - ms=10, - c=to_rgb("xkcd:reddish"), - lw=2, - label="l2", - ) - ax.loglog(convergence_elements, linf, marker="o", ms=10, c="k", lw=2, label="linf") - fig.legend(prop={"size": 20}) - - fig.show() + plot_convergence(results, SAVE_FIGURE=False, filename="") diff --git a/examples/FrictionValidationCases/axial_friction.py b/examples/FrictionValidationCases/axial_friction.py index 3435fbf3..6ab98d47 100644 --- a/examples/FrictionValidationCases/axial_friction.py +++ b/examples/FrictionValidationCases/axial_friction.py @@ -1,8 +1,9 @@ -__doc__ = """Axial friction validation, for detailed explanation refer to Gazzola et. al. R. Soc. 2018 +__doc__ = """Axial friction validation, for detailed explanation refer to Gazzola et al. R. Soc. 2018 section 4.1.4 and Appendix G """ import numpy as np import elastica as ea -from examples.FrictionValidationCases.friction_validation_postprocessing import ( + +from friction_validation_postprocessing import ( plot_axial_friction_validation, ) diff --git a/examples/FrictionValidationCases/rolling_friction_initial_velocity.py b/examples/FrictionValidationCases/rolling_friction_initial_velocity.py index dcdc5e3d..19ca5165 100644 --- a/examples/FrictionValidationCases/rolling_friction_initial_velocity.py +++ b/examples/FrictionValidationCases/rolling_friction_initial_velocity.py @@ -1,9 +1,10 @@ -__doc__ = """Rolling friction validation, for detailed explanation refer to Gazzola et. al. R. Soc. 2018 +__doc__ = """Rolling friction validation, for detailed explanation refer to Gazzola et al. R. Soc. 2018 section 4.1.4 and Appendix G """ import numpy as np import elastica as ea -from examples.FrictionValidationCases.friction_validation_postprocessing import ( + +from friction_validation_postprocessing import ( plot_friction_validation, ) diff --git a/examples/FrictionValidationCases/rolling_friction_on_inclined_plane.py b/examples/FrictionValidationCases/rolling_friction_on_inclined_plane.py index 20aae5e5..4cca61a8 100644 --- a/examples/FrictionValidationCases/rolling_friction_on_inclined_plane.py +++ b/examples/FrictionValidationCases/rolling_friction_on_inclined_plane.py @@ -1,9 +1,9 @@ -__doc__ = """Rolling friction validation, for detailed explanation refer to Gazzola et. al. R. Soc. 2018 +__doc__ = """Rolling friction validation, for detailed explanation refer to Gazzola et al. R. Soc. 2018 section 4.1.4 and Appendix G """ import numpy as np import elastica as ea -from examples.FrictionValidationCases.friction_validation_postprocessing import ( +from friction_validation_postprocessing import ( plot_friction_validation, ) diff --git a/examples/FrictionValidationCases/rolling_friction_torque.py b/examples/FrictionValidationCases/rolling_friction_torque.py index 57634f4b..b25d88dd 100644 --- a/examples/FrictionValidationCases/rolling_friction_torque.py +++ b/examples/FrictionValidationCases/rolling_friction_torque.py @@ -1,9 +1,9 @@ -__doc__ = """Rolling friction validation, for detailed explanation refer to Gazzola et. al. R. Soc. 2018 +__doc__ = """Rolling friction validation, for detailed explanation refer to Gazzola et al. R. Soc. 2018 section 4.1.4 and Appendix G """ import numpy as np import elastica as ea -from examples.FrictionValidationCases.friction_validation_postprocessing import ( +from friction_validation_postprocessing import ( plot_friction_validation, ) diff --git a/examples/HelicalBucklingCase/convergence_helicalbuckling.py b/examples/HelicalBucklingCase/convergence_helicalbuckling.py index d803c4bd..608b642e 100644 --- a/examples/HelicalBucklingCase/convergence_helicalbuckling.py +++ b/examples/HelicalBucklingCase/convergence_helicalbuckling.py @@ -1,14 +1,15 @@ -__doc__ = """Helical buckling convergence study, for detailed explanation refer to Gazzola et. al. R. Soc. 2018 +__doc__ = """Helical buckling convergence study, for detailed explanation refer to Gazzola et al. R. Soc. 2018 section 3.4.1 """ import numpy as np import elastica as ea -from examples.HelicalBucklingCase.helicalbuckling_postprocessing import ( + +from helicalbuckling_postprocessing import ( analytical_solution, envelope, plot_helicalbuckling, ) -from examples.convergence_functions import plot_convergence, calculate_error_norm +from convergence_functions import calculate_error_norm, plot_convergence class HelicalBucklingSimulator( diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction_case.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction_case.py index c8dfe251..85d502fe 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction_case.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction_case.py @@ -1,5 +1,5 @@ if __name__ == "__main__": - from examples.RigidBodyCases.RodRigidBodyContact.rod_cylinder_contact_friction import ( + from rod_cylinder_contact_friction import ( rod_cylinder_contact_friction_case, ) diff --git a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction_phase_space.py b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction_phase_space.py index 7b7ae1d1..f6ff5971 100644 --- a/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction_phase_space.py +++ b/examples/RigidBodyCases/RodRigidBodyContact/rod_cylinder_contact_friction_phase_space.py @@ -1,9 +1,9 @@ if __name__ == "__main__": import multiprocessing as mp - from examples.RigidBodyCases.RodRigidBodyContact.rod_cylinder_contact_friction import ( + from rod_cylinder_contact_friction import ( rod_cylinder_contact_friction_case, ) - from examples.RigidBodyCases.RodRigidBodyContact.post_processing import ( + from post_processing import ( plot_force_vs_energy, ) diff --git a/examples/TimoshenkoBeamCase/convergence_timoshenko.py b/examples/TimoshenkoBeamCase/convergence_timoshenko.py index f95f9f1d..5b06f9ec 100644 --- a/examples/TimoshenkoBeamCase/convergence_timoshenko.py +++ b/examples/TimoshenkoBeamCase/convergence_timoshenko.py @@ -3,11 +3,12 @@ import numpy as np import elastica as ea -from examples.TimoshenkoBeamCase.timoshenko_postprocessing import ( + +from timoshenko_postprocessing import ( plot_timoshenko, analytical_shearable, ) -from examples.convergence_functions import calculate_error_norm, plot_convergence +from convergence_functions import calculate_error_norm, plot_convergence class TimoshenkoBeamSimulator( From 0a3828548a9687195f4e684163008ed5c8f6ac89 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 12 Dec 2025 12:00:42 -0600 Subject: [PATCH 47/85] fix: correct rod position indexing in video plotting and update shear modulus calculation in helical buckling example --- examples/CatenaryCase/post_processing.py | 8 ++++---- .../HelicalBucklingCase/convergence_helicalbuckling.py | 9 ++++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/examples/CatenaryCase/post_processing.py b/examples/CatenaryCase/post_processing.py index 402764a6..45da2dfd 100644 --- a/examples/CatenaryCase/post_processing.py +++ b/examples/CatenaryCase/post_processing.py @@ -28,10 +28,10 @@ def plot_video( ax.set_ylabel("y [m]", fontsize=16) # plt.axis("equal") with writer.saving(fig, video_name, dpi=150): - rod_lines_2d = ax.plot(positions_over_time[0][2], positions_over_time[0][0])[0] - for time in tqdm(range(1, len(plot_params["time"]))): - rod_lines_2d.set_xdata([positions_over_time[time][0]]) - rod_lines_2d.set_ydata([positions_over_time[time][2]]) + rod_lines_2d = ax.plot(positions_over_time[0][0], positions_over_time[0][2])[0] + for time_idx in tqdm(range(1, len(plot_params["time"]))): + rod_lines_2d.set_xdata(positions_over_time[time_idx][0]) + rod_lines_2d.set_ydata(positions_over_time[time_idx][2]) writer.grab_frame() # Be a good boy and close figures diff --git a/examples/HelicalBucklingCase/convergence_helicalbuckling.py b/examples/HelicalBucklingCase/convergence_helicalbuckling.py index 608b642e..a551e3ba 100644 --- a/examples/HelicalBucklingCase/convergence_helicalbuckling.py +++ b/examples/HelicalBucklingCase/convergence_helicalbuckling.py @@ -42,9 +42,12 @@ def simulate_helicalbuckling_beam_with( E = 1e6 slack = 3 number_of_rotations = 27 - # For shear modulus of 1e4, nu is 99! - poisson_ratio = 99 - shear_matrix = np.repeat(1e5 * np.identity((3))[:, :, np.newaxis], n_elem, axis=2) + # For shear modulus of 1e5, poisson_ratio should be 9 + poisson_ratio = 9 + shear_modulus = E / (poisson_ratio + 1.0) + shear_matrix = np.repeat( + shear_modulus * np.identity((3))[:, :, np.newaxis], n_elem, axis=2 + ) temp_bend_matrix = np.zeros((3, 3)) np.fill_diagonal(temp_bend_matrix, [1.345, 1.345, 0.789]) bend_matrix = np.repeat(temp_bend_matrix[:, :, np.newaxis], n_elem - 1, axis=2) From 9d81c4940b89564a74353f79de6124bb9b07d8e7 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 12 Dec 2025 12:00:51 -0600 Subject: [PATCH 48/85] fix: update time indexing in video plotting functions for consistency across examples --- .../joint_cases_postprocessing.py | 68 ++++++++++--------- .../JointCases/joint_cases_postprocessing.py | 36 ++++++---- examples/KnotCase/knot_visualization.py | 24 +++---- 3 files changed, 70 insertions(+), 58 deletions(-) diff --git a/examples/ExperimentalCases/GenericSystemConnectionCases/joint_cases_postprocessing.py b/examples/ExperimentalCases/GenericSystemConnectionCases/joint_cases_postprocessing.py index 5b49be8c..71c68c93 100644 --- a/examples/ExperimentalCases/GenericSystemConnectionCases/joint_cases_postprocessing.py +++ b/examples/ExperimentalCases/GenericSystemConnectionCases/joint_cases_postprocessing.py @@ -84,7 +84,7 @@ def plot_video( ): # (time step, x/y/z, node) import matplotlib.animation as manimation - time = plot_params_rod1["time"] + time_list = plot_params_rod1["time"] position_of_rod1 = np.array(plot_params_rod1["position"]) position_of_rod2 = np.array(plot_params_rod2["position"]) position_of_cylinder = ( @@ -104,31 +104,31 @@ def plot_video( writer = FFMpegWriter(fps=fps, metadata=metadata) fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150) with writer.saving(fig, video_name, 100): - for time in range(1, len(time)): + for time_idx in range(1, len(time_list)): fig.clf() ax = plt.axes(projection="3d") # fig.add_subplot(111) ax.grid(which="minor", color="k", linestyle="--") ax.grid(which="major", color="k", linestyle="-") ax.plot( - position_of_rod1[time, 0], - position_of_rod1[time, 1], - position_of_rod1[time, 2], + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 1], + position_of_rod1[time_idx, 2], "or", label="rod1", ) ax.plot( - position_of_rod2[time, 0], - position_of_rod2[time, 1], - position_of_rod2[time, 2], + position_of_rod2[time_idx, 0], + position_of_rod2[time_idx, 1], + position_of_rod2[time_idx, 2], "o", c=to_rgb("xkcd:bluish"), label="rod2", ) if position_of_cylinder is not None: ax.plot( - position_of_cylinder[time, 0], - position_of_cylinder[time, 1], - position_of_cylinder[time, 2], + position_of_cylinder[time_idx, 0], + position_of_cylinder[time_idx, 1], + position_of_cylinder[time_idx, 2], "o", c=to_rgb("xkcd:greenish"), label="Cylinder CoM", @@ -140,7 +140,7 @@ def plot_video( stop=cylinder.length.squeeze() / 2, num=cylinder_axis_points.shape[1], ) - cylinder_director = director_of_cylinder[time, ...] + cylinder_director = director_of_cylinder[time_idx, ...] cylinder_director_batched = np.repeat( cylinder_director, repeats=cylinder_axis_points.shape[1], axis=2 ) @@ -150,7 +150,7 @@ def plot_video( cylinder_axis_points, ) # add offset position of CoM - cylinder_axis_points += position_of_cylinder[time, ...] + cylinder_axis_points += position_of_cylinder[time_idx, ...] ax.plot( cylinder_axis_points[0, :], cylinder_axis_points[1, :], @@ -178,7 +178,7 @@ def plot_video_xy( ): # (time step, x/y/z, node) import matplotlib.animation as manimation - time = plot_params_rod1["time"] + time_list = plot_params_rod1["time"] position_of_rod1 = np.array(plot_params_rod1["position"]) position_of_rod2 = np.array(plot_params_rod2["position"]) position_of_cylinder = ( @@ -199,22 +199,25 @@ def plot_video_xy( fig = plt.figure() plt.axis("equal") with writer.saving(fig, video_name, 100): - for time in range(1, len(time)): + for time_idx in range(1, len(time_list)): fig.clf() plt.plot( - position_of_rod1[time, 0], position_of_rod1[time, 1], "or", label="rod1" + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 1], + "or", + label="rod1", ) plt.plot( - position_of_rod2[time, 0], - position_of_rod2[time, 1], + position_of_rod2[time_idx, 0], + position_of_rod2[time_idx, 1], "o", c=to_rgb("xkcd:bluish"), label="rod2", ) if position_of_cylinder is not None: plt.plot( - position_of_cylinder[time, 0], - position_of_cylinder[time, 1], + position_of_cylinder[time_idx, 0], + position_of_cylinder[time_idx, 1], "o", c=to_rgb("xkcd:greenish"), label="cylinder", @@ -226,7 +229,7 @@ def plot_video_xy( stop=cylinder.length.squeeze() / 2, num=cylinder_axis_points.shape[1], ) - cylinder_director = director_of_cylinder[time, ...] + cylinder_director = director_of_cylinder[time_idx, ...] cylinder_director_batched = np.repeat( cylinder_director, repeats=cylinder_axis_points.shape[1], axis=2 ) @@ -236,7 +239,7 @@ def plot_video_xy( cylinder_axis_points, ) # add offset position of CoM - cylinder_axis_points += position_of_cylinder[time, ...] + cylinder_axis_points += position_of_cylinder[time_idx, ...] plt.plot( cylinder_axis_points[0, :], cylinder_axis_points[1, :], @@ -261,7 +264,7 @@ def plot_video_xz( ): # (time step, x/y/z, node) import matplotlib.animation as manimation - time = plot_params_rod1["time"] + time_list = plot_params_rod1["time"] position_of_rod1 = np.array(plot_params_rod1["position"]) position_of_rod2 = np.array(plot_params_rod2["position"]) position_of_cylinder = ( @@ -282,22 +285,25 @@ def plot_video_xz( fig = plt.figure() plt.axis("equal") with writer.saving(fig, video_name, 100): - for time in range(1, len(time)): + for time_idx in range(1, len(time_list)): fig.clf() plt.plot( - position_of_rod1[time, 0], position_of_rod1[time, 2], "or", label="rod1" + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 2], + "or", + label="rod1", ) plt.plot( - position_of_rod2[time, 0], - position_of_rod2[time, 2], + position_of_rod2[time_idx, 0], + position_of_rod2[time_idx, 2], "o", c=to_rgb("xkcd:bluish"), label="rod2", ) if position_of_cylinder is not None: plt.plot( - position_of_cylinder[time, 0], - position_of_cylinder[time, 2], + position_of_cylinder[time_idx, 0], + position_of_cylinder[time_idx, 2], "o", c=to_rgb("xkcd:greenish"), label="cylinder", @@ -309,7 +315,7 @@ def plot_video_xz( stop=cylinder.length.squeeze() / 2, num=cylinder_axis_points.shape[1], ) - cylinder_director = director_of_cylinder[time, ...] + cylinder_director = director_of_cylinder[time_idx, ...] cylinder_director_batched = np.repeat( cylinder_director, repeats=cylinder_axis_points.shape[1], axis=2 ) @@ -319,7 +325,7 @@ def plot_video_xz( cylinder_axis_points, ) # add offset position of CoM - cylinder_axis_points += position_of_cylinder[time, ...] + cylinder_axis_points += position_of_cylinder[time_idx, ...] plt.plot( cylinder_axis_points[0, :], cylinder_axis_points[2, :], diff --git a/examples/JointCases/joint_cases_postprocessing.py b/examples/JointCases/joint_cases_postprocessing.py index 8cf15bf0..9005fbf1 100644 --- a/examples/JointCases/joint_cases_postprocessing.py +++ b/examples/JointCases/joint_cases_postprocessing.py @@ -73,22 +73,22 @@ def plot_video( writer = FFMpegWriter(fps=fps, metadata=metadata) fig = plt.figure(figsize=(10, 8), frameon=True, dpi=150) with writer.saving(fig, video_name, 100): - for time in range(1, len(time)): + for time_idx in range(1, len(time)): fig.clf() ax = plt.axes(projection="3d") # fig.add_subplot(111) ax.grid(which="minor", color="k", linestyle="--") ax.grid(which="major", color="k", linestyle="-") ax.plot( - position_of_rod1[time, 0], - position_of_rod1[time, 1], - position_of_rod1[time, 2], + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 1], + position_of_rod1[time_idx, 2], "or", label="rod1", ) ax.plot( - position_of_rod2[time, 0], - position_of_rod2[time, 1], - position_of_rod2[time, 2], + position_of_rod2[time_idx, 0], + position_of_rod2[time_idx, 1], + position_of_rod2[time_idx, 2], "o", c=to_rgb("xkcd:bluish"), label="rod2", @@ -120,14 +120,17 @@ def plot_video_xy( fig = plt.figure() plt.axis("equal") with writer.saving(fig, video_name, 100): - for time in range(1, len(time)): + for time_idx in range(1, len(time)): fig.clf() plt.plot( - position_of_rod1[time, 0], position_of_rod1[time, 1], "or", label="rod1" + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 1], + "or", + label="rod1", ) plt.plot( - position_of_rod2[time, 0], - position_of_rod2[time, 1], + position_of_rod2[time_idx, 0], + position_of_rod2[time_idx, 1], "o", c=to_rgb("xkcd:bluish"), label="rod2", @@ -158,14 +161,17 @@ def plot_video_xz( fig = plt.figure() plt.axis("equal") with writer.saving(fig, video_name, 100): - for time in range(1, len(time)): + for time_idx in range(1, len(time)): fig.clf() plt.plot( - position_of_rod1[time, 0], position_of_rod1[time, 2], "or", label="rod1" + position_of_rod1[time_idx, 0], + position_of_rod1[time_idx, 2], + "or", + label="rod1", ) plt.plot( - position_of_rod2[time, 0], - position_of_rod2[time, 2], + position_of_rod2[time_idx, 0], + position_of_rod2[time_idx, 2], "o", c=to_rgb("xkcd:bluish"), label="rod2", diff --git a/examples/KnotCase/knot_visualization.py b/examples/KnotCase/knot_visualization.py index 05c6c42f..17a92f61 100644 --- a/examples/KnotCase/knot_visualization.py +++ b/examples/KnotCase/knot_visualization.py @@ -61,39 +61,39 @@ def plot_video3D( length=quiver_length * 0.8, ) with writer.saving(fig, video_name, dpi=100): - for time in range(1, len(t) - 1): - rod_lines_3d.set_xdata(positions_over_time[time][0]) - rod_lines_3d.set_ydata(positions_over_time[time][1]) - rod_lines_3d.set_3d_properties(positions_over_time[time][2]) # type: ignore + for time_idx in range(1, len(t) - 1): + rod_lines_3d.set_xdata(positions_over_time[time_idx][0]) + rod_lines_3d.set_ydata(positions_over_time[time_idx][1]) + rod_lines_3d.set_3d_properties(positions_over_time[time_idx][2]) # type: ignore targets_orientation_normal.remove() targets_orientation_normal = ax.quiver( - *base_position[time], - *base_orientation[time][0], + *base_position[time_idx], + *base_orientation[time_idx][0], color="r", length=quiver_length, ) targets_orientation_binormal.remove() targets_orientation_binormal = ax.quiver( - *base_position[time], - *base_orientation[time][1], + *base_position[time_idx], + *base_orientation[time_idx][1], color="g", length=quiver_length, ) targets_orientation_tangent.remove() targets_orientation_tangent = ax.quiver( - *base_position[time], - *base_orientation[time][2], + *base_position[time_idx], + *base_orientation[time_idx][2], color="b", length=quiver_length, ) normal.remove() normal = ax.quiver( - *elem_positions[time], - *directors_over_time[time][1], + *elem_positions[time_idx], + *directors_over_time[time_idx][1], color="k", alpha=0.5, length=quiver_length * 0.8, From 8a489a457f9c57daa36d3d0e23eb0585b8af0fbb Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 12 Dec 2025 12:00:59 -0600 Subject: [PATCH 49/85] fix: correct typos and improve clarity in README examples --- examples/README.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/examples/README.md b/examples/README.md index 97d2527a..a5c0893f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,6 @@ # PyElastica Examples -This directory contains number of examples of elastica. +This directory contains a number of examples of elastica. Each [example cases](#example-cases) are stored in separate subdirectories, containing case descriptions, run file, and all other data/script necessary to run. More [advanced cases](#advanced-cases) are stored in separate repository with its description. @@ -14,7 +14,7 @@ Examples can serve as a starting template for customized usages. * __Features__: CosseratRod, OneEndFixedRod, EndpointForces * [TimoshenkoBeamCase](./TimoshenkoBeamCase) * __Purpose__: Physical convergence test of simple Timoshenko beam. - * __Features__: CosseratRod, OneEndFixedRod, EndpointForces, + * __Features__: CosseratRod, OneEndFixedRod, EndpointForces * [FlexibleSwingingPendulumCase](./FlexibleSwingingPendulumCase) * __Purpose__: Physical convergence test of simple pendulum with flexible rod. * __Features__: CosseratRod, HingeBC, GravityForces @@ -31,13 +31,13 @@ Examples can serve as a starting template for customized usages. * __Purpose__: Demonstrate simple restoration with initial strain. * __Features__: CosseratRod * [CantileverDistributedLoad](./CantileverDistributedLoad) - * __Purpose__: Demonstrate the demformation of a straight cantilever under both conservative (like water pressure) and non-conservative (like gravity) distributed load, compared with numerical solutions from Tschisgale, Silvio (2019).["Chapter 3: Numerical models of partitioned problems"](https://nbn-resolving.org/urn:nbn:de:bsz:14-qucosa2-387063) Technische Univerisitat Dresden Institution of Fluid Mechanics + * __Purpose__: Demonstrate the deformation of a straight cantilever under both conservative (like water pressure) and non-conservative (like gravity) distributed load, compared with numerical solutions from Tschisgale, Silvio (2019).["Chapter 3: Numerical models of partitioned problems"](https://nbn-resolving.org/urn:nbn:de:bsz:14-qucosa2-387063) Technische Univerisitat Dresden Institution of Fluid Mechanics * __Features__: CosseratRod * [CantileverTransversalLoadCase](./CantileverTransversalLoadCase) - * __Purpose__: Demonstrate the demformation of a curved cantilever under transversal one-end load, and also do Physical convergence test, compared with numerical solutions from Tschisgale, Silvio (2019). + * __Purpose__: Demonstrate the deformation of a curved cantilever under transversal one-end load, and also do Physical convergence test, compared with numerical solutions from Tschisgale, Silvio (2019). * __Features__: CosseratRod * [TumblingUnconstrainedRod](./TumblingUnconstrainedRod) - * __Purpose__: Demostrate the dynamics of tumbling uncontrained rod, compared with analytical solution from [Hisao, Kou Hou (1998).](https://www.sciencedirect.com/science/article/pii/S0045782598001522), Computer methods in applied mechanics and engineering. + * __Purpose__: Demonstrate the dynamics of tumbling unconstrained rod, compared with analytical solution from [Hisao, Kou Hou (1998).](https://www.sciencedirect.com/science/article/pii/S0045782598001522), Computer methods in applied mechanics and engineering. * __Features__: CosseratRod * [FrictionValidationCases](./FrictionValidationCases) * __Purpose__: Physical validation of rolling and translational friction. @@ -67,10 +67,10 @@ Examples can serve as a starting template for customized usages. * [RodSelfContact](./RodContactCase/RodSelfContact) * [PlectonemesCase](./RodContactCase/RodSelfContact/PlectonemesCase) * __Purpose__: Demonstrates rod self contact with Plectoneme example, and how to use link-writhe-twist after simulation completed. - * __Features__: CosseratRod, SelonoidsBC, RodSelfContact, Link-Writhe-Twist + * __Features__: CosseratRod, SolenoidsBC, RodSelfContact, Link-Writhe-Twist * [SolenoidsCase](./RodContactCase/RodSelfContact/SolenoidsCase) * __Purpose__: Demonstrates rod self contact with Solenoid example, and how to use link-writhe-twist after simulation completed. - * __Features__: CosseratRod, SelonoidsBC, RodSelfContact, Link-Writhe-Twist + * __Features__: CosseratRod, SolenoidsBC, RodSelfContact, Link-Writhe-Twist * [BoundaryConditionsCases](./BoundaryConditionsCases) * __Purpose__: Demonstrate the usage of boundary conditions for constraining the movement of the system. * __Features__: GeneralConstraint, CosseratRod @@ -103,3 +103,6 @@ Examples can serve as a starting template for customized usages. * [ParallelConnectionExample](./ExperimentalCases/ParallelConnectionExample) * __Purpose__: Demonstrate the usage of parallel connection. * __Features__: connect two parallel rods +* [GenericSystemConnectionCases](./ExperimentalCases/GenericSystemConnectionCases) + * __Purpose__: Demonstrate the usage of generic system type connections for connecting different system types (rods and rigid bodies). + * __Features__: GenericSystemTypeFixedJoint, GenericSystemTypeFreeJoint, CosseratRod, Cylinder From d2545816e698f8654585d173985b93e668f4a391 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 12 Dec 2025 12:25:49 -0600 Subject: [PATCH 50/85] refactor: update type variable binding for system in ConstraintBase class --- elastica/boundary_conditions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/elastica/boundary_conditions.py b/elastica/boundary_conditions.py index 03bc090c..4a35343c 100644 --- a/elastica/boundary_conditions.py +++ b/elastica/boundary_conditions.py @@ -14,7 +14,7 @@ from elastica.typing import SystemType, RodType, RigidBodyType, ConstrainingIndex -S = TypeVar("S") +S = TypeVar("S", bound=SystemType) class ConstraintBase(ABC, Generic[S]): @@ -40,7 +40,7 @@ class ConstraintBase(ABC, Generic[S]): def __init__( self, *args: Any, - _system: "RodType | RigidBodyType", + _system: S, constrained_position_idx: ConstrainingIndex = (), constrained_director_idx: ConstrainingIndex = (), **kwargs: Any, From 69a374d88322c3ad29eecf206620a8b9fbc6b575 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 12 Dec 2025 13:20:04 -0600 Subject: [PATCH 51/85] fix: correct typos and improve clarity in documentation across multiple files --- docs/advanced/PackageDesign.md | 6 +- docs/api/connections.rst | 2 +- docs/api/contact.rst | 2 +- docs/archive/NCSA-NVIDIA-AI-Hackathon-2020.md | 67 +++++++++---------- docs/guide/discretization.md | 10 +-- docs/guide/workflow.md | 22 +++--- docs/index.rst | 4 +- docs/overview/installation.md | 2 +- 8 files changed, 56 insertions(+), 59 deletions(-) diff --git a/docs/advanced/PackageDesign.md b/docs/advanced/PackageDesign.md index 302577c8..b56e8bd5 100644 --- a/docs/advanced/PackageDesign.md +++ b/docs/advanced/PackageDesign.md @@ -46,14 +46,14 @@ Elastica package uses [structural subtyping](https://peps.python.org/pep-0544/) style StP text-align:left SymplecticStepperProtocol["SymplecticStepperProtocol\n• PositionVerlet"] style SymplecticStepperProtocol text-align:left - ExpplicitStepperProtocol["ExpplicitStepperProtocol\n(Unused)"] + ExplicitStepperProtocol["ExplicitStepperProtocol\n(Unused)"] end subgraph SystemCollection end SymST --> SystemCollection --> SymplecticStepperProtocol - ExpST --> SystemCollection --> ExpplicitStepperProtocol + ExpST --> SystemCollection --> ExplicitStepperProtocol StaticSystemType --> SystemCollection ``` @@ -104,7 +104,7 @@ Elastica package uses [structural subtyping](https://peps.python.org/pep-0544/) Contact -->|detect_contact_between| Synchronize Connection -->|connect| Synchronize Damping -->|dampen| ConstrainRates - Callback -->|collect_diagnosis| CallbackGroup + Callback -->|collect_diagnostics| CallbackGroup end end Sys --> StSys --> Feature diff --git a/docs/api/connections.rst b/docs/api/connections.rst index b616b4ca..0fec1ea3 100644 --- a/docs/api/connections.rst +++ b/docs/api/connections.rst @@ -21,7 +21,7 @@ Compatibility ~~~~~~~~~~~~~ =============================== ==== =========== -Connection / Joints Rod Rigid Body +Connection / Joints Rod Rigid Body =============================== ==== =========== FreeJoint ✅ ❌ FixedJoint ✅ ❌ diff --git a/docs/api/contact.rst b/docs/api/contact.rst index 039fc8e6..a795601e 100644 --- a/docs/api/contact.rst +++ b/docs/api/contact.rst @@ -11,7 +11,7 @@ Description .. note:: (CAUTION) The contact is recommended to be added at last. This is because contact forces often includes - friction that may depend on other normal forces and contraints to be calculated accurately. + friction that may depend on other normal forces and constraints to be calculated accurately. Be careful on the order of adding interactions. .. rubric:: Available Contact Classes diff --git a/docs/archive/NCSA-NVIDIA-AI-Hackathon-2020.md b/docs/archive/NCSA-NVIDIA-AI-Hackathon-2020.md index 3cb9928e..6908866f 100644 --- a/docs/archive/NCSA-NVIDIA-AI-Hackathon-2020.md +++ b/docs/archive/NCSA-NVIDIA-AI-Hackathon-2020.md @@ -6,46 +6,46 @@ The objective is to train a model to move a (cyber)-octopus with two soft arms and a head to reach a target location, and then grab an object. The octopus is modeled as an assembly of Cosserat rods and is activated by muscles surrounding its arms. Input to the mechanical model is the activation signals to the surrounding muscles, which causes it to contract, thus moving the arms. The output of the model comes from the octopus' environment. The mechanical model will be provided both for the octopus and its interaction with its environment. The goal is to find the correct muscle activation signals that make the octopus crawl to reach the target location and then make one arm to grab the object. ## Progression of specific goals -These goals build on each other, you need to successfully accomplish all prior goals to get credit for later goals. +These goals build on each other, you need to successfully accomplish all prior goals to get credit for later goals. 1) Make octopus crawl towards some direction. (5 points) -2) Make your octopus crawl to the target location. (7.5 points) +2) Make your octopus crawl to the target location. (7.5 points) 3) Make octopus to move the object using its arms. (7.5 points) -4) Have your octopus grab the object by wrapping one arm around the object. (10 points) +4) Have your octopus grab the object by wrapping one arm around the object. (10 points) 5) Make your octopus return to its starting location with the object. (20 points) -6) Generalize your policy to perform these tasks for an arbitrarily located object. (50 points) +6) Generalize your policy to perform these tasks for an arbitrarily located object. (50 points) ## Problem Context -Octopuses have flexible limbs made up of muscles with no internal bone structure. These limbs, know as muscular hydrostats, have an almost infinite number of degrees of freedom, allowing an octopus to perform complex actions with its arms, but also making them difficult to mathematically model. Attempts to model octopus arms are motivated not only by a desire to understand them biologically, but also to adapt their control ability and decision making processes to the rapidly developing field of soft robotics. We have developed a simulation package Elastica that models flexible 1-d rods, which can be used to represent octopus arms as a long, slender rod. We now want to learn methods for controlling these arms. +Octopuses have flexible limbs made up of muscles with no internal bone structure. These limbs, know as muscular hydrostats, have an almost infinite number of degrees of freedom, allowing an octopus to perform complex actions with its arms, but also making them difficult to mathematically model. Attempts to model octopus arms are motivated not only by a desire to understand them biologically, but also to adapt their control ability and decision making processes to the rapidly developing field of soft robotics. We have developed a simulation package Elastica that models flexible 1-d rods, which can be used to represent octopus arms as a long, slender rod. We now want to learn methods for controlling these arms. -You are being provided with a model of an octopus that consists of two arms connected by a head. Each arm can be controlled independently. These arms are actuated through the contraction of muscles in the arms. This muscle activation produces a torque profile along the arm, resulting in movement of the arm. The arms interact with the ground through friction. Your goal is to teach the octopus to crawl towards an object, grab it, and bring it back to where the octopus started. +You are being provided with a model of an octopus that consists of two arms connected by a head. Each arm can be controlled independently. These arms are actuated through the contraction of muscles in the arms. This muscle activation produces a torque profile along the arm, resulting in movement of the arm. The arms interact with the ground through friction. Your goal is to teach the octopus to crawl towards an object, grab it, and bring it back to where the octopus started. ## Controlling octopus arms with hierarchical basis functions -For this problem, we abstract the activation of the octopus muscles to the generation of a torque profile defined by the activation of a set of hierarchical radial basis function. Here we are using Gaussian basis functions. +For this problem, we abstract the activation of the octopus muscles to the generation of a torque profile defined by the activation of a set of hierarchical radial basis function. Here we are using Gaussian basis functions. image name image name -There are three levels of these basis functions, with 1 basis function in the first level, 2 in the second level and 4 in the third, leading to 7 basis functions in set. These levels have different maximum levels of activation. The lower levels have larger magnitudes than the higher levels, meaning they represent bulk motion of the rod while the higher levels allow finer control of the rod along the interval. In the code, the magnitude of each level will be fixed but you can choose the amount of activation at each level by setting the activation level between -1 and 1. +There are three levels of these basis functions, with 1 basis function in the first level, 2 in the second level and 4 in the third, leading to 7 basis functions in set. These levels have different maximum levels of activation. The lower levels have larger magnitudes than the higher levels, meaning they represent bulk motion of the rod while the higher levels allow finer control of the rod along the interval. In the code, the magnitude of each level will be fixed but you can choose the amount of activation at each level by setting the activation level between -1 and 1. -There are two bending modes (in the normal and binormal directions) and a twisting mode (in the tangent direction), so we define torques in these three different directions and independently for each arm. This yields six different sets of basis functions that can be activated for a total of 42 inputs. +There are two bending modes (in the normal and binormal directions) and a twisting mode (in the tangent direction), so we define torques in these three different directions and independently for each arm. This yields six different sets of basis functions that can be activated for a total of 42 inputs. ## Overview of provided Elastica code -We are providing you the Elastica software package which is written in Python. Elastica simulates the dynamics and kinematics of 1-d slender rods. We have set up the model for you such that you do not need to worry about the details of the model, only the activation patterns of the muscle. -In the provided `examples/ArmWithBasisFunctions/two_arm_octopus_ai_imp.py` file you will import the `Environment` class which will define and setup the simulation. +We are providing you the Elastica software package which is written in Python. Elastica simulates the dynamics and kinematics of 1-d slender rods. We have set up the model for you such that you do not need to worry about the details of the model, only the activation patterns of the muscle. +In the provided `examples/ArmWithBasisFunctions/two_arm_octopus_ai_imp.py` file you will import the `Environment` class which will define and setup the simulation. -`Environment` has three relevant functions: -* `Environment.reset(self)`: setups and initializes the simulation environment. Call this prior to running any simulations. -* `Environment.step(self, activation_array_list, time)`: takes one timestep for muscle activations defined in `activation_array_list`. -* `Environment.post_processing(self, filename_video)`: Makes 3D video based on saved data from simulation. Requires `ffmpeg`. -We do not suggest changing `Environment` as it may cause unintended consequences to the simulation. +`Environment` has three relevant functions: +* `Environment.reset(self)`: setups and initializes the simulation environment. Call this prior to running any simulations. +* `Environment.step(self, activation_array_list, time)`: takes one timestep for muscle activations defined in `activation_array_list`. +* `Environment.post_processing(self, filename_video)`: Makes 3D video based on saved data from simulation. Requires `ffmpeg`. +We do not suggest changing `Environment` as it may cause unintended consequences to the simulation. You will want to work within `main()` to interface with the simulations and develop your learning model. In `main()`, the first thing you need to define is the length of your simulation and initialize the environment. `final_time` is the length of time that your simulation will run unless exited early. You want to give your octopus enough time to complete the task, but too much time will lead to excessively long simulation times. -```python +```python # Set simulation final time final_time = 10.0 @@ -55,22 +55,22 @@ You will want to work within `main()` to interface with the simulations and deve total_steps, systems = env.reset() ``` -With your system initialized, you are now ready to perform the simulation. To perform the simulation there are two steps: +With your system initialized, you are now ready to perform the simulation. To perform the simulation there are two steps: 1) Evaluate the reward function and define the basis function activations -2) Perform time step +2) Perform time step -There is also a user defined stopping condition. When met, this will immediately end the simulation. This can be useful to end the simulation if the octopus successfully complete the task early, or has a sufficiently low reward function that there is no point continuing the simulation. +There is also a user defined stopping condition. When met, this will immediately end the simulation. This can be useful to end the simulation if the octopus successfully complete the task early, or has a sufficiently low reward function that there is no point continuing the simulation. ```python for i_sim in tqdm(range(total_steps)): """ Learning loop """ if i_sim % 200: """ Add your learning algorithm here to define activation """ - # This will be based on your observations of the system and - # evaluation of your reward function. + # This will be based on your observations of the system and + # evaluation of your reward function. shearable_rod = systems[0] - rigid_body = systems[1] - reward = reward_function() + rigid_body = systems[1] + reward = reward_function() activation = segment_activation_function() """ Perform time step """ @@ -85,7 +85,7 @@ There is also a user defined stopping condition. When met, this will immediately The state of the octopus is available in `shearable_rod`. The octopus consists of a series of 121 nodes. Nodes 0-49 relate to one arm, nodes 50-70 relate to the head, and nodes 71-120 relate to the second arm. `shearable_rod.position_collection` returns an array with entries relating to the position of each node. The state of the target object is available in `rigid_body`. -It is important to properly define the activation function. It consists of a list of lists defining the activation of the two arms in each of the the three modes of deformation. The activation function should be a list with three entries for the three modes of deformation. Each of these entries is in turn a list with two entries, which are arrays of the basis function activations for the two arms. +It is important to properly define the activation function. It consists of a list of lists defining the activation of the two arms in each of the the three modes of deformation. The activation function should be a list with three entries for the three modes of deformation. Each of these entries is in turn a list with two entries, which are arrays of the basis function activations for the two arms. ```python activation = [ @@ -97,22 +97,17 @@ It is important to properly define the activation function. It consists of a lis Each activation array has 7 entries that relate to the activation of different basis functions. The ordering goes from the top level to the bottom level of the hierarchy. Each entry can vary from -1 to 1. -`activation_array[0] ` -- One top level muscle segment -`activation_array[1:3]` -- Two mid level muscle segment -`activation_array[3:7]` -- Four bottom level muscle segment +`activation_array[0] ` -- One top level muscle segment +`activation_array[1:3]` -- Two mid level muscle segment +`activation_array[3:7]` -- Four bottom level muscle segment + - ## A few practical notes -1) To save a video of the octopus with `Environment.post_processing()`, you need to install `ffmeg`. You can download and install it [here](https://www.ffmpeg.org/). +1) To save a video of the octopus with `Environment.post_processing()`, you need to install `ffmpeg`. You can download and install it [here](https://www.ffmpeg.org/). 2) The timestep size is set to 40 μs. This is necessary to keep the simulation stable, however, you may not need to update your muscle activations that often. Varying the learning time step will change how often your octopus updates its behaviour. -3) There is a 15-20 second startup delay while the simulation is initialized. This is a one time cost whenever the Python script is run and resetting the simulation using `.rest()` does not incur this delay for subsequent simulations. +3) There is a 15-20 second startup delay while the simulation is initialized. This is a one time cost whenever the Python script is run and resetting the simulation using `.reset()` does not incur this delay for subsequent simulations. 4) We suggest installing `requirements.txt` and `optional-requirements.txt`, to run Elastica without any problem. - - - - - diff --git a/docs/guide/discretization.md b/docs/guide/discretization.md index 851ba5c7..5d9a5624 100644 --- a/docs/guide/discretization.md +++ b/docs/guide/discretization.md @@ -1,23 +1,23 @@ # Discretization -To help get you started building initial intuition about PyElastica, here are some general rules of thumb to follow. +To help get you started building initial intuition about PyElastica, here are some general rules of thumb to follow. :::{important} These are based on general observations of how simulations tend to behave and are not guaranteed to always hold. Particularly for choosing dx and dt, it is important to perform a separate convergence study for your specific case. ::: ## Number of elements per rod -Generally, the more flexible your rod, the more elements you need. It is important to always perform a convergence test for your simulation, however, 30-50 elements per rod is a good starting point. +Generally, the more flexible your rod, the more elements you need. It is important to always perform a convergence test for your simulation, however, 30-50 elements per rod is a good starting point. ## Choosing your dx and dt -Generally you will set your dx and then choose a stable dt. Your dx will be a combination of your problems length scale and the number of elements you want. Recall that units can be rescaled as long as they are consistent. If you have have a small rod, selecting a dx on the order of nm without scaling is 1e-9. This small value can cause numerical issues, so it is better to rescale your units so that nm $\sim O(1)$. +Generally you will set your dx and then choose a stable dt. Your dx will be a combination of your problems length scale and the number of elements you want. Recall that units can be rescaled as long as they are consistent. If you have a small rod, selecting a dx on the order of nm without scaling is 1e-9. This small value can cause numerical issues, so it is better to rescale your units so that nm $\sim O(1)$. When choosing your time step, there are a number of different conditions that can affect your choice. The most important consideration is that the time stepping algorithm remain stable. As a useful heuristic, we have found that dt = 0.01 dx $s/m$ tends to yield stable time steps, but depending on your problem this may not hold. If you wish to be able to resolve the propagation of different waves, then you need to make sure your dt is able to capture their propagation ($dt = dx \sqrt{\rho/G}$ for shear waves or $dt = dx \sqrt{\rho/E}$ for flexural waves). ## Run time scaling -PyElastica will scale linearly with the number of time steps, so if you halve your time step, your simulation will take twice as long to finish. +PyElastica will scale linearly with the number of time steps, so if you halve your time step, your simulation will take twice as long to finish. -The algorithms that PyElastica is based on scale linearly with the number of elements. However, due to overhead from calling functions in Python, PyElastica does not currently have a strong dependence on the number of nodes. Doubling the number of nodes may only lead to a 10-20% increase in run time. While this means you can decrease your dx without a large run time penalty, remember that you also need to adjust your dt, which will affect the run time. +The algorithms that PyElastica is based on scale linearly with the number of elements. However, due to overhead from calling functions in Python, PyElastica does not currently have a strong dependence on the number of nodes. Doubling the number of nodes may only lead to a 10-20% increase in run time. While this means you can decrease your dx without a large run time penalty, remember that you also need to adjust your dt, which will affect the run time. Adding additional interactions with the environment, such as friction or gravity, will increase run time. Most of these interactions only have a small effect on run time except for rod collision and/or self-intersection. As implemented, these are expensive routines ($O(N^2)$) and should be avoided if possible as they will substantially lengthen your run time. diff --git a/docs/guide/workflow.md b/docs/guide/workflow.md index d1bf6a5d..ff75fb89 100644 --- a/docs/guide/workflow.md +++ b/docs/guide/workflow.md @@ -6,7 +6,7 @@ When using PyElastica, users will setup a simulation in which they define a syst **A note on notation:** Like other FEA packages such as Abaqus, PyElastica does not enforce units. This means that you are required to make sure that all units for your input variables are consistent. When in doubt, SI units are always safe, however, if you have a very small length scale ($\sim$ nm), then you may need to rescale your units to avoid needing prohibitively small time steps and/or roundoff errors. ::: -

1. Setup Simulation

+## 1. Setup Simulation ```python from elastica.modules import ( @@ -46,7 +46,7 @@ We adopted a composition and mixin design paradigm in building elastica. The det ::: -

2. Create Rods

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

3.a Define Boundary Conditions, Forcings, and Connections

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

3.b Define Damping

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

4. Add Callback Functions (optional)

+## 4. Add Callback Functions (optional) If you want to know what happens to the rod during the course of the simulation, you must collect data during the simulation. Here, we demonstrate how the callback function can be defined to export the data you need. There is a base class `CallBackBaseClass` that can help with this. @@ -246,7 +246,7 @@ simulator.collect_diagnostics(...).using( ``` ::: -

5. Finalize Simulator

+## 5. Finalize Simulator Now that we have finished defining our rods, the different boundary conditions and connections between them, and how often we want to save data, we have finished setting up the simulation. We now need to finalize the simulator by calling @@ -256,11 +256,13 @@ simulator.finalize() This goes through and collects all the rods and applied conditions, preparing the system for the simulation. -

6. Set Timestepper

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

7. Post Process

+## 7. Post Process Once the simulation ends, it is time to analyze the data. If you defined a callback function, the data you outputted in available there (i.e. `callback_data_rod1`), otherwise you can access the final configuration of your system through your rod objects. For example, if you want the final position of one of your rods, you can get it from `rod1.position_collection[:]`. diff --git a/docs/index.rst b/docs/index.rst index 27a20815..479320b1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,9 +18,9 @@ Community :alt: on gitter We mainly use `git-issue`_ to communicate the roadmap, updates, helps, and bug fixes. -If you have problem using PyElastica, check if similar issue is reported in `git-issue`_. +If you have a problem using PyElastica, check if similar issue is reported in `git-issue`_. -We also opened `gitter` channel for short and immediate feedbacks. +We also opened `gitter` channel for short and immediate feedback. Contributing diff --git a/docs/overview/installation.md b/docs/overview/installation.md index 3887aade..1c28afe1 100644 --- a/docs/overview/installation.md +++ b/docs/overview/installation.md @@ -1,6 +1,6 @@ # Installation -## Instruction +## Instructions PyElastica requires Python 3.10+, which needs to be installed prior to using PyElastica. For information on installing Python, see [here](https://realpython.com/installing-python/). If you are interested in using a package manager like Conda, see [here](https://docs.conda.io/projects/conda/en/latest/user-guide/getting-started.html). From 46406a3dff6a5bdbccbd61b884c11f82bb7cf227 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 12 Dec 2025 14:57:35 -0600 Subject: [PATCH 52/85] mod: connection joint now derive from ConnectionBase instead of FreeJoint --- elastica/__init__.py | 1 + elastica/joint.py | 108 ++++++++++++++++++++++++++++++++----------- 2 files changed, 82 insertions(+), 27 deletions(-) diff --git a/elastica/__init__.py b/elastica/__init__.py index 96825850..ce8d1cdd 100644 --- a/elastica/__init__.py +++ b/elastica/__init__.py @@ -30,6 +30,7 @@ SlenderBodyTheory, ) from elastica.joint import ( + ConnectionBase, FreeJoint, FixedJoint, HingeJoint, diff --git a/elastica/joint.py b/elastica/joint.py index b95aedeb..a9c3f9d7 100644 --- a/elastica/joint.py +++ b/elastica/joint.py @@ -1,6 +1,9 @@ __doc__ = """ Module containing joint classes to connect multiple rods together. """ __all__ = ["FreeJoint", "HingeJoint", "FixedJoint", "get_relative_rotation_two_systems"] +from typing import TypeVar, Generic +from abc import ABC, abstractmethod + from elastica._rotations import _inv_rotate from elastica.typing import SystemType, RodType, ConnectionIndex, RigidBodyType @@ -8,15 +11,80 @@ from numpy.typing import NDArray -class FreeJoint: +S = TypeVar("S", bound=SystemType) + + +class ConnectionBase(ABC, Generic[S]): + """ + This Connection base class is for all system-to-system connections. + Every operator for Connections must be derived from this class. + """ + + @abstractmethod + def apply_forces( + self, + system_one: "RodType | RigidBodyType", + index_one: ConnectionIndex, + system_two: "RodType | RigidBodyType", + index_two: ConnectionIndex, + time: np.float64 = np.float64(0.0), + ) -> None: + """ + Apply connection force to the connected objects. + + Parameters + ---------- + system_one : RodType | RigidBodyType + Rod or rigid-body object + index_one : ConnectionIndex + Index of first system for connection. + system_two : RodType | RigidBodyType + Rod or rigid-body object + index_two : ConnectionIndex + Index of second system for connection. + + Returns + ------- + + """ + + @abstractmethod + def apply_torques( + self, + system_one: "RodType | RigidBodyType", + index_one: ConnectionIndex, + system_two: "RodType | RigidBodyType", + index_two: ConnectionIndex, + time: np.float64 = np.float64(0.0), + ) -> None: + """ + Apply connection torques to the connected objects. + + Parameters + ---------- + system_one : RodType | RigidBodyType + Rod or rigid-body object + index_one : ConnectionIndex + Index of first system for connection + system_two : RodType | RigidBodyType + Rod or rigid-body object + index_two : ConnectionIndex + Index of second system for connection. + + Returns + ------- + + """ + + +class FreeJoint(ConnectionBase): """ - This free joint class is the base class for all joints. Free or spherical - joints constrains the relative movement between two nodes (chosen by the user) + Free or spherical joints constrains the relative movement between two nodes (chosen by the user) by applying restoring forces. For implementation details, refer to Zhang et al. Nature Communications (2019). Notes ----- - Every new joint class must be derived from the FreeJoint class. + Alias for BallJoint and SphericalJoint Attributes ---------- @@ -59,11 +127,11 @@ def apply_forces( system_one : RodType | RigidBodyType Rod or rigid-body object index_one : ConnectionIndex - Index of first rod for joint. + Index of first system for connection. system_two : RodType | RigidBodyType Rod or rigid-body object index_two : ConnectionIndex - Index of second rod for joint. + Index of second system for connection. Returns ------- @@ -85,8 +153,6 @@ def apply_forces( system_one.external_forces[..., index_one] += contact_force system_two.external_forces[..., index_two] -= contact_force - return - def apply_torques( self, system_one: "RodType | RigidBodyType", @@ -96,26 +162,13 @@ def apply_torques( time: np.float64 = np.float64(0.0), ) -> None: """ - Apply restoring joint torques to the connected rod objects. - In FreeJoint class, this routine simply passes. + """ - Parameters - ---------- - system_one : RodType | RigidBodyType - Rod or rigid-body object - index_one : ConnectionIndex - Index of first rod for joint. - system_two : RodType | RigidBodyType - Rod or rigid-body object - index_two : ConnectionIndex - Index of second rod for joint. - - Returns - ------- - """ - pass +# ALIAS +BallJoint = FreeJoint +SphericalJoint = FreeJoint class HingeJoint(FreeJoint): @@ -338,6 +391,7 @@ def apply_torques( system_two.external_torques[..., index_two] += system_two_director @ torque +# TODO: Remove this def get_relative_rotation_two_systems( system_one: "RodType | RigidBodyType", index_one: ConnectionIndex, @@ -373,11 +427,11 @@ def get_relative_rotation_two_systems( system_one : RodType | RigidBodyType Rod or rigid-body object index_one : ConnectionIndex - Index of first rod for joint. + Index of first system for connection. system_two : RodType | RigidBodyType Rod or rigid-body object index_two : ConnectionIndex - Index of second rod for joint. + Index of second system for connection. Returns ------- From 8e32bf207c7fbd74da7db427ba497f3b87572118 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 12 Dec 2025 14:57:47 -0600 Subject: [PATCH 53/85] refactor: update joint classes to inherit from ConnectionBase instead of FreeJoint across multiple files --- docs/api/connections.rst | 5 ++++ .../generic_system_type_connection.py | 28 ++----------------- .../parallel_connection.py | 4 +-- elastica/modules/connections.py | 16 +++++------ elastica/modules/protocol.py | 2 +- .../MuscularFlagella/connection_flagella.py | 7 ++--- 6 files changed, 21 insertions(+), 41 deletions(-) diff --git a/docs/api/connections.rst b/docs/api/connections.rst index 0fec1ea3..16ea324f 100644 --- a/docs/api/connections.rst +++ b/docs/api/connections.rst @@ -13,6 +13,7 @@ Description .. autosummary:: :nosignatures: + ConnectionBase FreeJoint FixedJoint HingeJoint @@ -23,6 +24,7 @@ Compatibility =============================== ==== =========== Connection / Joints Rod Rigid Body =============================== ==== =========== +ConnectionBase ✅ ✅ FreeJoint ✅ ❌ FixedJoint ✅ ❌ HingeJoint ✅ ❌ @@ -31,6 +33,9 @@ HingeJoint ✅ ❌ Built-in Connection / Joint ------------------------------------- +.. autoclass:: ConnectionBase + :special-members: __init__,apply_forces,apply_torques + .. autoclass:: FreeJoint :special-members: __init__,apply_forces,apply_torques diff --git a/elastica/experimental/connection_contact_joint/generic_system_type_connection.py b/elastica/experimental/connection_contact_joint/generic_system_type_connection.py index 86c40fd2..9e957d1e 100644 --- a/elastica/experimental/connection_contact_joint/generic_system_type_connection.py +++ b/elastica/experimental/connection_contact_joint/generic_system_type_connection.py @@ -1,7 +1,7 @@ __doc__ = ( """ Module containing joint classes to connect rods and rigid bodies together. """ ) -from elastica.joint import FreeJoint, FixedJoint +from elastica.joint import ConnectionBase, FixedJoint from elastica.typing import SystemType import numpy as np from typing import Optional @@ -20,7 +20,7 @@ # - [x] Examples -class GenericSystemTypeFreeJoint(FreeJoint): +class GenericSystemTypeFreeJoint(ConnectionBase): """ Constrains the relative movement between two nodes by applying restoring forces. @@ -37,28 +37,6 @@ class GenericSystemTypeFreeJoint(FreeJoint): Describes for system two in the local coordinate system the translation from the node `index_two` (for rods) or the center of mass (for rigid bodies) to the joint. - - Examples - -------- - How to connect two Cosserat rods together using a spherical joint with a gap of 0.01 m in between. - - >>> simulator.connect(rod_one, rod_two, first_connect_idx=-1, second_connect_idx=0).using( - ... FreeJoint, - ... k=1e4, - ... nu=1, - ... point_system_one=np.array([0.0, 0.0, 0.005]), - ... point_system_two=np.array([0.0, 0.0, -0.005]), - ... ) - - How to connect the distal end of a CosseratRod with the base of a cylinder using a spherical joint. - - >>> simulator.connect(rod, cylinder, first_connect_idx=-1, second_connect_idx=0).using( - ... FreeJoint, - ... k=1e4, - ... nu=1, - ... point_system_two=np.array([0.0, 0.0, -cylinder.length / 2.]), - ... ) - """ # pass the k and nu for the forces @@ -202,8 +180,6 @@ def apply_torques( """ Apply restoring joint torques to the connected systems. - In FreeJoint class, this routine simply passes. - Parameters ---------- system_one : SystemType diff --git a/elastica/experimental/connection_contact_joint/parallel_connection.py b/elastica/experimental/connection_contact_joint/parallel_connection.py index 24980d63..f065beac 100644 --- a/elastica/experimental/connection_contact_joint/parallel_connection.py +++ b/elastica/experimental/connection_contact_joint/parallel_connection.py @@ -1,7 +1,7 @@ __doc__ = """Contains SurfaceJointSideBySide class which connects two parallel rods .""" import numpy as np from numba import njit -from elastica.joint import FreeJoint +from elastica.joint import ConnectionBase # Join the two rods from elastica._linalg import ( @@ -67,7 +67,7 @@ def get_connection_vector_straight_straight_rod( ) -class SurfaceJointSideBySide(FreeJoint): +class SurfaceJointSideBySide(ConnectionBase): """ TODO: documentation """ diff --git a/elastica/modules/connections.py b/elastica/modules/connections.py index e4a8e2c1..503c6b60 100644 --- a/elastica/modules/connections.py +++ b/elastica/modules/connections.py @@ -15,7 +15,7 @@ ) import numpy as np import functools -from elastica.joint import FreeJoint +from elastica.joint import ConnectionBase from .protocol import ConnectedSystemCollectionProtocol, ModuleProtocol @@ -90,7 +90,7 @@ def _finalize_connections(self: ConnectedSystemCollectionProtocol) -> None: first_sys_idx, second_sys_idx, first_connect_idx, second_connect_idx = ( connection.id() ) - connect_instance: FreeJoint = connection.instantiate() + connect_instance: ConnectionBase = connection.instantiate() func_force = functools.partial( connect_instance.apply_forces, @@ -156,7 +156,7 @@ def __init__( self._second_sys_n_lim: int = second_sys_nlim self.first_sys_connection_idx: ConnectionIndex = () self.second_sys_connection_idx: ConnectionIndex = () - self._connect_cls: Type[FreeJoint] + self._connect_cls: Type[ConnectionBase] def set_index( self, first_idx: ConnectionIndex, second_idx: ConnectionIndex @@ -239,7 +239,7 @@ def set_index( def using( self, - cls: Type[FreeJoint], + cls: Type[ConnectionBase], *args: Any, **kwargs: Any, ) -> None: @@ -261,8 +261,8 @@ def using( """ assert issubclass( - cls, FreeJoint - ), "{} is not a valid joint class. Did you forget to derive from FreeJoint?".format( + cls, ConnectionBase + ), "{} is not a valid connection class. Did you forget to derive from ConnectionBase?".format( cls ) self._connect_cls = cls @@ -279,7 +279,7 @@ def id( self.second_sys_connection_idx, ) - def instantiate(self) -> FreeJoint: + def instantiate(self) -> ConnectionBase: if not hasattr(self, "_connect_cls"): raise RuntimeError( "No connections provided to link rod id {0}" @@ -293,5 +293,5 @@ def instantiate(self) -> FreeJoint: except (TypeError, IndexError): raise TypeError( r"Unable to construct connection class.\n" - r"Did you provide all necessary joint properties?" + r"Did you provide all necessary connection properties?" ) diff --git a/elastica/modules/protocol.py b/elastica/modules/protocol.py index 116dd755..47b3ab8a 100644 --- a/elastica/modules/protocol.py +++ b/elastica/modules/protocol.py @@ -13,7 +13,7 @@ BlockSystemType, ConnectionIndex, ) -from elastica.joint import FreeJoint +from elastica.joint import ConnectionBase from elastica.callback_functions import CallBackBaseClass from elastica.boundary_conditions import ConstraintBase from elastica.dissipation import DamperBase diff --git a/examples/MuscularFlagella/connection_flagella.py b/examples/MuscularFlagella/connection_flagella.py index eb2d0ead..c059f55f 100644 --- a/examples/MuscularFlagella/connection_flagella.py +++ b/examples/MuscularFlagella/connection_flagella.py @@ -2,11 +2,11 @@ __all__ = ["MuscularFlagellaConnection"] import numpy as np from numba import njit -from elastica.joint import FreeJoint +from elastica.joint import ConnectionBase from elastica._linalg import _batch_matvec -class MuscularFlagellaConnection(FreeJoint): +class MuscularFlagellaConnection(ConnectionBase): """ This connection class is for Muscular Flagella and it is not generalizable. Since our goal is to replicate the experimental data. We assume muscular flagella is not moving out of plane. @@ -27,8 +27,7 @@ def __init__( normal : np.ndarray 1D array of floats. Normal direction of the rods. """ - super().__init__(k, nu=0) - + self.k = np.float64(k) self.normal = normal def apply_forces(self, system_one, index_one, system_two, index_two, time): From d9cfa44de895129b29a75ec208f0bd31363af408 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 12 Dec 2025 15:02:13 -0600 Subject: [PATCH 54/85] tests: improve error message clarity in connection tests --- .../generic_system_type_connection.py | 3 ++- tests/test_modules/test_connections.py | 21 ++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/elastica/experimental/connection_contact_joint/generic_system_type_connection.py b/elastica/experimental/connection_contact_joint/generic_system_type_connection.py index 9e957d1e..c52cbf99 100644 --- a/elastica/experimental/connection_contact_joint/generic_system_type_connection.py +++ b/elastica/experimental/connection_contact_joint/generic_system_type_connection.py @@ -66,7 +66,8 @@ def __init__( or the center of mass (for rigid bodies) to the joint. (default = np.array([0.0, 0.0, 0.0])) """ - super().__init__(k=k, nu=nu, **kwargs) + self.k = np.float64(k) + self.nu = np.float64(nu) self.point_system_one = ( point_system_one diff --git a/tests/test_modules/test_connections.py b/tests/test_modules/test_connections.py index eadf98a0..8de57f5a 100644 --- a/tests/test_modules/test_connections.py +++ b/tests/test_modules/test_connections.py @@ -126,11 +126,19 @@ def test_using_with_illegal_connect_throws_assertion_error( ): with pytest.raises(AssertionError) as excinfo: load_connect.using(illegal_connect) - assert "not a valid joint" in str(excinfo.value) - - from elastica.joint import FreeJoint, FixedJoint, HingeJoint + assert "not a valid" in str(excinfo.value) + + from elastica.joint import ( + FreeJoint, + FixedJoint, + HingeJoint, + BallJoint, + SphericalJoint, + ) - @pytest.mark.parametrize("legal_connect", [FreeJoint, HingeJoint, FixedJoint]) + @pytest.mark.parametrize( + "legal_connect", [FreeJoint, HingeJoint, FixedJoint, BallJoint, SphericalJoint] + ) def test_using_with_legal_connect(self, load_connect, legal_connect): connect = load_connect connect.using(legal_connect, 3, 4.0, "5", k=1, l_var="2", j=3.0) @@ -174,10 +182,7 @@ def mock_init(self, *args, **kwargs): # Actual test is here, this should not throw with pytest.raises(TypeError) as excinfo: _ = connect.instantiate() - assert ( - r"Unable to construct connection class.\nDid you provide all necessary joint properties?" - == str(excinfo.value) - ) + assert r"Unable to construct connection class" in str(excinfo.value) class TestConnectionsMixin: From dd25c4cc100a6791f979b0eac67099303bd103b9 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 12 Dec 2025 15:35:11 -0600 Subject: [PATCH 55/85] feat: organize code with static system type for plane --- elastica/__init__.py | 2 ++ elastica/contact_forces.py | 19 ++++++------ elastica/modules/base_system.py | 5 ++- elastica/modules/memory_block.py | 6 ---- elastica/surface/__init__.py | 1 - elastica/surface/plane.py | 7 +++-- elastica/surface/surface_base.py | 31 ------------------- elastica/systems/protocol.py | 8 +++-- elastica/typing.py | 9 ++---- .../snake_contact.py | 3 +- tests/test_contact_classes.py | 6 ++-- tests/test_modules/test_base_system.py | 4 +-- tests/test_modules/test_contact.py | 4 +-- 13 files changed, 34 insertions(+), 71 deletions(-) delete mode 100644 elastica/surface/surface_base.py diff --git a/elastica/__init__.py b/elastica/__init__.py index ce8d1cdd..0e0f5d1b 100644 --- a/elastica/__init__.py +++ b/elastica/__init__.py @@ -33,6 +33,8 @@ ConnectionBase, FreeJoint, FixedJoint, + BallJoint, # Alias + SphericalJoint, # Alias HingeJoint, ) from elastica.contact_forces import ( diff --git a/elastica/contact_forces.py b/elastica/contact_forces.py index f52dd127..417b6feb 100644 --- a/elastica/contact_forces.py +++ b/elastica/contact_forces.py @@ -1,13 +1,12 @@ __doc__ = """ Numba implementation module containing contact between rods and rigid bodies and other rods rigid bodies or surfaces.""" from typing import TypeVar, Generic, Type -from elastica.typing import RodType, SystemType, SurfaceType, CosseratRodProtocol +from elastica.typing import RodType, SystemType, CosseratRodProtocol from elastica.rod.rod_base import RodBase from elastica.rigidbody.cylinder import Cylinder from elastica.rigidbody.sphere import Sphere from elastica.surface.plane import Plane -from elastica.surface.surface_base import SurfaceBase from elastica.contact_utils import ( _prune_using_aabbs_rod_cylinder, _prune_using_aabbs_rod_rod, @@ -494,12 +493,12 @@ def __init__( @property def _allowed_system_two(self) -> list[Type]: - return [SurfaceBase] + return [Plane] def apply_contact( self, system_one: RodType, - system_two: SurfaceType, + system_two: Plane, time: np.float64 = np.float64(0.0), ) -> None: """ @@ -591,12 +590,12 @@ def __init__( @property def _allowed_system_two(self) -> list[Type]: - return [SurfaceBase] + return [Plane] def apply_contact( self, system_one: RodType, - system_two: SurfaceType, + system_two: Plane, time: np.float64 = np.float64(0.0), ) -> None: """ @@ -605,7 +604,7 @@ def apply_contact( Parameters ---------- system_one: RodType - system_two: SurfaceType + system_two: Plane """ @@ -678,12 +677,12 @@ def _allowed_system_one(self) -> list[Type]: @property def _allowed_system_two(self) -> list[Type]: - return [SurfaceBase] + return [Plane] def apply_contact( self, system_one: Cylinder, - system_two: SurfaceType, + system_two: Plane, time: np.float64 = np.float64(0.0), ) -> None: """ @@ -694,7 +693,7 @@ def apply_contact( Parameters ---------- system_one: Cylinder - system_two: SurfaceBase + system_two: Plane """ _calculate_contact_forces_cylinder_plane( diff --git a/elastica/modules/base_system.py b/elastica/modules/base_system.py index dcd5890a..21077719 100644 --- a/elastica/modules/base_system.py +++ b/elastica/modules/base_system.py @@ -10,7 +10,6 @@ from elastica.typing import ( SystemType, StaticSystemType, - StaticSystemProtocol, BlockSystemType, BlockSystemProtocol, SystemIdxType, @@ -26,7 +25,7 @@ from elastica.rod.rod_base import RodBase from elastica.rigidbody.rigid_body import RigidBodyBase -from elastica.surface.surface_base import SurfaceBase +from elastica.systems.protocol import StaticSystemBase from .memory_block import construct_memory_block_structures from .operator_group import OperatorGroupFIFO @@ -85,7 +84,7 @@ def __init__(self) -> None: self.allowed_sys_types: tuple[Type, ...] = ( RodBase, RigidBodyBase, - SurfaceBase, + StaticSystemBase, ) # List of systems to be integrated diff --git a/elastica/modules/memory_block.py b/elastica/modules/memory_block.py index d14900ee..c794d774 100644 --- a/elastica/modules/memory_block.py +++ b/elastica/modules/memory_block.py @@ -7,7 +7,6 @@ from elastica.typing import ( RodType, RigidBodyType, - SurfaceType, StaticSystemType, SystemIdxType, BlockSystemType, @@ -15,7 +14,6 @@ from elastica.rod.rod_base import RodBase from elastica.rigidbody.rigid_body import RigidBodyBase -from elastica.surface.surface_base import SurfaceBase from elastica.memory_block.memory_block_rod import MemoryBlockCosseratRod from elastica.memory_block.memory_block_rigid_body import MemoryBlockRigidBody @@ -50,10 +48,6 @@ def construct_memory_block_structures( temp_list_for_rigid_body_systems.append(rigid_body_system) temp_list_for_rigid_body_systems_idx.append(system_idx) - elif isinstance(sys_to_be_added, SurfaceBase): - # TODO: Surface type is passive system - pass - else: continue # No error:: any typechecking should be finished by BaseSystemCollection._check_type diff --git a/elastica/surface/__init__.py b/elastica/surface/__init__.py index efe78d06..5504e1f4 100644 --- a/elastica/surface/__init__.py +++ b/elastica/surface/__init__.py @@ -1,3 +1,2 @@ __doc__ = """Surface classes""" -from elastica.surface.surface_base import SurfaceBase from elastica.surface.plane import Plane diff --git a/elastica/surface/plane.py b/elastica/surface/plane.py index b12a3fea..7ebe7c8e 100644 --- a/elastica/surface/plane.py +++ b/elastica/surface/plane.py @@ -1,12 +1,15 @@ __doc__ = """""" -from elastica.surface.surface_base import SurfaceBase +from typing import Type +from elastica.systems.protocol import StaticSystemBase import numpy as np from numpy.typing import NDArray from elastica.utils import Tolerance -class Plane(SurfaceBase): +class Plane(StaticSystemBase): + REQUISITE_MODULES: list[Type] = [] + def __init__( self, plane_origin: NDArray[np.float64], plane_normal: NDArray[np.float64] ): diff --git a/elastica/surface/surface_base.py b/elastica/surface/surface_base.py deleted file mode 100644 index 37812ea7..00000000 --- a/elastica/surface/surface_base.py +++ /dev/null @@ -1,31 +0,0 @@ -__doc__ = """Base class for surfaces""" -from typing import TYPE_CHECKING, Type - -import numpy as np -from numpy.typing import NDArray - - -class SurfaceBase: - """ - Base class for all surfaces. - - Notes - ----- - All new surface classes must be derived from this SurfaceBase class. - - """ - - REQUISITE_MODULES: list[Type] = [] - - def __init__(self) -> None: - """ - SurfaceBase does not take any arguments. - """ - self.normal: NDArray[np.float64] # (3,) - self.origin: NDArray[np.float64] # (3, 1) - - -if TYPE_CHECKING: - from elastica.systems.protocol import StaticSystemProtocol - - _: StaticSystemProtocol = SurfaceBase() diff --git a/elastica/systems/protocol.py b/elastica/systems/protocol.py index 89d52d92..9075674b 100644 --- a/elastica/systems/protocol.py +++ b/elastica/systems/protocol.py @@ -1,6 +1,6 @@ __doc__ = """Base class for elastica system""" -from typing import Protocol, Type +from typing import Protocol, Type, runtime_checkable from elastica.typing import StateType, SystemType from elastica.rod.data_structures import _KinematicState, _DynamicState @@ -9,11 +9,13 @@ from numpy.typing import NDArray -class StaticSystemProtocol(Protocol): +# TODO: Better organize this part +@runtime_checkable +class StaticSystemBase(Protocol): REQUISITE_MODULES: list[Type] -class SystemProtocol(StaticSystemProtocol, Protocol): +class SystemProtocol(StaticSystemBase, Protocol): """ Protocol for all dynamic elastica system """ diff --git a/elastica/typing.py b/elastica/typing.py index 31ef9917..9c8805e9 100644 --- a/elastica/typing.py +++ b/elastica/typing.py @@ -16,14 +16,13 @@ # NEVER BACK-IMPORT ANY ELASTICA MODULES HERE from .rod.protocol import CosseratRodProtocol from .rigidbody.protocol import RigidBodyProtocol - from .surface.surface_base import SurfaceBase from .modules.base_system import BaseSystemCollection from .modules.protocol import SystemCollectionProtocol from .rod.data_structures import _State as State from .systems.protocol import ( + StaticSystemBase, SystemProtocol, - StaticSystemProtocol, SymplecticSystemProtocol, ) from .timestepper.protocol import ( @@ -35,20 +34,19 @@ else: CosseratRodProtocol = "CosseratRodProtocol" RigidBodyProtocol = "RigidBodyProtocol" - SurfaceBase = "SurfaceBase" BaseSystemCollection = "BaseSystemCollection" SystemCollectionProtocol = "SystemCollectionProtocol" State = "State" SystemProtocol = "SystemProtocol" - StaticSystemProtocol = "StaticSystemProtocol" + StaticSystemBase = "StaticSystemBase" SymplecticSystemProtocol = "SymplecticSystemProtocol" StepperProtocol = "StepperProtocol" SymplecticStepperProtocol = "SymplecticStepperProtocol" BlockSystemProtocol = "BlockSystemProtocol" -StaticSystemType: TypeAlias = "StaticSystemProtocol" +StaticSystemType: TypeAlias = "StaticSystemBase" SystemType: TypeAlias = SystemProtocol SystemIdxType: TypeAlias = int BlockSystemType: TypeAlias = "BlockSystemProtocol" @@ -64,7 +62,6 @@ RodType: TypeAlias = "CosseratRodProtocol" RigidBodyType: TypeAlias = "RigidBodyProtocol" -SurfaceType: TypeAlias = "SurfaceBase" SystemCollectionType: TypeAlias = "SystemCollectionProtocol" diff --git a/examples/ContinuumSnakeWithLiftingWaveCase/snake_contact.py b/examples/ContinuumSnakeWithLiftingWaveCase/snake_contact.py index 4fe160ff..44a3394d 100755 --- a/examples/ContinuumSnakeWithLiftingWaveCase/snake_contact.py +++ b/examples/ContinuumSnakeWithLiftingWaveCase/snake_contact.py @@ -25,7 +25,6 @@ from numba import njit from elastica.rod.rod_base import RodBase from elastica.surface import Plane -from elastica.surface.surface_base import SurfaceBase from elastica.contact_forces import NoContact from elastica.typing import RodType, SystemType @@ -291,7 +290,7 @@ def __init__( @property def _allowed_system_two(self) -> list[Type]: # Modify this list to include the allowed system types for contact - return [SurfaceBase] + return [Plane] def apply_contact( self, system_one: RodType, system_two: SystemType, time: float diff --git a/tests/test_contact_classes.py b/tests/test_contact_classes.py index e69a0ee6..29379d5f 100644 --- a/tests/test_contact_classes.py +++ b/tests/test_contact_classes.py @@ -686,7 +686,7 @@ def test_check_systems_validity_with_invalid_systems( # Testing Rod Plane Contact wrapper with incorrect type for second argument with pytest.raises(TypeError) as excinfo: rod_plane_contact._check_systems_validity(mock_rod, mock_list) - assert "System provided (list) must be derived from ['SurfaceBase']." == str( + assert "System provided (list) must be derived from ['Plane']." == str( excinfo.value ) @@ -959,7 +959,7 @@ def test_check_systems_validity_with_invalid_systems( # Testing Rod Plane Contact wrapper with incorrect type for second argument with pytest.raises(TypeError) as excinfo: rod_plane_contact._check_systems_validity(mock_rod, mock_list) - assert "System provided (list) must be derived from ['SurfaceBase']." == str( + assert "System provided (list) must be derived from ['Plane']." == str( excinfo.value ) @@ -1284,7 +1284,7 @@ def test_check_systems_validity_with_invalid_systems( # Testing Cylinder Plane Contact wrapper with incorrect type for second argument with pytest.raises(TypeError) as excinfo: cylinder_plane_contact._check_systems_validity(mock_cylinder, mock_list) - assert "System provided (list) must be derived from ['SurfaceBase']." == str( + assert "System provided (list) must be derived from ['Plane']." == str( excinfo.value ) diff --git a/tests/test_modules/test_base_system.py b/tests/test_modules/test_base_system.py index 8f4cfa2c..3f07bd6e 100644 --- a/tests/test_modules/test_base_system.py +++ b/tests/test_modules/test_base_system.py @@ -76,13 +76,13 @@ def test_extend_allowed_types(self, load_collection): from elastica.rod import RodBase from elastica.rigidbody import RigidBodyBase - from elastica.surface import SurfaceBase + from elastica.systems.protocol import StaticSystemBase # Types are extended in the fixture assert bsc.allowed_sys_types == ( RodBase, RigidBodyBase, - SurfaceBase, + StaticSystemBase, int, float, str, diff --git a/tests/test_modules/test_contact.py b/tests/test_modules/test_contact.py index 79abb7c5..bb9f2a79 100644 --- a/tests/test_modules/test_contact.py +++ b/tests/test_modules/test_contact.py @@ -90,7 +90,7 @@ class SystemCollectionWithContactMixin(BaseSystemCollection, Contact): from elastica.rod import RodBase from elastica.rigidbody import RigidBodyBase - from elastica.surface import SurfaceBase + from elastica.systems.protocol import StaticSystemBase class MockRod(RodBase): def __init__(self, *args, **kwargs): @@ -113,7 +113,7 @@ class MockRigidBody(RigidBodyBase): def __init__(self, *args, **kwargs): self.n_elems = 1 - class MockSurface(SurfaceBase): + class MockSurface(StaticSystemBase): def __init__(self, *args, **kwargs): self.n_facets = 1 From 3495d02547907a8ea4bc89585b20db8fa4303a62 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 14 Dec 2025 19:56:39 -0600 Subject: [PATCH 56/85] refactor: update system collection protocols and improve type handling across multiple modules --- elastica/modules/base_system.py | 10 +-- elastica/modules/callbacks.py | 23 +++--- elastica/modules/connections.py | 15 ++-- elastica/modules/constraints.py | 22 +++--- elastica/modules/contact.py | 19 +++-- elastica/modules/damping.py | 23 +++--- elastica/modules/forcing.py | 21 +++--- elastica/modules/memory_block.py | 9 +-- elastica/modules/protocol.py | 124 ++++++------------------------- elastica/restart.py | 4 +- 10 files changed, 88 insertions(+), 182 deletions(-) diff --git a/elastica/modules/base_system.py b/elastica/modules/base_system.py index 21077719..6a115d67 100644 --- a/elastica/modules/base_system.py +++ b/elastica/modules/base_system.py @@ -23,9 +23,7 @@ from collections.abc import MutableSequence -from elastica.rod.rod_base import RodBase -from elastica.rigidbody.rigid_body import RigidBodyBase -from elastica.systems.protocol import StaticSystemBase +from elastica.systems.protocol import StaticSystemBase, SystemProtocol from .memory_block import construct_memory_block_structures from .operator_group import OperatorGroupFIFO @@ -81,11 +79,7 @@ def __init__(self) -> None: super().__init__() # List of system types/bases that are allowed - self.allowed_sys_types: tuple[Type, ...] = ( - RodBase, - RigidBodyBase, - StaticSystemBase, - ) + self.allowed_sys_types: tuple[Type, ...] = (StaticSystemBase,) # List of systems to be integrated self.__systems: list[StaticSystemType] = [] diff --git a/elastica/modules/callbacks.py b/elastica/modules/callbacks.py index f390e4a6..0b8d4ed8 100644 --- a/elastica/modules/callbacks.py +++ b/elastica/modules/callbacks.py @@ -11,19 +11,14 @@ from elastica.typing import ( SystemType, SystemIdxType, - OperatorFinalizeType, - SystemProtocol, ) -from .protocol import ModuleProtocol +from .protocol import SystemCollectionProtocol, ModuleProtocol import functools -import numpy as np - from elastica.callback_functions import CallBackBaseClass -from .protocol import SystemCollectionWithCallbackProtocol - +# Callback takes data-structures and collection of SystemType SystemIdxDSType: TypeAlias = """ ( SystemIdxType @@ -40,7 +35,7 @@ """ -class CallBacks: +class CallBacks(SystemCollectionProtocol): """ CallBacks class is a module for calling callback functions, set by the user. If the user wants to collect data from the simulation, the simulator class has to be derived @@ -52,13 +47,15 @@ class CallBacks: List of call back classes defined for rod-like objects. """ - def __init__(self: SystemCollectionWithCallbackProtocol) -> None: - self._callback_list: list[ModuleProtocol] = [] + _callback_list: list[ModuleProtocol] + + def __init__(self) -> None: + self._callback_list = [] super(CallBacks, self).__init__() self._feature_group_finalize.append(self._finalize_callback) def collect_diagnostics( - self: SystemCollectionWithCallbackProtocol, + self, system: SystemDSType | EllipsisType, ) -> ModuleProtocol: """ @@ -77,7 +74,7 @@ def collect_diagnostics( """ sys_idx: SystemIdxDSType if system is Ellipsis: - sys_idx = tuple([self.get_system_index(sys) for sys in self]) + sys_idx = tuple([self.get_system_index(sys) for sys in self.systems()]) elif isinstance(system, list): sys_idx = [self.get_system_index(sys) for sys in system] elif isinstance(system, dict): @@ -96,7 +93,7 @@ def collect_diagnostics( return _callback - def _finalize_callback(self: SystemCollectionWithCallbackProtocol) -> None: + def _finalize_callback(self) -> None: # dev : the first index stores the rod index to collect data. for callback in self._callback_list: sys_id = callback.id() diff --git a/elastica/modules/connections.py b/elastica/modules/connections.py index 503c6b60..7fc57dea 100644 --- a/elastica/modules/connections.py +++ b/elastica/modules/connections.py @@ -8,7 +8,6 @@ from typing import Type, cast, Any from elastica.typing import ( SystemIdxType, - OperatorFinalizeType, ConnectionIndex, RodType, RigidBodyType, @@ -17,10 +16,10 @@ import functools from elastica.joint import ConnectionBase -from .protocol import ConnectedSystemCollectionProtocol, ModuleProtocol +from .protocol import SystemCollectionProtocol, ModuleProtocol -class Connections: +class Connections(SystemCollectionProtocol): """ The Connections class is a module for connecting rod-like objects using joints selected by the user. To connect two rod-like objects, the simulator class must be derived from @@ -32,13 +31,15 @@ class Connections: List of joint classes defined for rod-like objects. """ - def __init__(self: ConnectedSystemCollectionProtocol) -> None: - self._connections: list[ModuleProtocol] = [] + _connections: list[ModuleProtocol] + + def __init__(self) -> None: + self._connections = [] super(Connections, self).__init__() self._feature_group_finalize.append(self._finalize_connections) def connect( - self: ConnectedSystemCollectionProtocol, + self, first_rod: "RodType | RigidBodyType", second_rod: "RodType | RigidBodyType", first_connect_idx: ConnectionIndex = (), @@ -80,7 +81,7 @@ def connect( return _connect - def _finalize_connections(self: ConnectedSystemCollectionProtocol) -> None: + def _finalize_connections(self) -> None: # From stored _Connect objects, instantiate the joints and store it # dev : the first indices stores the # (first rod index, second_rod_idx, connection_idx_on_first_rod, connection_idx_on_second_rod) diff --git a/elastica/modules/constraints.py b/elastica/modules/constraints.py index 9664b757..aaf53a58 100644 --- a/elastica/modules/constraints.py +++ b/elastica/modules/constraints.py @@ -15,14 +15,14 @@ from elastica.typing import ( SystemIdxType, ConstrainingIndex, - RigidBodyType, RodType, + RigidBodyType, ) -from elastica.memory_block.protocol import BlockRodProtocol -from .protocol import ConstrainedSystemCollectionProtocol, ModuleProtocol +from elastica.typing import RodType +from .protocol import SystemCollectionProtocol, ModuleProtocol -class Constraints: +class Constraints(SystemCollectionProtocol): """ The Constraints class is a module for enforcing displacement boundary conditions. To enforce boundary conditions on rod-like objects, the simulator class @@ -34,14 +34,14 @@ class Constraints: List of boundary condition classes defined for rod-like objects. """ - def __init__(self: ConstrainedSystemCollectionProtocol) -> None: - self._constraints_list: list[ModuleProtocol] = [] + _constraints_list: list[ModuleProtocol] + + def __init__(self) -> None: + self._constraints_list = [] super(Constraints, self).__init__() self._feature_group_finalize.append(self._finalize_constraints) - def constrain( - self: ConstrainedSystemCollectionProtocol, system: "RodType | RigidBodyType" - ) -> ModuleProtocol: + def constrain(self, system: "RodType | RigidBodyType") -> ModuleProtocol: """ This method enforces a displacement boundary conditions to the relevant user-defined system or rod-like object. You must input the system or rod-like @@ -66,7 +66,7 @@ def constrain( return _constraint - def _finalize_constraints(self: ConstrainedSystemCollectionProtocol) -> None: + def _finalize_constraints(self) -> None: """ In case memory block have ring rod, then periodic boundaries have to be synched. In order to synchronize periodic boundaries, a new constrain for memory block rod added called as _ConstrainPeriodicBoundaries. This @@ -83,7 +83,7 @@ def _finalize_constraints(self: ConstrainedSystemCollectionProtocol) -> None: # Apply the constrain to synchronize the periodic boundaries of the memory rod. Find the memory block # sys idx among other systems added and then apply boundary conditions. memory_block_idx = self.get_system_index(block) - block_system = cast(BlockRodProtocol, self[memory_block_idx]) + block_system = cast(RodType, self[memory_block_idx]) self.constrain(block_system).using( _ConstrainPeriodicBoundaries, ) diff --git a/elastica/modules/contact.py b/elastica/modules/contact.py index f4dd15e4..22a3f46b 100644 --- a/elastica/modules/contact.py +++ b/elastica/modules/contact.py @@ -10,22 +10,19 @@ import functools from elastica.typing import ( SystemIdxType, - OperatorType, - StaticSystemType, SystemType, + StaticSystemType, ) -from .protocol import ContactedSystemCollectionProtocol, ModuleProtocol +from .protocol import SystemCollectionProtocol, ModuleProtocol import logging -import numpy as np - from elastica.contact_forces import NoContact logger = logging.getLogger(__name__) -class Contact: +class Contact(SystemCollectionProtocol): """ The Contact class is a module for applying contact between rod-like objects . To apply contact between rod-like objects, the simulator class must be derived from the Contact class. @@ -36,13 +33,15 @@ class Contact: List of contact classes defined for rod-like objects. """ - def __init__(self: ContactedSystemCollectionProtocol) -> None: - self._contacts: list[ModuleProtocol] = [] + _contacts: list[ModuleProtocol] + + def __init__(self) -> None: + self._contacts = [] super(Contact, self).__init__() self._feature_group_finalize.append(self._finalize_contact) def detect_contact_between( - self: ContactedSystemCollectionProtocol, + self, first_system: SystemType, second_system: "SystemType | StaticSystemType", ) -> ModuleProtocol: @@ -69,7 +68,7 @@ def detect_contact_between( return _contact - def _finalize_contact(self: ContactedSystemCollectionProtocol) -> None: + def _finalize_contact(self) -> None: # dev : the first indices stores the # (first_rod_idx, second_rod_idx) diff --git a/elastica/modules/damping.py b/elastica/modules/damping.py index 9d39c12b..fd46b667 100644 --- a/elastica/modules/damping.py +++ b/elastica/modules/damping.py @@ -13,14 +13,13 @@ import functools -import numpy as np - from elastica.dissipation import DamperBase -from elastica.typing import RodType, SystemType, SystemIdxType, CosseratRodProtocol -from .protocol import DampenedSystemCollectionProtocol, ModuleProtocol +from elastica.typing import RodType, SystemIdxType +from elastica.rod.rod_base import RodBase +from .protocol import SystemCollectionProtocol, ModuleProtocol -class Damping: +class Damping(SystemCollectionProtocol): """ The Damping class is a module for applying damping on rod-like objects, the simulator class must be derived from @@ -32,14 +31,14 @@ class Damping: List of damper classes defined for rod-like objects. """ - def __init__(self: DampenedSystemCollectionProtocol) -> None: - self._damping_list: List[ModuleProtocol] = [] + _damping_list: List[ModuleProtocol] + + def __init__(self) -> None: + self._damping_list = [] super().__init__() self._feature_group_finalize.append(self._finalize_dampers) - def dampen( - self: DampenedSystemCollectionProtocol, system: RodType - ) -> ModuleProtocol: + def dampen(self, system: "RodType") -> ModuleProtocol: """ This method applies damping on relevant user-defined system or rod-like object. You must input the system or rod-like @@ -63,7 +62,7 @@ def dampen( return _damper - def _finalize_dampers(self: DampenedSystemCollectionProtocol) -> None: + def _finalize_dampers(self) -> None: # From stored _Damping objects, instantiate the dissipation/damping # inplace : https://stackoverflow.com/a/1208792 @@ -143,7 +142,7 @@ def using(self, cls: Type[DamperBase], *args: Any, **kwargs: Any) -> None: def id(self) -> SystemIdxType: return self._sys_idx - def instantiate(self, rod: SystemType) -> DamperBase: + def instantiate(self, rod: "RodType") -> DamperBase: """Constructs a Damper class object after checks""" if not hasattr(self, "_damper_cls"): raise RuntimeError( diff --git a/elastica/modules/forcing.py b/elastica/modules/forcing.py index 38c43347..99c7a378 100644 --- a/elastica/modules/forcing.py +++ b/elastica/modules/forcing.py @@ -9,16 +9,15 @@ import functools from typing import Any, Type, List -import numpy as np - from elastica.external_forces import NoForces -from elastica.typing import SystemType, SystemIdxType, SystemProtocol -from .protocol import ForcedSystemCollectionProtocol, ModuleProtocol +from elastica.typing import SystemType, SystemIdxType +from elastica.systems.protocol import SystemProtocol +from .protocol import SystemCollectionProtocol, ModuleProtocol logger = logging.getLogger(__name__) -class Forcing: +class Forcing(SystemCollectionProtocol): """ The Forcing class is a module for applying boundary conditions that consist of applied external forces. To apply forcing on rod-like objects, @@ -30,14 +29,14 @@ class Forcing: List of forcing class defined for rod-like objects. """ - def __init__(self: ForcedSystemCollectionProtocol) -> None: - self._ext_forces_torques: List[ModuleProtocol] = [] + _ext_forces_torques: List[ModuleProtocol] + + def __init__(self) -> None: + self._ext_forces_torques = [] super().__init__() self._feature_group_finalize.append(self._finalize_forcing) - def add_forcing_to( - self: ForcedSystemCollectionProtocol, system: SystemType - ) -> ModuleProtocol: + def add_forcing_to(self, system: "SystemType") -> ModuleProtocol: """ This method applies external forces and torques on the relevant user-defined system or rod-like object. You must input the system @@ -61,7 +60,7 @@ def add_forcing_to( return _ext_force_torque - def _finalize_forcing(self: ForcedSystemCollectionProtocol) -> None: + def _finalize_forcing(self) -> None: # From stored _ExtForceTorque objects, and instantiate a Force # inplace : https://stackoverflow.com/a/1208792 diff --git a/elastica/modules/memory_block.py b/elastica/modules/memory_block.py index c794d774..10d1e7a0 100644 --- a/elastica/modules/memory_block.py +++ b/elastica/modules/memory_block.py @@ -2,7 +2,6 @@ This function is a module to construct memory blocks for different types of systems, such as Cosserat Rods, Rigid Body etc. """ -from typing import cast from elastica.typing import ( RodType, @@ -13,7 +12,7 @@ ) from elastica.rod.rod_base import RodBase -from elastica.rigidbody.rigid_body import RigidBodyBase +from elastica.rigidbody.rigid_body_base import RigidBodyBase from elastica.memory_block.memory_block_rod import MemoryBlockCosseratRod from elastica.memory_block.memory_block_rigid_body import MemoryBlockRigidBody @@ -39,13 +38,11 @@ def construct_memory_block_structures( for system_idx, sys_to_be_added in enumerate(systems): if isinstance(sys_to_be_added, RodBase): - rod_system = cast(RodType, sys_to_be_added) - temp_list_for_cosserat_rod_systems.append(rod_system) + temp_list_for_cosserat_rod_systems.append(sys_to_be_added) temp_list_for_cosserat_rod_systems_idx.append(system_idx) elif isinstance(sys_to_be_added, RigidBodyBase): - rigid_body_system = cast(RigidBodyType, sys_to_be_added) - temp_list_for_rigid_body_systems.append(rigid_body_system) + temp_list_for_rigid_body_systems.append(sys_to_be_added) temp_list_for_rigid_body_systems_idx.append(system_idx) else: diff --git a/elastica/modules/protocol.py b/elastica/modules/protocol.py index 47b3ab8a..7a34fd6d 100644 --- a/elastica/modules/protocol.py +++ b/elastica/modules/protocol.py @@ -1,4 +1,4 @@ -from typing import Protocol, Generator, TypeVar, Any, Type, overload, Iterator, Callable +from typing import Protocol, Generator, Any, Type, Callable, overload from typing import TYPE_CHECKING from elastica.typing import ( @@ -8,15 +8,8 @@ OperatorFinalizeType, StaticSystemType, SystemType, - RodType, - RigidBodyType, BlockSystemType, - ConnectionIndex, ) -from elastica.joint import ConnectionBase -from elastica.callback_functions import CallBackBaseClass -from elastica.boundary_conditions import ConstraintBase -from elastica.dissipation import DamperBase import numpy as np @@ -24,124 +17,51 @@ from .operator_group import OperatorGroupFIFO -class MixinProtocol(Protocol): - # def finalize(self) -> None: ... - ... +class ModuleProtocol(Protocol): + """Protocol for module handles (e.g., _Connect, _Constraint, _Damper, etc.).""" + def using(self, cls: Type[Any], *args: Any, **kwargs: Any) -> None: ... -M = TypeVar("M", bound=MixinProtocol) - - -class ModuleProtocol(Protocol[M]): - def using(self, cls: Type[M], *args: Any, **kwargs: Any) -> None: ... - - def instantiate(self, *args: Any, **kwargs: Any) -> M: ... + def instantiate(self, *args: Any, **kwargs: Any) -> Any: ... def id(self) -> Any: ... class SystemCollectionProtocol(Protocol): - def __len__(self) -> int: ... + """ + Protocol for system collections. - def systems(self) -> Generator[StaticSystemType, None, None]: ... - - def block_systems(self) -> Generator[BlockSystemType, None, None]: ... + This protocol defines the interface for system collections including + container operations, lifecycle methods, and internal feature groups + used for operator registration. + """ + # Container access @overload def __getitem__(self, i: slice) -> list[SystemType]: ... @overload def __getitem__(self, i: int) -> SystemType: ... def __getitem__(self, i: slice | int) -> "list[SystemType] | SystemType": ... - def __delitem__(self, i: slice | int) -> None: ... - def __setitem__(self, i: slice | int, value: SystemType) -> None: ... - def insert(self, i: int, value: SystemType) -> None: ... - def __iter__(self) -> Iterator[SystemType]: ... + def systems(self) -> Generator[StaticSystemType, None, None]: ... + + def block_systems(self) -> Generator[BlockSystemType, None, None]: ... def get_system_index( self, sys_to_be_added: "SystemType | StaticSystemType" ) -> SystemIdxType: ... - # Operator Group - _feature_group_synchronize: "OperatorGroupFIFO[OperatorType, ModuleProtocol]" - _feature_group_constrain_values: "OperatorGroupFIFO[OperatorType, ModuleProtocol]" - _feature_group_constrain_rates: "OperatorGroupFIFO[OperatorType, ModuleProtocol]" - _feature_group_damping: "OperatorGroupFIFO[OperatorType, ModuleProtocol]" - _feature_group_callback: "OperatorGroupFIFO[OperatorCallbackType, ModuleProtocol]" - + # Lifecycle methods def synchronize(self, time: np.float64) -> None: ... def constrain_values(self, time: np.float64) -> None: ... def constrain_rates(self, time: np.float64) -> None: ... def apply_callbacks(self, time: np.float64, current_step: int) -> None: ... - # Finalize Operations + # Internal feature groups for operator registration + _feature_group_synchronize: "OperatorGroupFIFO[OperatorType, ModuleProtocol]" + _feature_group_constrain_values: "OperatorGroupFIFO[OperatorType, ModuleProtocol]" + _feature_group_constrain_rates: "OperatorGroupFIFO[OperatorType, ModuleProtocol]" + _feature_group_damping: "OperatorGroupFIFO[OperatorType, ModuleProtocol]" + _feature_group_callback: "OperatorGroupFIFO[OperatorCallbackType, ModuleProtocol]" _feature_group_finalize: list[OperatorFinalizeType] - - def finalize(self) -> None: ... - _feature_group_on_close: "OperatorGroupFIFO[Callable, ModuleProtocol]" - - def close(self) -> None: ... - - -# Mixin Protocols (Used to type Self) -class ConnectedSystemCollectionProtocol(SystemCollectionProtocol, Protocol): - # Connection API - _connections: list[ModuleProtocol] - - def _finalize_connections(self) -> None: ... - - def connect( - self, - first_rod: "RodType | RigidBodyType", - second_rod: "RodType | RigidBodyType", - first_connect_idx: ConnectionIndex, - second_connect_idx: ConnectionIndex, - ) -> ModuleProtocol: ... - - -class ForcedSystemCollectionProtocol(SystemCollectionProtocol, Protocol): - # Forcing API - _ext_forces_torques: list[ModuleProtocol] - - def _finalize_forcing(self) -> None: ... - - def add_forcing_to(self, system: SystemType) -> ModuleProtocol: ... - - -class ContactedSystemCollectionProtocol(SystemCollectionProtocol, Protocol): - # Contact API - _contacts: list[ModuleProtocol] - - def _finalize_contact(self) -> None: ... - - def detect_contact_between( - self, first_system: SystemType, second_system: SystemType - ) -> ModuleProtocol: ... - - -class ConstrainedSystemCollectionProtocol(SystemCollectionProtocol, Protocol): - # Constraints API - _constraints_list: list[ModuleProtocol] - - def _finalize_constraints(self) -> None: ... - - def constrain(self, system: "RodType | RigidBodyType") -> ModuleProtocol: ... - - -class SystemCollectionWithCallbackProtocol(SystemCollectionProtocol, Protocol): - # CallBack API - _callback_list: list[ModuleProtocol] - - def _finalize_callback(self) -> None: ... - - def collect_diagnostics(self, system: SystemType) -> ModuleProtocol: ... - - -class DampenedSystemCollectionProtocol(SystemCollectionProtocol, Protocol): - # Damping API - _damping_list: list[ModuleProtocol] - - def _finalize_dampers(self) -> None: ... - - def dampen(self, system: RodType) -> ModuleProtocol: ... diff --git a/elastica/restart.py b/elastica/restart.py index 8c414ec9..fb9ae084 100644 --- a/elastica/restart.py +++ b/elastica/restart.py @@ -53,7 +53,7 @@ def save_state( os.makedirs(directory, exist_ok=True) # Save system state - for idx, system in enumerate(simulator): + for idx, system in enumerate(simulator.systems()): name = system.__class__.__name__ path = os.path.join(directory, f"{name}_{idx}.npz") np.savez(path, **system.__dict__) # type: ignore @@ -94,7 +94,7 @@ def load_state( time = meta["time"] # Load system state - for idx, system in enumerate(simulator): + for idx, system in enumerate(simulator.systems()): # TODO: Not exactly sure why this condition is necessary. if isinstance(system, (MemoryBlockCosseratRod, MemoryBlockRigidBody)): continue From 7943a724b0f0a6550adfd4d1e74989ff965e8575 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 14 Dec 2025 19:58:27 -0600 Subject: [PATCH 57/85] refactor: integrate energy computation and knot theory into RodBase, removing CosseratRodProtocol and slimming type handling --- elastica/rod/cosserat_rod.py | 58 ++++------------ elastica/rod/data_structures.py | 11 ++- elastica/rod/energy.py | 117 ++++++++++++++++++++++++++++++++ elastica/rod/knot_theory.py | 15 ++-- elastica/rod/protocol.py | 53 --------------- elastica/rod/rod_base.py | 80 ++++++++++++++++------ 6 files changed, 210 insertions(+), 124 deletions(-) create mode 100644 elastica/rod/energy.py delete mode 100644 elastica/rod/protocol.py diff --git a/elastica/rod/cosserat_rod.py b/elastica/rod/cosserat_rod.py index e47bcbad..66ff6f40 100644 --- a/elastica/rod/cosserat_rod.py +++ b/elastica/rod/cosserat_rod.py @@ -1,9 +1,9 @@ __doc__ = """ Rod classes and implementation details """ -from typing import TYPE_CHECKING, Any, Optional, Type +from typing import Any, Optional, Type from typing_extensions import Self from elastica.typing import RodType -from .protocol import CosseratRodProtocol +from elastica.rod import RodBase from numpy.typing import NDArray @@ -11,6 +11,7 @@ import functools import numba from elastica.rod import RodBase +from elastica.systems.protocol import SystemProtocol from elastica._linalg import ( _batch_cross, _batch_norm, @@ -70,7 +71,7 @@ def _compute_sigma_kappa_for_blockstructure(memory_block: RodType) -> None: ) -class CosseratRod(RodBase, KnotTheory): +class CosseratRod(RodBase, SystemProtocol): """ Cosserat Rod class. This is the preferred class for rods because it is derived from some of the essential base classes. @@ -153,7 +154,7 @@ class CosseratRod(RodBase, KnotTheory): REQUISITE_MODULES: list[Type] = [] def __init__( - self: CosseratRodProtocol, + self, n_elements: int, position: NDArray[np.float64], velocity: NDArray[np.float64], @@ -547,9 +548,7 @@ def ring_rod( rod.REQUISITE_MODULES.append(Constraints) return rod - def compute_internal_forces_and_torques( - self: CosseratRodProtocol, time: np.float64 - ) -> None: + def compute_internal_forces_and_torques(self, time: np.float64) -> None: """ Compute internal forces and torques. We need to compute internal forces and torques before the acceleration because they are used in interaction. Thus in order to speed up simulation, we will compute internal forces and torques @@ -604,7 +603,7 @@ def compute_internal_forces_and_torques( ) # Interface to time-stepper mixins (Symplectic, Explicit), which calls this method - def update_accelerations(self: CosseratRodProtocol, time: np.float64) -> None: + def update_accelerations(self, time: np.float64) -> None: """ Updates the acceleration variables @@ -626,14 +625,12 @@ def update_accelerations(self: CosseratRodProtocol, time: np.float64) -> None: self.dilatation, ) - def zeroed_out_external_forces_and_torques( - self: CosseratRodProtocol, time: np.float64 - ) -> None: + def zeroed_out_external_forces_and_torques(self, time: np.float64) -> None: _zeroed_out_external_forces_and_torques( self.external_forces, self.external_torques ) - def compute_translational_energy(self: CosseratRodProtocol) -> NDArray[np.float64]: + def compute_translational_energy(self) -> NDArray[np.float64]: """ Compute total translational energy of the rod at the instance. """ @@ -647,7 +644,7 @@ def compute_translational_energy(self: CosseratRodProtocol) -> NDArray[np.float6 ).sum() ) - def compute_rotational_energy(self: CosseratRodProtocol) -> NDArray[np.float64]: + def compute_rotational_energy(self) -> NDArray[np.float64]: """ Compute total rotational energy of the rod at the instance. """ @@ -657,9 +654,7 @@ def compute_rotational_energy(self: CosseratRodProtocol) -> NDArray[np.float64]: ) return 0.5 * np.einsum("ik,ik->k", self.omega_collection, J_omega_upon_e).sum() - def compute_velocity_center_of_mass( - self: CosseratRodProtocol, - ) -> NDArray[np.float64]: + def compute_velocity_center_of_mass(self) -> NDArray[np.float64]: """ Compute velocity center of mass of the rod at the instance. """ @@ -668,9 +663,7 @@ def compute_velocity_center_of_mass( return sum_mass_times_velocity / self.mass.sum() - def compute_position_center_of_mass( - self: CosseratRodProtocol, - ) -> NDArray[np.float64]: + def compute_position_center_of_mass(self) -> NDArray[np.float64]: """ Compute position center of mass of the rod at the instance. """ @@ -679,7 +672,7 @@ def compute_position_center_of_mass( return sum_mass_times_position / self.mass.sum() - def compute_bending_energy(self: CosseratRodProtocol) -> NDArray[np.float64]: + def compute_bending_energy(self) -> NDArray[np.float64]: """ Compute total bending energy of the rod at the instance. """ @@ -695,7 +688,7 @@ def compute_bending_energy(self: CosseratRodProtocol) -> NDArray[np.float64]: ).sum() ) - def compute_shear_energy(self: CosseratRodProtocol) -> NDArray[np.float64]: + def compute_shear_energy(self) -> NDArray[np.float64]: """ Compute total shear energy of the rod at the instance. """ @@ -1130,26 +1123,3 @@ def _zeroed_out_external_forces_and_torques( for i in range(3): for k in range(n_elems): external_torques[i, k] = 0.0 - - -if TYPE_CHECKING: - _: CosseratRodProtocol = CosseratRod.straight_rod( - 3, - np.zeros(3), - np.array([0, 1, 0]), - np.array([0, 0, 1]), - 1.0, - 0.1, - 1.0, - youngs_modulus=1.0, - ) - _: CosseratRodProtocol = CosseratRod.ring_rod( # type: ignore[no-redef] - 3, - np.zeros(3), - np.array([0, 1, 0]), - np.array([0, 0, 1]), - 1.0, - 0.1, - 1.0, - youngs_modulus=1.0, - ) diff --git a/elastica/rod/data_structures.py b/elastica/rod/data_structures.py index f7b248bc..92d463fe 100644 --- a/elastica/rod/data_structures.py +++ b/elastica/rod/data_structures.py @@ -50,7 +50,16 @@ class _RodSymplecticStepperMixin: - def __init__(self: SymplecticSystemProtocol) -> None: + + position_collection: NDArray[np.float64] + director_collection: NDArray[np.float64] + velocity_collection: NDArray[np.float64] + omega_collection: NDArray[np.float64] + + v_w_collection: NDArray[np.float64] + dvdt_dwdt_collection: NDArray[np.float64] + + def __init__(self) -> None: self.kinematic_states = _KinematicState( self.position_collection, self.director_collection ) diff --git a/elastica/rod/energy.py b/elastica/rod/energy.py new file mode 100644 index 00000000..4b5be68b --- /dev/null +++ b/elastica/rod/energy.py @@ -0,0 +1,117 @@ +""" +Energy computation mixin for Cosserat rods. + +This mixin provides methods to compute various energy quantities +(translational, rotational, bending, shear) and center of mass properties. +""" + +import numpy as np +from numpy.typing import NDArray + +from elastica._linalg import _batch_matvec, _batch_dot + + +class RodEnergy: + """ + Mixin class providing energy computation methods for rods. + + This mixin should be used with RodBase-derived classes that have + the required attributes (mass, velocity, omega, etc.). + + Example usage:: + + class MyRod(RodBase, RodEnergy): + ... + + rod = MyRod(...) + kinetic_energy = rod.compute_translational_energy() + bending_energy = rod.compute_bending_energy() + + """ + + # Required attributes (provided by RodBase-derived class) + mass: NDArray[np.float64] + velocity_collection: NDArray[np.float64] + position_collection: NDArray[np.float64] + omega_collection: NDArray[np.float64] + mass_second_moment_of_inertia: NDArray[np.float64] + dilatation: NDArray[np.float64] + kappa: NDArray[np.float64] + rest_kappa: NDArray[np.float64] + bend_matrix: NDArray[np.float64] + rest_voronoi_lengths: NDArray[np.float64] + sigma: NDArray[np.float64] + rest_sigma: NDArray[np.float64] + shear_matrix: NDArray[np.float64] + rest_lengths: NDArray[np.float64] + + def compute_translational_energy(self) -> NDArray[np.float64]: + """ + Compute total translational energy of the rod at the instance. + """ + return ( + 0.5 + * ( + self.mass + * np.einsum( + "ij, ij-> j", self.velocity_collection, self.velocity_collection + ) + ).sum() + ) + + def compute_rotational_energy(self) -> NDArray[np.float64]: + """ + Compute total rotational energy of the rod at the instance. + """ + J_omega_upon_e = ( + _batch_matvec(self.mass_second_moment_of_inertia, self.omega_collection) + / self.dilatation + ) + return 0.5 * np.einsum("ik,ik->k", self.omega_collection, J_omega_upon_e).sum() + + def compute_velocity_center_of_mass(self) -> NDArray[np.float64]: + """ + Compute velocity center of mass of the rod at the instance. + """ + mass_times_velocity = np.einsum("j,ij->ij", self.mass, self.velocity_collection) + sum_mass_times_velocity = np.einsum("ij->i", mass_times_velocity) + + return sum_mass_times_velocity / self.mass.sum() + + def compute_position_center_of_mass(self) -> NDArray[np.float64]: + """ + Compute position center of mass of the rod at the instance. + """ + mass_times_position = np.einsum("j,ij->ij", self.mass, self.position_collection) + sum_mass_times_position = np.einsum("ij->i", mass_times_position) + + return sum_mass_times_position / self.mass.sum() + + def compute_bending_energy(self) -> NDArray[np.float64]: + """ + Compute total bending energy of the rod at the instance. + """ + + kappa_diff = self.kappa - self.rest_kappa + bending_internal_torques = _batch_matvec(self.bend_matrix, kappa_diff) + + return ( + 0.5 + * ( + _batch_dot(kappa_diff, bending_internal_torques) + * self.rest_voronoi_lengths + ).sum() + ) + + def compute_shear_energy(self) -> NDArray[np.float64]: + """ + Compute total shear energy of the rod at the instance. + """ + + sigma_diff = self.sigma - self.rest_sigma + shear_internal_forces = _batch_matvec(self.shear_matrix, sigma_diff) + + return ( + 0.5 + * (_batch_dot(sigma_diff, shear_internal_forces) * self.rest_lengths).sum() + ) diff --git a/elastica/rod/knot_theory.py b/elastica/rod/knot_theory.py index f43cbabb..9f9c1569 100644 --- a/elastica/rod/knot_theory.py +++ b/elastica/rod/knot_theory.py @@ -14,11 +14,8 @@ from numpy.typing import NDArray from numba import njit -from elastica.rod.rod_base import RodBase from elastica._linalg import _batch_norm, _batch_dot, _batch_cross -from .protocol import CosseratRodProtocol - class KnotTheory: """ @@ -56,7 +53,13 @@ def __init__(self) -> None: """ - def compute_twist(self: CosseratRodProtocol) -> NDArray[np.float64]: + # Required attributes (provided by RodBase-derived class) + position_collection: NDArray[np.float64] + director_collection: NDArray[np.float64] + rest_lengths: NDArray[np.float64] + radius: NDArray[np.float64] + + def compute_twist(self) -> NDArray[np.float64]: """ See :ref:`api/rods:Knot Theory (Mixin)` for the detail. """ @@ -67,7 +70,7 @@ def compute_twist(self: CosseratRodProtocol) -> NDArray[np.float64]: return total_twist[0] def compute_writhe( - self: CosseratRodProtocol, + self, type_of_additional_segment: str = "next_tangent", alpha: float = 1.0, ) -> NDArray[np.float64]: @@ -90,7 +93,7 @@ def compute_writhe( )[0] def compute_link( - self: CosseratRodProtocol, + self, type_of_additional_segment: str = "next_tangent", alpha: float = 1.0, ) -> NDArray[np.float64]: diff --git a/elastica/rod/protocol.py b/elastica/rod/protocol.py deleted file mode 100644 index cfc19a82..00000000 --- a/elastica/rod/protocol.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import Protocol - -import numpy as np -from numpy.typing import NDArray - -from elastica.systems.protocol import SystemProtocol, SlenderBodyGeometryProtocol - - -class _CosseratRodEnergy(Protocol): - def compute_bending_energy(self) -> NDArray[np.float64]: ... - - def compute_shear_energy(self) -> NDArray[np.float64]: ... - - def compute_translational_energy(self) -> NDArray[np.float64]: ... - - def compute_rotational_energy(self) -> NDArray[np.float64]: ... - - -class CosseratRodProtocol( - SystemProtocol, SlenderBodyGeometryProtocol, _CosseratRodEnergy, Protocol -): - - mass: NDArray[np.float64] - volume: NDArray[np.float64] - radius: NDArray[np.float64] - tangents: NDArray[np.float64] - lengths: NDArray[np.float64] - rest_lengths: NDArray[np.float64] - rest_voronoi_lengths: NDArray[np.float64] - kappa: NDArray[np.float64] - sigma: NDArray[np.float64] - rest_kappa: NDArray[np.float64] - rest_sigma: NDArray[np.float64] - - internal_stress: NDArray[np.float64] - internal_couple: NDArray[np.float64] - dilatation: NDArray[np.float64] - dilatation_rate: NDArray[np.float64] - voronoi_dilatation: NDArray[np.float64] - - bend_matrix: NDArray[np.float64] - shear_matrix: NDArray[np.float64] - - mass_second_moment_of_inertia: NDArray[np.float64] - inv_mass_second_moment_of_inertia: NDArray[np.float64] - - ghost_voronoi_idx: NDArray[np.int32] - ghost_elems_idx: NDArray[np.int32] - - ring_rod_flag: bool - periodic_boundary_nodes_idx: NDArray[np.int32] - periodic_boundary_elems_idx: NDArray[np.int32] - periodic_boundary_voronoi_idx: NDArray[np.int32] diff --git a/elastica/rod/rod_base.py b/elastica/rod/rod_base.py index 3bd846b7..2ce30331 100644 --- a/elastica/rod/rod_base.py +++ b/elastica/rod/rod_base.py @@ -4,8 +4,11 @@ import numpy as np from numpy.typing import NDArray +from elastica.rod.energy import RodEnergy +from elastica.rod.knot_theory import KnotTheory -class RodBase: + +class RodBase(RodEnergy, KnotTheory): """ Base class for all rods. @@ -17,22 +20,59 @@ class RodBase: REQUISITE_MODULES: list[Type] = [] - def __init__(self) -> None: - """ - RodBase does not take any arguments. - """ - self.position_collection: NDArray[np.float64] - self.velocity_collection: NDArray[np.float64] - self.acceleration_collection: NDArray[np.float64] - self.director_collection: NDArray[np.float64] - self.omega_collection: NDArray[np.float64] - self.alpha_collection: NDArray[np.float64] - self.external_forces: NDArray[np.float64] - self.external_torques: NDArray[np.float64] - - self.ghost_voronoi_idx: NDArray[np.int32] - self.ghost_elems_idx: NDArray[np.int32] - - self.periodic_boundary_nodes_idx: NDArray[np.int32] - self.periodic_boundary_elems_idx: NDArray[np.int32] - self.periodic_boundary_voronoi_idx: NDArray[np.int32] + # Geometry + n_elems: int + n_nodes: int + + # State arrays + position_collection: NDArray[np.float64] + velocity_collection: NDArray[np.float64] + acceleration_collection: NDArray[np.float64] + director_collection: NDArray[np.float64] + omega_collection: NDArray[np.float64] + alpha_collection: NDArray[np.float64] + + # External forces/torques + external_forces: NDArray[np.float64] + external_torques: NDArray[np.float64] + + # Internal forces/torques + internal_forces: NDArray[np.float64] + internal_torques: NDArray[np.float64] + + # Rod-specific properties + mass: NDArray[np.float64] + volume: NDArray[np.float64] + radius: NDArray[np.float64] + tangents: NDArray[np.float64] + lengths: NDArray[np.float64] + rest_lengths: NDArray[np.float64] + rest_voronoi_lengths: NDArray[np.float64] + kappa: NDArray[np.float64] + sigma: NDArray[np.float64] + rest_kappa: NDArray[np.float64] + rest_sigma: NDArray[np.float64] + + internal_stress: NDArray[np.float64] + internal_couple: NDArray[np.float64] + dilatation: NDArray[np.float64] + dilatation_rate: NDArray[np.float64] + voronoi_dilatation: NDArray[np.float64] + + bend_matrix: NDArray[np.float64] + shear_matrix: NDArray[np.float64] + + mass_second_moment_of_inertia: NDArray[np.float64] + inv_mass_second_moment_of_inertia: NDArray[np.float64] + + # Ring rod / periodic boundary + ring_rod_flag: bool + ghost_voronoi_idx: NDArray[np.int32] + ghost_elems_idx: NDArray[np.int32] + periodic_boundary_nodes_idx: NDArray[np.int32] + periodic_boundary_elems_idx: NDArray[np.int32] + periodic_boundary_voronoi_idx: NDArray[np.int32] + + # Symplectic stepper state + v_w_collection: NDArray[np.float64] + dvdt_dwdt_collection: NDArray[np.float64] From 8590c56e697419fcbf581629cd453b97fc14a9de Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 14 Dec 2025 20:04:00 -0600 Subject: [PATCH 58/85] refactor: rename rigid body import and restructure rigid body classes by introducing RigidBodyBase, removing unused protocol --- elastica/rigidbody/__init__.py | 2 +- elastica/rigidbody/cylinder.py | 17 ++--------------- elastica/rigidbody/protocol.py | 18 ------------------ .../{rigid_body.py => rigid_body_base.py} | 4 +++- elastica/rigidbody/sphere.py | 14 ++------------ 5 files changed, 8 insertions(+), 47 deletions(-) delete mode 100644 elastica/rigidbody/protocol.py rename elastica/rigidbody/{rigid_body.py => rigid_body_base.py} (97%) diff --git a/elastica/rigidbody/__init__.py b/elastica/rigidbody/__init__.py index 732577df..1b3e8745 100644 --- a/elastica/rigidbody/__init__.py +++ b/elastica/rigidbody/__init__.py @@ -1,3 +1,3 @@ -from .rigid_body import RigidBodyBase +from .rigid_body_base import RigidBodyBase from .cylinder import Cylinder from .sphere import Sphere diff --git a/elastica/rigidbody/cylinder.py b/elastica/rigidbody/cylinder.py index cde0155d..d61d1562 100644 --- a/elastica/rigidbody/cylinder.py +++ b/elastica/rigidbody/cylinder.py @@ -1,14 +1,14 @@ __doc__ = """ Implementation of a rigid body cylinder. """ -from typing import TYPE_CHECKING +pass import numpy as np from numpy.typing import NDArray from elastica._linalg import _batch_cross from elastica.utils import MaxDimension -from elastica.rigidbody.rigid_body import RigidBodyBase +from elastica.rigidbody.rigid_body_base import RigidBodyBase class Cylinder(RigidBodyBase): @@ -114,16 +114,3 @@ def assert_check_lower_bound( self.director_collection[0, ...] = normal self.director_collection[1, ...] = binormal self.director_collection[2, ...] = tangents - - -if TYPE_CHECKING: - from .protocol import RigidBodyProtocol - - _: RigidBodyProtocol = Cylinder( - start=np.zeros(3), - direction=np.ones(3), - normal=np.ones(3), - base_length=1.0, - base_radius=1.0, - density=1.0, - ) diff --git a/elastica/rigidbody/protocol.py b/elastica/rigidbody/protocol.py deleted file mode 100644 index 7e6f0664..00000000 --- a/elastica/rigidbody/protocol.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Protocol - -import numpy as np -from numpy.typing import NDArray - -from elastica.systems.protocol import SystemProtocol, SlenderBodyGeometryProtocol - - -class RigidBodyProtocol(SystemProtocol, SlenderBodyGeometryProtocol, Protocol): - - mass: np.float64 - volume: np.float64 - length: np.float64 - tangents: NDArray[np.float64] - radius: np.float64 - - mass_second_moment_of_inertia: NDArray[np.float64] - inv_mass_second_moment_of_inertia: NDArray[np.float64] diff --git a/elastica/rigidbody/rigid_body.py b/elastica/rigidbody/rigid_body_base.py similarity index 97% rename from elastica/rigidbody/rigid_body.py rename to elastica/rigidbody/rigid_body_base.py index 99920854..17b8cd26 100644 --- a/elastica/rigidbody/rigid_body.py +++ b/elastica/rigidbody/rigid_body_base.py @@ -6,10 +6,12 @@ import numpy as np from numpy.typing import NDArray + from elastica._linalg import _batch_matvec, _batch_cross +from elastica.systems.protocol import SystemProtocol -class RigidBodyBase(ABC): +class RigidBodyBase(ABC, SystemProtocol): """ Base class for rigid body classes. diff --git a/elastica/rigidbody/sphere.py b/elastica/rigidbody/sphere.py index 154cdd48..bde000ad 100644 --- a/elastica/rigidbody/sphere.py +++ b/elastica/rigidbody/sphere.py @@ -1,14 +1,14 @@ __doc__ = """ Implementation of a sphere rigid body. """ -from typing import TYPE_CHECKING +pass import numpy as np from numpy.typing import NDArray from elastica._linalg import _batch_cross from elastica.utils import MaxDimension -from elastica.rigidbody.rigid_body import RigidBodyBase +from elastica.rigidbody.rigid_body_base import RigidBodyBase class Sphere(RigidBodyBase): @@ -81,13 +81,3 @@ def __init__( self.director_collection[0, ...] = normal self.director_collection[1, ...] = binormal self.director_collection[2, ...] = tangents - - -if TYPE_CHECKING: - from .protocol import RigidBodyProtocol - - _: RigidBodyProtocol = Sphere( - center=np.zeros(3), - base_radius=1.0, - density=1.0, - ) From 132d7b4a59e0a8a0ba269a54bc66038b01226896 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 14 Dec 2025 20:04:35 -0600 Subject: [PATCH 59/85] refactor: enhance protocol definitions for explicit and symplectic systems, improving type handling and organization --- elastica/experimental/timestepper/protocol.py | 39 +++++++++-- elastica/systems/protocol.py | 70 +++++++++++-------- elastica/timestepper/symplectic_steppers.py | 16 ++--- tests/analytical.py | 3 + 4 files changed, 83 insertions(+), 45 deletions(-) diff --git a/elastica/experimental/timestepper/protocol.py b/elastica/experimental/timestepper/protocol.py index b8d489fa..0606928a 100644 --- a/elastica/experimental/timestepper/protocol.py +++ b/elastica/experimental/timestepper/protocol.py @@ -1,22 +1,47 @@ from typing import Protocol -from elastica.typing import StepType, StateType -from elastica.systems.protocol import SystemProtocol, SlenderBodyGeometryProtocol +from elastica.typing import StepType +from elastica.systems.protocol import SystemProtocol from elastica.timestepper.protocol import StepperProtocol +from elastica.rod.data_structures import _State as StateType import numpy as np +from numpy.typing import NDArray -class ExplicitSystemProtocol(SystemProtocol, SlenderBodyGeometryProtocol, Protocol): - # TODO: Temporarily made to handle explicit stepper. - # Need to be refactored as the explicit stepper is further developed. +class ExplicitSystemProtocol(SystemProtocol, Protocol): + """ + Protocol defining the required interface for explicit time integration. + + TODO: Temporarily made to handle explicit stepper. + Need to be refactored as the explicit stepper is further developed. + """ + + # Geometry + n_nodes: int + n_elems: int + + # State arrays + position_collection: NDArray[np.float64] + velocity_collection: NDArray[np.float64] + acceleration_collection: NDArray[np.float64] + omega_collection: NDArray[np.float64] + alpha_collection: NDArray[np.float64] + director_collection: NDArray[np.float64] + + # Forces/torques + external_forces: NDArray[np.float64] + external_torques: NDArray[np.float64] + internal_forces: NDArray[np.float64] + internal_torques: NDArray[np.float64] + def __call__(self, time: np.float64, dt: np.float64) -> np.float64: ... + @property def state(self) -> StateType: ... + @state.setter def state(self, state: StateType) -> None: ... - @property - def n_elems(self) -> int: ... class MemoryProtocol(Protocol): diff --git a/elastica/systems/protocol.py b/elastica/systems/protocol.py index 9075674b..7d6ffb7b 100644 --- a/elastica/systems/protocol.py +++ b/elastica/systems/protocol.py @@ -1,7 +1,8 @@ __doc__ = """Base class for elastica system""" from typing import Protocol, Type, runtime_checkable -from elastica.typing import StateType, SystemType + +from abc import abstractmethod from elastica.rod.data_structures import _KinematicState, _DynamicState @@ -9,64 +10,73 @@ from numpy.typing import NDArray -# TODO: Better organize this part @runtime_checkable class StaticSystemBase(Protocol): + """ + Protocol for all static elastica system. Minimal requirement interface + to be included in the simulator. + """ + REQUISITE_MODULES: list[Type] class SystemProtocol(StaticSystemBase, Protocol): """ - Protocol for all dynamic elastica system + Protocol for all dynamic elastica system. """ + @abstractmethod def compute_internal_forces_and_torques(self, time: np.float64) -> None: ... + @abstractmethod def update_accelerations(self, time: np.float64) -> None: ... + @abstractmethod def zeroed_out_external_forces_and_torques(self, time: np.float64) -> None: ... -class SlenderBodyGeometryProtocol(Protocol): - @property - def n_nodes(self) -> int: ... - - @property - def n_elems(self) -> int: ... - - position_collection: NDArray[np.float64] - velocity_collection: NDArray[np.float64] - acceleration_collection: NDArray[np.float64] - - omega_collection: NDArray[np.float64] - alpha_collection: NDArray[np.float64] - director_collection: NDArray[np.float64] +class SymplecticSystemProtocol(SystemProtocol, Protocol): + """ + Protocol defining the required interface for symplectic time integration. + Typically, implementation of these properties are provided in data_structures.py + for the specific system, and use to build the block structure. - external_forces: NDArray[np.float64] - external_torques: NDArray[np.float64] + Any class used with the symplectic timesteppers in :mod:`elastica.timestepper` + (e.g., :class:`PositionVerlet`, :class:`PEFRL`) must satisfy this protocol. - internal_forces: NDArray[np.float64] - internal_torques: NDArray[np.float64] + The symplectic stepper accesses: + - ``n_nodes`` + - ``kinematic_states`` (position_collection, director_collection) + - ``dynamic_states`` (velocity_collection, omega_collection, rate_collection) + - ``dynamic_rates(time, prefac)`` to compute acceleration updates + See Also + -------- + elastica.timestepper.symplectic_steppers : Symplectic stepper implementations + elastica.rod.CosseratRod : A concrete implementation satisfying this protocol -class SymplecticSystemProtocol(SystemProtocol, SlenderBodyGeometryProtocol, Protocol): - """ - Protocol for system with symplectic state variables """ - v_w_collection: NDArray[np.float64] - dvdt_dwdt_collection: NDArray[np.float64] + n_nodes: int @property - def kinematic_states(self) -> _KinematicState: ... + def kinematic_states(self) -> _KinematicState: + """Return kinematic state.""" + ... @property - def dynamic_states(self) -> _DynamicState: ... + def dynamic_states(self) -> _DynamicState: + """Return dynamic state.""" + ... def kinematic_rates( self, time: np.float64, prefac: np.float64 - ) -> tuple[NDArray[np.float64], NDArray[np.float64]]: ... + ) -> tuple[NDArray[np.float64], NDArray[np.float64]]: + """Compute kinematic rates.""" + ... def dynamic_rates( self, time: np.float64, prefac: np.float64 - ) -> NDArray[np.float64]: ... + ) -> NDArray[np.float64]: + """Compute dynamic rates.""" + ... diff --git a/elastica/timestepper/symplectic_steppers.py b/elastica/timestepper/symplectic_steppers.py index c7da9e0a..a13b935a 100644 --- a/elastica/timestepper/symplectic_steppers.py +++ b/elastica/timestepper/symplectic_steppers.py @@ -190,8 +190,8 @@ def _first_kinematic_step( prefac, System.kinematic_states.position_collection, System.kinematic_states.director_collection, - System.velocity_collection, - System.omega_collection, + System.dynamic_states.velocity_collection, + System.dynamic_states.omega_collection, ) def _first_dynamic_step( @@ -250,8 +250,8 @@ def _first_kinematic_step( prefac, System.kinematic_states.position_collection, System.kinematic_states.director_collection, - System.velocity_collection, - System.omega_collection, + System.dynamic_states.velocity_collection, + System.dynamic_states.omega_collection, ) # System.kinematic_states += prefac * System.kinematic_rates(time, prefac) @@ -277,8 +277,8 @@ def _second_kinematic_step( prefac, System.kinematic_states.position_collection, System.kinematic_states.director_collection, - System.velocity_collection, - System.omega_collection, + System.dynamic_states.velocity_collection, + System.dynamic_states.omega_collection, ) # System.kinematic_states += prefac * System.kinematic_rates(time, prefac) @@ -305,8 +305,8 @@ def _third_kinematic_step( prefac, System.kinematic_states.position_collection, System.kinematic_states.director_collection, - System.velocity_collection, - System.omega_collection, + System.dynamic_states.velocity_collection, + System.dynamic_states.omega_collection, ) # System.kinematic_states += prefac * System.kinematic_rates(time, prefac) diff --git a/tests/analytical.py b/tests/analytical.py index 05b395e5..c045105c 100644 --- a/tests/analytical.py +++ b/tests/analytical.py @@ -65,6 +65,9 @@ def __init__(self, state): for k in range(blocksize + 1): self.rate_collection[:, k] = state self.n_kinematic_rates = blocksize + # velocity and omega are accessed via dynamic_states by the stepper + self.velocity_collection = self.rate_collection[..., 0].reshape(3, 1) + self.omega_collection = self.rate_collection[..., 1].reshape(3, 1) # class BaseLinearStatefulSystem: From 16c25fd64ce55c6eafd840049f5380c8b7d1a34b Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 14 Dec 2025 20:07:58 -0600 Subject: [PATCH 60/85] refactor: update rigid body and surface documentation, rename imports, and enhance type handling for clarity --- docs/api/rigidbody.rst | 2 +- docs/api/surface.rst | 19 +++++++++++-------- elastica/__init__.py | 2 +- elastica/contact_forces.py | 2 +- elastica/memory_block/protocol.py | 10 ---------- elastica/surface/plane.py | 9 +++++---- elastica/typing.py | 23 +++++++---------------- tests/test_modules/test_base_system.py | 16 ++++++---------- tests/test_rod/test_knot_theory.py | 9 --------- 9 files changed, 32 insertions(+), 60 deletions(-) diff --git a/docs/api/rigidbody.rst b/docs/api/rigidbody.rst index e7b863cb..13569b98 100644 --- a/docs/api/rigidbody.rst +++ b/docs/api/rigidbody.rst @@ -9,7 +9,7 @@ Rigid Body | Sphere | | +----------+----+ -.. automodule:: elastica.rigidbody.rigid_body +.. automodule:: elastica.rigidbody.rigid_body_base :members: :exclude-members: __weakref__, update_acceleration, zeroed_out_external_forces_and_torques diff --git a/docs/api/surface.rst b/docs/api/surface.rst index 55d4615d..6b68f328 100644 --- a/docs/api/surface.rst +++ b/docs/api/surface.rst @@ -1,15 +1,18 @@ Surface ========== -+----------+----+ -| type | | -+==========+====+ -| plane | | -+----------+----+ +Description +----------- -.. automodule:: elastica.surface.surface_base - :members: - :exclude-members: __weakref__ +Surface is a static system that does not change by the timestepping. + +Available Surface Types +----------------------- ++----------+---------------------+ +| type | description | ++==========+=====================+ +| plane | Plane static system.| ++----------+---------------------+ .. automodule:: elastica.surface.plane :members: diff --git a/elastica/__init__.py b/elastica/__init__.py index 0e0f5d1b..2af0066b 100644 --- a/elastica/__init__.py +++ b/elastica/__init__.py @@ -5,7 +5,7 @@ ) from elastica.rod.rod_base import RodBase from elastica.rod.cosserat_rod import CosseratRod -from elastica.rigidbody.rigid_body import RigidBodyBase +from elastica.rigidbody.rigid_body_base import RigidBodyBase from elastica.rigidbody.cylinder import Cylinder from elastica.rigidbody.sphere import Sphere from elastica.surface.plane import Plane diff --git a/elastica/contact_forces.py b/elastica/contact_forces.py index 417b6feb..e45b5ab4 100644 --- a/elastica/contact_forces.py +++ b/elastica/contact_forces.py @@ -1,7 +1,7 @@ __doc__ = """ Numba implementation module containing contact between rods and rigid bodies and other rods rigid bodies or surfaces.""" from typing import TypeVar, Generic, Type -from elastica.typing import RodType, SystemType, CosseratRodProtocol +from elastica.typing import RodType, SystemType from elastica.rod.rod_base import RodBase from elastica.rigidbody.cylinder import Cylinder diff --git a/elastica/memory_block/protocol.py b/elastica/memory_block/protocol.py index 51cd57d5..87bfe554 100644 --- a/elastica/memory_block/protocol.py +++ b/elastica/memory_block/protocol.py @@ -1,6 +1,4 @@ from typing import Protocol -from elastica.rod.protocol import CosseratRodProtocol -from elastica.rigidbody.protocol import RigidBodyProtocol from elastica.systems.protocol import SystemProtocol @@ -12,11 +10,3 @@ def n_systems(self) -> int: class BlockSystemProtocol(SystemProtocol, BlockProtocol, Protocol): pass - - -class BlockRodProtocol(BlockProtocol, CosseratRodProtocol, Protocol): - pass - - -class BlockRigidBodyProtocol(BlockProtocol, RigidBodyProtocol, Protocol): - pass diff --git a/elastica/surface/plane.py b/elastica/surface/plane.py index 7ebe7c8e..51794e1a 100644 --- a/elastica/surface/plane.py +++ b/elastica/surface/plane.py @@ -1,20 +1,21 @@ __doc__ = """""" -from typing import Type -from elastica.systems.protocol import StaticSystemBase import numpy as np from numpy.typing import NDArray from elastica.utils import Tolerance +from elastica.systems.protocol import StaticSystemBase class Plane(StaticSystemBase): - REQUISITE_MODULES: list[Type] = [] + """ + Plane static system. Static system does not change by the timestepping. + """ def __init__( self, plane_origin: NDArray[np.float64], plane_normal: NDArray[np.float64] ): """ - Plane surface initializer. + Plane initializer. Parameters ---------- diff --git a/elastica/typing.py b/elastica/typing.py index 9c8805e9..d6beabeb 100644 --- a/elastica/typing.py +++ b/elastica/typing.py @@ -14,12 +14,11 @@ if TYPE_CHECKING: # Used for type hinting without circular imports # NEVER BACK-IMPORT ANY ELASTICA MODULES HERE - from .rod.protocol import CosseratRodProtocol - from .rigidbody.protocol import RigidBodyProtocol + from .rod.rod_base import RodBase + from .rigidbody.rigid_body_base import RigidBodyBase from .modules.base_system import BaseSystemCollection from .modules.protocol import SystemCollectionProtocol - from .rod.data_structures import _State as State from .systems.protocol import ( StaticSystemBase, SystemProtocol, @@ -32,12 +31,11 @@ from .memory_block.protocol import BlockSystemProtocol else: - CosseratRodProtocol = "CosseratRodProtocol" - RigidBodyProtocol = "RigidBodyProtocol" + RodBase = "RodBase" + RigidBodyType = "RigidBodyBase" BaseSystemCollection = "BaseSystemCollection" SystemCollectionProtocol = "SystemCollectionProtocol" - State = "State" SystemProtocol = "SystemProtocol" StaticSystemBase = "StaticSystemBase" SymplecticSystemProtocol = "SymplecticSystemProtocol" @@ -47,26 +45,19 @@ StaticSystemType: TypeAlias = "StaticSystemBase" -SystemType: TypeAlias = SystemProtocol +SystemType: TypeAlias = "SystemProtocol" SystemIdxType: TypeAlias = int BlockSystemType: TypeAlias = "BlockSystemProtocol" - -# Mostly used in explicit stepper: for symplectic, use kinetic and dynamic state -StateType: TypeAlias = "State" - -# TODO: Maybe can be more specific. Up for discussion. StepType: TypeAlias = Callable[..., Any] SteppersOperatorsType: TypeAlias = tuple[tuple[StepType, ...], ...] - -RodType: TypeAlias = "CosseratRodProtocol" -RigidBodyType: TypeAlias = "RigidBodyProtocol" +RodType: TypeAlias = "RodBase" +RigidBodyType: TypeAlias = "RigidBodyBase" SystemCollectionType: TypeAlias = "SystemCollectionProtocol" # Indexing types -# TODO: Maybe just use slice?? ConstrainingIndex: TypeAlias = tuple[int, ...] ConnectionIndex: TypeAlias = ( int | np.int32 | list[int] | tuple[int, ...] | np.typing.NDArray[np.int32] diff --git a/tests/test_modules/test_base_system.py b/tests/test_modules/test_base_system.py index 3f07bd6e..6d907879 100644 --- a/tests/test_modules/test_base_system.py +++ b/tests/test_modules/test_base_system.py @@ -76,18 +76,14 @@ def test_extend_allowed_types(self, load_collection): from elastica.rod import RodBase from elastica.rigidbody import RigidBodyBase - from elastica.systems.protocol import StaticSystemBase + from elastica.systems.protocol import StaticSystemBase, SystemProtocol # Types are extended in the fixture - assert bsc.allowed_sys_types == ( - RodBase, - RigidBodyBase, - StaticSystemBase, - int, - float, - str, - np.ndarray, - ) + assert int in bsc.allowed_sys_types + assert float in bsc.allowed_sys_types + assert str in bsc.allowed_sys_types + assert np.ndarray in bsc.allowed_sys_types + assert StaticSystemBase in bsc.allowed_sys_types # Minimal requirement def test_extend_correctness(self, load_collection): """ diff --git a/tests/test_rod/test_knot_theory.py b/tests/test_rod/test_knot_theory.py index 03004d23..b6d8e91f 100644 --- a/tests/test_rod/test_knot_theory.py +++ b/tests/test_rod/test_knot_theory.py @@ -17,8 +17,6 @@ _compute_additional_segment, ) -from elastica.rod.protocol import CosseratRodProtocol - @pytest.fixture def knot_theory(): @@ -27,13 +25,6 @@ def knot_theory(): return knot_theory -def test_knot_theory_protocol(): - # To clear the protocol test coverage - with pytest.raises(TypeError) as e_info: - protocol = CosseratRodProtocol() - assert "cannot be instantiated" in e_info - - def test_knot_theory_mixin_methods(rng, knot_theory): class TestRodWithKnotTheory(MockTestRod, knot_theory.KnotTheory): def __init__(self): From 3f12b70747db71fe3f82c8859edde7b8e43e21de Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 14 Dec 2025 20:08:54 -0600 Subject: [PATCH 61/85] refactor: implement get_relative_rotation_two_systems function for computing relative rotation between two systems and update imports accordingly --- elastica/_rotations.py | 52 +++++++ elastica/joint.py | 55 +------- examples/JointCases/fixed_joint_torsion.py | 2 +- tests/test_math/test_rotations.py | 155 +++++++++++++++++++++ 4 files changed, 209 insertions(+), 55 deletions(-) diff --git a/elastica/_rotations.py b/elastica/_rotations.py index b58cc356..63f92a17 100644 --- a/elastica/_rotations.py +++ b/elastica/_rotations.py @@ -9,6 +9,7 @@ from numba import njit +from elastica.typing import RodType, RigidBodyType, ConnectionIndex from elastica._linalg import _batch_matmul @@ -385,3 +386,54 @@ def _inv_skew_symmetrize(matrix: NDArray[np.float64]) -> NDArray[np.float64]: vector[tgt_index] = matrix[src_i, src_j] return vector + + +def get_relative_rotation_two_systems( + system_one: "RodType | RigidBodyType", + index_one: "ConnectionIndex", + system_two: "RodType | RigidBodyType", + index_two: "ConnectionIndex", +) -> NDArray[np.float64]: + """ + Compute the relative rotation matrix C_12 between system one and system two at the specified elements. + + Examples + ---------- + How to get the relative rotation between two systems (e.g. the rotation from end of rod one to base of rod two): + + >>> rel_rot_mat = get_relative_rotation_two_systems(system1, -1, system2, 0) + + How to initialize a FixedJoint with a rest rotation between the two systems, + which is enforced throughout the simulation: + + >>> simulator.connect( + ... first_rod=system1, second_rod=system2, first_connect_idx=-1, second_connect_idx=0 + ... ).using( + ... FixedJoint, + ... ku=1e6, nu=0.0, kt=1e3, nut=0.0, + ... rest_rotation_matrix=get_relative_rotation_two_systems(system1, -1, system2, 0) + ... ) + + See Also + --------- + FixedJoint + + Parameters + ---------- + system_one : RodType | RigidBodyType + Rod or rigid-body object + index_one : ConnectionIndex + Index of first system for connection. + system_two : RodType | RigidBodyType + Rod or rigid-body object + index_two : ConnectionIndex + Index of second system for connection. + + Returns + ------- + relative_rotation_matrix : np.array + Relative rotation matrix C_12 between the two systems for their current state. + """ + director_one = system_one.director_collection[..., index_one] + director_two = system_two.director_collection[..., index_two] + return director_one @ director_two.T diff --git a/elastica/joint.py b/elastica/joint.py index a9c3f9d7..774c5a7e 100644 --- a/elastica/joint.py +++ b/elastica/joint.py @@ -1,5 +1,5 @@ __doc__ = """ Module containing joint classes to connect multiple rods together. """ -__all__ = ["FreeJoint", "HingeJoint", "FixedJoint", "get_relative_rotation_two_systems"] +__all__ = ["FreeJoint", "HingeJoint", "FixedJoint"] from typing import TypeVar, Generic from abc import ABC, abstractmethod @@ -389,56 +389,3 @@ def apply_torques( # The opposite torques will be applied to system one and two after rotating the torques into the local frame system_one.external_torques[..., index_one] -= system_one_director @ torque system_two.external_torques[..., index_two] += system_two_director @ torque - - -# TODO: Remove this -def get_relative_rotation_two_systems( - system_one: "RodType | RigidBodyType", - index_one: ConnectionIndex, - system_two: "RodType | RigidBodyType", - index_two: ConnectionIndex, -) -> NDArray[np.float64]: - """ - Compute the relative rotation matrix C_12 between system one and system two at the specified elements. - - Examples - ---------- - How to get the relative rotation between two systems (e.g. the rotation from end of rod one to base of rod two): - - >>> rel_rot_mat = get_relative_rotation_two_systems(system1, -1, system2, 0) - - How to initialize a FixedJoint with a rest rotation between the two systems, - which is enforced throughout the simulation: - - >>> simulator.connect( - ... first_rod=system1, second_rod=system2, first_connect_idx=-1, second_connect_idx=0 - ... ).using( - ... FixedJoint, - ... ku=1e6, nu=0.0, kt=1e3, nut=0.0, - ... rest_rotation_matrix=get_relative_rotation_two_systems(system1, -1, system2, 0) - ... ) - - See Also - --------- - FixedJoint - - Parameters - ---------- - system_one : RodType | RigidBodyType - Rod or rigid-body object - index_one : ConnectionIndex - Index of first system for connection. - system_two : RodType | RigidBodyType - Rod or rigid-body object - index_two : ConnectionIndex - Index of second system for connection. - - Returns - ------- - relative_rotation_matrix : np.array - Relative rotation matrix C_12 between the two systems for their current state. - """ - return ( - system_one.director_collection[..., index_one] - @ system_two.director_collection[..., index_two].T - ) diff --git a/examples/JointCases/fixed_joint_torsion.py b/examples/JointCases/fixed_joint_torsion.py index 4331d218..4181cea2 100644 --- a/examples/JointCases/fixed_joint_torsion.py +++ b/examples/JointCases/fixed_joint_torsion.py @@ -3,7 +3,7 @@ import numpy as np from collections import defaultdict import elastica as ea -from elastica.joint import get_relative_rotation_two_systems +from elastica._rotations import get_relative_rotation_two_systems from joint_cases_postprocessing import ( plot_position, plot_orientation, diff --git a/tests/test_math/test_rotations.py b/tests/test_math/test_rotations.py index 1acb664b..0b603ddb 100644 --- a/tests/test_math/test_rotations.py +++ b/tests/test_math/test_rotations.py @@ -8,11 +8,19 @@ _get_rotation_matrix, _rotate, _inv_rotate, + get_relative_rotation_two_systems, ) from elastica.utils import Tolerance +class MockRod: + """Mock rod class with only director_collection attribute for testing.""" + + def __init__(self, director_collection): + self.director_collection = director_collection + + @pytest.mark.parametrize("zcomp", [0.5, 1.0]) @pytest.mark.parametrize("dt", [0.3, 1.0]) def test_get_rotation_matrix_correct_rotation_about_z(zcomp, dt): @@ -373,3 +381,150 @@ def test_inv_rotate_correctness_on_circle_in_two_dimensions_with_different_direc assert test_axis_collection.shape == (3, blocksize - 1) assert_allclose(test_axis_collection, correct_axis_collection) assert_allclose(test_scaling, 0.0 * test_scaling + dtheta_di, atol=Tolerance.atol()) + + +def test_get_relative_rotation_two_systems_identity(): + """Test that two rods with identical orientations give identity rotation matrix.""" + # Create two mock rods with same orientation (identity rotation matrices) + n = 4 + identity_director = np.eye(3) + director_collection = np.tile(identity_director[..., np.newaxis], (1, 1, n + 1)) + + rod1 = MockRod(director_collection) + rod2 = MockRod(director_collection.copy()) + + # Test with different indices + rel_rot = get_relative_rotation_two_systems(rod1, 0, rod2, 0) + rel_rot_end = get_relative_rotation_two_systems(rod1, -1, rod2, -1) + + # Should be identity matrix (or very close to it) + identity = np.eye(3) + assert rel_rot.shape == (3, 3) + assert_allclose(rel_rot, identity, atol=Tolerance.atol()) + assert_allclose(rel_rot_end, identity, atol=Tolerance.atol()) + + +def test_get_relative_rotation_two_systems_known_rotation(): + """Test with rods rotated by a known angle.""" + n = 4 + + # First rod: identity rotation (aligned with standard axes) + director1 = np.eye(3) + director_collection1 = np.tile(director1[..., np.newaxis], (1, 1, n + 1)) + rod1 = MockRod(director_collection1) + + # Second rod: rotated 90 degrees about z-axis + # Rotation matrix for 90 degrees about z-axis + director2 = np.array( + [ + [0.0, 1.0, 0.0], + [-1.0, 0.0, 0.0], + [0.0, 0.0, 1.0], + ] + ) + director_collection2 = np.tile(director2[..., np.newaxis], (1, 1, n + 1)) + rod2 = MockRod(director_collection2) + + rel_rot = get_relative_rotation_two_systems(rod1, 0, rod2, 0) + + # Expected rotation matrix: Since rel_rot = director1 @ director2.T + # and director1 = I, we get director2.T + # director2 is a 90-degree rotation about z-axis, so director2.T is: + expected_rot = np.array( + [ + [0.0, -1.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 0.0, 1.0], + ] + ) + + assert rel_rot.shape == (3, 3) + assert_allclose(rel_rot, expected_rot, atol=Tolerance.atol()) + + +def test_get_relative_rotation_two_systems_properties(): + """Test that the relative rotation matrix has proper rotation matrix properties.""" + n = 4 + + # First rod: identity rotation + director1 = np.eye(3) + director_collection1 = np.tile(director1[..., np.newaxis], (1, 1, n + 1)) + rod1 = MockRod(director_collection1) + + # Second rod: rotated 90 degrees about z-axis + director2 = np.array( + [ + [0.0, 1.0, 0.0], + [-1.0, 0.0, 0.0], + [0.0, 0.0, 1.0], + ] + ) + director_collection2 = np.tile(director2[..., np.newaxis], (1, 1, n + 1)) + rod2 = MockRod(director_collection2) + + rel_rot = get_relative_rotation_two_systems(rod1, 0, rod2, 0) + + # Check orthogonality: R @ R.T should be identity + r_rt = rel_rot @ rel_rot.T + rt_r = rel_rot.T @ rel_rot + identity = np.eye(3) + + assert_allclose(r_rt, identity, atol=Tolerance.atol()) + assert_allclose(rt_r, identity, atol=Tolerance.atol()) + + # Check determinant is 1 (for proper rotation) + det = np.linalg.det(rel_rot) + assert_allclose(det, 1.0, atol=Tolerance.atol()) + + +@pytest.mark.parametrize("index1", [0, -1, 2]) +@pytest.mark.parametrize("index2", [0, -1, 1]) +def test_get_relative_rotation_two_systems_different_indices(index1, index2): + """Test function with different index combinations.""" + n = 4 + + # Create mock rods with identity rotations + director = np.eye(3) + director_collection = np.tile(director[..., np.newaxis], (1, 1, n + 1)) + + rod1 = MockRod(director_collection) + rod2 = MockRod(director_collection.copy()) + + rel_rot = get_relative_rotation_two_systems(rod1, index1, rod2, index2) + + # Should always return a 3x3 matrix + assert rel_rot.shape == (3, 3) + + # Should be a valid rotation matrix + r_rt = rel_rot @ rel_rot.T + assert_allclose(r_rt, np.eye(3), atol=Tolerance.atol()) + assert_allclose(np.linalg.det(rel_rot), 1.0, atol=Tolerance.atol()) + + +def test_get_relative_rotation_two_systems_inverse_relationship(): + """Test that rel_rot(system1, system2) is the inverse of rel_rot(system2, system1).""" + n = 4 + + # First rod: identity rotation + director1 = np.eye(3) + director_collection1 = np.tile(director1[..., np.newaxis], (1, 1, n + 1)) + rod1 = MockRod(director_collection1) + + # Second rod: rotated 90 degrees about z-axis + director2 = np.array( + [ + [0.0, 1.0, 0.0], + [-1.0, 0.0, 0.0], + [0.0, 0.0, 1.0], + ] + ) + director_collection2 = np.tile(director2[..., np.newaxis], (1, 1, n + 1)) + rod2 = MockRod(director_collection2) + + rel_rot_12 = get_relative_rotation_two_systems(rod1, 0, rod2, 0) + rel_rot_21 = get_relative_rotation_two_systems(rod2, 0, rod1, 0) + + # rel_rot_21 should be the transpose (inverse) of rel_rot_12 + assert_allclose(rel_rot_21, rel_rot_12.T, atol=Tolerance.atol()) + # Or equivalently, their product should be identity + assert_allclose(rel_rot_12 @ rel_rot_21, np.eye(3), atol=Tolerance.atol()) From 33d401054add359db4ad906ce3af62fcdbb4e3c6 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 14 Dec 2025 20:09:21 -0600 Subject: [PATCH 62/85] refactor: update imports in explicit steppers and base system modules for improved type handling --- elastica/experimental/timestepper/explicit_steppers.py | 3 ++- elastica/modules/base_system.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/elastica/experimental/timestepper/explicit_steppers.py b/elastica/experimental/timestepper/explicit_steppers.py index 3705ccd2..a8d96792 100644 --- a/elastica/experimental/timestepper/explicit_steppers.py +++ b/elastica/experimental/timestepper/explicit_steppers.py @@ -10,7 +10,6 @@ SystemCollectionType, StepType, SteppersOperatorsType, - StateType, ) from elastica.experimental.timestepper.protocol import ( ExplicitSystemProtocol, @@ -18,6 +17,8 @@ MemoryProtocol, ) +from elastica.rod.data_structures import _State as StateType + """ Developer Note diff --git a/elastica/modules/base_system.py b/elastica/modules/base_system.py index 6a115d67..8e8cae64 100644 --- a/elastica/modules/base_system.py +++ b/elastica/modules/base_system.py @@ -11,7 +11,6 @@ SystemType, StaticSystemType, BlockSystemType, - BlockSystemProtocol, SystemIdxType, OperatorType, OperatorCallbackType, From fce136943b109e52de291e2346f16ebdafc68ef3 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 14 Dec 2025 20:15:38 -0600 Subject: [PATCH 63/85] refactor: clean up imports and update type annotations in constraints, cylinder, sphere, and cosserat rod modules --- elastica/modules/constraints.py | 2 +- elastica/rigidbody/cylinder.py | 1 - elastica/rigidbody/sphere.py | 1 - elastica/rod/cosserat_rod.py | 5 +---- 4 files changed, 2 insertions(+), 7 deletions(-) diff --git a/elastica/modules/constraints.py b/elastica/modules/constraints.py index aaf53a58..b8106be7 100644 --- a/elastica/modules/constraints.py +++ b/elastica/modules/constraints.py @@ -18,7 +18,7 @@ RodType, RigidBodyType, ) -from elastica.typing import RodType +from elastica.typing import RodType # noqa: F811 from .protocol import SystemCollectionProtocol, ModuleProtocol diff --git a/elastica/rigidbody/cylinder.py b/elastica/rigidbody/cylinder.py index d61d1562..fe50d3a9 100644 --- a/elastica/rigidbody/cylinder.py +++ b/elastica/rigidbody/cylinder.py @@ -1,7 +1,6 @@ __doc__ = """ Implementation of a rigid body cylinder. """ -pass import numpy as np from numpy.typing import NDArray diff --git a/elastica/rigidbody/sphere.py b/elastica/rigidbody/sphere.py index bde000ad..b6fec184 100644 --- a/elastica/rigidbody/sphere.py +++ b/elastica/rigidbody/sphere.py @@ -1,7 +1,6 @@ __doc__ = """ Implementation of a sphere rigid body. """ -pass import numpy as np from numpy.typing import NDArray diff --git a/elastica/rod/cosserat_rod.py b/elastica/rod/cosserat_rod.py index 66ff6f40..d777d5c6 100644 --- a/elastica/rod/cosserat_rod.py +++ b/elastica/rod/cosserat_rod.py @@ -2,9 +2,6 @@ from typing import Any, Optional, Type from typing_extensions import Self -from elastica.typing import RodType -from elastica.rod import RodBase - from numpy.typing import NDArray import numpy as np @@ -37,7 +34,7 @@ def _get_z_vector() -> NDArray[np.float64]: return np.array([0.0, 0.0, 1.0]).reshape(3, -1) -def _compute_sigma_kappa_for_blockstructure(memory_block: RodType) -> None: +def _compute_sigma_kappa_for_blockstructure(memory_block: RodBase) -> None: """ This function is a wrapper to call functions which computes shear stretch, strain and bending twist and strain. From 06deb8ff64653818f2c9c2633e3bceca6bbb2218 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 14 Dec 2025 20:20:15 -0600 Subject: [PATCH 64/85] update codecov CI version --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e1b1f169..60b659a9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -119,6 +119,6 @@ jobs: run: | make test_coverage_xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} From 90b8d275fa92a1a52c32189f0aa6a5528b9e78ee Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 14 Dec 2025 20:26:18 -0600 Subject: [PATCH 65/85] refactor: add REQUISITE_MODULES type annotation to Plane class --- .pre-commit-config.yaml | 2 +- elastica/surface/plane.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 70d9c2c6..f0395a35 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ default_stages: [pre-commit, pre-push] repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.5.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: check-json diff --git a/elastica/surface/plane.py b/elastica/surface/plane.py index 51794e1a..23c0e057 100644 --- a/elastica/surface/plane.py +++ b/elastica/surface/plane.py @@ -1,4 +1,5 @@ __doc__ = """""" +from typing import Type import numpy as np from numpy.typing import NDArray @@ -11,6 +12,8 @@ class Plane(StaticSystemBase): Plane static system. Static system does not change by the timestepping. """ + REQUISITE_MODULES: list[Type] = [] + def __init__( self, plane_origin: NDArray[np.float64], plane_normal: NDArray[np.float64] ): From a5d04b706bec54fcc0c7c0e8775e30fddae27359 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 14 Dec 2025 21:16:35 -0600 Subject: [PATCH 66/85] refactor: correct typos and improve documentation across multiple modules --- Makefile | 2 +- README.md | 4 +- docs/Makefile | 2 +- docs/api/contact.rst | 4 +- elastica/_rotations.py | 29 ++-- elastica/boundary_conditions.py | 134 +++++++-------- elastica/callback_functions.py | 43 ++--- elastica/contact_forces.py | 91 +++++++--- elastica/dissipation.py | 21 ++- .../generic_system_type_connection.py | 90 +++++----- elastica/external_forces.py | 150 +++++++++-------- elastica/interaction.py | 57 ++++--- elastica/joint.py | 104 ++++++------ elastica/modules/base_system.py | 21 +-- elastica/modules/callbacks.py | 24 +-- elastica/modules/connections.py | 8 +- elastica/modules/constraints.py | 8 +- elastica/modules/contact.py | 10 +- elastica/modules/damping.py | 8 +- elastica/modules/forcing.py | 8 +- elastica/rigidbody/cylinder.py | 2 +- elastica/rigidbody/rigid_body_base.py | 2 +- elastica/rigidbody/sphere.py | 6 +- elastica/rod/cosserat_rod.py | 158 +++++++++--------- elastica/rod/factory_function.py | 38 +---- elastica/surface/plane.py | 17 +- elastica/utils.py | 10 +- elastica/version.py | 2 +- .../snake_forcing.py | 4 +- tests/test_external_forces.py | 6 +- 30 files changed, 559 insertions(+), 504 deletions(-) diff --git a/Makefile b/Makefile index 341b5ece..50b180f0 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,7 @@ black-check: .PHONY: flake8 flake8: uv run flake8 --version - uv run flake8 elastica tests + uv run flake8 elastica .PHONY: autoflake-check autoflake-check: diff --git a/README.md b/README.md index 6f8b68a8..837e6d6e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ PyElastica is the python implementation of **Elastica**: an *open-source* project for simulating assemblies of slender, one-dimensional structures using Cosserat Rod theory. -[![gallery][link-readme-gallary]][link-project-website] +[![gallery][link-readme-gallery]][link-project-website] Visit [www.cosseratrods.org][link-project-website] for more information and learn about Elastica and Cosserat rod theory. @@ -106,7 +106,7 @@ _Names arranged alphabetically_ [//]: # (Collection of URLs.) -[link-readme-gallary]: https://github.com/skim0119/PyElastica/blob/assets_logo/assets/alpha_gallery.gif +[link-readme-gallery]: https://github.com/skim0119/PyElastica/blob/assets_logo/assets/alpha_gallery.gif [link-project-website]: https://cosseratrods.org [link-lab-website]: http://mattia-lab.com/ diff --git a/docs/Makefile b/docs/Makefile index d4bb2cbb..5887be21 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,7 @@ # You can set these variables from the command line, and also # from the environment for the first two. -SPHINXOPTS ?= +SPHINXOPTS ?= -v -T SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build diff --git a/docs/api/contact.rst b/docs/api/contact.rst index a795601e..a029dff9 100644 --- a/docs/api/contact.rst +++ b/docs/api/contact.rst @@ -1,5 +1,5 @@ Contact -============================== +======= .. _contact: @@ -30,7 +30,7 @@ Description Built-in Contact Classes -------------------------------------- +------------------------ .. autoclass:: NoContact :special-members: __init__,apply_contact diff --git a/elastica/_rotations.py b/elastica/_rotations.py index 63f92a17..0e697c63 100644 --- a/elastica/_rotations.py +++ b/elastica/_rotations.py @@ -258,8 +258,8 @@ def _skew_symmetrize(vector: NDArray[np.float64]) -> NDArray[np.float64]: output : numpy.ndarray of shape (dim*dim, blocksize) corresponding to [0, -z, y, z, 0, -x, -y , x, 0] - Note - ---- + Notes + ----- Gets close to the hard-coded implementation in time but with slightly high memory requirement for iteration. @@ -359,20 +359,22 @@ def _get_skew_symmetric_pair( def _inv_skew_symmetrize(matrix: NDArray[np.float64]) -> NDArray[np.float64]: """ - Return the vector elements from a skew-symmetric matrix M + Return the vector elements from a skew-symmetric matrix M. Parameters ---------- - matrix : np.ndarray of dimension (dim, dim, blocksize) + matrix : numpy.ndarray + 3D (dim, dim, blocksize) array containing skew-symmetric matrices. Returns ------- - vector : np.ndarray of dimension (dim, blocksize) + vector : numpy.ndarray + 2D (dim, blocksize) array containing the extracted vector elements. - Note - ---- - Harcoded : 2.28 µs ± 63.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) - This : 2.91 µs ± 58.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) + Notes + ----- + Hardcoded: 2.28 µs ± 63.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) + This: 2.91 µs ± 58.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) """ dim, dim, blocksize = matrix.shape @@ -398,7 +400,7 @@ def get_relative_rotation_two_systems( Compute the relative rotation matrix C_12 between system one and system two at the specified elements. Examples - ---------- + -------- How to get the relative rotation between two systems (e.g. the rotation from end of rod one to base of rod two): >>> rel_rot_mat = get_relative_rotation_two_systems(system1, -1, system2, 0) @@ -415,7 +417,7 @@ def get_relative_rotation_two_systems( ... ) See Also - --------- + -------- FixedJoint Parameters @@ -431,8 +433,9 @@ def get_relative_rotation_two_systems( Returns ------- - relative_rotation_matrix : np.array - Relative rotation matrix C_12 between the two systems for their current state. + relative_rotation_matrix : numpy.ndarray + 2D (3, 3) array containing the relative rotation matrix C_12 between the two systems + for their current state. """ director_one = system_one.director_collection[..., index_one] director_two = system_two.director_collection[..., index_two] diff --git a/elastica/boundary_conditions.py b/elastica/boundary_conditions.py index 4a35343c..435826d4 100644 --- a/elastica/boundary_conditions.py +++ b/elastica/boundary_conditions.py @@ -1,4 +1,4 @@ -__doc__ = """ Built-in boundary condition implementationss """ +__doc__ = """ Built-in boundary condition implementations """ from typing import Any, Optional, TypeVar, Generic @@ -24,13 +24,11 @@ class ConstraintBase(ABC, Generic[S]): ----- Constraint class must inherit ConstraintBase class. - - Attributes - ---------- - _system : RodType or RigidBodyType - _constrained_position_idx : NDArray[np.int32] - _constrained_director_idx : NDArray[np.int32] - + Attributes + ---------- + _system : RodType or RigidBodyType + _constrained_position_idx : NDArray[np.int32] + _constrained_director_idx : NDArray[np.int32] """ _system: S @@ -131,7 +129,7 @@ class OneEndFixedBC(ConstraintBase): Examples -------- - How to fix one ends of the rod: + How to fix one end of the rod: >>> simulator.constrain(rod).using( ... OneEndFixedBC, @@ -196,22 +194,18 @@ def compute_constrain_values( fixed_directors_collection: NDArray[np.float64], ) -> None: """ - Computes constrain values in numba njit decorator + Computes constrain values in numba njit decorator. Parameters ---------- position_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. - fixed_position : numpy.ndarray + 2D (dim, blocksize) array containing data with 'float' type. + fixed_position_collection : numpy.ndarray 2D (dim, 1) array containing data with 'float' type. director_collection : numpy.ndarray - 3D (dim, dim, blocksize) array containing data with `float` type. - fixed_directors : numpy.ndarray + 3D (dim, dim, blocksize) array containing data with 'float' type. + fixed_directors_collection : numpy.ndarray 3D (dim, dim, 1) array containing data with 'float' type. - - Returns - ------- - """ position_collection[..., 0] = fixed_position_collection director_collection[..., 0] = fixed_directors_collection @@ -223,18 +217,14 @@ def compute_constrain_rates( omega_collection: NDArray[np.float64], ) -> None: """ - Compute contrain rates in numba njit decorator + Compute constrain rates in numba njit decorator Parameters ---------- velocity_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. + 2D (dim, blocksize) array containing data with 'float' type. omega_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. - - Returns - ------- - + 2D (dim, blocksize) array containing data with 'float' type. """ velocity_collection[..., 0] = 0.0 omega_collection[..., 0] = 0.0 @@ -289,7 +279,7 @@ def __init__( np.array of type bool indicating which translational degrees of freedom (dof) to constrain. If entry is True, the corresponding dof will be constrained. If None, we constrain all dofs. rotational_constraint_selector: Optional[np.ndarray] - np.array of type bool indicating which translational degrees of freedom (dof) to constrain. + np.array of type bool indicating which rotational degrees of freedom (dof) to constrain. If entry is True, the corresponding dof will be constrained. """ super().__init__(**kwargs) @@ -372,14 +362,14 @@ def nb_constrain_translational_values( constraint_selector: NDArray[np.int32], ) -> None: """ - Computes constrain values in numba njit decorator + Computes constrain values in numba njit decorator. Parameters ---------- position_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. + 2D (dim, blocksize) array containing data with 'float' type. fixed_position_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. + 2D (dim, blocksize) array containing data with 'float' type. indices : numpy.ndarray 1D array containing the index of constraining nodes constraint_selector: numpy.ndarray @@ -414,7 +404,7 @@ def nb_constrain_translational_rates( Parameters ---------- velocity_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. + 2D (dim, blocksize) array containing data with 'float' type. indices : numpy.ndarray 1D array containing the index of constraining nodes constraint_selector: numpy.ndarray @@ -446,9 +436,9 @@ def nb_constrain_rotational_rates( Parameters ---------- director_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. + 2D (dim, blocksize) array containing data with 'float' type. omega_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. + 2D (dim, blocksize) array containing data with 'float' type. indices : numpy.ndarray 1D array containing the index of constraining nodes constraint_selector: numpy.ndarray @@ -562,15 +552,16 @@ def nb_constraint_rotational_values( indices: NDArray[np.int32], ) -> None: """ - Computes constrain values in numba njit decorator + Computes constrain values in numba njit decorator. + Parameters ---------- director_collection : numpy.ndarray - 3D (dim, dim, blocksize) array containing data with `float` type. + 3D (dim, dim, blocksize) array containing data with 'float' type. fixed_director_collection : numpy.ndarray - 3D (dim, dim, blocksize) array containing data with `float` type. + 3D (dim, dim, blocksize) array containing data with 'float' type. indices : numpy.ndarray - 1D array containing the index of constraining nodes + 1D array containing the index of constraining nodes. """ block_size = indices.size for i in range(block_size): @@ -585,15 +576,16 @@ def nb_constrain_translational_values( indices: NDArray[np.int32], ) -> None: """ - Computes constrain values in numba njit decorator + Computes constrain values in numba njit decorator. + Parameters ---------- position_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. + 2D (dim, blocksize) array containing data with 'float' type. fixed_position_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. + 2D (dim, blocksize) array containing data with 'float' type. indices : numpy.ndarray - 1D array containing the index of constraining nodes + 1D array containing the index of constraining nodes. """ block_size = indices.size for i in range(block_size): @@ -606,13 +598,14 @@ def nb_constrain_translational_rates( velocity_collection: NDArray[np.float64], indices: NDArray[np.int32] ) -> None: """ - Compute constrain rates in numba njit decorator + Compute constrain rates in numba njit decorator. + Parameters ---------- velocity_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. + 2D (dim, blocksize) array containing data with 'float' type. indices : numpy.ndarray - 1D array containing the index of constraining nodes + 1D array containing the index of constraining nodes. """ block_size = indices.size @@ -628,13 +621,14 @@ def nb_constrain_rotational_rates( omega_collection: NDArray[np.float64], indices: NDArray[np.int32] ) -> None: """ - Compute constrain rates in numba njit decorator + Compute constrain rates in numba njit decorator. + Parameters ---------- omega_collection : numpy.ndarray - 2D (dim, blocksize) array containing data with `float` type. + 2D (dim, blocksize) array containing data with 'float' type. indices : numpy.ndarray - 1D array containing the index of constraining nodes + 1D array containing the index of constraining nodes. """ block_size = indices.size @@ -654,28 +648,28 @@ class HelicalBucklingBC(ConstraintBase): `Example case (helical buckling) `_ - Attributes - ---------- - twisting_time: float - Time to complete twist. - final_start_position: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - Position of first node of rod after twist completed. - final_end_position: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - Position of last node of rod after twist completed. - ang_vel: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - Angular velocity of rod during twisting time. - shrink_vel: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. - Shrink velocity of rod during twisting time. - final_start_directors: numpy.ndarray - 3D (dim, dim, 1) array containing data with 'float' type. - Directors of first element of rod after twist completed. - final_end_directors: numpy.ndarray - 3D (dim, dim, 1) array containing data with 'float' type. - Directors of last element of rod after twist completed. + Attributes + ---------- + twisting_time: float + Time to complete twist. + final_start_position: numpy.ndarray + 2D (dim, 1) array containing data with 'float' type. + Position of first node of rod after twist completed. + final_end_position: numpy.ndarray + 2D (dim, 1) array containing data with 'float' type. + Position of last node of rod after twist completed. + ang_vel: numpy.ndarray + 2D (dim, 1) array containing data with 'float' type. + Angular velocity of rod during twisting time. + shrink_vel: numpy.ndarray + 2D (dim, 1) array containing data with 'float' type. + Shrink velocity of rod during twisting time. + final_start_directors: numpy.ndarray + 3D (dim, dim, 1) array containing data with 'float' type. + Directors of first element of rod after twist completed. + final_end_directors: numpy.ndarray + 3D (dim, dim, 1) array containing data with 'float' type. + Directors of last element of rod after twist completed. """ def __init__( @@ -718,7 +712,7 @@ def __init__( super().__init__(**kwargs) self.twisting_time = np.float64(twisting_time) - angel_vel_scalar = np.float64( + angle_vel_scalar = np.float64( (2.0 * number_of_rotations * np.pi / self.twisting_time) / 2.0 ) shrink_vel_scalar = np.float64(slack / (self.twisting_time * 2.0)) @@ -730,7 +724,7 @@ def __init__( self.final_start_position = position_start + slack / 2.0 * direction self.final_end_position = position_end - slack / 2.0 * direction - self.ang_vel = angel_vel_scalar * direction + self.ang_vel = angle_vel_scalar * direction self.shrink_vel = shrink_vel_scalar * direction theta = np.float64(number_of_rotations * np.pi) diff --git a/elastica/callback_functions.py b/elastica/callback_functions.py index 42345586..2bcd42e1 100644 --- a/elastica/callback_functions.py +++ b/elastica/callback_functions.py @@ -62,12 +62,12 @@ class MyCallBack(CallBackBaseClass): This is just an example of a callback class, this class as an example/template to write new call back classes in your client file. - Attributes - ---------- - sample_every: int - Collect data using make_callback method every sampling step. - callback_params: dict - Collected callback data is saved in this dictionary. + Attributes + ---------- + sample_every: int + Collect data using make_callback method every sampling step. + callback_params: dict + Collected callback data is saved in this dictionary. """ def __init__(self, step_skip: int, callback_params: dict) -> None: @@ -107,16 +107,16 @@ class ExportCallBack(CallBackBaseClass): If one wants to customize the saving data, we recommend to override `make_callback` method. - Attributes - ---------- - AVAILABLE_METHOD - Supported method to save the file. We recommend - binary save to maintain the tensor structure of - data. - FILE_SIZE_CUTOFF - Maximum buffer size for each file. If the buffer - size exceed, new file is created. Actual size of - the file is expected to be marginally larger. + Attributes + ---------- + AVAILABLE_METHOD + Supported method to save the file. We recommend + binary save to maintain the tensor structure of + data. + FILE_SIZE_CUTOFF + Maximum buffer size for each file. If the buffer + size exceed, new file is created. Actual size of + the file is expected to be marginally larger. """ AVAILABLE_METHOD = ["pickle", "npz", "tempfile"] @@ -208,15 +208,16 @@ def make_callback( self, system: "RodType | RigidBodyType", time: np.float64, current_step: int ) -> None: """ + Collect simulation data at specified intervals. Parameters ---------- - system : - Each part of the system (i.e. rod, rigid body, etc) - time : - simulation time unit + system : RodType | RigidBodyType + Each part of the system (i.e. rod, rigid body, etc). + time : float + Simulation time. current_step : int - simulation step + Current simulation step. """ if current_step % self.step_skip == 0: position = system.position_collection.copy() diff --git a/elastica/contact_forces.py b/elastica/contact_forces.py index e45b5ab4..c8f2fe62 100644 --- a/elastica/contact_forces.py +++ b/elastica/contact_forces.py @@ -1,4 +1,6 @@ -__doc__ = """ Numba implementation module containing contact between rods and rigid bodies and other rods rigid bodies or surfaces.""" +__doc__ = """ +Numba implementation module containing contact between rods and rigid bodies and other rods rigid bodies or surfaces. +""" from typing import TypeVar, Generic, Type from elastica.typing import RodType, SystemType @@ -78,14 +80,17 @@ def apply_contact( time: np.float64 = np.float64(0.0), ) -> None: """ - Apply contact forces and torques between two system object.. - + Apply contact forces and torques between two system objects. In NoContact class, this routine simply passes. Parameters ---------- - system_one - system_two + system_one : SystemType + First system object. + system_two : SystemType + Second system object. + time : float + The time of simulation. """ @@ -129,8 +134,12 @@ def apply_contact( Parameters ---------- - system_one: RodType - system_two: RodType + system_one : RodType + First rod object. + system_two : RodType + Second rod object. + time : float + The time of simulation. """ # First, check for a global AABB bounding box, and see whether that @@ -173,7 +182,7 @@ def apply_contact( class RodCylinderContact(NoContact): """ This class is for applying contact forces between rod-cylinder. - If you are want to apply contact forces between rod and cylinder, first system is always rod and second system + If you want to apply contact forces between rod and cylinder, first system is always rod and second system is always cylinder. In addition to the contact forces, user can define apply friction forces between rod and cylinder that are in contact. For details on friction model refer to this [1]_. @@ -193,7 +202,6 @@ class RodCylinderContact(NoContact): ... nu=10, ... ) - .. [1] Preclik T., Popa Constantin., Rude U., Regularizing a Time-Stepping Method for Rigid Multibody Dynamics, Multibody Dynamics 2011, ECCOMAS. URL: https://www10.cs.fau.de/publications/papers/2011/Preclik_Multibody_Ext_Abstr.pdf """ @@ -235,6 +243,19 @@ def apply_contact( system_two: Cylinder, time: np.float64 = np.float64(0.0), ) -> None: + """ + Apply contact forces and torques between RodType object and Cylinder object. + + Parameters + ---------- + system_one : RodType + Rod object. + system_two : Cylinder + Cylinder object. + time : float + The time of simulation. + + """ # First, check for a global AABB bounding box, and see whether that # intersects if _prune_using_aabbs_rod_cylinder( @@ -332,8 +353,12 @@ def apply_contact( Parameters ---------- - system_one: RodType - system_two: RodType + system_one : RodType + Rod object. + system_two : RodType + Same rod object as system_one (self-contact). + time : float + The time of simulation. """ _calculate_contact_forces_self_rod( @@ -357,6 +382,8 @@ class RodSphereContact(NoContact): In addition to the contact forces, user can define apply friction forces between rod and sphere that are in contact. For details on friction model refer to this [1]_. + .. [1] Preclik T., Popa Constantin., Rude U., Regularizing a Time-Stepping Method for Rigid Multibody Dynamics, Multibody Dynamics 2011, ECCOMAS. URL: https://www10.cs.fau.de/publications/papers/2011/Preclik_Multibody_Ext_Abstr.pdf + Notes ----- The `velocity_damping_coefficient` is set to a high value (e.g. 1e4) to minimize slip and simulate stiction @@ -372,7 +399,6 @@ class RodSphereContact(NoContact): ... nu=10, ... ) - .. [1] Preclik T., Popa Constantin., Rude U., Regularizing a Time-Stepping Method for Rigid Multibody Dynamics, Multibody Dynamics 2011, ECCOMAS. URL: https://www10.cs.fau.de/publications/papers/2011/Preclik_Multibody_Ext_Abstr.pdf """ def __init__( @@ -383,6 +409,7 @@ def __init__( friction_coefficient: float = 0.0, ) -> None: """ + Parameters ---------- k : float @@ -416,8 +443,12 @@ def apply_contact( Parameters ---------- - system_one: RodType - system_two: Sphere + system_one : RodType + Rod object. + system_two : Sphere + Sphere object. + time : float + The time of simulation. """ # First, check for a global AABB bounding box, and see whether that @@ -506,10 +537,12 @@ def apply_contact( Parameters ---------- - system_one: object + system_one : RodType Rod object. - system_two: object + system_two : Plane Plane object. + time : float + The time of simulation. """ _calculate_contact_forces_rod_plane( @@ -557,6 +590,7 @@ def __init__( kinetic_mu_array: NDArray[np.float64], ) -> None: """ + Parameters ---------- k : float @@ -603,11 +637,14 @@ def apply_contact( Parameters ---------- - system_one: RodType - system_two: Plane + system_one : RodType + Rod object. + system_two : Plane + Plane object. + time : float + The time of simulation. """ - _calculate_contact_forces_rod_plane_with_anisotropic_friction( system_two.origin, system_two.normal, @@ -651,6 +688,7 @@ class CylinderPlaneContact(NoContact): ... k=1e4, ... nu=10, ... ) + """ def __init__( @@ -659,6 +697,7 @@ def __init__( nu: float, ) -> None: """ + Parameters ---------- k : float @@ -686,14 +725,18 @@ def apply_contact( time: np.float64 = np.float64(0.0), ) -> None: """ - This function computes the plane force response on the cylinder, in the - case of contact. Contact model given in Eqn 4.8 Gazzola et. al. RSoS 2018 paper - is used. + Compute the plane force response on the cylinder in the case of contact. + + Contact model given in Eqn 4.8 Gazzola et al. RSoS 2018 paper is used. Parameters ---------- - system_one: Cylinder - system_two: Plane + system_one : Cylinder + Cylinder object. + system_two : Plane + Plane object. + time : float + The time of simulation. """ _calculate_contact_forces_cylinder_plane( diff --git a/elastica/dissipation.py b/elastica/dissipation.py index e6c3f31f..cc246f61 100644 --- a/elastica/dissipation.py +++ b/elastica/dissipation.py @@ -19,13 +19,13 @@ class DamperBase(Generic[T], ABC): - """Base class for damping module implementations. + """ + Base class for damping module implementations. Notes ----- All damper classes must inherit DamperBase class. - Attributes ---------- system : RodBase @@ -58,7 +58,6 @@ def system(self) -> T: @abstractmethod def dampen_rates(self, system: T, time: np.float64) -> None: - # TODO: In the future, we can remove rod and use self.system """ Dampen rates (velocity and/or omega) of a rod object. @@ -134,9 +133,9 @@ class AnalyticalLinearDamper(DamperBase): about the simulation becoming unstable. This now leads to a streamlined procedure for tuning the `damping_constant`: - 1. Set a high value for `damping_constant` to first acheive a stable simulation. + 1. Set a high value for `damping_constant` to first achieve a stable simulation. 2. If you feel the simulation is overdamped, reduce `damping_constant` until you - feel the simulation is underdamped, and expected dynamics are recovered. + feel the simulation is underdamped, and expected dynamics are recovered. """ def __init__(self, time_step: np.float64, **kwargs: Any) -> None: @@ -326,7 +325,7 @@ class LaplaceDissipationFilter(DamperBase): def __init__(self, filter_order: int, **kwargs: Any) -> None: """ - Filter damper initializer + Filter damper initializer. Parameters ---------- @@ -334,6 +333,12 @@ def __init__(self, filter_order: int, **kwargs: Any) -> None: Filter order, which corresponds to the number of times the Laplacian operator is applied. Increasing `filter_order` implies higher-order/weaker filtering. + + Raises + ------ + ValueError + If filter_order is not a positive integer. + """ super().__init__(**kwargs) if not (filter_order > 0 and isinstance(filter_order, int)): @@ -375,13 +380,13 @@ def _filter_function_periodic_condition_ring_rod( ) -> None: blocksize = velocity_filter_term.shape[1] - # Transfer velocity to an array which has periodic boundaries and synchornize boundaries + # Transfer velocity to an array which has periodic boundaries and synchronize boundaries velocity_collection_with_periodic_bc = np.empty((3, blocksize)) velocity_collection_with_periodic_bc[:, 1:-1] = velocity_collection[:] velocity_collection_with_periodic_bc[:, 0] = velocity_collection[:, -1] velocity_collection_with_periodic_bc[:, -1] = velocity_collection[:, 0] - # Transfer omega to an array which has periodic boundaries and synchornize boundaries + # Transfer omega to an array which has periodic boundaries and synchronize boundaries omega_collection_with_periodic_bc = np.empty((3, blocksize)) omega_collection_with_periodic_bc[:, 1:-1] = omega_collection[:] omega_collection_with_periodic_bc[:, 0] = omega_collection[:, -1] diff --git a/elastica/experimental/connection_contact_joint/generic_system_type_connection.py b/elastica/experimental/connection_contact_joint/generic_system_type_connection.py index c52cbf99..7b3551a8 100644 --- a/elastica/experimental/connection_contact_joint/generic_system_type_connection.py +++ b/elastica/experimental/connection_contact_joint/generic_system_type_connection.py @@ -200,51 +200,51 @@ class GenericSystemTypeFixedJoint(GenericSystemTypeFreeJoint): The fixed joint class restricts the relative movement and rotation between two nodes and elements by applying restoring forces and torques. - Attributes - ---------- - k : float - Stiffness coefficient of the joint. - nu : float - Damping coefficient of the joint. - kt : float - Rotational stiffness coefficient of the joint. - nut : float - Rotational damping coefficient of the joint. - point_system_one : numpy.ndarray - Describes for system one in the local coordinate system the translation from the node `index_one` (for rods) - or the center of mass (for rigid bodies) to the joint. - point_system_two : numpy.ndarray - Describes for system two in the local coordinate system the translation from the node `index_two` (for rods) - or the center of mass (for rigid bodies) to the joint. - rest_rotation_matrix : np.ndarray - 2D (3,3) array containing data with 'float' type. - Rest 3x3 rotation matrix from system one to system two at the connected elements. - Instead of aligning the directors of both systems directly, a desired rest rotational matrix labeled C_12* - is enforced. - - Examples - -------- - How to connect two Cosserat rods together using a fixed joint while aligning the tangents (e.g. local z-axis). - - >>> simulator.connect(rod_one, rod_two).using( - ... FixedJoint, - ... k=1e4, - ... nu=1, - ... ) - - How to connect a cosserat rod with the base of a cylinder using a fixed joint, where the cylinder is rotated - by 45 degrees around the y-axis. - - >>> from scipy.spatial.transform import Rotation - ... simulator.connect(rod, cylinder).using( - ... FixedJoint, - ... k=1e5, - ... nu=1e0, - ... kt=1e3, - ... nut=1e-3, - ... point_system_two=np.array([0, 0, -cylinder.length / 2]), - ... rest_rotation_matrix=Rotation.from_euler('y', np.pi / 4, degrees=False).as_matrix(), - ... ) + Attributes + ---------- + k : float + Stiffness coefficient of the joint. + nu : float + Damping coefficient of the joint. + kt : float + Rotational stiffness coefficient of the joint. + nut : float + Rotational damping coefficient of the joint. + point_system_one : numpy.ndarray + Describes for system one in the local coordinate system the translation from the node `index_one` (for rods) + or the center of mass (for rigid bodies) to the joint. + point_system_two : numpy.ndarray + Describes for system two in the local coordinate system the translation from the node `index_two` (for rods) + or the center of mass (for rigid bodies) to the joint. + rest_rotation_matrix : np.ndarray + 2D (3,3) array containing data with 'float' type. + Rest 3x3 rotation matrix from system one to system two at the connected elements. + Instead of aligning the directors of both systems directly, a desired rest rotational matrix labeled C_12* + is enforced. + + Examples + -------- + How to connect two Cosserat rods together using a fixed joint while aligning the tangents (e.g. local z-axis). + + >>> simulator.connect(rod_one, rod_two).using( + ... FixedJoint, + ... k=1e4, + ... nu=1, + ... ) + + How to connect a cosserat rod with the base of a cylinder using a fixed joint, where the cylinder is rotated + by 45 degrees around the y-axis. + + >>> from scipy.spatial.transform import Rotation + ... simulator.connect(rod, cylinder).using( + ... FixedJoint, + ... k=1e5, + ... nu=1e0, + ... kt=1e3, + ... nut=1e-3, + ... point_system_two=np.array([0, 0, -cylinder.length / 2]), + ... rest_rotation_matrix=Rotation.from_euler('y', np.pi / 4, degrees=False).as_matrix(), + ... ) """ def __init__( diff --git a/elastica/external_forces.py b/elastica/external_forces.py index 6f10ea00..74d7c6c9 100644 --- a/elastica/external_forces.py +++ b/elastica/external_forces.py @@ -1,5 +1,4 @@ -__doc__ = """ Numba implementation module for boundary condition implementations that apply -external forces to the system.""" +__doc__ = """Numba implementation module for external forces applied to objects.""" from typing import TypeVar, Generic @@ -69,10 +68,20 @@ class GravityForces(NoForces): """ This class applies a constant gravitational force to the entire rod. - Attributes - ---------- - acc_gravity: numpy.ndarray - 1D (dim) array containing data with 'float' type. Gravitational acceleration vector. + Attributes + ---------- + acc_gravity : numpy.ndarray + 1D (dim) array containing data with 'float' type. Gravitational acceleration vector. + + Examples + -------- + How to apply gravity to a rod: + + >>> simulator.add_forcing_to(rod).using( + ... GravityForces, + ... acc_gravity=np.array([0.0, -9.80665, 0.0]), + ... ) + """ def __init__( @@ -107,7 +116,7 @@ def compute_gravity_forces( external_forces: NDArray[np.float64], ) -> None: """ - This function add gravitational forces on the nodes. We are + This function adds gravitational forces on the nodes. We are using njit decorated function to increase the speed. Parameters @@ -127,14 +136,14 @@ class EndpointForces(NoForces): """ This class applies constant forces on the endpoint nodes. - Attributes - ---------- - start_force: numpy.ndarray - 1D (dim) array containing data with 'float' type. Force applied to first node of the system. - end_force: numpy.ndarray - 1D (dim) array containing data with 'float' type. Force applied to last node of the system. - ramp_up_time: float - Applied forces are ramped up until ramp up time. + Attributes + ---------- + start_force: numpy.ndarray + 1D (dim) array containing data with 'float' type. Force applied to first node of the system. + end_force: numpy.ndarray + 1D (dim) array containing data with 'float' type. Force applied to last node of the system. + ramp_up_time: float + Applied forces are ramped up until ramp up time. """ def __init__( @@ -192,13 +201,14 @@ def compute_end_point_forces( 2D (dim, blocksize) array containing data with 'float' type. External force vector. start_force: numpy.ndarray 1D (dim) array containing data with 'float' type. + Force applied to first node of the system. end_force: numpy.ndarray 1D (dim) array containing data with 'float' type. Force applied to last node of the system. time: float + The time of simulation. ramp_up_time: float Applied forces are ramped up until ramp up time. - """ factor = min(1.0, float(time / ramp_up_time)) external_forces[..., 0] += start_force * factor @@ -209,10 +219,10 @@ class UniformTorques(NoForces): """ This class applies a uniform torque to the entire rod. - Attributes - ---------- - torque: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. Total torque applied to a rod-like object. + Attributes + ---------- + torque : numpy.ndarray + 2D (dim, 1) array containing data with 'float' type. Total torque applied to a rod-like object. """ def __init__( @@ -251,10 +261,10 @@ class UniformForces(NoForces): """ This class applies a uniform force to the entire rod. - Attributes - ---------- - force: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. Total force applied to a rod-like object. + Attributes + ---------- + force : numpy.ndarray + 2D (dim, 1) array containing data with 'float' type. Total force applied to a rod-like object. """ def __init__( @@ -297,20 +307,20 @@ class MuscleTorques(NoForces): as a traveling wave. For implementation details refer to Gazzola et. al. RSoS. (2018). - Attributes - ---------- - direction: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. Muscle torque direction. - angular_frequency: float - Angular frequency of traveling wave. - wave_number: float - Wave number of traveling wave. - phase_shift: float - Phase shift of traveling wave. - ramp_up_time: float - Applied muscle torques are ramped up until ramp up time. - my_spline: numpy.ndarray - 1D (blocksize) array containing data with 'float' type. Generated spline. + Attributes + ---------- + direction: numpy.ndarray + 2D (dim, 1) array containing data with 'float' type. Muscle torque direction. + angular_frequency: float + Angular frequency of traveling wave. + wave_number: float + Wave number of traveling wave. + phase_shift: float + Phase shift of traveling wave. + ramp_up_time: float + Applied muscle torques are ramped up until ramp up time. + my_spline: numpy.ndarray + 1D (blocksize) array containing data with 'float' type. Generated spline. """ def __init__( @@ -331,7 +341,7 @@ def __init__( ---------- base_length: float Rest length of the rod-like object. - b_coeff: nump.ndarray + b_coeff: numpy.ndarray 1D array containing data with 'float' type. Beta coefficients for beta-spline. period: float @@ -342,7 +352,10 @@ def __init__( Phase shift of traveling wave. direction: numpy.ndarray 1D (dim) array containing data with 'float' type. Muscle torque direction. - ramp_up_time: np.float64 + rest_lengths: numpy.ndarray + 1D (n_elems) array containing data with 'float' type. + Rod element lengths at rest configuration. + ramp_up_time: float Applied muscle torques are ramped up until ramp up time. with_spline: boolean Option to use beta-spline. @@ -423,7 +436,7 @@ def compute_muscle_torques( external_torques[..., 1:], _batch_matvec(director_collection, torque)[..., 1:], ) - inplace_substraction( + inplace_subtraction( external_torques[..., :-1], _batch_matvec(director_collection[..., :-1], torque[..., 1:]), ) @@ -456,23 +469,22 @@ def inplace_addition( @njit(cache=True) # type: ignore -def inplace_substraction( +def inplace_subtraction( external_force_or_torque: NDArray[np.float64], force_or_torque: NDArray[np.float64], ) -> None: """ - This function does inplace substraction. First argument + This function does inplace subtraction. First argument `external_force_or_torque` is the system.external_forces or system.external_torques. Second argument force or torque - vector to be substracted. + vector to be subtracted. + Parameters ---------- external_force_or_torque: numpy.ndarray 2D (dim, blocksize) array containing data with 'float' type. force_or_torque: numpy.ndarray 2D (dim, blocksize) array containing data with 'float' type. - - """ blocksize = force_or_torque.shape[1] for i in range(3): @@ -485,25 +497,24 @@ class EndpointForcesSinusoidal(NoForces): This class applies sinusoidally varying forces to the ends of a rod. Forces are applied in a plane, which is defined by the tangent_direction and normal_direction. - Attributes - ---------- - start_force_mag: float - Magnitude of the force that is applied to the start of the rod (node 0). - end_force_mag: float - Magnitude of the force that is applied to the end of the rod (node -1). - ramp_up_time: float - Applied forces are applied in the normal direction until time reaches ramp_up_time. - normal_direction: np.ndarray - An array (3,) contains type float. - This is the normal direction of the rod. - roll_direction: np.ndarray - An array (3,) contains type float. - This is the direction perpendicular to rod tangent, and rod normal. - - Notes - ----- - In order to see example how to use this class, see joint examples. + Attributes + ---------- + start_force_mag: float + Magnitude of the force that is applied to the start of the rod (node 0). + end_force_mag: float + Magnitude of the force that is applied to the end of the rod (node -1). + ramp_up_time: float + Applied forces are applied in the normal direction until time reaches ramp_up_time. + normal_direction: numpy.ndarray + 1D (3,) array containing data with 'float' type. + This is the normal direction of the rod. + roll_direction: numpy.ndarray + 1D (3,) array containing data with 'float' type. + This is the direction perpendicular to rod tangent, and rod normal. + Notes + ----- + In order to see example how to use this class, see joint examples. """ def __init__( @@ -519,7 +530,6 @@ def __init__( ), # FIXME: avoid mutable default ) -> None: """ - Parameters ---------- start_force_mag: float @@ -527,12 +537,12 @@ def __init__( end_force_mag: float Magnitude of the force that is applied to the end of the system (node -1). ramp_up_time: float - Applied muscle torques are ramped up until ramp up time. - tangent_direction: np.ndarray - An array (3,) contains type float. + Applied forces are ramped up until ramp up time. + tangent_direction: numpy.ndarray + 1D (3,) array containing data with 'float' type. This is the tangent direction of the system, or normal of the plane that forces applied. - normal_direction: np.ndarray - An array (3,) contains type float. + normal_direction: numpy.ndarray + 1D (3,) array containing data with 'float' type. This is the normal direction of the system. """ super(EndpointForcesSinusoidal, self).__init__() diff --git a/elastica/interaction.py b/elastica/interaction.py index d279e3e1..ab5c8d2d 100644 --- a/elastica/interaction.py +++ b/elastica/interaction.py @@ -34,9 +34,7 @@ def sum_over_elements(input: NDArray[np.float64]) -> np.float64: ------- float - """ - """ - Developer Note + Notes ----- Faster than sum(), .sum() and np.sum() @@ -86,7 +84,7 @@ def slender_body_forces( Rod-like object velocity collection. dynamic_viscosity: float Dynamic viscosity of the fluid. - length: numpy.ndarray + lengths: numpy.ndarray 1D (blocksize) array containing data with 'float' type. Rod-like object element lengths. radius: numpy.ndarray @@ -170,11 +168,10 @@ class SlenderBodyTheory(NoForces): forces on the body using the slender body theory given in Eq. 4.13 of Gazzola et al. RSoS (2018). - Attributes - ---------- - dynamic_viscosity: float - Dynamic viscosity of the fluid. - + Attributes + ---------- + dynamic_viscosity: float + Dynamic viscosity of the fluid. """ def __init__(self, dynamic_viscosity: float) -> None: @@ -211,9 +208,14 @@ def apply_forces(self, system: RodType, time: np.float64 = np.float64(0.0)) -> N _elements_to_nodes_inplace(stokes_force, system.external_forces) -# base class for interaction -# only applies normal force no friction class InteractionPlaneRigidBody(NoForces): + """ + Interaction class for applying contact forces between a rigid body and a plane. + + This class applies normal contact forces (no friction) when a rigid body + contacts a plane surface. + """ + def __init__( self, k: float, @@ -221,6 +223,23 @@ def __init__( plane_origin: NDArray[np.float64], plane_normal: NDArray[np.float64], ) -> None: + """ + Initialize the plane-rigid body interaction. + + Parameters + ---------- + k : float + Contact spring constant. + nu : float + Contact damping constant. + plane_origin : numpy.ndarray + 1D (3,) or 2D (3, 1) array containing data with 'float' type. + Origin of the plane. + plane_normal : numpy.ndarray + 1D (3,) array containing data with 'float' type. + Normal vector of the plane. + + """ self.k = np.float64(k) self.nu = np.float64(nu) self.surface_tol = np.float64(1e-4) @@ -231,16 +250,16 @@ def apply_forces( self, system: RigidBodyType, time: np.float64 = np.float64(0.0) ) -> None: """ - This function computes the plane force response on the rigid body, in the - case of contact. Contact model given in Eqn 4.8 Gazzola et. al. RSoS 2018 paper - is used. + Compute the plane force response on the rigid body in the case of contact. + + Contact model given in Eqn 4.8 Gazzola et. al. RSoS 2018 paper is used. + Parameters ---------- - system - - Returns - ------- - magnitude of the plane response + system : RigidBodyType + Rigid body object. + time : float + The time of simulation. """ _calculate_contact_forces_cylinder_plane( self.plane_origin, diff --git a/elastica/joint.py b/elastica/joint.py index 774c5a7e..2b147aee 100644 --- a/elastica/joint.py +++ b/elastica/joint.py @@ -42,10 +42,6 @@ def apply_forces( Rod or rigid-body object index_two : ConnectionIndex Index of second system for connection. - - Returns - ------- - """ @abstractmethod @@ -70,10 +66,6 @@ def apply_torques( Rod or rigid-body object index_two : ConnectionIndex Index of second system for connection. - - Returns - ------- - """ @@ -86,12 +78,12 @@ class FreeJoint(ConnectionBase): ----- Alias for BallJoint and SphericalJoint - Attributes - ---------- - k: float - Stiffness coefficient of the joint. - nu: float - Damping coefficient of the joint. + Attributes + ---------- + k: float + Stiffness coefficient of the joint. + nu: float + Damping coefficient of the joint. """ # pass the k and nu for the forces @@ -132,10 +124,6 @@ def apply_forces( Rod or rigid-body object index_two : ConnectionIndex Index of second system for connection. - - Returns - ------- - """ end_distance_vector = ( system_two.position_collection[..., index_two] @@ -162,7 +150,22 @@ def apply_torques( time: np.float64 = np.float64(0.0), ) -> None: """ + Apply joint torques to the connected objects. + In FreeJoint class, this routine simply passes. + + Parameters + ---------- + system_one : RodType | RigidBodyType + Rod or rigid-body object. + index_one : ConnectionIndex + Index of first system for connection. + system_two : RodType | RigidBodyType + Rod or rigid-body object. + index_two : ConnectionIndex + Index of second system for connection. + time : float + The time of simulation. """ @@ -179,16 +182,16 @@ class HingeJoint(FreeJoint): implementation details, refer to Zhang et. al. Nature Communications (2019). - Attributes - ---------- - k: float - Stiffness coefficient of the joint. - nu: float - Damping coefficient of the joint. - kt: float - Rotational stiffness coefficient of the joint. - normal_direction: numpy.ndarray - 2D (dim, 1) array containing data with 'float' type. Constraint rotation direction. + Attributes + ---------- + k: float + Stiffness coefficient of the joint. + nu: float + Damping coefficient of the joint. + kt: float + Rotational stiffness coefficient of the joint. + normal_direction: numpy.ndarray + 2D (dim, 1) array containing data with 'float' type. Constraint rotation direction. """ # TODO: IN WRAPPER COMPUTE THE NORMAL DIRECTION OR ASK USER TO GIVE INPUT, IF NOT THROW ERROR @@ -267,25 +270,25 @@ class FixedJoint(FreeJoint): For implementation details, refer to Zhang et al. Nature Communications (2019). - Notes - ----- - Issue #131 : Add constraint in twisting, add rest_rotation_matrix (v0.3.0) - - Attributes - ---------- - k: float - Stiffness coefficient of the joint. - nu: float - Damping coefficient of the joint. - kt: float - Rotational stiffness coefficient of the joint. - nut: float - Rotational damping coefficient of the joint. - rest_rotation_matrix: np.array - 2D (3,3) array containing data with 'float' type. - Rest 3x3 rotation matrix from system one to system two at the connected elements. - Instead of aligning the directors of both systems directly, a desired rest rotational matrix labeled C_12* - is enforced. + Notes + ----- + Issue #131 : Add constraint in twisting, add rest_rotation_matrix (v0.3.0) + + Attributes + ---------- + k: float + Stiffness coefficient of the joint. + nu: float + Damping coefficient of the joint. + kt: float + Rotational stiffness coefficient of the joint. + nut: float + Rotational damping coefficient of the joint. + rest_rotation_matrix: numpy.ndarray + 2D (3, 3) array containing data with 'float' type. + Rest 3x3 rotation matrix from system one to system two at the connected elements. + Instead of aligning the directors of both systems directly, a desired rest rotational matrix labeled C_12* + is enforced. """ def __init__( @@ -297,7 +300,6 @@ def __init__( rest_rotation_matrix: NDArray[np.float64] | None = None, ) -> None: """ - Parameters ---------- k: float @@ -306,10 +308,10 @@ def __init__( Damping coefficient of the joint. kt: float Rotational stiffness coefficient of the joint. - nut: float = 0. + nut: float Rotational damping coefficient of the joint. - rest_rotation_matrix: np.array | None - 2D (3,3) array containing data with 'float' type. + rest_rotation_matrix: numpy.ndarray | None + 2D (3, 3) array containing data with 'float' type. Rest 3x3 rotation matrix from system one to system two at the connected elements. If provided, the rest rotation matrix is enforced between the two systems throughout the simulation. If not provided, `rest_rotation_matrix` is initialized to the identity matrix, diff --git a/elastica/modules/base_system.py b/elastica/modules/base_system.py index 8e8cae64..a99c7ad1 100644 --- a/elastica/modules/base_system.py +++ b/elastica/modules/base_system.py @@ -23,6 +23,7 @@ from collections.abc import MutableSequence from elastica.systems.protocol import StaticSystemBase, SystemProtocol +from elastica.memory_block.protocol import BlockSystemProtocol # noqa: F811 from .memory_block import construct_memory_block_structures from .operator_group import OperatorGroupFIFO @@ -33,16 +34,16 @@ class BaseSystemCollection(MutableSequence): """ Base System for simulator classes. Every simulation class written by the user must be derived from the BaseSystemCollection class; otherwise the simulation will - proceed. - - Attributes - ---------- - allowed_sys_types: tuple[Type] - Tuple of allowed type rod-like objects. Here use a base class for objects, i.e. RodBase. - systems: Callable - Returns all system objects. Once finalize, block objects are also included. - blocks: Callable - Returns block objects. Should be called after finalize. + not proceed. + + Attributes + ---------- + allowed_sys_types: tuple[Type] + Tuple of allowed type rod-like objects. Here use a base class for objects, i.e. RodBase. + systems: Callable + Returns all system objects. Once finalize, block objects are also included. + blocks: Callable + Returns block objects. Should be called after finalize. Notes ----- diff --git a/elastica/modules/callbacks.py b/elastica/modules/callbacks.py index 0b8d4ed8..e3be9e5a 100644 --- a/elastica/modules/callbacks.py +++ b/elastica/modules/callbacks.py @@ -41,10 +41,10 @@ class CallBacks(SystemCollectionProtocol): wants to collect data from the simulation, the simulator class has to be derived from the CallBacks class. - Attributes - ---------- - _callback_list: list - List of call back classes defined for rod-like objects. + Attributes + ---------- + _callback_list: list + List of call back classes defined for rod-like objects. """ _callback_list: list[ModuleProtocol] @@ -125,14 +125,14 @@ class _CallBack: """ CallBack module private class - Attributes - ---------- - _sys_idx: rod object index - _callback_cls: list - *args - Variable length argument list. - **kwargs - Arbitrary keyword arguments. + Attributes + ---------- + _sys_idx: rod object index + _callback_cls: list + *args + Variable length argument list. + **kwargs + Arbitrary keyword arguments. """ def __init__(self, sys_idx: SystemIdxDSType): diff --git a/elastica/modules/connections.py b/elastica/modules/connections.py index 7fc57dea..17f9d774 100644 --- a/elastica/modules/connections.py +++ b/elastica/modules/connections.py @@ -25,10 +25,10 @@ class Connections(SystemCollectionProtocol): by the user. To connect two rod-like objects, the simulator class must be derived from the Connections class. - Attributes - ---------- - _connections: list - List of joint classes defined for rod-like objects. + Attributes + ---------- + _connections: list + List of joint classes defined for rod-like objects. """ _connections: list[ModuleProtocol] diff --git a/elastica/modules/constraints.py b/elastica/modules/constraints.py index b8106be7..fbf48cac 100644 --- a/elastica/modules/constraints.py +++ b/elastica/modules/constraints.py @@ -28,10 +28,10 @@ class Constraints(SystemCollectionProtocol): To enforce boundary conditions on rod-like objects, the simulator class must be derived from Constraints class. - Attributes - ---------- - _constraints: list - List of boundary condition classes defined for rod-like objects. + Attributes + ---------- + _constraints: list + List of boundary condition classes defined for rod-like objects. """ _constraints_list: list[ModuleProtocol] diff --git a/elastica/modules/contact.py b/elastica/modules/contact.py index 22a3f46b..5647e472 100644 --- a/elastica/modules/contact.py +++ b/elastica/modules/contact.py @@ -24,13 +24,13 @@ class Contact(SystemCollectionProtocol): """ - The Contact class is a module for applying contact between rod-like objects . To apply contact between rod-like objects, + The Contact class is a module for applying contact between rod-like objects. To apply contact between rod-like objects, the simulator class must be derived from the Contact class. - Attributes - ---------- - _contacts: list - List of contact classes defined for rod-like objects. + Attributes + ---------- + _contacts: list + List of contact classes defined for rod-like objects. """ _contacts: list[ModuleProtocol] diff --git a/elastica/modules/damping.py b/elastica/modules/damping.py index fd46b667..a45e0beb 100644 --- a/elastica/modules/damping.py +++ b/elastica/modules/damping.py @@ -25,10 +25,10 @@ class Damping(SystemCollectionProtocol): on rod-like objects, the simulator class must be derived from Damping class. - Attributes - ---------- - _dampers: list - List of damper classes defined for rod-like objects. + Attributes + ---------- + _dampers: list + List of damper classes defined for rod-like objects. """ _damping_list: List[ModuleProtocol] diff --git a/elastica/modules/forcing.py b/elastica/modules/forcing.py index 99c7a378..3af1b19a 100644 --- a/elastica/modules/forcing.py +++ b/elastica/modules/forcing.py @@ -23,10 +23,10 @@ class Forcing(SystemCollectionProtocol): consist of applied external forces. To apply forcing on rod-like objects, the simulator class must be derived from the Forcing class. - Attributes - ---------- - _ext_forces_torques: list - List of forcing class defined for rod-like objects. + Attributes + ---------- + _ext_forces_torques: list + List of forcing class defined for rod-like objects. """ _ext_forces_torques: List[ModuleProtocol] diff --git a/elastica/rigidbody/cylinder.py b/elastica/rigidbody/cylinder.py index fe50d3a9..06cadb02 100644 --- a/elastica/rigidbody/cylinder.py +++ b/elastica/rigidbody/cylinder.py @@ -49,7 +49,7 @@ def assert_check_lower_bound( ) -> None: assert ( to_check > lower_bound - ), f"Value for '{name}' ({to_check}) must be at lease {lower_bound}. " + ), f"Value for '{name}' ({to_check}) must be at least {lower_bound}. " assert_check_array_size(start, "start") assert_check_array_size(direction, "direction") diff --git a/elastica/rigidbody/rigid_body_base.py b/elastica/rigidbody/rigid_body_base.py index 17b8cd26..eca9b1f7 100644 --- a/elastica/rigidbody/rigid_body_base.py +++ b/elastica/rigidbody/rigid_body_base.py @@ -1,4 +1,4 @@ -__doc__ = """""" +__doc__ = """Base class for rigid body implementations""" from typing import Type diff --git a/elastica/rigidbody/sphere.py b/elastica/rigidbody/sphere.py index b6fec184..2e8392a5 100644 --- a/elastica/rigidbody/sphere.py +++ b/elastica/rigidbody/sphere.py @@ -11,6 +11,10 @@ class Sphere(RigidBodyBase): + """ + Rigid sphere class. + """ + def __init__( self, center: NDArray[np.float64], @@ -22,7 +26,7 @@ def __init__( Parameters ---------- - center : NDArray[np.float64] + center : numpy.ndarray base_radius : float density : float """ diff --git a/elastica/rod/cosserat_rod.py b/elastica/rod/cosserat_rod.py index d777d5c6..f2c586c0 100644 --- a/elastica/rod/cosserat_rod.py +++ b/elastica/rod/cosserat_rod.py @@ -73,79 +73,79 @@ class CosseratRod(RodBase, SystemProtocol): Cosserat Rod class. This is the preferred class for rods because it is derived from some of the essential base classes. - Attributes - ---------- - n_elems: int - The number of elements of the rod. - position_collection: NDArray[np.float64] - 2D (dim, n_nodes) array containing data with 'float' type. - Array containing node position vectors. - velocity_collection: NDArray[np.float64] - 2D (dim, n_nodes) array containing data with 'float' type. - Array containing node velocity vectors. - acceleration_collection: NDArray[np.float64] - 2D (dim, n_nodes) array containing data with 'float' type. - Array containing node acceleration vectors. - omega_collection: NDArray[np.float64] - 2D (dim, n_elems) array containing data with 'float' type. - Array containing element angular velocity vectors. - alpha_collection: NDArray[np.float64] - 2D (dim, n_elems) array containing data with 'float' type. - Array contining element angular acceleration vectors. - director_collection: NDArray[np.float64] - 3D (dim, dim, n_elems) array containing data with 'float' type. - Array containing element director matrices. - rest_lengths: NDArray[np.float64] - 1D (n_elems) array containing data with 'float' type. - Rod element lengths at rest configuration. - density: NDArray[np.float64] - 1D (n_elems) array containing data with 'float' type. - Rod elements densities. - volume: NDArray[np.float64] - 1D (n_elems) array containing data with 'float' type. - Rod element volumes. - mass: NDArray[np.float64] - 1D (n_nodes) array containing data with 'float' type. - Rod node masses. Note that masses are stored on the nodes, not on elements. - mass_second_moment_of_inertia: NDArray[np.float64] - 3D (dim, dim, n_elems) array containing data with 'float' type. - Rod element mass second moment of interia. - inv_mass_second_moment_of_inertia: NDArray[np.float64] - 3D (dim, dim, n_elems) array containing data with 'float' type. - Rod element inverse mass moment of inertia. - rest_voronoi_lengths: NDArray[np.float64] - 1D (n_voronoi) array containing data with 'float' type. - Rod lengths on the voronoi domain at the rest configuration. - internal_forces: NDArray[np.float64] - 2D (dim, n_nodes) array containing data with 'float' type. - Rod node internal forces. Note that internal forces are stored on the node, not on elements. - internal_torques: NDArray[np.float64] - 2D (dim, n_elems) array containing data with 'float' type. - Rod element internal torques. - external_forces: NDArray[np.float64] - 2D (dim, n_nodes) array containing data with 'float' type. - External forces acting on rod nodes. - external_torques: NDArray[np.float64] - 2D (dim, n_elems) array containing data with 'float' type. - External torques acting on rod elements. - lengths: NDArray[np.float64] - 1D (n_elems) array containing data with 'float' type. - Rod element lengths. - tangents: NDArray[np.float64] - 2D (dim, n_elems) array containing data with 'float' type. - Rod element tangent vectors. - radius: NDArray[np.float64] - 1D (n_elems) array containing data with 'float' type. - Rod element radius. - dilatation: NDArray[np.float64] - 1D (n_elems) array containing data with 'float' type. - Rod element dilatation. - voronoi_dilatation: NDArray[np.float64] - 1D (n_voronoi) array containing data with 'float' type. - Rod dilatation on voronoi domain. - dilatation_rate: NDArray[np.float64] - 1D (n_elems) array containing data with 'float' type. - Rod element dilatation rates. + Attributes + ---------- + n_elems: int + The number of elements of the rod. + position_collection: NDArray[np.float64] + 2D (dim, n_nodes) array containing data with 'float' type. + Array containing node position vectors. + velocity_collection: NDArray[np.float64] + 2D (dim, n_nodes) array containing data with 'float' type. + Array containing node velocity vectors. + acceleration_collection: NDArray[np.float64] + 2D (dim, n_nodes) array containing data with 'float' type. + Array containing node acceleration vectors. + omega_collection: NDArray[np.float64] + 2D (dim, n_elems) array containing data with 'float' type. + Array containing element angular velocity vectors. + alpha_collection: NDArray[np.float64] + 2D (dim, n_elems) array containing data with 'float' type. + Array containing element angular acceleration vectors. + director_collection: NDArray[np.float64] + 3D (dim, dim, n_elems) array containing data with 'float' type. + Array containing element director matrices. + rest_lengths: NDArray[np.float64] + 1D (n_elems) array containing data with 'float' type. + Rod element lengths at rest configuration. + density: NDArray[np.float64] + 1D (n_elems) array containing data with 'float' type. + Rod element densities. + volume: NDArray[np.float64] + 1D (n_elems) array containing data with 'float' type. + Rod element volumes. + mass: NDArray[np.float64] + 1D (n_nodes) array containing data with 'float' type. + Rod node masses. Note that masses are stored on the nodes, not on elements. + mass_second_moment_of_inertia: NDArray[np.float64] + 3D (dim, dim, n_elems) array containing data with 'float' type. + Rod element mass second moment of inertia. + inv_mass_second_moment_of_inertia: NDArray[np.float64] + 3D (dim, dim, n_elems) array containing data with 'float' type. + Rod element inverse mass moment of inertia. + rest_voronoi_lengths: NDArray[np.float64] + 1D (n_voronoi) array containing data with 'float' type. + Rod lengths on the voronoi domain at the rest configuration. + internal_forces: NDArray[np.float64] + 2D (dim, n_nodes) array containing data with 'float' type. + Rod node internal forces. Note that internal forces are stored on the node, not on elements. + internal_torques: NDArray[np.float64] + 2D (dim, n_elems) array containing data with 'float' type. + Rod element internal torques. + external_forces: NDArray[np.float64] + 2D (dim, n_nodes) array containing data with 'float' type. + External forces acting on rod nodes. + external_torques: NDArray[np.float64] + 2D (dim, n_elems) array containing data with 'float' type. + External torques acting on rod elements. + lengths: NDArray[np.float64] + 1D (n_elems) array containing data with 'float' type. + Rod element lengths. + tangents: NDArray[np.float64] + 2D (dim, n_elems) array containing data with 'float' type. + Rod element tangent vectors. + radius: NDArray[np.float64] + 1D (n_elems) array containing data with 'float' type. + Rod element radius. + dilatation: NDArray[np.float64] + 1D (n_elems) array containing data with 'float' type. + Rod element dilatation. + voronoi_dilatation: NDArray[np.float64] + 1D (n_voronoi) array containing data with 'float' type. + Rod dilatation on voronoi domain. + dilatation_rate: NDArray[np.float64] + 1D (n_elems) array containing data with 'float' type. + Rod element dilatation rates. """ REQUISITE_MODULES: list[Type] = [] @@ -411,7 +411,7 @@ def ring_rod( **kwargs: Any, ) -> Self: """ - Cosserat rod constructor for straight-rod geometry. + Cosserat rod constructor for ring-rod geometry. Notes @@ -423,7 +423,7 @@ def ring_rod( Parameters ---------- n_elements : int - Number of element. Must be greater than 3. Generarally recommended to start with 40-50, and adjust the resolution. + Number of element. Must be greater than 3. Generally recommended to start with 40-50, and adjust the resolution. ring_center_position : NDArray[np.float64] Center coordinate for ring rod in 3D direction : NDArray[np.float64] @@ -459,7 +459,7 @@ def ring_rod( "For reference see the class elastica.dissipation.AnalyticalLinearDamper(),\n" "and for usage check examples/axial_stretching.py" ) - # Straight rod is not ring rod set flag to false + # Ring rod flag set to true ring_rod_flag = True ( n_elements, @@ -673,7 +673,6 @@ def compute_bending_energy(self) -> NDArray[np.float64]: """ Compute total bending energy of the rod at the instance. """ - kappa_diff = self.kappa - self.rest_kappa bending_internal_torques = _batch_matvec(self.bend_matrix, kappa_diff) @@ -689,7 +688,6 @@ def compute_shear_energy(self) -> NDArray[np.float64]: """ Compute total shear energy of the rod at the instance. """ - sigma_diff = self.sigma - self.rest_sigma shear_internal_forces = _batch_matvec(self.shear_matrix, sigma_diff) @@ -751,11 +749,11 @@ def _compute_all_dilatations( for k in range(lengths.shape[0]): dilatation[k] = lengths[k] / rest_lengths[k] - # Cmopute eq (3.4) from 2018 RSOS paper + # Compute eq (3.4) from 2018 RSOS paper # Note : we can use trapezoidal kernel, but it has padding and will be slower voronoi_lengths = position_average(lengths) - # Cmopute eq (3.45 from 2018 RSOS paper + # Compute eq (3.4) from 2018 RSOS paper for k in range(voronoi_lengths.shape[0]): voronoi_dilatation[k] = voronoi_lengths[k] / rest_voronoi_lengths[k] diff --git a/elastica/rod/factory_function.py b/elastica/rod/factory_function.py index 2765e872..792c2ea5 100644 --- a/elastica/rod/factory_function.py +++ b/elastica/rod/factory_function.py @@ -62,7 +62,7 @@ def allocate( log = logging.getLogger() if "poisson_ratio" in kwargs: - # Deprecation warning for poission_ratio + # Deprecation warning for poisson_ratio raise NameError( "Poisson's ratio is deprecated for Cosserat Rod for clarity. Please provide shear_modulus instead." ) @@ -333,42 +333,6 @@ def allocate( ) -""" -Cosserat rod constructor for straight-rod or ring rod geometry. - - -Notes ------ -Since we expect the Cosserat Rod to simulate soft rod, Poisson's ratio is set to 0.5 by default. -It is possible to give additional argument "shear_modulus" or "poisson_ratio" to specify extra modulus. - - -Parameters ----------- -n_elements : int - Number of element. Must be greater than 3. Generally recommended to start with 40-50, and adjust the resolution. -direction : NDArray[3, float] - Direction of the rod in 3D -normal : NDArray[3, float] - Normal vector of the rod in 3D -base_length : float - Total length of the rod -base_radius : float - Uniform radius of the rod -density : float - Density of the rod -youngs_modulus : float - Young's modulus -**kwargs : dict, optional - The "position" and/or "directors" can be overrided by passing "position" and "directors" argument. - Remember, the shape of the "position" is (3,n_elements+1) and the shape of the "directors" is (3,3,n_elements). - -Returns -------- - -""" - - def _assert_dim(vector: np.ndarray, max_dim: int, name: str) -> None: assert vector.ndim < max_dim, ( f"Input {name} dimension is not correct {vector.shape}" diff --git a/elastica/surface/plane.py b/elastica/surface/plane.py index 23c0e057..cc317a52 100644 --- a/elastica/surface/plane.py +++ b/elastica/surface/plane.py @@ -1,4 +1,4 @@ -__doc__ = """""" +__doc__ = """Module containing plane surface implementation for contact interactions.""" from typing import Type import numpy as np @@ -10,6 +10,13 @@ class Plane(StaticSystemBase): """ Plane static system. Static system does not change by the timestepping. + + Attributes + ---------- + normal : numpy.ndarray + 1D (3,) array containing the normal vector of the plane. + origin : numpy.ndarray + 2D (3, 1) array containing the origin of the plane. """ REQUISITE_MODULES: list[Type] = [] @@ -22,12 +29,12 @@ def __init__( Parameters ---------- - plane_origin: np.ndarray + plane_origin: numpy.ndarray + 1D (3,) or 2D (3, 1) array containing data with 'float' type. Origin of the plane. - Expect (3,1)-shaped array. - plane_normal: np.ndarray + plane_normal: numpy.ndarray + 1D (3,) or 2D (3, 1) array containing data with 'float' type. The normal vector of the plane, must be normalized. - Expect (3,1)-shaped array. """ assert np.allclose( diff --git a/elastica/utils.py b/elastica/utils.py index 553db0e1..0e3ac70a 100644 --- a/elastica/utils.py +++ b/elastica/utils.py @@ -170,17 +170,21 @@ def _bspline( # type: ignore[no-any-unimported] Parameters ---------- - t_coeff : np.array + t_coeff : numpy.ndarray The spline coefficients, denoted by :math:`beta_i`. Note that the first and the last values are set to zero by default. - l_centreline : float + l_centerline : float The length of the centerline in meters. Returns ------- - spline : scipy.interpolate.Bspline class + spline : scipy.interpolate.BSpline A spline class that can be called as spline(x), where x are the points at which the spline needs to be evaluated. + ctr_pts : numpy.ndarray + Control points. + ctr_coeffs : numpy.ndarray + Control coefficients. """ # Divide into n_control_pts number of points (n_ctr_pts-1) regions control_pts = l_centerline * np.linspace(0.0, 1.0, t_coeff.shape[0] - 2) diff --git a/elastica/version.py b/elastica/version.py index 4bfcae0e..8a1604bb 100644 --- a/elastica/version.py +++ b/elastica/version.py @@ -1,6 +1,6 @@ import importlib.metadata try: - VERSION = importlib.metadata.version("elastica") + VERSION = importlib.metadata.version("pyelastica") except importlib.metadata.PackageNotFoundError: VERSION = "unknown" diff --git a/examples/ContinuumSnakeWithLiftingWaveCase/snake_forcing.py b/examples/ContinuumSnakeWithLiftingWaveCase/snake_forcing.py index e12216a5..7fedcce9 100755 --- a/examples/ContinuumSnakeWithLiftingWaveCase/snake_forcing.py +++ b/examples/ContinuumSnakeWithLiftingWaveCase/snake_forcing.py @@ -12,7 +12,7 @@ from elastica.external_forces import NoForces from elastica.external_forces import ( inplace_addition, - inplace_substraction, + inplace_subtraction, ) @@ -174,7 +174,7 @@ def compute_muscle_torques( external_torques[..., 1:], _batch_matvec(director_collection[..., 1:], torque), ) - inplace_substraction( + inplace_subtraction( external_torques[..., :-1], _batch_matvec(director_collection[..., :-1], torque), ) diff --git a/tests/test_external_forces.py b/tests/test_external_forces.py index a10b910b..1d9bf72a 100644 --- a/tests/test_external_forces.py +++ b/tests/test_external_forces.py @@ -13,7 +13,7 @@ UniformForces, MuscleTorques, inplace_addition, - inplace_substraction, + inplace_subtraction, EndpointForcesSinusoidal, ) from elastica.utils import Tolerance @@ -297,7 +297,7 @@ def test_inplace_addition(rng, n_elem): @pytest.mark.parametrize("n_elem", [33, 59, 100]) -def test_inplace_substraction(rng, n_elem): +def test_inplace_subtraction(rng, n_elem): """ This test is for inplace substraction written using Numba njit functions Parameters @@ -317,6 +317,6 @@ def test_inplace_substraction(rng, n_elem): correct_vector = first_input_vector - second_input_vector test_vector = first_input_vector.copy() - inplace_substraction(test_vector, second_input_vector) + inplace_subtraction(test_vector, second_input_vector) assert_allclose(correct_vector, test_vector, atol=Tolerance.atol()) From c90a8fbd2ff123fd8ece06f42551b53568316a1e Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 14 Dec 2025 23:05:28 -0600 Subject: [PATCH 67/85] update documentation and configuration files for next release --- .github/dependabot.yml | 2 +- .github/workflows/main.yml | 10 +- CONTRIBUTING.md | 2 +- docs/README.md | 10 +- pyproject.toml | 8 +- uv.lock | 2118 ++++++++++++++++++------------------ 6 files changed, 1072 insertions(+), 1078 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b47ad188..8717de9c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,5 +10,5 @@ updates: schedule: interval: "daily" open-pull-requests-limit: 99 - target-branch: "update-v1" + target-branch: "master" versioning-strategy: lockfile-only diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 60b659a9..1317163a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,14 +3,6 @@ name: CI # Controls when the action will run. on: [push, pull_request] -# Older settings: -# Triggers the workflow on push request events for the master branch, -# and pull request events for all branches. -#on: -# push: -# branches: [ master ] -# pull_request: -# branches: [ '**' ] # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -102,7 +94,7 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Set up cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ matrix.path }} key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }}-${{ hashFiles('uv.lock') }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e4108c52..9358a501 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -121,7 +121,7 @@ Please don't hesitate improving [documentation](https://github.com/GazzolaLab/Py We also have many related projects in separate repositories that utilize the PyElastica as a core library. Since the package is often used for research purpose, many on-going projects are typically not published. -If you are interested in hearing more, please contact one of our the maintainer. +If you are interested in hearing more, please contact the maintainer. ### Pull requests diff --git a/docs/README.md b/docs/README.md index ca199fe8..094b63ce 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,8 +8,8 @@ In addition, we utilize the following extensions to enhance the documentation :c ## Build documentation -The `sphinx` is already initialized in `docs` directory. -In order to build the documentation, you will need additional +The `sphinx` is already initialized in `docs` directory. +In order to build the documentation, you will need additional packages listed in extra dependencies. ```bash @@ -27,7 +27,7 @@ Use `make help` for other options. # Contribution -The documentation-related commits will be collected in the branch `doc_**` separate from `master` branch, and merged into `master` collectively. Ideally, the updates in the documentation branch will seek upcoming version update (i.e. `update-**` branch) and merged shortly after the version release. If an update is critical and urgent, create PR directly to `master` branch. +Documentation-related commits should be pushed to the appropriate `update-**` branch. These changes will be incorporated into the `master` branch upon the corresponding release. For urgent or critical documentation updates, you may create a pull request directly to `master`. ## User Guide @@ -39,7 +39,7 @@ These files will be managed in `docs` directory. ## API documentation The docstring for function or modules are automatically parsed using `sphinx`+`numpydoc`. -Any inline function description, such as +Any inline function description, such as ```py """ This is the form of a docstring. @@ -62,4 +62,4 @@ will be parsed and displayed in API documentation. See `numpydoc` for more detai `ReadtheDocs` runs `sphinx` internally and maintain the documentation website. We will always activate the `stable` and `latest` version, and few past-documentations will also be available for the support. -@nmnaughton and @skim449 has access to the `ReadtheDocs` account. +@nmnaughton and [@skim0119](https://github.com/skim0119) has access to the `ReadtheDocs` account. diff --git a/pyproject.toml b/pyproject.toml index bc467f98..38b3e80e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ documentation = "https://docs.cosseratrods.org/en/latest/" classifiers = [ "License :: OSI Approved :: MIT License", - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Programming Language :: Python", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -33,9 +33,6 @@ dependencies = [ "scipy", "tqdm", "matplotlib", - "mypy", - "mypy-extensions", - "flake8", "cma", ] @@ -74,6 +71,9 @@ dev = [ "codecov", "click", "autoflake", + "mypy", + "mypy-extensions", + "flake8", ] [tool.black] diff --git a/uv.lock b/uv.lock index 6be9f1c9..bad56e1f 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.11'", @@ -12,18 +13,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899 } +sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903 }, + { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, ] [[package]] name = "alabaster" version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210 } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929 }, + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] [[package]] @@ -34,18 +35,18 @@ dependencies = [ { name = "pyflakes" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2a/cb/486f912d6171bc5748c311a2984a301f4e2d054833a1da78485866c71522/autoflake-2.3.1.tar.gz", hash = "sha256:c98b75dc5b0a86459c4f01a1d32ac7eb4338ec4317a4469515ff1e687ecd909e", size = 27642 } +sdist = { url = "https://files.pythonhosted.org/packages/2a/cb/486f912d6171bc5748c311a2984a301f4e2d054833a1da78485866c71522/autoflake-2.3.1.tar.gz", hash = "sha256:c98b75dc5b0a86459c4f01a1d32ac7eb4338ec4317a4469515ff1e687ecd909e", size = 27642, upload-time = "2024-03-13T03:41:28.977Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/ee/3fd29bf416eb4f1c5579cf12bf393ae954099258abd7bde03c4f9716ef6b/autoflake-2.3.1-py3-none-any.whl", hash = "sha256:3ae7495db9084b7b32818b4140e6dc4fc280b712fb414f5b8fe57b0a8e85a840", size = 32483 }, + { url = "https://files.pythonhosted.org/packages/a2/ee/3fd29bf416eb4f1c5579cf12bf393ae954099258abd7bde03c4f9716ef6b/autoflake-2.3.1-py3-none-any.whl", hash = "sha256:3ae7495db9084b7b32818b4140e6dc4fc280b712fb414f5b8fe57b0a8e85a840", size = 32483, upload-time = "2024-03-13T03:41:26.969Z" }, ] [[package]] name = "babel" version = "2.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, ] [[package]] @@ -56,9 +57,9 @@ dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285 }, + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" }, ] [[package]] @@ -74,104 +75,104 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 } +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419 }, - { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080 }, - { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886 }, - { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404 }, - { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372 }, - { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865 }, - { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699 }, - { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028 }, - { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988 }, - { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985 }, - { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816 }, - { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 }, - { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 }, - { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 }, - { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 }, - { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 }, - { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 }, + { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419, upload-time = "2025-01-29T05:37:06.642Z" }, + { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080, upload-time = "2025-01-29T05:37:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886, upload-time = "2025-01-29T04:18:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404, upload-time = "2025-01-29T04:19:04.296Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372, upload-time = "2025-01-29T05:37:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865, upload-time = "2025-01-29T05:37:14.309Z" }, + { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699, upload-time = "2025-01-29T04:18:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028, upload-time = "2025-01-29T04:18:51.711Z" }, + { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988, upload-time = "2025-01-29T05:37:16.707Z" }, + { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985, upload-time = "2025-01-29T05:37:18.273Z" }, + { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816, upload-time = "2025-01-29T04:18:33.823Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860, upload-time = "2025-01-29T04:19:12.944Z" }, + { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" }, + { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" }, + { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, ] [[package]] name = "certifi" version = "2025.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386 } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216 }, + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, ] [[package]] name = "cfgv" version = "3.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818 }, - { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649 }, - { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045 }, - { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356 }, - { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471 }, - { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317 }, - { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368 }, - { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491 }, - { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695 }, - { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849 }, - { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091 }, - { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445 }, - { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782 }, - { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 }, - { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 }, - { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 }, - { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 }, - { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 }, - { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 }, - { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 }, - { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 }, - { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 }, - { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 }, - { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 }, - { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 }, - { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 }, - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936 }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790 }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924 }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626 }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567 }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957 }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408 }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399 }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815 }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537 }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565 }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357 }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776 }, - { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622 }, - { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435 }, - { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653 }, - { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231 }, - { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243 }, - { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442 }, - { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147 }, - { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057 }, - { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454 }, - { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174 }, - { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166 }, - { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 }, - { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] [[package]] @@ -181,9 +182,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 } +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 }, + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, ] [[package]] @@ -193,9 +194,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/fe/bd65ec131f5679900c5e874ef60d088849e299c0dba6d98cce69e56b2d98/cma-4.3.0.tar.gz", hash = "sha256:faa8933e9d55e199c052dd114d123d8d9a3ca914d932629e8b6e77200681a206", size = 284531 } +sdist = { url = "https://files.pythonhosted.org/packages/ca/fe/bd65ec131f5679900c5e874ef60d088849e299c0dba6d98cce69e56b2d98/cma-4.3.0.tar.gz", hash = "sha256:faa8933e9d55e199c052dd114d123d8d9a3ca914d932629e8b6e77200681a206", size = 284531, upload-time = "2025-07-24T11:32:18.261Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/99/351d5a95a481068d83e1f1967b9d00237e70519fc6d4d5e9e390b94e5714/cma-4.3.0-py3-none-any.whl", hash = "sha256:65ad8799e6438b8b82c82c11aad070727e5887915ea6f2c17d7ca0eea940c57a", size = 295869 }, + { url = "https://files.pythonhosted.org/packages/32/99/351d5a95a481068d83e1f1967b9d00237e70519fc6d4d5e9e390b94e5714/cma-4.3.0-py3-none-any.whl", hash = "sha256:65ad8799e6438b8b82c82c11aad070727e5887915ea6f2c17d7ca0eea940c57a", size = 295869, upload-time = "2025-07-24T11:32:13.722Z" }, ] [[package]] @@ -206,18 +207,18 @@ dependencies = [ { name = "coverage" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/bb/594b26d2c85616be6195a64289c578662678afa4910cef2d3ce8417cf73e/codecov-2.1.13.tar.gz", hash = "sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c", size = 21416 } +sdist = { url = "https://files.pythonhosted.org/packages/2c/bb/594b26d2c85616be6195a64289c578662678afa4910cef2d3ce8417cf73e/codecov-2.1.13.tar.gz", hash = "sha256:2362b685633caeaf45b9951a9b76ce359cd3581dd515b430c6c3f5dfb4d92a8c", size = 21416, upload-time = "2023-04-17T23:11:39.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/02/18785edcdf6266cdd6c6dc7635f1cbeefd9a5b4c3bb8aff8bd681e9dd095/codecov-2.1.13-py2.py3-none-any.whl", hash = "sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5", size = 16512 }, + { url = "https://files.pythonhosted.org/packages/af/02/18785edcdf6266cdd6c6dc7635f1cbeefd9a5b4c3bb8aff8bd681e9dd095/codecov-2.1.13-py2.py3-none-any.whl", hash = "sha256:c2ca5e51bba9ebb43644c43d0690148a55086f7f5e6fd36170858fa4206744d5", size = 16512, upload-time = "2023-04-17T23:11:37.344Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] @@ -230,64 +231,64 @@ resolution-markers = [ dependencies = [ { name = "numpy", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551 }, - { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399 }, - { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061 }, - { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956 }, - { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872 }, - { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027 }, - { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641 }, - { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075 }, - { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534 }, - { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188 }, - { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636 }, - { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636 }, - { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053 }, - { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985 }, - { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750 }, - { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246 }, - { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728 }, - { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762 }, - { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196 }, - { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017 }, - { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580 }, - { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530 }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688 }, - { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331 }, - { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963 }, - { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681 }, - { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674 }, - { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480 }, - { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489 }, - { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042 }, - { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630 }, - { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670 }, - { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694 }, - { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986 }, - { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060 }, - { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747 }, - { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895 }, - { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098 }, - { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535 }, - { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096 }, - { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090 }, - { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643 }, - { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443 }, - { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865 }, - { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162 }, - { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355 }, - { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935 }, - { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168 }, - { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550 }, - { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214 }, - { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681 }, - { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101 }, - { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599 }, - { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807 }, - { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729 }, - { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791 }, +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, ] [[package]] @@ -300,164 +301,164 @@ resolution-markers = [ dependencies = [ { name = "numpy", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773 }, - { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149 }, - { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222 }, - { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234 }, - { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555 }, - { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238 }, - { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218 }, - { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867 }, - { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677 }, - { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234 }, - { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123 }, - { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419 }, - { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979 }, - { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653 }, - { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536 }, - { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397 }, - { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601 }, - { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288 }, - { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386 }, - { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018 }, - { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567 }, - { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655 }, - { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257 }, - { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034 }, - { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672 }, - { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234 }, - { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169 }, - { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859 }, - { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062 }, - { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932 }, - { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024 }, - { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578 }, - { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524 }, - { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730 }, - { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897 }, - { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751 }, - { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486 }, - { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106 }, - { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548 }, - { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297 }, - { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023 }, - { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157 }, - { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570 }, - { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713 }, - { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189 }, - { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251 }, - { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810 }, - { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871 }, - { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264 }, - { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819 }, - { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650 }, - { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833 }, - { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692 }, - { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424 }, - { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300 }, - { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769 }, - { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892 }, - { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748 }, - { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554 }, - { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118 }, - { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555 }, - { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295 }, - { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027 }, - { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428 }, - { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331 }, - { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831 }, - { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809 }, - { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593 }, - { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202 }, - { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207 }, - { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315 }, +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, ] [[package]] name = "coverage" version = "7.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/76/17780846fc7aade1e66712e1e27dd28faa0a5d987a1f433610974959eaa8/coverage-7.10.2.tar.gz", hash = "sha256:5d6e6d84e6dd31a8ded64759626627247d676a23c1b892e1326f7c55c8d61055", size = 820754 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/5f/5ce748ab3f142593698aff5f8a0cf020775aa4e24b9d8748b5a56b64d3f8/coverage-7.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:79f0283ab5e6499fd5fe382ca3d62afa40fb50ff227676a3125d18af70eabf65", size = 215003 }, - { url = "https://files.pythonhosted.org/packages/f4/ed/507088561217b000109552139802fa99c33c16ad19999c687b601b3790d0/coverage-7.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4545e906f595ee8ab8e03e21be20d899bfc06647925bc5b224ad7e8c40e08b8", size = 215391 }, - { url = "https://files.pythonhosted.org/packages/79/1b/0f496259fe137c4c5e1e8eaff496fb95af88b71700f5e57725a4ddbe742b/coverage-7.10.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ae385e1d58fbc6a9b1c315e5510ac52281e271478b45f92ca9b5ad42cf39643f", size = 242367 }, - { url = "https://files.pythonhosted.org/packages/b9/8e/5a8835fb0122a2e2a108bf3527931693c4625fdc4d953950a480b9625852/coverage-7.10.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f0cbe5f7dd19f3a32bac2251b95d51c3b89621ac88a2648096ce40f9a5aa1e7", size = 243627 }, - { url = "https://files.pythonhosted.org/packages/c3/96/6a528429c2e0e8d85261764d0cd42e51a429510509bcc14676ee5d1bb212/coverage-7.10.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd17f427f041f6b116dc90b4049c6f3e1230524407d00daa2d8c7915037b5947", size = 245485 }, - { url = "https://files.pythonhosted.org/packages/bf/82/1fba935c4d02c33275aca319deabf1f22c0f95f2c0000bf7c5f276d6f7b4/coverage-7.10.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7f10ca4cde7b466405cce0a0e9971a13eb22e57a5ecc8b5f93a81090cc9c7eb9", size = 243429 }, - { url = "https://files.pythonhosted.org/packages/fc/a8/c8dc0a57a729fc93be33ab78f187a8f52d455fa8f79bfb379fe23b45868d/coverage-7.10.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3b990df23dd51dccce26d18fb09fd85a77ebe46368f387b0ffba7a74e470b31b", size = 242104 }, - { url = "https://files.pythonhosted.org/packages/b9/6f/0b7da1682e2557caeed299a00897b42afde99a241a01eba0197eb982b90f/coverage-7.10.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc3902584d25c7eef57fb38f440aa849a26a3a9f761a029a72b69acfca4e31f8", size = 242397 }, - { url = "https://files.pythonhosted.org/packages/2d/e4/54dc833dadccd519c04a28852f39a37e522bad35d70cfe038817cdb8f168/coverage-7.10.2-cp310-cp310-win32.whl", hash = "sha256:9dd37e9ac00d5eb72f38ed93e3cdf2280b1dbda3bb9b48c6941805f265ad8d87", size = 217502 }, - { url = "https://files.pythonhosted.org/packages/c3/e7/2f78159c4c127549172f427dff15b02176329327bf6a6a1fcf1f603b5456/coverage-7.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:99d16f15cb5baf0729354c5bd3080ae53847a4072b9ba1e10957522fb290417f", size = 218388 }, - { url = "https://files.pythonhosted.org/packages/6e/53/0125a6fc0af4f2687b4e08b0fb332cd0d5e60f3ca849e7456f995d022656/coverage-7.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c3b210d79925a476dfc8d74c7d53224888421edebf3a611f3adae923e212b27", size = 215119 }, - { url = "https://files.pythonhosted.org/packages/0e/2e/960d9871de9152dbc9ff950913c6a6e9cf2eb4cc80d5bc8f93029f9f2f9f/coverage-7.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf67d1787cd317c3f8b2e4c6ed1ae93497be7e30605a0d32237ac37a37a8a322", size = 215511 }, - { url = "https://files.pythonhosted.org/packages/3f/34/68509e44995b9cad806d81b76c22bc5181f3535bca7cd9c15791bfd8951e/coverage-7.10.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:069b779d03d458602bc0e27189876e7d8bdf6b24ac0f12900de22dd2154e6ad7", size = 245513 }, - { url = "https://files.pythonhosted.org/packages/ef/d4/9b12f357413248ce40804b0f58030b55a25b28a5c02db95fb0aa50c5d62c/coverage-7.10.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c2de4cb80b9990e71c62c2d3e9f3ec71b804b1f9ca4784ec7e74127e0f42468", size = 247350 }, - { url = "https://files.pythonhosted.org/packages/b6/40/257945eda1f72098e4a3c350b1d68fdc5d7d032684a0aeb6c2391153ecf4/coverage-7.10.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75bf7ab2374a7eb107602f1e07310cda164016cd60968abf817b7a0b5703e288", size = 249516 }, - { url = "https://files.pythonhosted.org/packages/ff/55/8987f852ece378cecbf39a367f3f7ec53351e39a9151b130af3a3045b83f/coverage-7.10.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3f37516458ec1550815134937f73d6d15b434059cd10f64678a2068f65c62406", size = 247241 }, - { url = "https://files.pythonhosted.org/packages/df/ae/da397de7a42a18cea6062ed9c3b72c50b39e0b9e7b2893d7172d3333a9a1/coverage-7.10.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:de3c6271c482c250d3303fb5c6bdb8ca025fff20a67245e1425df04dc990ece9", size = 245274 }, - { url = "https://files.pythonhosted.org/packages/4e/64/7baa895eb55ec0e1ec35b988687ecd5d4475ababb0d7ae5ca3874dd90ee7/coverage-7.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:98a838101321ac3089c9bb1d4bfa967e8afed58021fda72d7880dc1997f20ae1", size = 245882 }, - { url = "https://files.pythonhosted.org/packages/24/6c/1fd76a0bd09ae75220ae9775a8290416d726f0e5ba26ea72346747161240/coverage-7.10.2-cp311-cp311-win32.whl", hash = "sha256:f2a79145a531a0e42df32d37be5af069b4a914845b6f686590739b786f2f7bce", size = 217541 }, - { url = "https://files.pythonhosted.org/packages/5f/2d/8c18fb7a6e74c79fd4661e82535bc8c68aee12f46c204eabf910b097ccc9/coverage-7.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:e4f5f1320f8ee0d7cfa421ceb257bef9d39fd614dd3ddcfcacd284d4824ed2c2", size = 218426 }, - { url = "https://files.pythonhosted.org/packages/da/40/425bb35e4ff7c7af177edf5dffd4154bc2a677b27696afe6526d75c77fec/coverage-7.10.2-cp311-cp311-win_arm64.whl", hash = "sha256:d8f2d83118f25328552c728b8e91babf93217db259ca5c2cd4dd4220b8926293", size = 217116 }, - { url = "https://files.pythonhosted.org/packages/4e/1e/2c752bdbbf6f1199c59b1a10557fbb6fb3dc96b3c0077b30bd41a5922c1f/coverage-7.10.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:890ad3a26da9ec7bf69255b9371800e2a8da9bc223ae5d86daeb940b42247c83", size = 215311 }, - { url = "https://files.pythonhosted.org/packages/68/6a/84277d73a2cafb96e24be81b7169372ba7ff28768ebbf98e55c85a491b0f/coverage-7.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38fd1ccfca7838c031d7a7874d4353e2f1b98eb5d2a80a2fe5732d542ae25e9c", size = 215550 }, - { url = "https://files.pythonhosted.org/packages/b5/e7/5358b73b46ac76f56cc2de921eeabd44fabd0b7ff82ea4f6b8c159c4d5dc/coverage-7.10.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:76c1ffaaf4f6f0f6e8e9ca06f24bb6454a7a5d4ced97a1bc466f0d6baf4bd518", size = 246564 }, - { url = "https://files.pythonhosted.org/packages/7c/0e/b0c901dd411cb7fc0cfcb28ef0dc6f3049030f616bfe9fc4143aecd95901/coverage-7.10.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86da8a3a84b79ead5c7d0e960c34f580bc3b231bb546627773a3f53c532c2f21", size = 248993 }, - { url = "https://files.pythonhosted.org/packages/0e/4e/a876db272072a9e0df93f311e187ccdd5f39a190c6d1c1f0b6e255a0d08e/coverage-7.10.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99cef9731c8a39801830a604cc53c93c9e57ea8b44953d26589499eded9576e0", size = 250454 }, - { url = "https://files.pythonhosted.org/packages/64/d6/1222dc69f8dd1be208d55708a9f4a450ad582bf4fa05320617fea1eaa6d8/coverage-7.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea58b112f2966a8b91eb13f5d3b1f8bb43c180d624cd3283fb33b1cedcc2dd75", size = 248365 }, - { url = "https://files.pythonhosted.org/packages/62/e3/40fd71151064fc315c922dd9a35e15b30616f00146db1d6a0b590553a75a/coverage-7.10.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:20f405188d28da9522b7232e51154e1b884fc18d0b3a10f382d54784715bbe01", size = 246562 }, - { url = "https://files.pythonhosted.org/packages/fc/14/8aa93ddcd6623ddaef5d8966268ac9545b145bce4fe7b1738fd1c3f0d957/coverage-7.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:64586ce42bbe0da4d9f76f97235c545d1abb9b25985a8791857690f96e23dc3b", size = 247772 }, - { url = "https://files.pythonhosted.org/packages/07/4e/dcb1c01490623c61e2f2ea85cb185fa6a524265bb70eeb897d3c193efeb9/coverage-7.10.2-cp312-cp312-win32.whl", hash = "sha256:bc2e69b795d97ee6d126e7e22e78a509438b46be6ff44f4dccbb5230f550d340", size = 217710 }, - { url = "https://files.pythonhosted.org/packages/79/16/e8aab4162b5f80ad2e5e1f54b1826e2053aa2f4db508b864af647f00c239/coverage-7.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:adda2268b8cf0d11f160fad3743b4dfe9813cd6ecf02c1d6397eceaa5b45b388", size = 218499 }, - { url = "https://files.pythonhosted.org/packages/06/7f/c112ec766e8f1131ce8ce26254be028772757b2d1e63e4f6a4b0ad9a526c/coverage-7.10.2-cp312-cp312-win_arm64.whl", hash = "sha256:164429decd0d6b39a0582eaa30c67bf482612c0330572343042d0ed9e7f15c20", size = 217154 }, - { url = "https://files.pythonhosted.org/packages/8d/04/9b7a741557f93c0ed791b854d27aa8d9fe0b0ce7bb7c52ca1b0f2619cb74/coverage-7.10.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aca7b5645afa688de6d4f8e89d30c577f62956fefb1bad021490d63173874186", size = 215337 }, - { url = "https://files.pythonhosted.org/packages/02/a4/8d1088cd644750c94bc305d3cf56082b4cdf7fb854a25abb23359e74892f/coverage-7.10.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:96e5921342574a14303dfdb73de0019e1ac041c863743c8fe1aa6c2b4a257226", size = 215596 }, - { url = "https://files.pythonhosted.org/packages/01/2f/643a8d73343f70e162d8177a3972b76e306b96239026bc0c12cfde4f7c7a/coverage-7.10.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11333094c1bff621aa811b67ed794865cbcaa99984dedea4bd9cf780ad64ecba", size = 246145 }, - { url = "https://files.pythonhosted.org/packages/1f/4a/722098d1848db4072cda71b69ede1e55730d9063bf868375264d0d302bc9/coverage-7.10.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6eb586fa7d2aee8d65d5ae1dd71414020b2f447435c57ee8de8abea0a77d5074", size = 248492 }, - { url = "https://files.pythonhosted.org/packages/3f/b0/8a6d7f326f6e3e6ed398cde27f9055e860a1e858317001835c521673fb60/coverage-7.10.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d358f259d8019d4ef25d8c5b78aca4c7af25e28bd4231312911c22a0e824a57", size = 249927 }, - { url = "https://files.pythonhosted.org/packages/bb/21/1aaadd3197b54d1e61794475379ecd0f68d8fc5c2ebd352964dc6f698a3d/coverage-7.10.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5250bda76e30382e0a2dcd68d961afcab92c3a7613606e6269855c6979a1b0bb", size = 248138 }, - { url = "https://files.pythonhosted.org/packages/48/65/be75bafb2bdd22fd8bf9bf63cd5873b91bb26ec0d68f02d4b8b09c02decb/coverage-7.10.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a91e027d66eff214d88d9afbe528e21c9ef1ecdf4956c46e366c50f3094696d0", size = 246111 }, - { url = "https://files.pythonhosted.org/packages/5e/30/a4f0c5e249c3cc60e6c6f30d8368e372f2d380eda40e0434c192ac27ccf5/coverage-7.10.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:228946da741558904e2c03ce870ba5efd9cd6e48cbc004d9a27abee08100a15a", size = 247493 }, - { url = "https://files.pythonhosted.org/packages/85/99/f09b9493e44a75cf99ca834394c12f8cb70da6c1711ee296534f97b52729/coverage-7.10.2-cp313-cp313-win32.whl", hash = "sha256:95e23987b52d02e7c413bf2d6dc6288bd5721beb518052109a13bfdc62c8033b", size = 217756 }, - { url = "https://files.pythonhosted.org/packages/2d/bb/cbcb09103be330c7d26ff0ab05c4a8861dd2e254656fdbd3eb7600af4336/coverage-7.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:f35481d42c6d146d48ec92d4e239c23f97b53a3f1fbd2302e7c64336f28641fe", size = 218526 }, - { url = "https://files.pythonhosted.org/packages/37/8f/8bfb4e0bca52c00ab680767c0dd8cfd928a2a72d69897d9b2d5d8b5f63f5/coverage-7.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:65b451949cb789c346f9f9002441fc934d8ccedcc9ec09daabc2139ad13853f7", size = 217176 }, - { url = "https://files.pythonhosted.org/packages/1e/25/d458ba0bf16a8204a88d74dbb7ec5520f29937ffcbbc12371f931c11efd2/coverage-7.10.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e8415918856a3e7d57a4e0ad94651b761317de459eb74d34cc1bb51aad80f07e", size = 216058 }, - { url = "https://files.pythonhosted.org/packages/0b/1c/af4dfd2d7244dc7610fed6d59d57a23ea165681cd764445dc58d71ed01a6/coverage-7.10.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f287a25a8ca53901c613498e4a40885b19361a2fe8fbfdbb7f8ef2cad2a23f03", size = 216273 }, - { url = "https://files.pythonhosted.org/packages/8e/67/ec5095d4035c6e16368226fa9cb15f77f891194c7e3725aeefd08e7a3e5a/coverage-7.10.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:75cc1a3f8c88c69bf16a871dab1fe5a7303fdb1e9f285f204b60f1ee539b8fc0", size = 257513 }, - { url = "https://files.pythonhosted.org/packages/1c/47/be5550b57a3a8ba797de4236b0fd31031f88397b2afc84ab3c2d4cf265f6/coverage-7.10.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca07fa78cc9d26bc8c4740de1abd3489cf9c47cc06d9a8ab3d552ff5101af4c0", size = 259377 }, - { url = "https://files.pythonhosted.org/packages/37/50/b12a4da1382e672305c2d17cd3029dc16b8a0470de2191dbf26b91431378/coverage-7.10.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2e117e64c26300032755d4520cd769f2623cde1a1d1c3515b05a3b8add0ade1", size = 261516 }, - { url = "https://files.pythonhosted.org/packages/db/41/4d3296dbd33dd8da178171540ca3391af7c0184c0870fd4d4574ac290290/coverage-7.10.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:daaf98009977f577b71f8800208f4d40d4dcf5c2db53d4d822787cdc198d76e1", size = 259110 }, - { url = "https://files.pythonhosted.org/packages/ea/f1/b409959ecbc0cec0e61e65683b22bacaa4a3b11512f834e16dd8ffbc37db/coverage-7.10.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ea8d8fe546c528535c761ba424410bbeb36ba8a0f24be653e94b70c93fd8a8ca", size = 257248 }, - { url = "https://files.pythonhosted.org/packages/48/ab/7076dc1c240412e9267d36ec93e9e299d7659f6a5c1e958f87e998b0fb6d/coverage-7.10.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fe024d40ac31eb8d5aae70215b41dafa264676caa4404ae155f77d2fa95c37bb", size = 258063 }, - { url = "https://files.pythonhosted.org/packages/1e/77/f6b51a0288f8f5f7dcc7c89abdd22cf514f3bc5151284f5cd628917f8e10/coverage-7.10.2-cp313-cp313t-win32.whl", hash = "sha256:8f34b09f68bdadec122ffad312154eda965ade433559cc1eadd96cca3de5c824", size = 218433 }, - { url = "https://files.pythonhosted.org/packages/7b/6d/547a86493e25270ce8481543e77f3a0aa3aa872c1374246b7b76273d66eb/coverage-7.10.2-cp313-cp313t-win_amd64.whl", hash = "sha256:71d40b3ac0f26fa9ffa6ee16219a714fed5c6ec197cdcd2018904ab5e75bcfa3", size = 219523 }, - { url = "https://files.pythonhosted.org/packages/ff/d5/3c711e38eaf9ab587edc9bed232c0298aed84e751a9f54aaa556ceaf7da6/coverage-7.10.2-cp313-cp313t-win_arm64.whl", hash = "sha256:abb57fdd38bf6f7dcc66b38dafb7af7c5fdc31ac6029ce373a6f7f5331d6f60f", size = 217739 }, - { url = "https://files.pythonhosted.org/packages/71/53/83bafa669bb9d06d4c8c6a055d8d05677216f9480c4698fb183ba7ec5e47/coverage-7.10.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a3e853cc04987c85ec410905667eed4bf08b1d84d80dfab2684bb250ac8da4f6", size = 215328 }, - { url = "https://files.pythonhosted.org/packages/1d/6c/30827a9c5a48a813e865fbaf91e2db25cce990bd223a022650ef2293fe11/coverage-7.10.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0100b19f230df72c90fdb36db59d3f39232391e8d89616a7de30f677da4f532b", size = 215608 }, - { url = "https://files.pythonhosted.org/packages/bb/a0/c92d85948056ddc397b72a3d79d36d9579c53cb25393ed3c40db7d33b193/coverage-7.10.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9c1cd71483ea78331bdfadb8dcec4f4edfb73c7002c1206d8e0af6797853f5be", size = 246111 }, - { url = "https://files.pythonhosted.org/packages/c2/cf/d695cf86b2559aadd072c91720a7844be4fb82cb4a3b642a2c6ce075692d/coverage-7.10.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f75dbf4899e29a37d74f48342f29279391668ef625fdac6d2f67363518056a1", size = 248419 }, - { url = "https://files.pythonhosted.org/packages/ce/0a/03206aec4a05986e039418c038470d874045f6e00426b0c3879adc1f9251/coverage-7.10.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7df481e7508de1c38b9b8043da48d94931aefa3e32b47dd20277e4978ed5b95", size = 250038 }, - { url = "https://files.pythonhosted.org/packages/ab/9b/b3bd6bd52118c12bc4cf319f5baba65009c9beea84e665b6b9f03fa3f180/coverage-7.10.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:835f39e618099325e7612b3406f57af30ab0a0af350490eff6421e2e5f608e46", size = 248066 }, - { url = "https://files.pythonhosted.org/packages/80/cc/bfa92e261d3e055c851a073e87ba6a3bff12a1f7134233e48a8f7d855875/coverage-7.10.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:12e52b5aa00aa720097d6947d2eb9e404e7c1101ad775f9661ba165ed0a28303", size = 245909 }, - { url = "https://files.pythonhosted.org/packages/12/80/c8df15db4847710c72084164f615ae900af1ec380dce7f74a5678ccdf5e1/coverage-7.10.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:718044729bf1fe3e9eb9f31b52e44ddae07e434ec050c8c628bf5adc56fe4bdd", size = 247329 }, - { url = "https://files.pythonhosted.org/packages/04/6f/cb66e1f7124d5dd9ced69f889f02931419cb448125e44a89a13f4e036124/coverage-7.10.2-cp314-cp314-win32.whl", hash = "sha256:f256173b48cc68486299d510a3e729a96e62c889703807482dbf56946befb5c8", size = 218007 }, - { url = "https://files.pythonhosted.org/packages/8c/e1/3d4be307278ce32c1b9d95cc02ee60d54ddab784036101d053ec9e4fe7f5/coverage-7.10.2-cp314-cp314-win_amd64.whl", hash = "sha256:2e980e4179f33d9b65ac4acb86c9c0dde904098853f27f289766657ed16e07b3", size = 218802 }, - { url = "https://files.pythonhosted.org/packages/ec/66/1e43bbeb66c55a5a5efec70f1c153cf90cfc7f1662ab4ebe2d844de9122c/coverage-7.10.2-cp314-cp314-win_arm64.whl", hash = "sha256:14fb5b6641ab5b3c4161572579f0f2ea8834f9d3af2f7dd8fbaecd58ef9175cc", size = 217397 }, - { url = "https://files.pythonhosted.org/packages/81/01/ae29c129217f6110dc694a217475b8aecbb1b075d8073401f868c825fa99/coverage-7.10.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e96649ac34a3d0e6491e82a2af71098e43be2874b619547c3282fc11d3840a4b", size = 216068 }, - { url = "https://files.pythonhosted.org/packages/a2/50/6e9221d4139f357258f36dfa1d8cac4ec56d9d5acf5fdcc909bb016954d7/coverage-7.10.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1a2e934e9da26341d342d30bfe91422bbfdb3f1f069ec87f19b2909d10d8dcc4", size = 216285 }, - { url = "https://files.pythonhosted.org/packages/eb/ec/89d1d0c0ece0d296b4588e0ef4df185200456d42a47f1141335f482c2fc5/coverage-7.10.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:651015dcd5fd9b5a51ca79ece60d353cacc5beaf304db750407b29c89f72fe2b", size = 257603 }, - { url = "https://files.pythonhosted.org/packages/82/06/c830af66734671c778fc49d35b58339e8f0687fbd2ae285c3f96c94da092/coverage-7.10.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81bf6a32212f9f66da03d63ecb9cd9bd48e662050a937db7199dbf47d19831de", size = 259568 }, - { url = "https://files.pythonhosted.org/packages/60/57/f280dd6f1c556ecc744fbf39e835c33d3ae987d040d64d61c6f821e87829/coverage-7.10.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d800705f6951f75a905ea6feb03fff8f3ea3468b81e7563373ddc29aa3e5d1ca", size = 261691 }, - { url = "https://files.pythonhosted.org/packages/54/2b/c63a0acbd19d99ec32326164c23df3a4e18984fb86e902afdd66ff7b3d83/coverage-7.10.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:248b5394718e10d067354448dc406d651709c6765669679311170da18e0e9af8", size = 259166 }, - { url = "https://files.pythonhosted.org/packages/fd/c5/cd2997dcfcbf0683634da9df52d3967bc1f1741c1475dd0e4722012ba9ef/coverage-7.10.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5c61675a922b569137cf943770d7ad3edd0202d992ce53ac328c5ff68213ccf4", size = 257241 }, - { url = "https://files.pythonhosted.org/packages/16/26/c9e30f82fdad8d47aee90af4978b18c88fa74369ae0f0ba0dbf08cee3a80/coverage-7.10.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:52d708b5fd65589461381fa442d9905f5903d76c086c6a4108e8e9efdca7a7ed", size = 258139 }, - { url = "https://files.pythonhosted.org/packages/c9/99/bdb7bd00bebcd3dedfb895fa9af8e46b91422993e4a37ac634a5f1113790/coverage-7.10.2-cp314-cp314t-win32.whl", hash = "sha256:916369b3b914186b2c5e5ad2f7264b02cff5df96cdd7cdad65dccd39aa5fd9f0", size = 218809 }, - { url = "https://files.pythonhosted.org/packages/eb/5e/56a7852e38a04d1520dda4dfbfbf74a3d6dec932c20526968f7444763567/coverage-7.10.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5b9d538e8e04916a5df63052d698b30c74eb0174f2ca9cd942c981f274a18eaf", size = 219926 }, - { url = "https://files.pythonhosted.org/packages/e0/12/7fbe6b9c52bb9d627e9556f9f2edfdbe88b315e084cdecc9afead0c3b36a/coverage-7.10.2-cp314-cp314t-win_arm64.whl", hash = "sha256:04c74f9ef1f925456a9fd23a7eef1103126186d0500ef9a0acb0bd2514bdc7cc", size = 217925 }, - { url = "https://files.pythonhosted.org/packages/18/d8/9b768ac73a8ac2d10c080af23937212434a958c8d2a1c84e89b450237942/coverage-7.10.2-py3-none-any.whl", hash = "sha256:95db3750dd2e6e93d99fa2498f3a1580581e49c494bddccc6f85c5c21604921f", size = 206973 }, +sdist = { url = "https://files.pythonhosted.org/packages/ee/76/17780846fc7aade1e66712e1e27dd28faa0a5d987a1f433610974959eaa8/coverage-7.10.2.tar.gz", hash = "sha256:5d6e6d84e6dd31a8ded64759626627247d676a23c1b892e1326f7c55c8d61055", size = 820754, upload-time = "2025-08-04T00:35:17.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/5f/5ce748ab3f142593698aff5f8a0cf020775aa4e24b9d8748b5a56b64d3f8/coverage-7.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:79f0283ab5e6499fd5fe382ca3d62afa40fb50ff227676a3125d18af70eabf65", size = 215003, upload-time = "2025-08-04T00:33:02.977Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ed/507088561217b000109552139802fa99c33c16ad19999c687b601b3790d0/coverage-7.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4545e906f595ee8ab8e03e21be20d899bfc06647925bc5b224ad7e8c40e08b8", size = 215391, upload-time = "2025-08-04T00:33:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/79/1b/0f496259fe137c4c5e1e8eaff496fb95af88b71700f5e57725a4ddbe742b/coverage-7.10.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ae385e1d58fbc6a9b1c315e5510ac52281e271478b45f92ca9b5ad42cf39643f", size = 242367, upload-time = "2025-08-04T00:33:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8e/5a8835fb0122a2e2a108bf3527931693c4625fdc4d953950a480b9625852/coverage-7.10.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f0cbe5f7dd19f3a32bac2251b95d51c3b89621ac88a2648096ce40f9a5aa1e7", size = 243627, upload-time = "2025-08-04T00:33:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/c3/96/6a528429c2e0e8d85261764d0cd42e51a429510509bcc14676ee5d1bb212/coverage-7.10.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd17f427f041f6b116dc90b4049c6f3e1230524407d00daa2d8c7915037b5947", size = 245485, upload-time = "2025-08-04T00:33:10.29Z" }, + { url = "https://files.pythonhosted.org/packages/bf/82/1fba935c4d02c33275aca319deabf1f22c0f95f2c0000bf7c5f276d6f7b4/coverage-7.10.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7f10ca4cde7b466405cce0a0e9971a13eb22e57a5ecc8b5f93a81090cc9c7eb9", size = 243429, upload-time = "2025-08-04T00:33:11.909Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a8/c8dc0a57a729fc93be33ab78f187a8f52d455fa8f79bfb379fe23b45868d/coverage-7.10.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3b990df23dd51dccce26d18fb09fd85a77ebe46368f387b0ffba7a74e470b31b", size = 242104, upload-time = "2025-08-04T00:33:13.467Z" }, + { url = "https://files.pythonhosted.org/packages/b9/6f/0b7da1682e2557caeed299a00897b42afde99a241a01eba0197eb982b90f/coverage-7.10.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc3902584d25c7eef57fb38f440aa849a26a3a9f761a029a72b69acfca4e31f8", size = 242397, upload-time = "2025-08-04T00:33:14.682Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e4/54dc833dadccd519c04a28852f39a37e522bad35d70cfe038817cdb8f168/coverage-7.10.2-cp310-cp310-win32.whl", hash = "sha256:9dd37e9ac00d5eb72f38ed93e3cdf2280b1dbda3bb9b48c6941805f265ad8d87", size = 217502, upload-time = "2025-08-04T00:33:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e7/2f78159c4c127549172f427dff15b02176329327bf6a6a1fcf1f603b5456/coverage-7.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:99d16f15cb5baf0729354c5bd3080ae53847a4072b9ba1e10957522fb290417f", size = 218388, upload-time = "2025-08-04T00:33:17.4Z" }, + { url = "https://files.pythonhosted.org/packages/6e/53/0125a6fc0af4f2687b4e08b0fb332cd0d5e60f3ca849e7456f995d022656/coverage-7.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c3b210d79925a476dfc8d74c7d53224888421edebf3a611f3adae923e212b27", size = 215119, upload-time = "2025-08-04T00:33:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2e/960d9871de9152dbc9ff950913c6a6e9cf2eb4cc80d5bc8f93029f9f2f9f/coverage-7.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf67d1787cd317c3f8b2e4c6ed1ae93497be7e30605a0d32237ac37a37a8a322", size = 215511, upload-time = "2025-08-04T00:33:20.32Z" }, + { url = "https://files.pythonhosted.org/packages/3f/34/68509e44995b9cad806d81b76c22bc5181f3535bca7cd9c15791bfd8951e/coverage-7.10.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:069b779d03d458602bc0e27189876e7d8bdf6b24ac0f12900de22dd2154e6ad7", size = 245513, upload-time = "2025-08-04T00:33:21.896Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d4/9b12f357413248ce40804b0f58030b55a25b28a5c02db95fb0aa50c5d62c/coverage-7.10.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4c2de4cb80b9990e71c62c2d3e9f3ec71b804b1f9ca4784ec7e74127e0f42468", size = 247350, upload-time = "2025-08-04T00:33:23.917Z" }, + { url = "https://files.pythonhosted.org/packages/b6/40/257945eda1f72098e4a3c350b1d68fdc5d7d032684a0aeb6c2391153ecf4/coverage-7.10.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75bf7ab2374a7eb107602f1e07310cda164016cd60968abf817b7a0b5703e288", size = 249516, upload-time = "2025-08-04T00:33:25.5Z" }, + { url = "https://files.pythonhosted.org/packages/ff/55/8987f852ece378cecbf39a367f3f7ec53351e39a9151b130af3a3045b83f/coverage-7.10.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3f37516458ec1550815134937f73d6d15b434059cd10f64678a2068f65c62406", size = 247241, upload-time = "2025-08-04T00:33:26.767Z" }, + { url = "https://files.pythonhosted.org/packages/df/ae/da397de7a42a18cea6062ed9c3b72c50b39e0b9e7b2893d7172d3333a9a1/coverage-7.10.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:de3c6271c482c250d3303fb5c6bdb8ca025fff20a67245e1425df04dc990ece9", size = 245274, upload-time = "2025-08-04T00:33:28.494Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/7baa895eb55ec0e1ec35b988687ecd5d4475ababb0d7ae5ca3874dd90ee7/coverage-7.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:98a838101321ac3089c9bb1d4bfa967e8afed58021fda72d7880dc1997f20ae1", size = 245882, upload-time = "2025-08-04T00:33:30.048Z" }, + { url = "https://files.pythonhosted.org/packages/24/6c/1fd76a0bd09ae75220ae9775a8290416d726f0e5ba26ea72346747161240/coverage-7.10.2-cp311-cp311-win32.whl", hash = "sha256:f2a79145a531a0e42df32d37be5af069b4a914845b6f686590739b786f2f7bce", size = 217541, upload-time = "2025-08-04T00:33:31.376Z" }, + { url = "https://files.pythonhosted.org/packages/5f/2d/8c18fb7a6e74c79fd4661e82535bc8c68aee12f46c204eabf910b097ccc9/coverage-7.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:e4f5f1320f8ee0d7cfa421ceb257bef9d39fd614dd3ddcfcacd284d4824ed2c2", size = 218426, upload-time = "2025-08-04T00:33:32.976Z" }, + { url = "https://files.pythonhosted.org/packages/da/40/425bb35e4ff7c7af177edf5dffd4154bc2a677b27696afe6526d75c77fec/coverage-7.10.2-cp311-cp311-win_arm64.whl", hash = "sha256:d8f2d83118f25328552c728b8e91babf93217db259ca5c2cd4dd4220b8926293", size = 217116, upload-time = "2025-08-04T00:33:34.302Z" }, + { url = "https://files.pythonhosted.org/packages/4e/1e/2c752bdbbf6f1199c59b1a10557fbb6fb3dc96b3c0077b30bd41a5922c1f/coverage-7.10.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:890ad3a26da9ec7bf69255b9371800e2a8da9bc223ae5d86daeb940b42247c83", size = 215311, upload-time = "2025-08-04T00:33:35.524Z" }, + { url = "https://files.pythonhosted.org/packages/68/6a/84277d73a2cafb96e24be81b7169372ba7ff28768ebbf98e55c85a491b0f/coverage-7.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38fd1ccfca7838c031d7a7874d4353e2f1b98eb5d2a80a2fe5732d542ae25e9c", size = 215550, upload-time = "2025-08-04T00:33:37.109Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e7/5358b73b46ac76f56cc2de921eeabd44fabd0b7ff82ea4f6b8c159c4d5dc/coverage-7.10.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:76c1ffaaf4f6f0f6e8e9ca06f24bb6454a7a5d4ced97a1bc466f0d6baf4bd518", size = 246564, upload-time = "2025-08-04T00:33:38.33Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0e/b0c901dd411cb7fc0cfcb28ef0dc6f3049030f616bfe9fc4143aecd95901/coverage-7.10.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86da8a3a84b79ead5c7d0e960c34f580bc3b231bb546627773a3f53c532c2f21", size = 248993, upload-time = "2025-08-04T00:33:39.555Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4e/a876db272072a9e0df93f311e187ccdd5f39a190c6d1c1f0b6e255a0d08e/coverage-7.10.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99cef9731c8a39801830a604cc53c93c9e57ea8b44953d26589499eded9576e0", size = 250454, upload-time = "2025-08-04T00:33:41.023Z" }, + { url = "https://files.pythonhosted.org/packages/64/d6/1222dc69f8dd1be208d55708a9f4a450ad582bf4fa05320617fea1eaa6d8/coverage-7.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea58b112f2966a8b91eb13f5d3b1f8bb43c180d624cd3283fb33b1cedcc2dd75", size = 248365, upload-time = "2025-08-04T00:33:42.376Z" }, + { url = "https://files.pythonhosted.org/packages/62/e3/40fd71151064fc315c922dd9a35e15b30616f00146db1d6a0b590553a75a/coverage-7.10.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:20f405188d28da9522b7232e51154e1b884fc18d0b3a10f382d54784715bbe01", size = 246562, upload-time = "2025-08-04T00:33:43.663Z" }, + { url = "https://files.pythonhosted.org/packages/fc/14/8aa93ddcd6623ddaef5d8966268ac9545b145bce4fe7b1738fd1c3f0d957/coverage-7.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:64586ce42bbe0da4d9f76f97235c545d1abb9b25985a8791857690f96e23dc3b", size = 247772, upload-time = "2025-08-04T00:33:45.068Z" }, + { url = "https://files.pythonhosted.org/packages/07/4e/dcb1c01490623c61e2f2ea85cb185fa6a524265bb70eeb897d3c193efeb9/coverage-7.10.2-cp312-cp312-win32.whl", hash = "sha256:bc2e69b795d97ee6d126e7e22e78a509438b46be6ff44f4dccbb5230f550d340", size = 217710, upload-time = "2025-08-04T00:33:46.378Z" }, + { url = "https://files.pythonhosted.org/packages/79/16/e8aab4162b5f80ad2e5e1f54b1826e2053aa2f4db508b864af647f00c239/coverage-7.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:adda2268b8cf0d11f160fad3743b4dfe9813cd6ecf02c1d6397eceaa5b45b388", size = 218499, upload-time = "2025-08-04T00:33:48.048Z" }, + { url = "https://files.pythonhosted.org/packages/06/7f/c112ec766e8f1131ce8ce26254be028772757b2d1e63e4f6a4b0ad9a526c/coverage-7.10.2-cp312-cp312-win_arm64.whl", hash = "sha256:164429decd0d6b39a0582eaa30c67bf482612c0330572343042d0ed9e7f15c20", size = 217154, upload-time = "2025-08-04T00:33:49.299Z" }, + { url = "https://files.pythonhosted.org/packages/8d/04/9b7a741557f93c0ed791b854d27aa8d9fe0b0ce7bb7c52ca1b0f2619cb74/coverage-7.10.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aca7b5645afa688de6d4f8e89d30c577f62956fefb1bad021490d63173874186", size = 215337, upload-time = "2025-08-04T00:33:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/02/a4/8d1088cd644750c94bc305d3cf56082b4cdf7fb854a25abb23359e74892f/coverage-7.10.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:96e5921342574a14303dfdb73de0019e1ac041c863743c8fe1aa6c2b4a257226", size = 215596, upload-time = "2025-08-04T00:33:52.33Z" }, + { url = "https://files.pythonhosted.org/packages/01/2f/643a8d73343f70e162d8177a3972b76e306b96239026bc0c12cfde4f7c7a/coverage-7.10.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11333094c1bff621aa811b67ed794865cbcaa99984dedea4bd9cf780ad64ecba", size = 246145, upload-time = "2025-08-04T00:33:53.641Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4a/722098d1848db4072cda71b69ede1e55730d9063bf868375264d0d302bc9/coverage-7.10.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6eb586fa7d2aee8d65d5ae1dd71414020b2f447435c57ee8de8abea0a77d5074", size = 248492, upload-time = "2025-08-04T00:33:55.366Z" }, + { url = "https://files.pythonhosted.org/packages/3f/b0/8a6d7f326f6e3e6ed398cde27f9055e860a1e858317001835c521673fb60/coverage-7.10.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d358f259d8019d4ef25d8c5b78aca4c7af25e28bd4231312911c22a0e824a57", size = 249927, upload-time = "2025-08-04T00:33:57.042Z" }, + { url = "https://files.pythonhosted.org/packages/bb/21/1aaadd3197b54d1e61794475379ecd0f68d8fc5c2ebd352964dc6f698a3d/coverage-7.10.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5250bda76e30382e0a2dcd68d961afcab92c3a7613606e6269855c6979a1b0bb", size = 248138, upload-time = "2025-08-04T00:33:58.329Z" }, + { url = "https://files.pythonhosted.org/packages/48/65/be75bafb2bdd22fd8bf9bf63cd5873b91bb26ec0d68f02d4b8b09c02decb/coverage-7.10.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a91e027d66eff214d88d9afbe528e21c9ef1ecdf4956c46e366c50f3094696d0", size = 246111, upload-time = "2025-08-04T00:33:59.899Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/a4f0c5e249c3cc60e6c6f30d8368e372f2d380eda40e0434c192ac27ccf5/coverage-7.10.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:228946da741558904e2c03ce870ba5efd9cd6e48cbc004d9a27abee08100a15a", size = 247493, upload-time = "2025-08-04T00:34:01.619Z" }, + { url = "https://files.pythonhosted.org/packages/85/99/f09b9493e44a75cf99ca834394c12f8cb70da6c1711ee296534f97b52729/coverage-7.10.2-cp313-cp313-win32.whl", hash = "sha256:95e23987b52d02e7c413bf2d6dc6288bd5721beb518052109a13bfdc62c8033b", size = 217756, upload-time = "2025-08-04T00:34:03.277Z" }, + { url = "https://files.pythonhosted.org/packages/2d/bb/cbcb09103be330c7d26ff0ab05c4a8861dd2e254656fdbd3eb7600af4336/coverage-7.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:f35481d42c6d146d48ec92d4e239c23f97b53a3f1fbd2302e7c64336f28641fe", size = 218526, upload-time = "2025-08-04T00:34:04.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/8f/8bfb4e0bca52c00ab680767c0dd8cfd928a2a72d69897d9b2d5d8b5f63f5/coverage-7.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:65b451949cb789c346f9f9002441fc934d8ccedcc9ec09daabc2139ad13853f7", size = 217176, upload-time = "2025-08-04T00:34:05.973Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/d458ba0bf16a8204a88d74dbb7ec5520f29937ffcbbc12371f931c11efd2/coverage-7.10.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e8415918856a3e7d57a4e0ad94651b761317de459eb74d34cc1bb51aad80f07e", size = 216058, upload-time = "2025-08-04T00:34:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1c/af4dfd2d7244dc7610fed6d59d57a23ea165681cd764445dc58d71ed01a6/coverage-7.10.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f287a25a8ca53901c613498e4a40885b19361a2fe8fbfdbb7f8ef2cad2a23f03", size = 216273, upload-time = "2025-08-04T00:34:09.073Z" }, + { url = "https://files.pythonhosted.org/packages/8e/67/ec5095d4035c6e16368226fa9cb15f77f891194c7e3725aeefd08e7a3e5a/coverage-7.10.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:75cc1a3f8c88c69bf16a871dab1fe5a7303fdb1e9f285f204b60f1ee539b8fc0", size = 257513, upload-time = "2025-08-04T00:34:10.403Z" }, + { url = "https://files.pythonhosted.org/packages/1c/47/be5550b57a3a8ba797de4236b0fd31031f88397b2afc84ab3c2d4cf265f6/coverage-7.10.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca07fa78cc9d26bc8c4740de1abd3489cf9c47cc06d9a8ab3d552ff5101af4c0", size = 259377, upload-time = "2025-08-04T00:34:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/37/50/b12a4da1382e672305c2d17cd3029dc16b8a0470de2191dbf26b91431378/coverage-7.10.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2e117e64c26300032755d4520cd769f2623cde1a1d1c3515b05a3b8add0ade1", size = 261516, upload-time = "2025-08-04T00:34:13.608Z" }, + { url = "https://files.pythonhosted.org/packages/db/41/4d3296dbd33dd8da178171540ca3391af7c0184c0870fd4d4574ac290290/coverage-7.10.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:daaf98009977f577b71f8800208f4d40d4dcf5c2db53d4d822787cdc198d76e1", size = 259110, upload-time = "2025-08-04T00:34:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/b409959ecbc0cec0e61e65683b22bacaa4a3b11512f834e16dd8ffbc37db/coverage-7.10.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ea8d8fe546c528535c761ba424410bbeb36ba8a0f24be653e94b70c93fd8a8ca", size = 257248, upload-time = "2025-08-04T00:34:16.501Z" }, + { url = "https://files.pythonhosted.org/packages/48/ab/7076dc1c240412e9267d36ec93e9e299d7659f6a5c1e958f87e998b0fb6d/coverage-7.10.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fe024d40ac31eb8d5aae70215b41dafa264676caa4404ae155f77d2fa95c37bb", size = 258063, upload-time = "2025-08-04T00:34:18.338Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/f6b51a0288f8f5f7dcc7c89abdd22cf514f3bc5151284f5cd628917f8e10/coverage-7.10.2-cp313-cp313t-win32.whl", hash = "sha256:8f34b09f68bdadec122ffad312154eda965ade433559cc1eadd96cca3de5c824", size = 218433, upload-time = "2025-08-04T00:34:19.71Z" }, + { url = "https://files.pythonhosted.org/packages/7b/6d/547a86493e25270ce8481543e77f3a0aa3aa872c1374246b7b76273d66eb/coverage-7.10.2-cp313-cp313t-win_amd64.whl", hash = "sha256:71d40b3ac0f26fa9ffa6ee16219a714fed5c6ec197cdcd2018904ab5e75bcfa3", size = 219523, upload-time = "2025-08-04T00:34:21.171Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d5/3c711e38eaf9ab587edc9bed232c0298aed84e751a9f54aaa556ceaf7da6/coverage-7.10.2-cp313-cp313t-win_arm64.whl", hash = "sha256:abb57fdd38bf6f7dcc66b38dafb7af7c5fdc31ac6029ce373a6f7f5331d6f60f", size = 217739, upload-time = "2025-08-04T00:34:22.514Z" }, + { url = "https://files.pythonhosted.org/packages/71/53/83bafa669bb9d06d4c8c6a055d8d05677216f9480c4698fb183ba7ec5e47/coverage-7.10.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a3e853cc04987c85ec410905667eed4bf08b1d84d80dfab2684bb250ac8da4f6", size = 215328, upload-time = "2025-08-04T00:34:23.991Z" }, + { url = "https://files.pythonhosted.org/packages/1d/6c/30827a9c5a48a813e865fbaf91e2db25cce990bd223a022650ef2293fe11/coverage-7.10.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0100b19f230df72c90fdb36db59d3f39232391e8d89616a7de30f677da4f532b", size = 215608, upload-time = "2025-08-04T00:34:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a0/c92d85948056ddc397b72a3d79d36d9579c53cb25393ed3c40db7d33b193/coverage-7.10.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9c1cd71483ea78331bdfadb8dcec4f4edfb73c7002c1206d8e0af6797853f5be", size = 246111, upload-time = "2025-08-04T00:34:26.857Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/d695cf86b2559aadd072c91720a7844be4fb82cb4a3b642a2c6ce075692d/coverage-7.10.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f75dbf4899e29a37d74f48342f29279391668ef625fdac6d2f67363518056a1", size = 248419, upload-time = "2025-08-04T00:34:28.726Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0a/03206aec4a05986e039418c038470d874045f6e00426b0c3879adc1f9251/coverage-7.10.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7df481e7508de1c38b9b8043da48d94931aefa3e32b47dd20277e4978ed5b95", size = 250038, upload-time = "2025-08-04T00:34:30.061Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9b/b3bd6bd52118c12bc4cf319f5baba65009c9beea84e665b6b9f03fa3f180/coverage-7.10.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:835f39e618099325e7612b3406f57af30ab0a0af350490eff6421e2e5f608e46", size = 248066, upload-time = "2025-08-04T00:34:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/bfa92e261d3e055c851a073e87ba6a3bff12a1f7134233e48a8f7d855875/coverage-7.10.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:12e52b5aa00aa720097d6947d2eb9e404e7c1101ad775f9661ba165ed0a28303", size = 245909, upload-time = "2025-08-04T00:34:32.943Z" }, + { url = "https://files.pythonhosted.org/packages/12/80/c8df15db4847710c72084164f615ae900af1ec380dce7f74a5678ccdf5e1/coverage-7.10.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:718044729bf1fe3e9eb9f31b52e44ddae07e434ec050c8c628bf5adc56fe4bdd", size = 247329, upload-time = "2025-08-04T00:34:34.388Z" }, + { url = "https://files.pythonhosted.org/packages/04/6f/cb66e1f7124d5dd9ced69f889f02931419cb448125e44a89a13f4e036124/coverage-7.10.2-cp314-cp314-win32.whl", hash = "sha256:f256173b48cc68486299d510a3e729a96e62c889703807482dbf56946befb5c8", size = 218007, upload-time = "2025-08-04T00:34:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e1/3d4be307278ce32c1b9d95cc02ee60d54ddab784036101d053ec9e4fe7f5/coverage-7.10.2-cp314-cp314-win_amd64.whl", hash = "sha256:2e980e4179f33d9b65ac4acb86c9c0dde904098853f27f289766657ed16e07b3", size = 218802, upload-time = "2025-08-04T00:34:37.35Z" }, + { url = "https://files.pythonhosted.org/packages/ec/66/1e43bbeb66c55a5a5efec70f1c153cf90cfc7f1662ab4ebe2d844de9122c/coverage-7.10.2-cp314-cp314-win_arm64.whl", hash = "sha256:14fb5b6641ab5b3c4161572579f0f2ea8834f9d3af2f7dd8fbaecd58ef9175cc", size = 217397, upload-time = "2025-08-04T00:34:39.15Z" }, + { url = "https://files.pythonhosted.org/packages/81/01/ae29c129217f6110dc694a217475b8aecbb1b075d8073401f868c825fa99/coverage-7.10.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e96649ac34a3d0e6491e82a2af71098e43be2874b619547c3282fc11d3840a4b", size = 216068, upload-time = "2025-08-04T00:34:40.648Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/6e9221d4139f357258f36dfa1d8cac4ec56d9d5acf5fdcc909bb016954d7/coverage-7.10.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1a2e934e9da26341d342d30bfe91422bbfdb3f1f069ec87f19b2909d10d8dcc4", size = 216285, upload-time = "2025-08-04T00:34:42.441Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ec/89d1d0c0ece0d296b4588e0ef4df185200456d42a47f1141335f482c2fc5/coverage-7.10.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:651015dcd5fd9b5a51ca79ece60d353cacc5beaf304db750407b29c89f72fe2b", size = 257603, upload-time = "2025-08-04T00:34:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/82/06/c830af66734671c778fc49d35b58339e8f0687fbd2ae285c3f96c94da092/coverage-7.10.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81bf6a32212f9f66da03d63ecb9cd9bd48e662050a937db7199dbf47d19831de", size = 259568, upload-time = "2025-08-04T00:34:45.519Z" }, + { url = "https://files.pythonhosted.org/packages/60/57/f280dd6f1c556ecc744fbf39e835c33d3ae987d040d64d61c6f821e87829/coverage-7.10.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d800705f6951f75a905ea6feb03fff8f3ea3468b81e7563373ddc29aa3e5d1ca", size = 261691, upload-time = "2025-08-04T00:34:47.019Z" }, + { url = "https://files.pythonhosted.org/packages/54/2b/c63a0acbd19d99ec32326164c23df3a4e18984fb86e902afdd66ff7b3d83/coverage-7.10.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:248b5394718e10d067354448dc406d651709c6765669679311170da18e0e9af8", size = 259166, upload-time = "2025-08-04T00:34:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c5/cd2997dcfcbf0683634da9df52d3967bc1f1741c1475dd0e4722012ba9ef/coverage-7.10.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5c61675a922b569137cf943770d7ad3edd0202d992ce53ac328c5ff68213ccf4", size = 257241, upload-time = "2025-08-04T00:34:51.038Z" }, + { url = "https://files.pythonhosted.org/packages/16/26/c9e30f82fdad8d47aee90af4978b18c88fa74369ae0f0ba0dbf08cee3a80/coverage-7.10.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:52d708b5fd65589461381fa442d9905f5903d76c086c6a4108e8e9efdca7a7ed", size = 258139, upload-time = "2025-08-04T00:34:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/c9/99/bdb7bd00bebcd3dedfb895fa9af8e46b91422993e4a37ac634a5f1113790/coverage-7.10.2-cp314-cp314t-win32.whl", hash = "sha256:916369b3b914186b2c5e5ad2f7264b02cff5df96cdd7cdad65dccd39aa5fd9f0", size = 218809, upload-time = "2025-08-04T00:34:54.075Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5e/56a7852e38a04d1520dda4dfbfbf74a3d6dec932c20526968f7444763567/coverage-7.10.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5b9d538e8e04916a5df63052d698b30c74eb0174f2ca9cd942c981f274a18eaf", size = 219926, upload-time = "2025-08-04T00:34:55.643Z" }, + { url = "https://files.pythonhosted.org/packages/e0/12/7fbe6b9c52bb9d627e9556f9f2edfdbe88b315e084cdecc9afead0c3b36a/coverage-7.10.2-cp314-cp314t-win_arm64.whl", hash = "sha256:04c74f9ef1f925456a9fd23a7eef1103126186d0500ef9a0acb0bd2514bdc7cc", size = 217925, upload-time = "2025-08-04T00:34:57.564Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/9b768ac73a8ac2d10c080af23937212434a958c8d2a1c84e89b450237942/coverage-7.10.2-py3-none-any.whl", hash = "sha256:95db3750dd2e6e93d99fa2498f3a1580581e49c494bddccc6f85c5c21604921f", size = 206973, upload-time = "2025-08-04T00:35:15.918Z" }, ] [package.optional-dependencies] @@ -469,27 +470,27 @@ toml = [ name = "cycler" version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] [[package]] name = "distlib" version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605 } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047 }, + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] [[package]] name = "docutils" version = "0.21.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, ] [[package]] @@ -499,18 +500,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 } +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 }, + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] [[package]] name = "filelock" version = "3.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 }, + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, ] [[package]] @@ -522,86 +523,86 @@ dependencies = [ { name = "pycodestyle" }, { name = "pyflakes" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326 } +sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922 }, + { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, ] [[package]] name = "fonttools" version = "4.59.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/27/ec3c723bfdf86f34c5c82bf6305df3e0f0d8ea798d2d3a7cb0c0a866d286/fonttools-4.59.0.tar.gz", hash = "sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14", size = 3532521 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/1f/3dcae710b7c4b56e79442b03db64f6c9f10c3348f7af40339dffcefb581e/fonttools-4.59.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96", size = 2761846 }, - { url = "https://files.pythonhosted.org/packages/eb/0e/ae3a1884fa1549acac1191cc9ec039142f6ac0e9cbc139c2e6a3dab967da/fonttools-4.59.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df", size = 2332060 }, - { url = "https://files.pythonhosted.org/packages/75/46/58bff92a7216829159ac7bdb1d05a48ad1b8ab8c539555f12d29fdecfdd4/fonttools-4.59.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e93df708c69a193fc7987192f94df250f83f3851fda49413f02ba5dded639482", size = 4852354 }, - { url = "https://files.pythonhosted.org/packages/05/57/767e31e48861045d89691128bd81fd4c62b62150f9a17a666f731ce4f197/fonttools-4.59.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:62224a9bb85b4b66d1b46d45cbe43d71cbf8f527d332b177e3b96191ffbc1e64", size = 4781132 }, - { url = "https://files.pythonhosted.org/packages/d7/78/adb5e9b0af5c6ce469e8b0e112f144eaa84b30dd72a486e9c778a9b03b31/fonttools-4.59.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8974b2a266b54c96709bd5e239979cddfd2dbceed331aa567ea1d7c4a2202db", size = 4832901 }, - { url = "https://files.pythonhosted.org/packages/ac/92/bc3881097fbf3d56d112bec308c863c058e5d4c9c65f534e8ae58450ab8a/fonttools-4.59.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:209b75943d158f610b78320eacb5539aa9e920bee2c775445b2846c65d20e19d", size = 4940140 }, - { url = "https://files.pythonhosted.org/packages/4a/54/39cdb23f0eeda2e07ae9cb189f2b6f41da89aabc682d3a387b3ff4a4ed29/fonttools-4.59.0-cp310-cp310-win32.whl", hash = "sha256:4c908a7036f0f3677f8afa577bcd973e3e20ddd2f7c42a33208d18bee95cdb6f", size = 2215890 }, - { url = "https://files.pythonhosted.org/packages/d8/eb/f8388d9e19f95d8df2449febe9b1a38ddd758cfdb7d6de3a05198d785d61/fonttools-4.59.0-cp310-cp310-win_amd64.whl", hash = "sha256:8b4309a2775e4feee7356e63b163969a215d663399cce1b3d3b65e7ec2d9680e", size = 2260191 }, - { url = "https://files.pythonhosted.org/packages/06/96/520733d9602fa1bf6592e5354c6721ac6fc9ea72bc98d112d0c38b967199/fonttools-4.59.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c", size = 2782387 }, - { url = "https://files.pythonhosted.org/packages/87/6a/170fce30b9bce69077d8eec9bea2cfd9f7995e8911c71be905e2eba6368b/fonttools-4.59.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5", size = 2342194 }, - { url = "https://files.pythonhosted.org/packages/b0/b6/7c8166c0066856f1408092f7968ac744060cf72ca53aec9036106f57eeca/fonttools-4.59.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705", size = 5032333 }, - { url = "https://files.pythonhosted.org/packages/eb/0c/707c5a19598eafcafd489b73c4cb1c142102d6197e872f531512d084aa76/fonttools-4.59.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464", size = 4974422 }, - { url = "https://files.pythonhosted.org/packages/f6/e7/6d33737d9fe632a0f59289b6f9743a86d2a9d0673de2a0c38c0f54729822/fonttools-4.59.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38", size = 5010631 }, - { url = "https://files.pythonhosted.org/packages/63/e1/a4c3d089ab034a578820c8f2dff21ef60daf9668034a1e4fb38bb1cc3398/fonttools-4.59.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6", size = 5122198 }, - { url = "https://files.pythonhosted.org/packages/09/77/ca82b9c12fa4de3c520b7760ee61787640cf3fde55ef1b0bfe1de38c8153/fonttools-4.59.0-cp311-cp311-win32.whl", hash = "sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757", size = 2214216 }, - { url = "https://files.pythonhosted.org/packages/ab/25/5aa7ca24b560b2f00f260acf32c4cf29d7aaf8656e159a336111c18bc345/fonttools-4.59.0-cp311-cp311-win_amd64.whl", hash = "sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0", size = 2261879 }, - { url = "https://files.pythonhosted.org/packages/e2/77/b1c8af22f4265e951cd2e5535dbef8859efcef4fb8dee742d368c967cddb/fonttools-4.59.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b", size = 2767562 }, - { url = "https://files.pythonhosted.org/packages/ff/5a/aeb975699588176bb357e8b398dfd27e5d3a2230d92b81ab8cbb6187358d/fonttools-4.59.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2", size = 2335168 }, - { url = "https://files.pythonhosted.org/packages/54/97/c6101a7e60ae138c4ef75b22434373a0da50a707dad523dd19a4889315bf/fonttools-4.59.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b", size = 4909850 }, - { url = "https://files.pythonhosted.org/packages/bd/6c/fa4d18d641054f7bff878cbea14aa9433f292b9057cb1700d8e91a4d5f4f/fonttools-4.59.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1", size = 4955131 }, - { url = "https://files.pythonhosted.org/packages/20/5c/331947fc1377deb928a69bde49f9003364f5115e5cbe351eea99e39412a2/fonttools-4.59.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e", size = 4899667 }, - { url = "https://files.pythonhosted.org/packages/8a/46/b66469dfa26b8ff0baa7654b2cc7851206c6d57fe3abdabbaab22079a119/fonttools-4.59.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e", size = 5051349 }, - { url = "https://files.pythonhosted.org/packages/2e/05/ebfb6b1f3a4328ab69787d106a7d92ccde77ce66e98659df0f9e3f28d93d/fonttools-4.59.0-cp312-cp312-win32.whl", hash = "sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b", size = 2201315 }, - { url = "https://files.pythonhosted.org/packages/09/45/d2bdc9ea20bbadec1016fd0db45696d573d7a26d95ab5174ffcb6d74340b/fonttools-4.59.0-cp312-cp312-win_amd64.whl", hash = "sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01", size = 2249408 }, - { url = "https://files.pythonhosted.org/packages/f3/bb/390990e7c457d377b00890d9f96a3ca13ae2517efafb6609c1756e213ba4/fonttools-4.59.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2", size = 2758704 }, - { url = "https://files.pythonhosted.org/packages/df/6f/d730d9fcc9b410a11597092bd2eb9ca53e5438c6cb90e4b3047ce1b723e9/fonttools-4.59.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2", size = 2330764 }, - { url = "https://files.pythonhosted.org/packages/75/b4/b96bb66f6f8cc4669de44a158099b249c8159231d254ab6b092909388be5/fonttools-4.59.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4", size = 4890699 }, - { url = "https://files.pythonhosted.org/packages/b5/57/7969af50b26408be12baa317c6147588db5b38af2759e6df94554dbc5fdb/fonttools-4.59.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97", size = 4952934 }, - { url = "https://files.pythonhosted.org/packages/d6/e2/dd968053b6cf1f46c904f5bd409b22341477c017d8201619a265e50762d3/fonttools-4.59.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c", size = 4892319 }, - { url = "https://files.pythonhosted.org/packages/6b/95/a59810d8eda09129f83467a4e58f84205dc6994ebaeb9815406363e07250/fonttools-4.59.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c", size = 5034753 }, - { url = "https://files.pythonhosted.org/packages/a5/84/51a69ee89ff8d1fea0c6997e946657e25a3f08513de8435fe124929f3eef/fonttools-4.59.0-cp313-cp313-win32.whl", hash = "sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3", size = 2199688 }, - { url = "https://files.pythonhosted.org/packages/a0/ee/f626cd372932d828508137a79b85167fdcf3adab2e3bed433f295c596c6a/fonttools-4.59.0-cp313-cp313-win_amd64.whl", hash = "sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe", size = 2248560 }, - { url = "https://files.pythonhosted.org/packages/d0/9c/df0ef2c51845a13043e5088f7bb988ca6cd5bb82d5d4203d6a158aa58cf2/fonttools-4.59.0-py3-none-any.whl", hash = "sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d", size = 1128050 }, +sdist = { url = "https://files.pythonhosted.org/packages/8a/27/ec3c723bfdf86f34c5c82bf6305df3e0f0d8ea798d2d3a7cb0c0a866d286/fonttools-4.59.0.tar.gz", hash = "sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14", size = 3532521, upload-time = "2025-07-16T12:04:54.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/1f/3dcae710b7c4b56e79442b03db64f6c9f10c3348f7af40339dffcefb581e/fonttools-4.59.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96", size = 2761846, upload-time = "2025-07-16T12:03:33.267Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0e/ae3a1884fa1549acac1191cc9ec039142f6ac0e9cbc139c2e6a3dab967da/fonttools-4.59.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df", size = 2332060, upload-time = "2025-07-16T12:03:36.472Z" }, + { url = "https://files.pythonhosted.org/packages/75/46/58bff92a7216829159ac7bdb1d05a48ad1b8ab8c539555f12d29fdecfdd4/fonttools-4.59.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e93df708c69a193fc7987192f94df250f83f3851fda49413f02ba5dded639482", size = 4852354, upload-time = "2025-07-16T12:03:39.102Z" }, + { url = "https://files.pythonhosted.org/packages/05/57/767e31e48861045d89691128bd81fd4c62b62150f9a17a666f731ce4f197/fonttools-4.59.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:62224a9bb85b4b66d1b46d45cbe43d71cbf8f527d332b177e3b96191ffbc1e64", size = 4781132, upload-time = "2025-07-16T12:03:41.415Z" }, + { url = "https://files.pythonhosted.org/packages/d7/78/adb5e9b0af5c6ce469e8b0e112f144eaa84b30dd72a486e9c778a9b03b31/fonttools-4.59.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8974b2a266b54c96709bd5e239979cddfd2dbceed331aa567ea1d7c4a2202db", size = 4832901, upload-time = "2025-07-16T12:03:43.115Z" }, + { url = "https://files.pythonhosted.org/packages/ac/92/bc3881097fbf3d56d112bec308c863c058e5d4c9c65f534e8ae58450ab8a/fonttools-4.59.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:209b75943d158f610b78320eacb5539aa9e920bee2c775445b2846c65d20e19d", size = 4940140, upload-time = "2025-07-16T12:03:44.781Z" }, + { url = "https://files.pythonhosted.org/packages/4a/54/39cdb23f0eeda2e07ae9cb189f2b6f41da89aabc682d3a387b3ff4a4ed29/fonttools-4.59.0-cp310-cp310-win32.whl", hash = "sha256:4c908a7036f0f3677f8afa577bcd973e3e20ddd2f7c42a33208d18bee95cdb6f", size = 2215890, upload-time = "2025-07-16T12:03:46.961Z" }, + { url = "https://files.pythonhosted.org/packages/d8/eb/f8388d9e19f95d8df2449febe9b1a38ddd758cfdb7d6de3a05198d785d61/fonttools-4.59.0-cp310-cp310-win_amd64.whl", hash = "sha256:8b4309a2775e4feee7356e63b163969a215d663399cce1b3d3b65e7ec2d9680e", size = 2260191, upload-time = "2025-07-16T12:03:48.908Z" }, + { url = "https://files.pythonhosted.org/packages/06/96/520733d9602fa1bf6592e5354c6721ac6fc9ea72bc98d112d0c38b967199/fonttools-4.59.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c", size = 2782387, upload-time = "2025-07-16T12:03:51.424Z" }, + { url = "https://files.pythonhosted.org/packages/87/6a/170fce30b9bce69077d8eec9bea2cfd9f7995e8911c71be905e2eba6368b/fonttools-4.59.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5", size = 2342194, upload-time = "2025-07-16T12:03:53.295Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/7c8166c0066856f1408092f7968ac744060cf72ca53aec9036106f57eeca/fonttools-4.59.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705", size = 5032333, upload-time = "2025-07-16T12:03:55.177Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0c/707c5a19598eafcafd489b73c4cb1c142102d6197e872f531512d084aa76/fonttools-4.59.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464", size = 4974422, upload-time = "2025-07-16T12:03:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e7/6d33737d9fe632a0f59289b6f9743a86d2a9d0673de2a0c38c0f54729822/fonttools-4.59.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38", size = 5010631, upload-time = "2025-07-16T12:03:59.449Z" }, + { url = "https://files.pythonhosted.org/packages/63/e1/a4c3d089ab034a578820c8f2dff21ef60daf9668034a1e4fb38bb1cc3398/fonttools-4.59.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6", size = 5122198, upload-time = "2025-07-16T12:04:01.542Z" }, + { url = "https://files.pythonhosted.org/packages/09/77/ca82b9c12fa4de3c520b7760ee61787640cf3fde55ef1b0bfe1de38c8153/fonttools-4.59.0-cp311-cp311-win32.whl", hash = "sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757", size = 2214216, upload-time = "2025-07-16T12:04:03.515Z" }, + { url = "https://files.pythonhosted.org/packages/ab/25/5aa7ca24b560b2f00f260acf32c4cf29d7aaf8656e159a336111c18bc345/fonttools-4.59.0-cp311-cp311-win_amd64.whl", hash = "sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0", size = 2261879, upload-time = "2025-07-16T12:04:05.015Z" }, + { url = "https://files.pythonhosted.org/packages/e2/77/b1c8af22f4265e951cd2e5535dbef8859efcef4fb8dee742d368c967cddb/fonttools-4.59.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b", size = 2767562, upload-time = "2025-07-16T12:04:06.895Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5a/aeb975699588176bb357e8b398dfd27e5d3a2230d92b81ab8cbb6187358d/fonttools-4.59.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2", size = 2335168, upload-time = "2025-07-16T12:04:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/54/97/c6101a7e60ae138c4ef75b22434373a0da50a707dad523dd19a4889315bf/fonttools-4.59.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b", size = 4909850, upload-time = "2025-07-16T12:04:10.761Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6c/fa4d18d641054f7bff878cbea14aa9433f292b9057cb1700d8e91a4d5f4f/fonttools-4.59.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1", size = 4955131, upload-time = "2025-07-16T12:04:12.846Z" }, + { url = "https://files.pythonhosted.org/packages/20/5c/331947fc1377deb928a69bde49f9003364f5115e5cbe351eea99e39412a2/fonttools-4.59.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e", size = 4899667, upload-time = "2025-07-16T12:04:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/8a/46/b66469dfa26b8ff0baa7654b2cc7851206c6d57fe3abdabbaab22079a119/fonttools-4.59.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e", size = 5051349, upload-time = "2025-07-16T12:04:16.388Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ebfb6b1f3a4328ab69787d106a7d92ccde77ce66e98659df0f9e3f28d93d/fonttools-4.59.0-cp312-cp312-win32.whl", hash = "sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b", size = 2201315, upload-time = "2025-07-16T12:04:18.557Z" }, + { url = "https://files.pythonhosted.org/packages/09/45/d2bdc9ea20bbadec1016fd0db45696d573d7a26d95ab5174ffcb6d74340b/fonttools-4.59.0-cp312-cp312-win_amd64.whl", hash = "sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01", size = 2249408, upload-time = "2025-07-16T12:04:20.489Z" }, + { url = "https://files.pythonhosted.org/packages/f3/bb/390990e7c457d377b00890d9f96a3ca13ae2517efafb6609c1756e213ba4/fonttools-4.59.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2", size = 2758704, upload-time = "2025-07-16T12:04:22.217Z" }, + { url = "https://files.pythonhosted.org/packages/df/6f/d730d9fcc9b410a11597092bd2eb9ca53e5438c6cb90e4b3047ce1b723e9/fonttools-4.59.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2", size = 2330764, upload-time = "2025-07-16T12:04:23.985Z" }, + { url = "https://files.pythonhosted.org/packages/75/b4/b96bb66f6f8cc4669de44a158099b249c8159231d254ab6b092909388be5/fonttools-4.59.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4", size = 4890699, upload-time = "2025-07-16T12:04:25.664Z" }, + { url = "https://files.pythonhosted.org/packages/b5/57/7969af50b26408be12baa317c6147588db5b38af2759e6df94554dbc5fdb/fonttools-4.59.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97", size = 4952934, upload-time = "2025-07-16T12:04:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e2/dd968053b6cf1f46c904f5bd409b22341477c017d8201619a265e50762d3/fonttools-4.59.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c", size = 4892319, upload-time = "2025-07-16T12:04:30.074Z" }, + { url = "https://files.pythonhosted.org/packages/6b/95/a59810d8eda09129f83467a4e58f84205dc6994ebaeb9815406363e07250/fonttools-4.59.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c", size = 5034753, upload-time = "2025-07-16T12:04:32.292Z" }, + { url = "https://files.pythonhosted.org/packages/a5/84/51a69ee89ff8d1fea0c6997e946657e25a3f08513de8435fe124929f3eef/fonttools-4.59.0-cp313-cp313-win32.whl", hash = "sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3", size = 2199688, upload-time = "2025-07-16T12:04:34.444Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ee/f626cd372932d828508137a79b85167fdcf3adab2e3bed433f295c596c6a/fonttools-4.59.0-cp313-cp313-win_amd64.whl", hash = "sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe", size = 2248560, upload-time = "2025-07-16T12:04:36.034Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/df0ef2c51845a13043e5088f7bb988ca6cd5bb82d5d4203d6a158aa58cf2/fonttools-4.59.0-py3-none-any.whl", hash = "sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d", size = 1128050, upload-time = "2025-07-16T12:04:52.687Z" }, ] [[package]] name = "identify" version = "2.6.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254, upload-time = "2025-05-23T20:37:53.3Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145 }, + { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] [[package]] name = "imagesize" version = "1.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] [[package]] @@ -611,124 +612,124 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "kiwisolver" version = "1.4.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623 }, - { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720 }, - { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413 }, - { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826 }, - { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231 }, - { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938 }, - { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799 }, - { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362 }, - { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695 }, - { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802 }, - { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646 }, - { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260 }, - { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633 }, - { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885 }, - { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175 }, - { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635 }, - { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717 }, - { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413 }, - { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994 }, - { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804 }, - { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690 }, - { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839 }, - { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109 }, - { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269 }, - { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468 }, - { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394 }, - { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901 }, - { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306 }, - { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966 }, - { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311 }, - { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152 }, - { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555 }, - { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067 }, - { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443 }, - { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728 }, - { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388 }, - { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849 }, - { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533 }, - { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898 }, - { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605 }, - { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801 }, - { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077 }, - { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410 }, - { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853 }, - { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424 }, - { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156 }, - { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555 }, - { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071 }, - { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053 }, - { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278 }, - { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139 }, - { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517 }, - { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952 }, - { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132 }, - { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997 }, - { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060 }, - { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471 }, - { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793 }, - { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855 }, - { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430 }, - { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294 }, - { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736 }, - { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194 }, - { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942 }, - { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341 }, - { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455 }, - { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138 }, - { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857 }, - { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129 }, - { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538 }, - { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661 }, - { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710 }, - { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213 }, - { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403 }, - { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657 }, - { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948 }, - { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186 }, - { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279 }, - { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762 }, +sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538, upload-time = "2024-12-24T18:30:51.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623, upload-time = "2024-12-24T18:28:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720, upload-time = "2024-12-24T18:28:19.158Z" }, + { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413, upload-time = "2024-12-24T18:28:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826, upload-time = "2024-12-24T18:28:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231, upload-time = "2024-12-24T18:28:23.851Z" }, + { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938, upload-time = "2024-12-24T18:28:26.687Z" }, + { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799, upload-time = "2024-12-24T18:28:30.538Z" }, + { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362, upload-time = "2024-12-24T18:28:32.943Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695, upload-time = "2024-12-24T18:28:35.641Z" }, + { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802, upload-time = "2024-12-24T18:28:38.357Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646, upload-time = "2024-12-24T18:28:40.941Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260, upload-time = "2024-12-24T18:28:42.273Z" }, + { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633, upload-time = "2024-12-24T18:28:44.87Z" }, + { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885, upload-time = "2024-12-24T18:28:47.346Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175, upload-time = "2024-12-24T18:28:49.651Z" }, + { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635, upload-time = "2024-12-24T18:28:51.826Z" }, + { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717, upload-time = "2024-12-24T18:28:54.256Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413, upload-time = "2024-12-24T18:28:55.184Z" }, + { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994, upload-time = "2024-12-24T18:28:57.493Z" }, + { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804, upload-time = "2024-12-24T18:29:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690, upload-time = "2024-12-24T18:29:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839, upload-time = "2024-12-24T18:29:02.685Z" }, + { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109, upload-time = "2024-12-24T18:29:04.113Z" }, + { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269, upload-time = "2024-12-24T18:29:05.488Z" }, + { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468, upload-time = "2024-12-24T18:29:06.79Z" }, + { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394, upload-time = "2024-12-24T18:29:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901, upload-time = "2024-12-24T18:29:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306, upload-time = "2024-12-24T18:29:12.644Z" }, + { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966, upload-time = "2024-12-24T18:29:14.089Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311, upload-time = "2024-12-24T18:29:15.892Z" }, + { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152, upload-time = "2024-12-24T18:29:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555, upload-time = "2024-12-24T18:29:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067, upload-time = "2024-12-24T18:29:20.096Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443, upload-time = "2024-12-24T18:29:22.843Z" }, + { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728, upload-time = "2024-12-24T18:29:24.463Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388, upload-time = "2024-12-24T18:29:25.776Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849, upload-time = "2024-12-24T18:29:27.202Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533, upload-time = "2024-12-24T18:29:28.638Z" }, + { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898, upload-time = "2024-12-24T18:29:30.368Z" }, + { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605, upload-time = "2024-12-24T18:29:33.151Z" }, + { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801, upload-time = "2024-12-24T18:29:34.584Z" }, + { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077, upload-time = "2024-12-24T18:29:36.138Z" }, + { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410, upload-time = "2024-12-24T18:29:39.991Z" }, + { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853, upload-time = "2024-12-24T18:29:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424, upload-time = "2024-12-24T18:29:44.38Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156, upload-time = "2024-12-24T18:29:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555, upload-time = "2024-12-24T18:29:46.37Z" }, + { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071, upload-time = "2024-12-24T18:29:47.333Z" }, + { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053, upload-time = "2024-12-24T18:29:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278, upload-time = "2024-12-24T18:29:51.164Z" }, + { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139, upload-time = "2024-12-24T18:29:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517, upload-time = "2024-12-24T18:29:53.941Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952, upload-time = "2024-12-24T18:29:56.523Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132, upload-time = "2024-12-24T18:29:57.989Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997, upload-time = "2024-12-24T18:29:59.393Z" }, + { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060, upload-time = "2024-12-24T18:30:01.338Z" }, + { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471, upload-time = "2024-12-24T18:30:04.574Z" }, + { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793, upload-time = "2024-12-24T18:30:06.25Z" }, + { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855, upload-time = "2024-12-24T18:30:07.535Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430, upload-time = "2024-12-24T18:30:08.504Z" }, + { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294, upload-time = "2024-12-24T18:30:09.508Z" }, + { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736, upload-time = "2024-12-24T18:30:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194, upload-time = "2024-12-24T18:30:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942, upload-time = "2024-12-24T18:30:18.927Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341, upload-time = "2024-12-24T18:30:22.102Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455, upload-time = "2024-12-24T18:30:24.947Z" }, + { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138, upload-time = "2024-12-24T18:30:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857, upload-time = "2024-12-24T18:30:28.86Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129, upload-time = "2024-12-24T18:30:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538, upload-time = "2024-12-24T18:30:33.334Z" }, + { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661, upload-time = "2024-12-24T18:30:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710, upload-time = "2024-12-24T18:30:37.281Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213, upload-time = "2024-12-24T18:30:40.019Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403, upload-time = "2024-12-24T18:30:41.372Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657, upload-time = "2024-12-24T18:30:42.392Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948, upload-time = "2024-12-24T18:30:44.703Z" }, + { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186, upload-time = "2024-12-24T18:30:45.654Z" }, + { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279, upload-time = "2024-12-24T18:30:47.951Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762, upload-time = "2024-12-24T18:30:48.903Z" }, ] [[package]] name = "llvmlite" version = "0.44.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/75/d4863ddfd8ab5f6e70f4504cf8cc37f4e986ec6910f4ef8502bb7d3c1c71/llvmlite-0.44.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9fbadbfba8422123bab5535b293da1cf72f9f478a65645ecd73e781f962ca614", size = 28132306 }, - { url = "https://files.pythonhosted.org/packages/37/d9/6e8943e1515d2f1003e8278819ec03e4e653e2eeb71e4d00de6cfe59424e/llvmlite-0.44.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cccf8eb28f24840f2689fb1a45f9c0f7e582dd24e088dcf96e424834af11f791", size = 26201096 }, - { url = "https://files.pythonhosted.org/packages/aa/46/8ffbc114def88cc698906bf5acab54ca9fdf9214fe04aed0e71731fb3688/llvmlite-0.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7202b678cdf904823c764ee0fe2dfe38a76981f4c1e51715b4cb5abb6cf1d9e8", size = 42361859 }, - { url = "https://files.pythonhosted.org/packages/30/1c/9366b29ab050a726af13ebaae8d0dff00c3c58562261c79c635ad4f5eb71/llvmlite-0.44.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40526fb5e313d7b96bda4cbb2c85cd5374e04d80732dd36a282d72a560bb6408", size = 41184199 }, - { url = "https://files.pythonhosted.org/packages/69/07/35e7c594b021ecb1938540f5bce543ddd8713cff97f71d81f021221edc1b/llvmlite-0.44.0-cp310-cp310-win_amd64.whl", hash = "sha256:41e3839150db4330e1b2716c0be3b5c4672525b4c9005e17c7597f835f351ce2", size = 30332381 }, - { url = "https://files.pythonhosted.org/packages/b5/e2/86b245397052386595ad726f9742e5223d7aea999b18c518a50e96c3aca4/llvmlite-0.44.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:eed7d5f29136bda63b6d7804c279e2b72e08c952b7c5df61f45db408e0ee52f3", size = 28132305 }, - { url = "https://files.pythonhosted.org/packages/ff/ec/506902dc6870249fbe2466d9cf66d531265d0f3a1157213c8f986250c033/llvmlite-0.44.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ace564d9fa44bb91eb6e6d8e7754977783c68e90a471ea7ce913bff30bd62427", size = 26201090 }, - { url = "https://files.pythonhosted.org/packages/99/fe/d030f1849ebb1f394bb3f7adad5e729b634fb100515594aca25c354ffc62/llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5d22c3bfc842668168a786af4205ec8e3ad29fb1bc03fd11fd48460d0df64c1", size = 42361858 }, - { url = "https://files.pythonhosted.org/packages/d7/7a/ce6174664b9077fc673d172e4c888cb0b128e707e306bc33fff8c2035f0d/llvmlite-0.44.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f01a394e9c9b7b1d4e63c327b096d10f6f0ed149ef53d38a09b3749dcf8c9610", size = 41184200 }, - { url = "https://files.pythonhosted.org/packages/5f/c6/258801143975a6d09a373f2641237992496e15567b907a4d401839d671b8/llvmlite-0.44.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8489634d43c20cd0ad71330dde1d5bc7b9966937a263ff1ec1cebb90dc50955", size = 30331193 }, - { url = "https://files.pythonhosted.org/packages/15/86/e3c3195b92e6e492458f16d233e58a1a812aa2bfbef9bdd0fbafcec85c60/llvmlite-0.44.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:1d671a56acf725bf1b531d5ef76b86660a5ab8ef19bb6a46064a705c6ca80aad", size = 28132297 }, - { url = "https://files.pythonhosted.org/packages/d6/53/373b6b8be67b9221d12b24125fd0ec56b1078b660eeae266ec388a6ac9a0/llvmlite-0.44.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f79a728e0435493611c9f405168682bb75ffd1fbe6fc360733b850c80a026db", size = 26201105 }, - { url = "https://files.pythonhosted.org/packages/cb/da/8341fd3056419441286c8e26bf436923021005ece0bff5f41906476ae514/llvmlite-0.44.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0143a5ef336da14deaa8ec26c5449ad5b6a2b564df82fcef4be040b9cacfea9", size = 42361901 }, - { url = "https://files.pythonhosted.org/packages/53/ad/d79349dc07b8a395a99153d7ce8b01d6fcdc9f8231355a5df55ded649b61/llvmlite-0.44.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d752f89e31b66db6f8da06df8b39f9b91e78c5feea1bf9e8c1fba1d1c24c065d", size = 41184247 }, - { url = "https://files.pythonhosted.org/packages/e2/3b/a9a17366af80127bd09decbe2a54d8974b6d8b274b39bf47fbaedeec6307/llvmlite-0.44.0-cp312-cp312-win_amd64.whl", hash = "sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1", size = 30332380 }, - { url = "https://files.pythonhosted.org/packages/89/24/4c0ca705a717514c2092b18476e7a12c74d34d875e05e4d742618ebbf449/llvmlite-0.44.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:319bddd44e5f71ae2689859b7203080716448a3cd1128fb144fe5c055219d516", size = 28132306 }, - { url = "https://files.pythonhosted.org/packages/01/cf/1dd5a60ba6aee7122ab9243fd614abcf22f36b0437cbbe1ccf1e3391461c/llvmlite-0.44.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c58867118bad04a0bb22a2e0068c693719658105e40009ffe95c7000fcde88e", size = 26201090 }, - { url = "https://files.pythonhosted.org/packages/d2/1b/656f5a357de7135a3777bd735cc7c9b8f23b4d37465505bd0eaf4be9befe/llvmlite-0.44.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46224058b13c96af1365290bdfebe9a6264ae62fb79b2b55693deed11657a8bf", size = 42361904 }, - { url = "https://files.pythonhosted.org/packages/d8/e1/12c5f20cb9168fb3464a34310411d5ad86e4163c8ff2d14a2b57e5cc6bac/llvmlite-0.44.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0097052c32bf721a4efc03bd109d335dfa57d9bffb3d4c24cc680711b8b4fc", size = 41184245 }, - { url = "https://files.pythonhosted.org/packages/d0/81/e66fc86539293282fd9cb7c9417438e897f369e79ffb62e1ae5e5154d4dd/llvmlite-0.44.0-cp313-cp313-win_amd64.whl", hash = "sha256:2fb7c4f2fb86cbae6dca3db9ab203eeea0e22d73b99bc2341cdf9de93612e930", size = 30331193 }, +sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880, upload-time = "2025-01-20T11:14:41.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/75/d4863ddfd8ab5f6e70f4504cf8cc37f4e986ec6910f4ef8502bb7d3c1c71/llvmlite-0.44.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9fbadbfba8422123bab5535b293da1cf72f9f478a65645ecd73e781f962ca614", size = 28132306, upload-time = "2025-01-20T11:12:18.634Z" }, + { url = "https://files.pythonhosted.org/packages/37/d9/6e8943e1515d2f1003e8278819ec03e4e653e2eeb71e4d00de6cfe59424e/llvmlite-0.44.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cccf8eb28f24840f2689fb1a45f9c0f7e582dd24e088dcf96e424834af11f791", size = 26201096, upload-time = "2025-01-20T11:12:24.544Z" }, + { url = "https://files.pythonhosted.org/packages/aa/46/8ffbc114def88cc698906bf5acab54ca9fdf9214fe04aed0e71731fb3688/llvmlite-0.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7202b678cdf904823c764ee0fe2dfe38a76981f4c1e51715b4cb5abb6cf1d9e8", size = 42361859, upload-time = "2025-01-20T11:12:31.839Z" }, + { url = "https://files.pythonhosted.org/packages/30/1c/9366b29ab050a726af13ebaae8d0dff00c3c58562261c79c635ad4f5eb71/llvmlite-0.44.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40526fb5e313d7b96bda4cbb2c85cd5374e04d80732dd36a282d72a560bb6408", size = 41184199, upload-time = "2025-01-20T11:12:40.049Z" }, + { url = "https://files.pythonhosted.org/packages/69/07/35e7c594b021ecb1938540f5bce543ddd8713cff97f71d81f021221edc1b/llvmlite-0.44.0-cp310-cp310-win_amd64.whl", hash = "sha256:41e3839150db4330e1b2716c0be3b5c4672525b4c9005e17c7597f835f351ce2", size = 30332381, upload-time = "2025-01-20T11:12:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e2/86b245397052386595ad726f9742e5223d7aea999b18c518a50e96c3aca4/llvmlite-0.44.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:eed7d5f29136bda63b6d7804c279e2b72e08c952b7c5df61f45db408e0ee52f3", size = 28132305, upload-time = "2025-01-20T11:12:53.936Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ec/506902dc6870249fbe2466d9cf66d531265d0f3a1157213c8f986250c033/llvmlite-0.44.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ace564d9fa44bb91eb6e6d8e7754977783c68e90a471ea7ce913bff30bd62427", size = 26201090, upload-time = "2025-01-20T11:12:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/99/fe/d030f1849ebb1f394bb3f7adad5e729b634fb100515594aca25c354ffc62/llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5d22c3bfc842668168a786af4205ec8e3ad29fb1bc03fd11fd48460d0df64c1", size = 42361858, upload-time = "2025-01-20T11:13:07.623Z" }, + { url = "https://files.pythonhosted.org/packages/d7/7a/ce6174664b9077fc673d172e4c888cb0b128e707e306bc33fff8c2035f0d/llvmlite-0.44.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f01a394e9c9b7b1d4e63c327b096d10f6f0ed149ef53d38a09b3749dcf8c9610", size = 41184200, upload-time = "2025-01-20T11:13:20.058Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c6/258801143975a6d09a373f2641237992496e15567b907a4d401839d671b8/llvmlite-0.44.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8489634d43c20cd0ad71330dde1d5bc7b9966937a263ff1ec1cebb90dc50955", size = 30331193, upload-time = "2025-01-20T11:13:26.976Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/e3c3195b92e6e492458f16d233e58a1a812aa2bfbef9bdd0fbafcec85c60/llvmlite-0.44.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:1d671a56acf725bf1b531d5ef76b86660a5ab8ef19bb6a46064a705c6ca80aad", size = 28132297, upload-time = "2025-01-20T11:13:32.57Z" }, + { url = "https://files.pythonhosted.org/packages/d6/53/373b6b8be67b9221d12b24125fd0ec56b1078b660eeae266ec388a6ac9a0/llvmlite-0.44.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f79a728e0435493611c9f405168682bb75ffd1fbe6fc360733b850c80a026db", size = 26201105, upload-time = "2025-01-20T11:13:38.744Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/8341fd3056419441286c8e26bf436923021005ece0bff5f41906476ae514/llvmlite-0.44.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0143a5ef336da14deaa8ec26c5449ad5b6a2b564df82fcef4be040b9cacfea9", size = 42361901, upload-time = "2025-01-20T11:13:46.711Z" }, + { url = "https://files.pythonhosted.org/packages/53/ad/d79349dc07b8a395a99153d7ce8b01d6fcdc9f8231355a5df55ded649b61/llvmlite-0.44.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d752f89e31b66db6f8da06df8b39f9b91e78c5feea1bf9e8c1fba1d1c24c065d", size = 41184247, upload-time = "2025-01-20T11:13:56.159Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3b/a9a17366af80127bd09decbe2a54d8974b6d8b274b39bf47fbaedeec6307/llvmlite-0.44.0-cp312-cp312-win_amd64.whl", hash = "sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1", size = 30332380, upload-time = "2025-01-20T11:14:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/89/24/4c0ca705a717514c2092b18476e7a12c74d34d875e05e4d742618ebbf449/llvmlite-0.44.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:319bddd44e5f71ae2689859b7203080716448a3cd1128fb144fe5c055219d516", size = 28132306, upload-time = "2025-01-20T11:14:09.035Z" }, + { url = "https://files.pythonhosted.org/packages/01/cf/1dd5a60ba6aee7122ab9243fd614abcf22f36b0437cbbe1ccf1e3391461c/llvmlite-0.44.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c58867118bad04a0bb22a2e0068c693719658105e40009ffe95c7000fcde88e", size = 26201090, upload-time = "2025-01-20T11:14:15.401Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1b/656f5a357de7135a3777bd735cc7c9b8f23b4d37465505bd0eaf4be9befe/llvmlite-0.44.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46224058b13c96af1365290bdfebe9a6264ae62fb79b2b55693deed11657a8bf", size = 42361904, upload-time = "2025-01-20T11:14:22.949Z" }, + { url = "https://files.pythonhosted.org/packages/d8/e1/12c5f20cb9168fb3464a34310411d5ad86e4163c8ff2d14a2b57e5cc6bac/llvmlite-0.44.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0097052c32bf721a4efc03bd109d335dfa57d9bffb3d4c24cc680711b8b4fc", size = 41184245, upload-time = "2025-01-20T11:14:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/d0/81/e66fc86539293282fd9cb7c9417438e897f369e79ffb62e1ae5e5154d4dd/llvmlite-0.44.0-cp313-cp313-win_amd64.whl", hash = "sha256:2fb7c4f2fb86cbae6dca3db9ab203eeea0e22d73b99bc2341cdf9de93612e930", size = 30331193, upload-time = "2025-01-20T11:14:38.578Z" }, ] [[package]] @@ -738,67 +739,67 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, - { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, - { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, - { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, - { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, - { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, - { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, - { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, - { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, - { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, ] [[package]] @@ -817,71 +818,71 @@ dependencies = [ { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/91/f2939bb60b7ebf12478b030e0d7f340247390f402b3b189616aad790c366/matplotlib-3.10.5.tar.gz", hash = "sha256:352ed6ccfb7998a00881692f38b4ca083c691d3e275b4145423704c34c909076", size = 34804044 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/89/5355cdfe43242cb4d1a64a67cb6831398b665ad90e9702c16247cbd8d5ab/matplotlib-3.10.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5d4773a6d1c106ca05cb5a5515d277a6bb96ed09e5c8fab6b7741b8fcaa62c8f", size = 8229094 }, - { url = "https://files.pythonhosted.org/packages/34/bc/ba802650e1c69650faed261a9df004af4c6f21759d7a1ec67fe972f093b3/matplotlib-3.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc88af74e7ba27de6cbe6faee916024ea35d895ed3d61ef6f58c4ce97da7185a", size = 8091464 }, - { url = "https://files.pythonhosted.org/packages/ac/64/8d0c8937dee86c286625bddb1902efacc3e22f2b619f5b5a8df29fe5217b/matplotlib-3.10.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:64c4535419d5617f7363dad171a5a59963308e0f3f813c4bed6c9e6e2c131512", size = 8653163 }, - { url = "https://files.pythonhosted.org/packages/11/dc/8dfc0acfbdc2fc2336c72561b7935cfa73db9ca70b875d8d3e1b3a6f371a/matplotlib-3.10.5-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a277033048ab22d34f88a3c5243938cef776493f6201a8742ed5f8b553201343", size = 9490635 }, - { url = "https://files.pythonhosted.org/packages/54/02/e3fdfe0f2e9fb05f3a691d63876639dbf684170fdcf93231e973104153b4/matplotlib-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e4a6470a118a2e93022ecc7d3bd16b3114b2004ea2bf014fff875b3bc99b70c6", size = 9539036 }, - { url = "https://files.pythonhosted.org/packages/c1/29/82bf486ff7f4dbedfb11ccc207d0575cbe3be6ea26f75be514252bde3d70/matplotlib-3.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:7e44cada61bec8833c106547786814dd4a266c1b2964fd25daa3804f1b8d4467", size = 8093529 }, - { url = "https://files.pythonhosted.org/packages/aa/c7/1f2db90a1d43710478bb1e9b57b162852f79234d28e4f48a28cc415aa583/matplotlib-3.10.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dcfc39c452c6a9f9028d3e44d2d721484f665304857188124b505b2c95e1eecf", size = 8239216 }, - { url = "https://files.pythonhosted.org/packages/82/6d/ca6844c77a4f89b1c9e4d481c412e1d1dbabf2aae2cbc5aa2da4a1d6683e/matplotlib-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:903352681b59f3efbf4546985142a9686ea1d616bb054b09a537a06e4b892ccf", size = 8102130 }, - { url = "https://files.pythonhosted.org/packages/1d/1e/5e187a30cc673a3e384f3723e5f3c416033c1d8d5da414f82e4e731128ea/matplotlib-3.10.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:080c3676a56b8ee1c762bcf8fca3fe709daa1ee23e6ef06ad9f3fc17332f2d2a", size = 8666471 }, - { url = "https://files.pythonhosted.org/packages/03/c0/95540d584d7d645324db99a845ac194e915ef75011a0d5e19e1b5cee7e69/matplotlib-3.10.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b4984d5064a35b6f66d2c11d668565f4389b1119cc64db7a4c1725bc11adffc", size = 9500518 }, - { url = "https://files.pythonhosted.org/packages/ba/2e/e019352099ea58b4169adb9c6e1a2ad0c568c6377c2b677ee1f06de2adc7/matplotlib-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3967424121d3a46705c9fa9bdb0931de3228f13f73d7bb03c999c88343a89d89", size = 9552372 }, - { url = "https://files.pythonhosted.org/packages/b7/81/3200b792a5e8b354f31f4101ad7834743ad07b6d620259f2059317b25e4d/matplotlib-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:33775bbeb75528555a15ac29396940128ef5613cf9a2d31fb1bfd18b3c0c0903", size = 8100634 }, - { url = "https://files.pythonhosted.org/packages/52/46/a944f6f0c1f5476a0adfa501969d229ce5ae60cf9a663be0e70361381f89/matplotlib-3.10.5-cp311-cp311-win_arm64.whl", hash = "sha256:c61333a8e5e6240e73769d5826b9a31d8b22df76c0778f8480baf1b4b01c9420", size = 7978880 }, - { url = "https://files.pythonhosted.org/packages/66/1e/c6f6bcd882d589410b475ca1fc22e34e34c82adff519caf18f3e6dd9d682/matplotlib-3.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:00b6feadc28a08bd3c65b2894f56cf3c94fc8f7adcbc6ab4516ae1e8ed8f62e2", size = 8253056 }, - { url = "https://files.pythonhosted.org/packages/53/e6/d6f7d1b59413f233793dda14419776f5f443bcccb2dfc84b09f09fe05dbe/matplotlib-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee98a5c5344dc7f48dc261b6ba5d9900c008fc12beb3fa6ebda81273602cc389", size = 8110131 }, - { url = "https://files.pythonhosted.org/packages/66/2b/bed8a45e74957549197a2ac2e1259671cd80b55ed9e1fe2b5c94d88a9202/matplotlib-3.10.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a17e57e33de901d221a07af32c08870ed4528db0b6059dce7d7e65c1122d4bea", size = 8669603 }, - { url = "https://files.pythonhosted.org/packages/7e/a7/315e9435b10d057f5e52dfc603cd353167ae28bb1a4e033d41540c0067a4/matplotlib-3.10.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97b9d6443419085950ee4a5b1ee08c363e5c43d7176e55513479e53669e88468", size = 9508127 }, - { url = "https://files.pythonhosted.org/packages/7f/d9/edcbb1f02ca99165365d2768d517898c22c6040187e2ae2ce7294437c413/matplotlib-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ceefe5d40807d29a66ae916c6a3915d60ef9f028ce1927b84e727be91d884369", size = 9566926 }, - { url = "https://files.pythonhosted.org/packages/3b/d9/6dd924ad5616c97b7308e6320cf392c466237a82a2040381163b7500510a/matplotlib-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:c04cba0f93d40e45b3c187c6c52c17f24535b27d545f757a2fffebc06c12b98b", size = 8107599 }, - { url = "https://files.pythonhosted.org/packages/0e/f3/522dc319a50f7b0279fbe74f86f7a3506ce414bc23172098e8d2bdf21894/matplotlib-3.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:a41bcb6e2c8e79dc99c5511ae6f7787d2fb52efd3d805fff06d5d4f667db16b2", size = 7978173 }, - { url = "https://files.pythonhosted.org/packages/8d/05/4f3c1f396075f108515e45cb8d334aff011a922350e502a7472e24c52d77/matplotlib-3.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:354204db3f7d5caaa10e5de74549ef6a05a4550fdd1c8f831ab9bca81efd39ed", size = 8253586 }, - { url = "https://files.pythonhosted.org/packages/2f/2c/e084415775aac7016c3719fe7006cdb462582c6c99ac142f27303c56e243/matplotlib-3.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b072aac0c3ad563a2b3318124756cb6112157017f7431626600ecbe890df57a1", size = 8110715 }, - { url = "https://files.pythonhosted.org/packages/52/1b/233e3094b749df16e3e6cd5a44849fd33852e692ad009cf7de00cf58ddf6/matplotlib-3.10.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d52fd5b684d541b5a51fb276b2b97b010c75bee9aa392f96b4a07aeb491e33c7", size = 8669397 }, - { url = "https://files.pythonhosted.org/packages/e8/ec/03f9e003a798f907d9f772eed9b7c6a9775d5bd00648b643ebfb88e25414/matplotlib-3.10.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee7a09ae2f4676276f5a65bd9f2bd91b4f9fbaedf49f40267ce3f9b448de501f", size = 9508646 }, - { url = "https://files.pythonhosted.org/packages/91/e7/c051a7a386680c28487bca27d23b02d84f63e3d2a9b4d2fc478e6a42e37e/matplotlib-3.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ba6c3c9c067b83481d647af88b4e441d532acdb5ef22178a14935b0b881188f4", size = 9567424 }, - { url = "https://files.pythonhosted.org/packages/36/c2/24302e93ff431b8f4173ee1dd88976c8d80483cadbc5d3d777cef47b3a1c/matplotlib-3.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:07442d2692c9bd1cceaa4afb4bbe5b57b98a7599de4dabfcca92d3eea70f9ebe", size = 8107809 }, - { url = "https://files.pythonhosted.org/packages/0b/33/423ec6a668d375dad825197557ed8fbdb74d62b432c1ed8235465945475f/matplotlib-3.10.5-cp313-cp313-win_arm64.whl", hash = "sha256:48fe6d47380b68a37ccfcc94f009530e84d41f71f5dae7eda7c4a5a84aa0a674", size = 7978078 }, - { url = "https://files.pythonhosted.org/packages/51/17/521fc16ec766455c7bb52cc046550cf7652f6765ca8650ff120aa2d197b6/matplotlib-3.10.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b80eb8621331449fc519541a7461987f10afa4f9cfd91afcd2276ebe19bd56c", size = 8295590 }, - { url = "https://files.pythonhosted.org/packages/f8/12/23c28b2c21114c63999bae129fce7fd34515641c517ae48ce7b7dcd33458/matplotlib-3.10.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47a388908e469d6ca2a6015858fa924e0e8a2345a37125948d8e93a91c47933e", size = 8158518 }, - { url = "https://files.pythonhosted.org/packages/81/f8/aae4eb25e8e7190759f3cb91cbeaa344128159ac92bb6b409e24f8711f78/matplotlib-3.10.5-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b6b49167d208358983ce26e43aa4196073b4702858670f2eb111f9a10652b4b", size = 8691815 }, - { url = "https://files.pythonhosted.org/packages/d0/ba/450c39ebdd486bd33a359fc17365ade46c6a96bf637bbb0df7824de2886c/matplotlib-3.10.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a8da0453a7fd8e3da114234ba70c5ba9ef0e98f190309ddfde0f089accd46ea", size = 9522814 }, - { url = "https://files.pythonhosted.org/packages/89/11/9c66f6a990e27bb9aa023f7988d2d5809cb98aa39c09cbf20fba75a542ef/matplotlib-3.10.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52c6573dfcb7726a9907b482cd5b92e6b5499b284ffacb04ffbfe06b3e568124", size = 9573917 }, - { url = "https://files.pythonhosted.org/packages/b3/69/8b49394de92569419e5e05e82e83df9b749a0ff550d07631ea96ed2eb35a/matplotlib-3.10.5-cp313-cp313t-win_amd64.whl", hash = "sha256:a23193db2e9d64ece69cac0c8231849db7dd77ce59c7b89948cf9d0ce655a3ce", size = 8181034 }, - { url = "https://files.pythonhosted.org/packages/47/23/82dc435bb98a2fc5c20dffcac8f0b083935ac28286413ed8835df40d0baa/matplotlib-3.10.5-cp313-cp313t-win_arm64.whl", hash = "sha256:56da3b102cf6da2776fef3e71cd96fcf22103a13594a18ac9a9b31314e0be154", size = 8023337 }, - { url = "https://files.pythonhosted.org/packages/ac/e0/26b6cfde31f5383503ee45dcb7e691d45dadf0b3f54639332b59316a97f8/matplotlib-3.10.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:96ef8f5a3696f20f55597ffa91c28e2e73088df25c555f8d4754931515512715", size = 8253591 }, - { url = "https://files.pythonhosted.org/packages/c1/89/98488c7ef7ea20ea659af7499628c240a608b337af4be2066d644cfd0a0f/matplotlib-3.10.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:77fab633e94b9da60512d4fa0213daeb76d5a7b05156840c4fd0399b4b818837", size = 8112566 }, - { url = "https://files.pythonhosted.org/packages/52/67/42294dfedc82aea55e1a767daf3263aacfb5a125f44ba189e685bab41b6f/matplotlib-3.10.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27f52634315e96b1debbfdc5c416592edcd9c4221bc2f520fd39c33db5d9f202", size = 9513281 }, - { url = "https://files.pythonhosted.org/packages/e7/68/f258239e0cf34c2cbc816781c7ab6fca768452e6bf1119aedd2bd4a882a3/matplotlib-3.10.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:525f6e28c485c769d1f07935b660c864de41c37fd716bfa64158ea646f7084bb", size = 9780873 }, - { url = "https://files.pythonhosted.org/packages/89/64/f4881554006bd12e4558bd66778bdd15d47b00a1f6c6e8b50f6208eda4b3/matplotlib-3.10.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1f5f3ec4c191253c5f2b7c07096a142c6a1c024d9f738247bfc8e3f9643fc975", size = 9568954 }, - { url = "https://files.pythonhosted.org/packages/06/f8/42779d39c3f757e1f012f2dda3319a89fb602bd2ef98ce8faf0281f4febd/matplotlib-3.10.5-cp314-cp314-win_amd64.whl", hash = "sha256:707f9c292c4cd4716f19ab8a1f93f26598222cd931e0cd98fbbb1c5994bf7667", size = 8237465 }, - { url = "https://files.pythonhosted.org/packages/cf/f8/153fd06b5160f0cd27c8b9dd797fcc9fb56ac6a0ebf3c1f765b6b68d3c8a/matplotlib-3.10.5-cp314-cp314-win_arm64.whl", hash = "sha256:21a95b9bf408178d372814de7baacd61c712a62cae560b5e6f35d791776f6516", size = 8108898 }, - { url = "https://files.pythonhosted.org/packages/9a/ee/c4b082a382a225fe0d2a73f1f57cf6f6f132308805b493a54c8641006238/matplotlib-3.10.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a6b310f95e1102a8c7c817ef17b60ee5d1851b8c71b63d9286b66b177963039e", size = 8295636 }, - { url = "https://files.pythonhosted.org/packages/30/73/2195fa2099718b21a20da82dfc753bf2af58d596b51aefe93e359dd5915a/matplotlib-3.10.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:94986a242747a0605cb3ff1cb98691c736f28a59f8ffe5175acaeb7397c49a5a", size = 8158575 }, - { url = "https://files.pythonhosted.org/packages/f6/e9/a08cdb34618a91fa08f75e6738541da5cacde7c307cea18ff10f0d03fcff/matplotlib-3.10.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ff10ea43288f0c8bab608a305dc6c918cc729d429c31dcbbecde3b9f4d5b569", size = 9522815 }, - { url = "https://files.pythonhosted.org/packages/4e/bb/34d8b7e0d1bb6d06ef45db01dfa560d5a67b1c40c0b998ce9ccde934bb09/matplotlib-3.10.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f6adb644c9d040ffb0d3434e440490a66cf73dbfa118a6f79cd7568431f7a012", size = 9783514 }, - { url = "https://files.pythonhosted.org/packages/12/09/d330d1e55dcca2e11b4d304cc5227f52e2512e46828d6249b88e0694176e/matplotlib-3.10.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4fa40a8f98428f789a9dcacd625f59b7bc4e3ef6c8c7c80187a7a709475cf592", size = 9573932 }, - { url = "https://files.pythonhosted.org/packages/eb/3b/f70258ac729aa004aca673800a53a2b0a26d49ca1df2eaa03289a1c40f81/matplotlib-3.10.5-cp314-cp314t-win_amd64.whl", hash = "sha256:95672a5d628b44207aab91ec20bf59c26da99de12b88f7e0b1fb0a84a86ff959", size = 8322003 }, - { url = "https://files.pythonhosted.org/packages/5b/60/3601f8ce6d76a7c81c7f25a0e15fde0d6b66226dd187aa6d2838e6374161/matplotlib-3.10.5-cp314-cp314t-win_arm64.whl", hash = "sha256:2efaf97d72629e74252e0b5e3c46813e9eeaa94e011ecf8084a971a31a97f40b", size = 8153849 }, - { url = "https://files.pythonhosted.org/packages/e4/eb/7d4c5de49eb78294e1a8e2be8a6ecff8b433e921b731412a56cd1abd3567/matplotlib-3.10.5-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b5fa2e941f77eb579005fb804026f9d0a1082276118d01cc6051d0d9626eaa7f", size = 8222360 }, - { url = "https://files.pythonhosted.org/packages/16/8a/e435db90927b66b16d69f8f009498775f4469f8de4d14b87856965e58eba/matplotlib-3.10.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1fc0d2a3241cdcb9daaca279204a3351ce9df3c0e7e621c7e04ec28aaacaca30", size = 8087462 }, - { url = "https://files.pythonhosted.org/packages/0b/dd/06c0e00064362f5647f318e00b435be2ff76a1bdced97c5eaf8347311fbe/matplotlib-3.10.5-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8dee65cb1424b7dc982fe87895b5613d4e691cc57117e8af840da0148ca6c1d7", size = 8659802 }, - { url = "https://files.pythonhosted.org/packages/dc/d6/e921be4e1a5f7aca5194e1f016cb67ec294548e530013251f630713e456d/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:160e125da27a749481eaddc0627962990f6029811dbeae23881833a011a0907f", size = 8233224 }, - { url = "https://files.pythonhosted.org/packages/ec/74/a2b9b04824b9c349c8f1b2d21d5af43fa7010039427f2b133a034cb09e59/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac3d50760394d78a3c9be6b28318fe22b494c4fcf6407e8fd4794b538251899b", size = 8098539 }, - { url = "https://files.pythonhosted.org/packages/fc/66/cd29ebc7f6c0d2a15d216fb572573e8fc38bd5d6dec3bd9d7d904c0949f7/matplotlib-3.10.5-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c49465bf689c4d59d174d0c7795fb42a21d4244d11d70e52b8011987367ac61", size = 8672192 }, +sdist = { url = "https://files.pythonhosted.org/packages/43/91/f2939bb60b7ebf12478b030e0d7f340247390f402b3b189616aad790c366/matplotlib-3.10.5.tar.gz", hash = "sha256:352ed6ccfb7998a00881692f38b4ca083c691d3e275b4145423704c34c909076", size = 34804044, upload-time = "2025-07-31T18:09:33.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/89/5355cdfe43242cb4d1a64a67cb6831398b665ad90e9702c16247cbd8d5ab/matplotlib-3.10.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5d4773a6d1c106ca05cb5a5515d277a6bb96ed09e5c8fab6b7741b8fcaa62c8f", size = 8229094, upload-time = "2025-07-31T18:07:36.507Z" }, + { url = "https://files.pythonhosted.org/packages/34/bc/ba802650e1c69650faed261a9df004af4c6f21759d7a1ec67fe972f093b3/matplotlib-3.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc88af74e7ba27de6cbe6faee916024ea35d895ed3d61ef6f58c4ce97da7185a", size = 8091464, upload-time = "2025-07-31T18:07:38.864Z" }, + { url = "https://files.pythonhosted.org/packages/ac/64/8d0c8937dee86c286625bddb1902efacc3e22f2b619f5b5a8df29fe5217b/matplotlib-3.10.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:64c4535419d5617f7363dad171a5a59963308e0f3f813c4bed6c9e6e2c131512", size = 8653163, upload-time = "2025-07-31T18:07:41.141Z" }, + { url = "https://files.pythonhosted.org/packages/11/dc/8dfc0acfbdc2fc2336c72561b7935cfa73db9ca70b875d8d3e1b3a6f371a/matplotlib-3.10.5-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a277033048ab22d34f88a3c5243938cef776493f6201a8742ed5f8b553201343", size = 9490635, upload-time = "2025-07-31T18:07:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/54/02/e3fdfe0f2e9fb05f3a691d63876639dbf684170fdcf93231e973104153b4/matplotlib-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e4a6470a118a2e93022ecc7d3bd16b3114b2004ea2bf014fff875b3bc99b70c6", size = 9539036, upload-time = "2025-07-31T18:07:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/c1/29/82bf486ff7f4dbedfb11ccc207d0575cbe3be6ea26f75be514252bde3d70/matplotlib-3.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:7e44cada61bec8833c106547786814dd4a266c1b2964fd25daa3804f1b8d4467", size = 8093529, upload-time = "2025-07-31T18:07:49.553Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c7/1f2db90a1d43710478bb1e9b57b162852f79234d28e4f48a28cc415aa583/matplotlib-3.10.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dcfc39c452c6a9f9028d3e44d2d721484f665304857188124b505b2c95e1eecf", size = 8239216, upload-time = "2025-07-31T18:07:51.947Z" }, + { url = "https://files.pythonhosted.org/packages/82/6d/ca6844c77a4f89b1c9e4d481c412e1d1dbabf2aae2cbc5aa2da4a1d6683e/matplotlib-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:903352681b59f3efbf4546985142a9686ea1d616bb054b09a537a06e4b892ccf", size = 8102130, upload-time = "2025-07-31T18:07:53.65Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1e/5e187a30cc673a3e384f3723e5f3c416033c1d8d5da414f82e4e731128ea/matplotlib-3.10.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:080c3676a56b8ee1c762bcf8fca3fe709daa1ee23e6ef06ad9f3fc17332f2d2a", size = 8666471, upload-time = "2025-07-31T18:07:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/03/c0/95540d584d7d645324db99a845ac194e915ef75011a0d5e19e1b5cee7e69/matplotlib-3.10.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b4984d5064a35b6f66d2c11d668565f4389b1119cc64db7a4c1725bc11adffc", size = 9500518, upload-time = "2025-07-31T18:07:57.199Z" }, + { url = "https://files.pythonhosted.org/packages/ba/2e/e019352099ea58b4169adb9c6e1a2ad0c568c6377c2b677ee1f06de2adc7/matplotlib-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3967424121d3a46705c9fa9bdb0931de3228f13f73d7bb03c999c88343a89d89", size = 9552372, upload-time = "2025-07-31T18:07:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/b7/81/3200b792a5e8b354f31f4101ad7834743ad07b6d620259f2059317b25e4d/matplotlib-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:33775bbeb75528555a15ac29396940128ef5613cf9a2d31fb1bfd18b3c0c0903", size = 8100634, upload-time = "2025-07-31T18:08:01.801Z" }, + { url = "https://files.pythonhosted.org/packages/52/46/a944f6f0c1f5476a0adfa501969d229ce5ae60cf9a663be0e70361381f89/matplotlib-3.10.5-cp311-cp311-win_arm64.whl", hash = "sha256:c61333a8e5e6240e73769d5826b9a31d8b22df76c0778f8480baf1b4b01c9420", size = 7978880, upload-time = "2025-07-31T18:08:03.407Z" }, + { url = "https://files.pythonhosted.org/packages/66/1e/c6f6bcd882d589410b475ca1fc22e34e34c82adff519caf18f3e6dd9d682/matplotlib-3.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:00b6feadc28a08bd3c65b2894f56cf3c94fc8f7adcbc6ab4516ae1e8ed8f62e2", size = 8253056, upload-time = "2025-07-31T18:08:05.385Z" }, + { url = "https://files.pythonhosted.org/packages/53/e6/d6f7d1b59413f233793dda14419776f5f443bcccb2dfc84b09f09fe05dbe/matplotlib-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee98a5c5344dc7f48dc261b6ba5d9900c008fc12beb3fa6ebda81273602cc389", size = 8110131, upload-time = "2025-07-31T18:08:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/66/2b/bed8a45e74957549197a2ac2e1259671cd80b55ed9e1fe2b5c94d88a9202/matplotlib-3.10.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a17e57e33de901d221a07af32c08870ed4528db0b6059dce7d7e65c1122d4bea", size = 8669603, upload-time = "2025-07-31T18:08:09.064Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a7/315e9435b10d057f5e52dfc603cd353167ae28bb1a4e033d41540c0067a4/matplotlib-3.10.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97b9d6443419085950ee4a5b1ee08c363e5c43d7176e55513479e53669e88468", size = 9508127, upload-time = "2025-07-31T18:08:10.845Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d9/edcbb1f02ca99165365d2768d517898c22c6040187e2ae2ce7294437c413/matplotlib-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ceefe5d40807d29a66ae916c6a3915d60ef9f028ce1927b84e727be91d884369", size = 9566926, upload-time = "2025-07-31T18:08:13.186Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d9/6dd924ad5616c97b7308e6320cf392c466237a82a2040381163b7500510a/matplotlib-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:c04cba0f93d40e45b3c187c6c52c17f24535b27d545f757a2fffebc06c12b98b", size = 8107599, upload-time = "2025-07-31T18:08:15.116Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f3/522dc319a50f7b0279fbe74f86f7a3506ce414bc23172098e8d2bdf21894/matplotlib-3.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:a41bcb6e2c8e79dc99c5511ae6f7787d2fb52efd3d805fff06d5d4f667db16b2", size = 7978173, upload-time = "2025-07-31T18:08:21.518Z" }, + { url = "https://files.pythonhosted.org/packages/8d/05/4f3c1f396075f108515e45cb8d334aff011a922350e502a7472e24c52d77/matplotlib-3.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:354204db3f7d5caaa10e5de74549ef6a05a4550fdd1c8f831ab9bca81efd39ed", size = 8253586, upload-time = "2025-07-31T18:08:23.107Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2c/e084415775aac7016c3719fe7006cdb462582c6c99ac142f27303c56e243/matplotlib-3.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b072aac0c3ad563a2b3318124756cb6112157017f7431626600ecbe890df57a1", size = 8110715, upload-time = "2025-07-31T18:08:24.675Z" }, + { url = "https://files.pythonhosted.org/packages/52/1b/233e3094b749df16e3e6cd5a44849fd33852e692ad009cf7de00cf58ddf6/matplotlib-3.10.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d52fd5b684d541b5a51fb276b2b97b010c75bee9aa392f96b4a07aeb491e33c7", size = 8669397, upload-time = "2025-07-31T18:08:26.778Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ec/03f9e003a798f907d9f772eed9b7c6a9775d5bd00648b643ebfb88e25414/matplotlib-3.10.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee7a09ae2f4676276f5a65bd9f2bd91b4f9fbaedf49f40267ce3f9b448de501f", size = 9508646, upload-time = "2025-07-31T18:08:28.848Z" }, + { url = "https://files.pythonhosted.org/packages/91/e7/c051a7a386680c28487bca27d23b02d84f63e3d2a9b4d2fc478e6a42e37e/matplotlib-3.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ba6c3c9c067b83481d647af88b4e441d532acdb5ef22178a14935b0b881188f4", size = 9567424, upload-time = "2025-07-31T18:08:30.726Z" }, + { url = "https://files.pythonhosted.org/packages/36/c2/24302e93ff431b8f4173ee1dd88976c8d80483cadbc5d3d777cef47b3a1c/matplotlib-3.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:07442d2692c9bd1cceaa4afb4bbe5b57b98a7599de4dabfcca92d3eea70f9ebe", size = 8107809, upload-time = "2025-07-31T18:08:33.928Z" }, + { url = "https://files.pythonhosted.org/packages/0b/33/423ec6a668d375dad825197557ed8fbdb74d62b432c1ed8235465945475f/matplotlib-3.10.5-cp313-cp313-win_arm64.whl", hash = "sha256:48fe6d47380b68a37ccfcc94f009530e84d41f71f5dae7eda7c4a5a84aa0a674", size = 7978078, upload-time = "2025-07-31T18:08:36.764Z" }, + { url = "https://files.pythonhosted.org/packages/51/17/521fc16ec766455c7bb52cc046550cf7652f6765ca8650ff120aa2d197b6/matplotlib-3.10.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b80eb8621331449fc519541a7461987f10afa4f9cfd91afcd2276ebe19bd56c", size = 8295590, upload-time = "2025-07-31T18:08:38.521Z" }, + { url = "https://files.pythonhosted.org/packages/f8/12/23c28b2c21114c63999bae129fce7fd34515641c517ae48ce7b7dcd33458/matplotlib-3.10.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47a388908e469d6ca2a6015858fa924e0e8a2345a37125948d8e93a91c47933e", size = 8158518, upload-time = "2025-07-31T18:08:40.195Z" }, + { url = "https://files.pythonhosted.org/packages/81/f8/aae4eb25e8e7190759f3cb91cbeaa344128159ac92bb6b409e24f8711f78/matplotlib-3.10.5-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b6b49167d208358983ce26e43aa4196073b4702858670f2eb111f9a10652b4b", size = 8691815, upload-time = "2025-07-31T18:08:42.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ba/450c39ebdd486bd33a359fc17365ade46c6a96bf637bbb0df7824de2886c/matplotlib-3.10.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a8da0453a7fd8e3da114234ba70c5ba9ef0e98f190309ddfde0f089accd46ea", size = 9522814, upload-time = "2025-07-31T18:08:44.914Z" }, + { url = "https://files.pythonhosted.org/packages/89/11/9c66f6a990e27bb9aa023f7988d2d5809cb98aa39c09cbf20fba75a542ef/matplotlib-3.10.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52c6573dfcb7726a9907b482cd5b92e6b5499b284ffacb04ffbfe06b3e568124", size = 9573917, upload-time = "2025-07-31T18:08:47.038Z" }, + { url = "https://files.pythonhosted.org/packages/b3/69/8b49394de92569419e5e05e82e83df9b749a0ff550d07631ea96ed2eb35a/matplotlib-3.10.5-cp313-cp313t-win_amd64.whl", hash = "sha256:a23193db2e9d64ece69cac0c8231849db7dd77ce59c7b89948cf9d0ce655a3ce", size = 8181034, upload-time = "2025-07-31T18:08:48.943Z" }, + { url = "https://files.pythonhosted.org/packages/47/23/82dc435bb98a2fc5c20dffcac8f0b083935ac28286413ed8835df40d0baa/matplotlib-3.10.5-cp313-cp313t-win_arm64.whl", hash = "sha256:56da3b102cf6da2776fef3e71cd96fcf22103a13594a18ac9a9b31314e0be154", size = 8023337, upload-time = "2025-07-31T18:08:50.791Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e0/26b6cfde31f5383503ee45dcb7e691d45dadf0b3f54639332b59316a97f8/matplotlib-3.10.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:96ef8f5a3696f20f55597ffa91c28e2e73088df25c555f8d4754931515512715", size = 8253591, upload-time = "2025-07-31T18:08:53.254Z" }, + { url = "https://files.pythonhosted.org/packages/c1/89/98488c7ef7ea20ea659af7499628c240a608b337af4be2066d644cfd0a0f/matplotlib-3.10.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:77fab633e94b9da60512d4fa0213daeb76d5a7b05156840c4fd0399b4b818837", size = 8112566, upload-time = "2025-07-31T18:08:55.116Z" }, + { url = "https://files.pythonhosted.org/packages/52/67/42294dfedc82aea55e1a767daf3263aacfb5a125f44ba189e685bab41b6f/matplotlib-3.10.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27f52634315e96b1debbfdc5c416592edcd9c4221bc2f520fd39c33db5d9f202", size = 9513281, upload-time = "2025-07-31T18:08:56.885Z" }, + { url = "https://files.pythonhosted.org/packages/e7/68/f258239e0cf34c2cbc816781c7ab6fca768452e6bf1119aedd2bd4a882a3/matplotlib-3.10.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:525f6e28c485c769d1f07935b660c864de41c37fd716bfa64158ea646f7084bb", size = 9780873, upload-time = "2025-07-31T18:08:59.241Z" }, + { url = "https://files.pythonhosted.org/packages/89/64/f4881554006bd12e4558bd66778bdd15d47b00a1f6c6e8b50f6208eda4b3/matplotlib-3.10.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1f5f3ec4c191253c5f2b7c07096a142c6a1c024d9f738247bfc8e3f9643fc975", size = 9568954, upload-time = "2025-07-31T18:09:01.244Z" }, + { url = "https://files.pythonhosted.org/packages/06/f8/42779d39c3f757e1f012f2dda3319a89fb602bd2ef98ce8faf0281f4febd/matplotlib-3.10.5-cp314-cp314-win_amd64.whl", hash = "sha256:707f9c292c4cd4716f19ab8a1f93f26598222cd931e0cd98fbbb1c5994bf7667", size = 8237465, upload-time = "2025-07-31T18:09:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f8/153fd06b5160f0cd27c8b9dd797fcc9fb56ac6a0ebf3c1f765b6b68d3c8a/matplotlib-3.10.5-cp314-cp314-win_arm64.whl", hash = "sha256:21a95b9bf408178d372814de7baacd61c712a62cae560b5e6f35d791776f6516", size = 8108898, upload-time = "2025-07-31T18:09:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/c4b082a382a225fe0d2a73f1f57cf6f6f132308805b493a54c8641006238/matplotlib-3.10.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a6b310f95e1102a8c7c817ef17b60ee5d1851b8c71b63d9286b66b177963039e", size = 8295636, upload-time = "2025-07-31T18:09:07.306Z" }, + { url = "https://files.pythonhosted.org/packages/30/73/2195fa2099718b21a20da82dfc753bf2af58d596b51aefe93e359dd5915a/matplotlib-3.10.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:94986a242747a0605cb3ff1cb98691c736f28a59f8ffe5175acaeb7397c49a5a", size = 8158575, upload-time = "2025-07-31T18:09:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e9/a08cdb34618a91fa08f75e6738541da5cacde7c307cea18ff10f0d03fcff/matplotlib-3.10.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ff10ea43288f0c8bab608a305dc6c918cc729d429c31dcbbecde3b9f4d5b569", size = 9522815, upload-time = "2025-07-31T18:09:11.191Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/34d8b7e0d1bb6d06ef45db01dfa560d5a67b1c40c0b998ce9ccde934bb09/matplotlib-3.10.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f6adb644c9d040ffb0d3434e440490a66cf73dbfa118a6f79cd7568431f7a012", size = 9783514, upload-time = "2025-07-31T18:09:13.307Z" }, + { url = "https://files.pythonhosted.org/packages/12/09/d330d1e55dcca2e11b4d304cc5227f52e2512e46828d6249b88e0694176e/matplotlib-3.10.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4fa40a8f98428f789a9dcacd625f59b7bc4e3ef6c8c7c80187a7a709475cf592", size = 9573932, upload-time = "2025-07-31T18:09:15.335Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3b/f70258ac729aa004aca673800a53a2b0a26d49ca1df2eaa03289a1c40f81/matplotlib-3.10.5-cp314-cp314t-win_amd64.whl", hash = "sha256:95672a5d628b44207aab91ec20bf59c26da99de12b88f7e0b1fb0a84a86ff959", size = 8322003, upload-time = "2025-07-31T18:09:17.416Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/3601f8ce6d76a7c81c7f25a0e15fde0d6b66226dd187aa6d2838e6374161/matplotlib-3.10.5-cp314-cp314t-win_arm64.whl", hash = "sha256:2efaf97d72629e74252e0b5e3c46813e9eeaa94e011ecf8084a971a31a97f40b", size = 8153849, upload-time = "2025-07-31T18:09:19.673Z" }, + { url = "https://files.pythonhosted.org/packages/e4/eb/7d4c5de49eb78294e1a8e2be8a6ecff8b433e921b731412a56cd1abd3567/matplotlib-3.10.5-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b5fa2e941f77eb579005fb804026f9d0a1082276118d01cc6051d0d9626eaa7f", size = 8222360, upload-time = "2025-07-31T18:09:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/16/8a/e435db90927b66b16d69f8f009498775f4469f8de4d14b87856965e58eba/matplotlib-3.10.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1fc0d2a3241cdcb9daaca279204a3351ce9df3c0e7e621c7e04ec28aaacaca30", size = 8087462, upload-time = "2025-07-31T18:09:23.504Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/06c0e00064362f5647f318e00b435be2ff76a1bdced97c5eaf8347311fbe/matplotlib-3.10.5-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8dee65cb1424b7dc982fe87895b5613d4e691cc57117e8af840da0148ca6c1d7", size = 8659802, upload-time = "2025-07-31T18:09:25.256Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d6/e921be4e1a5f7aca5194e1f016cb67ec294548e530013251f630713e456d/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:160e125da27a749481eaddc0627962990f6029811dbeae23881833a011a0907f", size = 8233224, upload-time = "2025-07-31T18:09:27.512Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/a2b9b04824b9c349c8f1b2d21d5af43fa7010039427f2b133a034cb09e59/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac3d50760394d78a3c9be6b28318fe22b494c4fcf6407e8fd4794b538251899b", size = 8098539, upload-time = "2025-07-31T18:09:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/fc/66/cd29ebc7f6c0d2a15d216fb572573e8fc38bd5d6dec3bd9d7d904c0949f7/matplotlib-3.10.5-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c49465bf689c4d59d174d0c7795fb42a21d4244d11d70e52b8011987367ac61", size = 8672192, upload-time = "2025-07-31T18:09:31.407Z" }, ] [[package]] name = "mccabe" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658 } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350 }, + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, ] [[package]] @@ -891,18 +892,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542 } +sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542, upload-time = "2024-09-09T20:27:49.564Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316 }, + { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316, upload-time = "2024-09-09T20:27:48.397Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] @@ -915,48 +916,48 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/a9/3d7aa83955617cdf02f94e50aab5c830d205cfa4320cf124ff64acce3a8e/mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972", size = 11003299 }, - { url = "https://files.pythonhosted.org/packages/83/e8/72e62ff837dd5caaac2b4a5c07ce769c8e808a00a65e5d8f94ea9c6f20ab/mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7", size = 10125451 }, - { url = "https://files.pythonhosted.org/packages/7d/10/f3f3543f6448db11881776f26a0ed079865926b0c841818ee22de2c6bbab/mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df", size = 11916211 }, - { url = "https://files.pythonhosted.org/packages/06/bf/63e83ed551282d67bb3f7fea2cd5561b08d2bb6eb287c096539feb5ddbc5/mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390", size = 12652687 }, - { url = "https://files.pythonhosted.org/packages/69/66/68f2eeef11facf597143e85b694a161868b3b006a5fbad50e09ea117ef24/mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94", size = 12896322 }, - { url = "https://files.pythonhosted.org/packages/a3/87/8e3e9c2c8bd0d7e071a89c71be28ad088aaecbadf0454f46a540bda7bca6/mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b", size = 9507962 }, - { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009 }, - { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482 }, - { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883 }, - { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215 }, - { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956 }, - { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307 }, - { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295 }, - { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355 }, - { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285 }, - { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895 }, - { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025 }, - { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664 }, - { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338 }, - { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066 }, - { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473 }, - { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296 }, - { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657 }, - { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320 }, - { url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037 }, - { url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550 }, - { url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963 }, - { url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189 }, - { url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322 }, - { url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879 }, - { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411 }, +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/a9/3d7aa83955617cdf02f94e50aab5c830d205cfa4320cf124ff64acce3a8e/mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972", size = 11003299, upload-time = "2025-07-31T07:54:06.425Z" }, + { url = "https://files.pythonhosted.org/packages/83/e8/72e62ff837dd5caaac2b4a5c07ce769c8e808a00a65e5d8f94ea9c6f20ab/mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7", size = 10125451, upload-time = "2025-07-31T07:53:52.974Z" }, + { url = "https://files.pythonhosted.org/packages/7d/10/f3f3543f6448db11881776f26a0ed079865926b0c841818ee22de2c6bbab/mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df", size = 11916211, upload-time = "2025-07-31T07:53:18.879Z" }, + { url = "https://files.pythonhosted.org/packages/06/bf/63e83ed551282d67bb3f7fea2cd5561b08d2bb6eb287c096539feb5ddbc5/mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390", size = 12652687, upload-time = "2025-07-31T07:53:30.544Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/68f2eeef11facf597143e85b694a161868b3b006a5fbad50e09ea117ef24/mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94", size = 12896322, upload-time = "2025-07-31T07:53:50.74Z" }, + { url = "https://files.pythonhosted.org/packages/a3/87/8e3e9c2c8bd0d7e071a89c71be28ad088aaecbadf0454f46a540bda7bca6/mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b", size = 9507962, upload-time = "2025-07-31T07:53:08.431Z" }, + { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" }, + { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307, upload-time = "2025-07-31T07:53:59.734Z" }, + { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" }, + { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" }, + { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" }, + { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664, upload-time = "2025-07-31T07:54:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" }, + { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" }, + { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" }, + { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" }, + { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" }, + { url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037, upload-time = "2025-07-31T07:54:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550, upload-time = "2025-07-31T07:53:41.307Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963, upload-time = "2025-07-31T07:53:16.878Z" }, + { url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189, upload-time = "2025-07-31T07:54:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322, upload-time = "2025-07-31T07:53:10.551Z" }, + { url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879, upload-time = "2025-07-31T07:52:56.683Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] @@ -972,18 +973,18 @@ dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985 } +sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579 }, + { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" }, ] [[package]] name = "nodeenv" version = "1.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] [[package]] @@ -994,90 +995,90 @@ dependencies = [ { name = "llvmlite" }, { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/ca/f470be59552ccbf9531d2d383b67ae0b9b524d435fb4a0d229fef135116e/numba-0.61.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:cf9f9fc00d6eca0c23fc840817ce9f439b9f03c8f03d6246c0e7f0cb15b7162a", size = 2775663 }, - { url = "https://files.pythonhosted.org/packages/f5/13/3bdf52609c80d460a3b4acfb9fdb3817e392875c0d6270cf3fd9546f138b/numba-0.61.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ea0247617edcb5dd61f6106a56255baab031acc4257bddaeddb3a1003b4ca3fd", size = 2778344 }, - { url = "https://files.pythonhosted.org/packages/e2/7d/bfb2805bcfbd479f04f835241ecf28519f6e3609912e3a985aed45e21370/numba-0.61.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae8c7a522c26215d5f62ebec436e3d341f7f590079245a2f1008dfd498cc1642", size = 3824054 }, - { url = "https://files.pythonhosted.org/packages/e3/27/797b2004745c92955470c73c82f0e300cf033c791f45bdecb4b33b12bdea/numba-0.61.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd1e74609855aa43661edffca37346e4e8462f6903889917e9f41db40907daa2", size = 3518531 }, - { url = "https://files.pythonhosted.org/packages/b1/c6/c2fb11e50482cb310afae87a997707f6c7d8a48967b9696271347441f650/numba-0.61.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae45830b129c6137294093b269ef0a22998ccc27bf7cf096ab8dcf7bca8946f9", size = 2831612 }, - { url = "https://files.pythonhosted.org/packages/3f/97/c99d1056aed767503c228f7099dc11c402906b42a4757fec2819329abb98/numba-0.61.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:efd3db391df53aaa5cfbee189b6c910a5b471488749fd6606c3f33fc984c2ae2", size = 2775825 }, - { url = "https://files.pythonhosted.org/packages/95/9e/63c549f37136e892f006260c3e2613d09d5120672378191f2dc387ba65a2/numba-0.61.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49c980e4171948ffebf6b9a2520ea81feed113c1f4890747ba7f59e74be84b1b", size = 2778695 }, - { url = "https://files.pythonhosted.org/packages/97/c8/8740616c8436c86c1b9a62e72cb891177d2c34c2d24ddcde4c390371bf4c/numba-0.61.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3945615cd73c2c7eba2a85ccc9c1730c21cd3958bfcf5a44302abae0fb07bb60", size = 3829227 }, - { url = "https://files.pythonhosted.org/packages/fc/06/66e99ae06507c31d15ff3ecd1f108f2f59e18b6e08662cd5f8a5853fbd18/numba-0.61.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbfdf4eca202cebade0b7d43896978e146f39398909a42941c9303f82f403a18", size = 3523422 }, - { url = "https://files.pythonhosted.org/packages/0f/a4/2b309a6a9f6d4d8cfba583401c7c2f9ff887adb5d54d8e2e130274c0973f/numba-0.61.2-cp311-cp311-win_amd64.whl", hash = "sha256:76bcec9f46259cedf888041b9886e257ae101c6268261b19fda8cfbc52bec9d1", size = 2831505 }, - { url = "https://files.pythonhosted.org/packages/b4/a0/c6b7b9c615cfa3b98c4c63f4316e3f6b3bbe2387740277006551784218cd/numba-0.61.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:34fba9406078bac7ab052efbf0d13939426c753ad72946baaa5bf9ae0ebb8dd2", size = 2776626 }, - { url = "https://files.pythonhosted.org/packages/92/4a/fe4e3c2ecad72d88f5f8cd04e7f7cff49e718398a2fac02d2947480a00ca/numba-0.61.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ddce10009bc097b080fc96876d14c051cc0c7679e99de3e0af59014dab7dfe8", size = 2779287 }, - { url = "https://files.pythonhosted.org/packages/9a/2d/e518df036feab381c23a624dac47f8445ac55686ec7f11083655eb707da3/numba-0.61.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b1bb509d01f23d70325d3a5a0e237cbc9544dd50e50588bc581ba860c213546", size = 3885928 }, - { url = "https://files.pythonhosted.org/packages/10/0f/23cced68ead67b75d77cfcca3df4991d1855c897ee0ff3fe25a56ed82108/numba-0.61.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48a53a3de8f8793526cbe330f2a39fe9a6638efcbf11bd63f3d2f9757ae345cd", size = 3577115 }, - { url = "https://files.pythonhosted.org/packages/68/1d/ddb3e704c5a8fb90142bf9dc195c27db02a08a99f037395503bfbc1d14b3/numba-0.61.2-cp312-cp312-win_amd64.whl", hash = "sha256:97cf4f12c728cf77c9c1d7c23707e4d8fb4632b46275f8f3397de33e5877af18", size = 2831929 }, - { url = "https://files.pythonhosted.org/packages/0b/f3/0fe4c1b1f2569e8a18ad90c159298d862f96c3964392a20d74fc628aee44/numba-0.61.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:3a10a8fc9afac40b1eac55717cece1b8b1ac0b946f5065c89e00bde646b5b154", size = 2771785 }, - { url = "https://files.pythonhosted.org/packages/e9/71/91b277d712e46bd5059f8a5866862ed1116091a7cb03bd2704ba8ebe015f/numba-0.61.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d3bcada3c9afba3bed413fba45845f2fb9cd0d2b27dd58a1be90257e293d140", size = 2773289 }, - { url = "https://files.pythonhosted.org/packages/0d/e0/5ea04e7ad2c39288c0f0f9e8d47638ad70f28e275d092733b5817cf243c9/numba-0.61.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdbca73ad81fa196bd53dc12e3aaf1564ae036e0c125f237c7644fe64a4928ab", size = 3893918 }, - { url = "https://files.pythonhosted.org/packages/17/58/064f4dcb7d7e9412f16ecf80ed753f92297e39f399c905389688cf950b81/numba-0.61.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f154aaea625fb32cfbe3b80c5456d514d416fcdf79733dd69c0df3a11348e9e", size = 3584056 }, - { url = "https://files.pythonhosted.org/packages/af/a4/6d3a0f2d3989e62a18749e1e9913d5fa4910bbb3e3311a035baea6caf26d/numba-0.61.2-cp313-cp313-win_amd64.whl", hash = "sha256:59321215e2e0ac5fa928a8020ab00b8e57cda8a97384963ac0dfa4d4e6aa54e7", size = 2831846 }, +sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615, upload-time = "2025-04-09T02:58:07.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/ca/f470be59552ccbf9531d2d383b67ae0b9b524d435fb4a0d229fef135116e/numba-0.61.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:cf9f9fc00d6eca0c23fc840817ce9f439b9f03c8f03d6246c0e7f0cb15b7162a", size = 2775663, upload-time = "2025-04-09T02:57:34.143Z" }, + { url = "https://files.pythonhosted.org/packages/f5/13/3bdf52609c80d460a3b4acfb9fdb3817e392875c0d6270cf3fd9546f138b/numba-0.61.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ea0247617edcb5dd61f6106a56255baab031acc4257bddaeddb3a1003b4ca3fd", size = 2778344, upload-time = "2025-04-09T02:57:36.609Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7d/bfb2805bcfbd479f04f835241ecf28519f6e3609912e3a985aed45e21370/numba-0.61.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae8c7a522c26215d5f62ebec436e3d341f7f590079245a2f1008dfd498cc1642", size = 3824054, upload-time = "2025-04-09T02:57:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/e3/27/797b2004745c92955470c73c82f0e300cf033c791f45bdecb4b33b12bdea/numba-0.61.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd1e74609855aa43661edffca37346e4e8462f6903889917e9f41db40907daa2", size = 3518531, upload-time = "2025-04-09T02:57:39.709Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c6/c2fb11e50482cb310afae87a997707f6c7d8a48967b9696271347441f650/numba-0.61.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae45830b129c6137294093b269ef0a22998ccc27bf7cf096ab8dcf7bca8946f9", size = 2831612, upload-time = "2025-04-09T02:57:41.559Z" }, + { url = "https://files.pythonhosted.org/packages/3f/97/c99d1056aed767503c228f7099dc11c402906b42a4757fec2819329abb98/numba-0.61.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:efd3db391df53aaa5cfbee189b6c910a5b471488749fd6606c3f33fc984c2ae2", size = 2775825, upload-time = "2025-04-09T02:57:43.442Z" }, + { url = "https://files.pythonhosted.org/packages/95/9e/63c549f37136e892f006260c3e2613d09d5120672378191f2dc387ba65a2/numba-0.61.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49c980e4171948ffebf6b9a2520ea81feed113c1f4890747ba7f59e74be84b1b", size = 2778695, upload-time = "2025-04-09T02:57:44.968Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/8740616c8436c86c1b9a62e72cb891177d2c34c2d24ddcde4c390371bf4c/numba-0.61.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3945615cd73c2c7eba2a85ccc9c1730c21cd3958bfcf5a44302abae0fb07bb60", size = 3829227, upload-time = "2025-04-09T02:57:46.63Z" }, + { url = "https://files.pythonhosted.org/packages/fc/06/66e99ae06507c31d15ff3ecd1f108f2f59e18b6e08662cd5f8a5853fbd18/numba-0.61.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbfdf4eca202cebade0b7d43896978e146f39398909a42941c9303f82f403a18", size = 3523422, upload-time = "2025-04-09T02:57:48.222Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a4/2b309a6a9f6d4d8cfba583401c7c2f9ff887adb5d54d8e2e130274c0973f/numba-0.61.2-cp311-cp311-win_amd64.whl", hash = "sha256:76bcec9f46259cedf888041b9886e257ae101c6268261b19fda8cfbc52bec9d1", size = 2831505, upload-time = "2025-04-09T02:57:50.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a0/c6b7b9c615cfa3b98c4c63f4316e3f6b3bbe2387740277006551784218cd/numba-0.61.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:34fba9406078bac7ab052efbf0d13939426c753ad72946baaa5bf9ae0ebb8dd2", size = 2776626, upload-time = "2025-04-09T02:57:51.857Z" }, + { url = "https://files.pythonhosted.org/packages/92/4a/fe4e3c2ecad72d88f5f8cd04e7f7cff49e718398a2fac02d2947480a00ca/numba-0.61.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ddce10009bc097b080fc96876d14c051cc0c7679e99de3e0af59014dab7dfe8", size = 2779287, upload-time = "2025-04-09T02:57:53.658Z" }, + { url = "https://files.pythonhosted.org/packages/9a/2d/e518df036feab381c23a624dac47f8445ac55686ec7f11083655eb707da3/numba-0.61.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b1bb509d01f23d70325d3a5a0e237cbc9544dd50e50588bc581ba860c213546", size = 3885928, upload-time = "2025-04-09T02:57:55.206Z" }, + { url = "https://files.pythonhosted.org/packages/10/0f/23cced68ead67b75d77cfcca3df4991d1855c897ee0ff3fe25a56ed82108/numba-0.61.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48a53a3de8f8793526cbe330f2a39fe9a6638efcbf11bd63f3d2f9757ae345cd", size = 3577115, upload-time = "2025-04-09T02:57:56.818Z" }, + { url = "https://files.pythonhosted.org/packages/68/1d/ddb3e704c5a8fb90142bf9dc195c27db02a08a99f037395503bfbc1d14b3/numba-0.61.2-cp312-cp312-win_amd64.whl", hash = "sha256:97cf4f12c728cf77c9c1d7c23707e4d8fb4632b46275f8f3397de33e5877af18", size = 2831929, upload-time = "2025-04-09T02:57:58.45Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f3/0fe4c1b1f2569e8a18ad90c159298d862f96c3964392a20d74fc628aee44/numba-0.61.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:3a10a8fc9afac40b1eac55717cece1b8b1ac0b946f5065c89e00bde646b5b154", size = 2771785, upload-time = "2025-04-09T02:57:59.96Z" }, + { url = "https://files.pythonhosted.org/packages/e9/71/91b277d712e46bd5059f8a5866862ed1116091a7cb03bd2704ba8ebe015f/numba-0.61.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d3bcada3c9afba3bed413fba45845f2fb9cd0d2b27dd58a1be90257e293d140", size = 2773289, upload-time = "2025-04-09T02:58:01.435Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e0/5ea04e7ad2c39288c0f0f9e8d47638ad70f28e275d092733b5817cf243c9/numba-0.61.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdbca73ad81fa196bd53dc12e3aaf1564ae036e0c125f237c7644fe64a4928ab", size = 3893918, upload-time = "2025-04-09T02:58:02.933Z" }, + { url = "https://files.pythonhosted.org/packages/17/58/064f4dcb7d7e9412f16ecf80ed753f92297e39f399c905389688cf950b81/numba-0.61.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f154aaea625fb32cfbe3b80c5456d514d416fcdf79733dd69c0df3a11348e9e", size = 3584056, upload-time = "2025-04-09T02:58:04.538Z" }, + { url = "https://files.pythonhosted.org/packages/af/a4/6d3a0f2d3989e62a18749e1e9913d5fa4910bbb3e3311a035baea6caf26d/numba-0.61.2-cp313-cp313-win_amd64.whl", hash = "sha256:59321215e2e0ac5fa928a8020ab00b8e57cda8a97384963ac0dfa4d4e6aa54e7", size = 2831846, upload-time = "2025-04-09T02:58:06.125Z" }, ] [[package]] name = "numpy" version = "2.2.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245 }, - { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048 }, - { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542 }, - { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301 }, - { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320 }, - { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050 }, - { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034 }, - { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185 }, - { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149 }, - { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620 }, - { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963 }, - { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743 }, - { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616 }, - { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579 }, - { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005 }, - { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570 }, - { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548 }, - { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521 }, - { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866 }, - { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455 }, - { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348 }, - { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362 }, - { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103 }, - { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382 }, - { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462 }, - { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618 }, - { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511 }, - { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783 }, - { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506 }, - { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190 }, - { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828 }, - { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006 }, - { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765 }, - { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736 }, - { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719 }, - { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072 }, - { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213 }, - { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632 }, - { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532 }, - { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885 }, - { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467 }, - { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144 }, - { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217 }, - { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014 }, - { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935 }, - { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122 }, - { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143 }, - { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260 }, - { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225 }, - { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374 }, - { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391 }, - { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754 }, - { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476 }, - { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666 }, +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, ] [[package]] @@ -1089,147 +1090,147 @@ dependencies = [ { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/19/7721093e25804cc82c7c1cdab0cce6b9343451828fc2ce249cee10646db5/numpydoc-1.9.0.tar.gz", hash = "sha256:5fec64908fe041acc4b3afc2a32c49aab1540cf581876f5563d68bb129e27c5b", size = 91451 } +sdist = { url = "https://files.pythonhosted.org/packages/2f/19/7721093e25804cc82c7c1cdab0cce6b9343451828fc2ce249cee10646db5/numpydoc-1.9.0.tar.gz", hash = "sha256:5fec64908fe041acc4b3afc2a32c49aab1540cf581876f5563d68bb129e27c5b", size = 91451, upload-time = "2025-06-24T12:22:55.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/62/5783d8924fca72529defb2c7dbe2070d49224d2dba03a85b20b37adb24d8/numpydoc-1.9.0-py3-none-any.whl", hash = "sha256:8a2983b2d62bfd0a8c470c7caa25e7e0c3d163875cdec12a8a1034020a9d1135", size = 64871 }, + { url = "https://files.pythonhosted.org/packages/26/62/5783d8924fca72529defb2c7dbe2070d49224d2dba03a85b20b37adb24d8/numpydoc-1.9.0-py3-none-any.whl", hash = "sha256:8a2983b2d62bfd0a8c470c7caa25e7e0c3d163875cdec12a8a1034020a9d1135", size = 64871, upload-time = "2025-06-24T12:22:53.701Z" }, ] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] name = "pathspec" version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] [[package]] name = "pillow" version = "11.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554 }, - { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548 }, - { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742 }, - { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087 }, - { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350 }, - { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840 }, - { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005 }, - { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372 }, - { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090 }, - { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988 }, - { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899 }, - { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531 }, - { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560 }, - { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978 }, - { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168 }, - { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053 }, - { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273 }, - { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043 }, - { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516 }, - { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768 }, - { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055 }, - { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079 }, - { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800 }, - { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296 }, - { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726 }, - { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652 }, - { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787 }, - { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236 }, - { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950 }, - { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358 }, - { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079 }, - { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324 }, - { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067 }, - { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328 }, - { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652 }, - { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443 }, - { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474 }, - { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038 }, - { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407 }, - { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094 }, - { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503 }, - { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574 }, - { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060 }, - { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407 }, - { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841 }, - { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450 }, - { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055 }, - { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110 }, - { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547 }, - { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554 }, - { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132 }, - { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001 }, - { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814 }, - { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124 }, - { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186 }, - { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546 }, - { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102 }, - { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803 }, - { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520 }, - { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116 }, - { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597 }, - { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246 }, - { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336 }, - { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699 }, - { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789 }, - { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386 }, - { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911 }, - { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383 }, - { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385 }, - { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129 }, - { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580 }, - { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860 }, - { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694 }, - { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888 }, - { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330 }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089 }, - { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206 }, - { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370 }, - { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500 }, - { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835 }, - { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556 }, - { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625 }, - { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207 }, - { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939 }, - { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166 }, - { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482 }, - { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596 }, - { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566 }, - { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618 }, - { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248 }, - { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963 }, - { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170 }, - { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505 }, - { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598 }, +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" }, + { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" }, + { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" }, + { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" }, + { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, + { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, + { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, + { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, + { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, + { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, + { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, + { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, + { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" }, + { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" }, + { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" }, + { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, ] [[package]] name = "platformdirs" version = "4.3.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] @@ -1243,18 +1244,18 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424 } +sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707 }, + { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, ] [[package]] name = "pycodestyle" version = "2.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472 } +sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594 }, + { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, ] [[package]] @@ -1272,9 +1273,9 @@ dependencies = [ { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/ea/3ab478cccacc2e8ef69892c42c44ae547bae089f356c4b47caf61730958d/pydata_sphinx_theme-0.15.4.tar.gz", hash = "sha256:7762ec0ac59df3acecf49fd2f889e1b4565dbce8b88b2e29ee06fdd90645a06d", size = 2400673 } +sdist = { url = "https://files.pythonhosted.org/packages/67/ea/3ab478cccacc2e8ef69892c42c44ae547bae089f356c4b47caf61730958d/pydata_sphinx_theme-0.15.4.tar.gz", hash = "sha256:7762ec0ac59df3acecf49fd2f889e1b4565dbce8b88b2e29ee06fdd90645a06d", size = 2400673, upload-time = "2024-06-25T19:28:45.041Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/d3/c622950d87a2ffd1654208733b5bd1c5645930014abed8f4c0d74863988b/pydata_sphinx_theme-0.15.4-py3-none-any.whl", hash = "sha256:2136ad0e9500d0949f96167e63f3e298620040aea8f9c74621959eda5d4cf8e6", size = 4640157 }, + { url = "https://files.pythonhosted.org/packages/e7/d3/c622950d87a2ffd1654208733b5bd1c5645930014abed8f4c0d74863988b/pydata_sphinx_theme-0.15.4-py3-none-any.whl", hash = "sha256:2136ad0e9500d0949f96167e63f3e298620040aea8f9c74621959eda5d4cf8e6", size = 4640157, upload-time = "2024-06-25T19:28:42.383Z" }, ] [[package]] @@ -1283,10 +1284,7 @@ version = "1.0.0" source = { editable = "." } dependencies = [ { name = "cma" }, - { name = "flake8" }, { name = "matplotlib" }, - { name = "mypy" }, - { name = "mypy-extensions" }, { name = "numba" }, { name = "numpy" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -1301,6 +1299,9 @@ dev = [ { name = "click" }, { name = "codecov" }, { name = "coverage" }, + { name = "flake8" }, + { name = "mypy" }, + { name = "mypy-extensions" }, { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-cov" }, @@ -1332,10 +1333,10 @@ requires-dist = [ { name = "codecov", marker = "extra == 'dev'" }, { name = "coverage", marker = "extra == 'dev'" }, { name = "docutils", marker = "extra == 'docs'", specifier = ">=0.18" }, - { name = "flake8" }, + { name = "flake8", marker = "extra == 'dev'" }, { name = "matplotlib" }, - { name = "mypy" }, - { name = "mypy-extensions" }, + { name = "mypy", marker = "extra == 'dev'" }, + { name = "mypy-extensions", marker = "extra == 'dev'" }, { name = "myst-parser", marker = "extra == 'docs'", specifier = ">=1.0" }, { name = "numba" }, { name = "numpy" }, @@ -1356,32 +1357,33 @@ requires-dist = [ { name = "sphinxcontrib-video", marker = "extra == 'docs'", specifier = ">=0.4.1" }, { name = "tqdm" }, ] +provides-extras = ["docs", "dev"] [[package]] name = "pyflakes" version = "3.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669 } +sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551 }, + { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, ] [[package]] name = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pyparsing" version = "3.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 }, + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" }, ] [[package]] @@ -1397,9 +1399,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714 } +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474 }, + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, ] [[package]] @@ -1411,9 +1413,9 @@ dependencies = [ { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432 } +sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644 }, + { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, ] [[package]] @@ -1425,9 +1427,9 @@ dependencies = [ { name = "pytest" }, { name = "pytest-metadata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/ab/4862dcb5a8a514bd87747e06b8d55483c0c9e987e1b66972336946e49b49/pytest_html-4.1.1.tar.gz", hash = "sha256:70a01e8ae5800f4a074b56a4cb1025c8f4f9b038bba5fe31e3c98eb996686f07", size = 150773 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/ab/4862dcb5a8a514bd87747e06b8d55483c0c9e987e1b66972336946e49b49/pytest_html-4.1.1.tar.gz", hash = "sha256:70a01e8ae5800f4a074b56a4cb1025c8f4f9b038bba5fe31e3c98eb996686f07", size = 150773, upload-time = "2023-11-07T15:44:28.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/c7/c160021cbecd956cc1a6f79e5fe155f7868b2e5b848f1320dad0b3e3122f/pytest_html-4.1.1-py3-none-any.whl", hash = "sha256:c8152cea03bd4e9bee6d525573b67bbc6622967b72b9628dda0ea3e2a0b5dd71", size = 23491 }, + { url = "https://files.pythonhosted.org/packages/c8/c7/c160021cbecd956cc1a6f79e5fe155f7868b2e5b848f1320dad0b3e3122f/pytest_html-4.1.1-py3-none-any.whl", hash = "sha256:c8152cea03bd4e9bee6d525573b67bbc6622967b72b9628dda0ea3e2a0b5dd71", size = 23491, upload-time = "2023-11-07T15:44:27.149Z" }, ] [[package]] @@ -1437,9 +1439,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/85/8c969f8bec4e559f8f2b958a15229a35495f5b4ce499f6b865eac54b878d/pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8", size = 9952 } +sdist = { url = "https://files.pythonhosted.org/packages/a6/85/8c969f8bec4e559f8f2b958a15229a35495f5b4ce499f6b865eac54b878d/pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8", size = 9952, upload-time = "2024-02-12T19:38:44.887Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/43/7e7b2ec865caa92f67b8f0e9231a798d102724ca4c0e1f414316be1c1ef2/pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b", size = 11428 }, + { url = "https://files.pythonhosted.org/packages/3e/43/7e7b2ec865caa92f67b8f0e9231a798d102724ca4c0e1f414316be1c1ef2/pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b", size = 11428, upload-time = "2024-02-12T19:38:42.531Z" }, ] [[package]] @@ -1449,9 +1451,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241 } +sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923 }, + { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, ] [[package]] @@ -1462,9 +1464,9 @@ dependencies = [ { name = "numpy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/b7/11c928ffabbb79d86cc120b9b67b74c2ac0d132b3ea19f14024f7f0a0e4c/pytest-rng-1.0.0.tar.gz", hash = "sha256:9d9ee96557246756072133ff9b990588f28f12d3e80357cad959ef0b05aed9fa", size = 14789 } +sdist = { url = "https://files.pythonhosted.org/packages/e1/b7/11c928ffabbb79d86cc120b9b67b74c2ac0d132b3ea19f14024f7f0a0e4c/pytest-rng-1.0.0.tar.gz", hash = "sha256:9d9ee96557246756072133ff9b990588f28f12d3e80357cad959ef0b05aed9fa", size = 14789, upload-time = "2019-08-08T19:25:57.681Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/66/40c88e87c8731d96bb2f9a49329785d24236debb9685f6aaccc52536e697/pytest_rng-1.0.0-py3-none-any.whl", hash = "sha256:346e76a34f19c1f70e1059567460df9edf34aa6b41441c8707bf9ed40446b9c7", size = 7992 }, + { url = "https://files.pythonhosted.org/packages/ac/66/40c88e87c8731d96bb2f9a49329785d24236debb9685f6aaccc52536e697/pytest_rng-1.0.0-py3-none-any.whl", hash = "sha256:346e76a34f19c1f70e1059567460df9edf34aa6b41441c8707bf9ed40446b9c7", size = 7992, upload-time = "2019-08-08T19:25:55.363Z" }, ] [[package]] @@ -1474,62 +1476,62 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] [[package]] name = "readthedocs-sphinx-search" version = "0.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8f/96/0c51439e3dbc634cf5328ffb173ff759b7fc9abf3276e78bf71d9fc0aa51/readthedocs-sphinx-search-0.3.2.tar.gz", hash = "sha256:277773bfa28566a86694c08e568d5a648cd80f22826545555a764d6d20c365fb", size = 21949 } +sdist = { url = "https://files.pythonhosted.org/packages/8f/96/0c51439e3dbc634cf5328ffb173ff759b7fc9abf3276e78bf71d9fc0aa51/readthedocs-sphinx-search-0.3.2.tar.gz", hash = "sha256:277773bfa28566a86694c08e568d5a648cd80f22826545555a764d6d20c365fb", size = 21949, upload-time = "2024-01-15T16:46:22.565Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/3c/41bc9d7d4d936a73e380423f23996bee1691e17598d8a03c062be6aac640/readthedocs_sphinx_search-0.3.2-py3-none-any.whl", hash = "sha256:58716fd21f01581e6e67bf3bc02e79c77e10dc58b5f8e4c7cc1977e013eda173", size = 21379 }, + { url = "https://files.pythonhosted.org/packages/04/3c/41bc9d7d4d936a73e380423f23996bee1691e17598d8a03c062be6aac640/readthedocs_sphinx_search-0.3.2-py3-none-any.whl", hash = "sha256:58716fd21f01581e6e67bf3bc02e79c77e10dc58b5f8e4c7cc1977e013eda173", size = 21379, upload-time = "2024-01-15T16:46:20.552Z" }, ] [[package]] @@ -1542,18 +1544,18 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258 } +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 }, + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] [[package]] name = "roman-numerals-py" version = "3.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017 } +sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017, upload-time = "2025-02-22T07:34:54.333Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742 }, + { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742, upload-time = "2025-02-22T07:34:52.422Z" }, ] [[package]] @@ -1566,53 +1568,53 @@ resolution-markers = [ dependencies = [ { name = "numpy", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770 }, - { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511 }, - { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151 }, - { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732 }, - { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617 }, - { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964 }, - { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749 }, - { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383 }, - { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201 }, - { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255 }, - { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035 }, - { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499 }, - { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602 }, - { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415 }, - { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622 }, - { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796 }, - { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684 }, - { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504 }, - { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735 }, - { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284 }, - { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958 }, - { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454 }, - { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199 }, - { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455 }, - { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140 }, - { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549 }, - { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184 }, - { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256 }, - { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540 }, - { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115 }, - { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884 }, - { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018 }, - { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716 }, - { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342 }, - { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869 }, - { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851 }, - { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011 }, - { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407 }, - { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030 }, - { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709 }, - { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045 }, - { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062 }, - { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132 }, - { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503 }, - { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097 }, +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, ] [[package]] @@ -1625,89 +1627,89 @@ resolution-markers = [ dependencies = [ { name = "numpy", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/4a/b927028464795439faec8eaf0b03b011005c487bb2d07409f28bf30879c4/scipy-1.16.1.tar.gz", hash = "sha256:44c76f9e8b6e8e488a586190ab38016e4ed2f8a038af7cd3defa903c0a2238b3", size = 30580861 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/91/812adc6f74409b461e3a5fa97f4f74c769016919203138a3bf6fc24ba4c5/scipy-1.16.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:c033fa32bab91dc98ca59d0cf23bb876454e2bb02cbe592d5023138778f70030", size = 36552519 }, - { url = "https://files.pythonhosted.org/packages/47/18/8e355edcf3b71418d9e9f9acd2708cc3a6c27e8f98fde0ac34b8a0b45407/scipy-1.16.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6e5c2f74e5df33479b5cd4e97a9104c511518fbd979aa9b8f6aec18b2e9ecae7", size = 28638010 }, - { url = "https://files.pythonhosted.org/packages/d9/eb/e931853058607bdfbc11b86df19ae7a08686121c203483f62f1ecae5989c/scipy-1.16.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0a55ffe0ba0f59666e90951971a884d1ff6f4ec3275a48f472cfb64175570f77", size = 20909790 }, - { url = "https://files.pythonhosted.org/packages/45/0c/be83a271d6e96750cd0be2e000f35ff18880a46f05ce8b5d3465dc0f7a2a/scipy-1.16.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f8a5d6cd147acecc2603fbd382fed6c46f474cccfcf69ea32582e033fb54dcfe", size = 23513352 }, - { url = "https://files.pythonhosted.org/packages/7c/bf/fe6eb47e74f762f933cca962db7f2c7183acfdc4483bd1c3813cfe83e538/scipy-1.16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb18899127278058bcc09e7b9966d41a5a43740b5bb8dcba401bd983f82e885b", size = 33534643 }, - { url = "https://files.pythonhosted.org/packages/bb/ba/63f402e74875486b87ec6506a4f93f6d8a0d94d10467280f3d9d7837ce3a/scipy-1.16.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adccd93a2fa937a27aae826d33e3bfa5edf9aa672376a4852d23a7cd67a2e5b7", size = 35376776 }, - { url = "https://files.pythonhosted.org/packages/c3/b4/04eb9d39ec26a1b939689102da23d505ea16cdae3dbb18ffc53d1f831044/scipy-1.16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:18aca1646a29ee9a0625a1be5637fa798d4d81fdf426481f06d69af828f16958", size = 35698906 }, - { url = "https://files.pythonhosted.org/packages/04/d6/bb5468da53321baeb001f6e4e0d9049eadd175a4a497709939128556e3ec/scipy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d85495cef541729a70cdddbbf3e6b903421bc1af3e8e3a9a72a06751f33b7c39", size = 38129275 }, - { url = "https://files.pythonhosted.org/packages/c4/94/994369978509f227cba7dfb9e623254d0d5559506fe994aef4bea3ed469c/scipy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:226652fca853008119c03a8ce71ffe1b3f6d2844cc1686e8f9806edafae68596", size = 38644572 }, - { url = "https://files.pythonhosted.org/packages/f8/d9/ec4864f5896232133f51382b54a08de91a9d1af7a76dfa372894026dfee2/scipy-1.16.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81b433bbeaf35728dad619afc002db9b189e45eebe2cd676effe1fb93fef2b9c", size = 36575194 }, - { url = "https://files.pythonhosted.org/packages/5c/6d/40e81ecfb688e9d25d34a847dca361982a6addf8e31f0957b1a54fbfa994/scipy-1.16.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:886cc81fdb4c6903a3bb0464047c25a6d1016fef77bb97949817d0c0d79f9e04", size = 28594590 }, - { url = "https://files.pythonhosted.org/packages/0e/37/9f65178edfcc629377ce9a64fc09baebea18c80a9e57ae09a52edf84880b/scipy-1.16.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:15240c3aac087a522b4eaedb09f0ad061753c5eebf1ea430859e5bf8640d5919", size = 20866458 }, - { url = "https://files.pythonhosted.org/packages/2c/7b/749a66766871ea4cb1d1ea10f27004db63023074c22abed51f22f09770e0/scipy-1.16.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f81a25805f3659b48126b5053d9e823d3215e4a63730b5e1671852a1705921", size = 23539318 }, - { url = "https://files.pythonhosted.org/packages/c4/db/8d4afec60eb833a666434d4541a3151eedbf2494ea6d4d468cbe877f00cd/scipy-1.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6c62eea7f607f122069b9bad3f99489ddca1a5173bef8a0c75555d7488b6f725", size = 33292899 }, - { url = "https://files.pythonhosted.org/packages/51/1e/79023ca3bbb13a015d7d2757ecca3b81293c663694c35d6541b4dca53e98/scipy-1.16.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f965bbf3235b01c776115ab18f092a95aa74c271a52577bcb0563e85738fd618", size = 35162637 }, - { url = "https://files.pythonhosted.org/packages/b6/49/0648665f9c29fdaca4c679182eb972935b3b4f5ace41d323c32352f29816/scipy-1.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f006e323874ffd0b0b816d8c6a8e7f9a73d55ab3b8c3f72b752b226d0e3ac83d", size = 35490507 }, - { url = "https://files.pythonhosted.org/packages/62/8f/66cbb9d6bbb18d8c658f774904f42a92078707a7c71e5347e8bf2f52bb89/scipy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8fd15fc5085ab4cca74cb91fe0a4263b1f32e4420761ddae531ad60934c2119", size = 37923998 }, - { url = "https://files.pythonhosted.org/packages/14/c3/61f273ae550fbf1667675701112e380881905e28448c080b23b5a181df7c/scipy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7b8013c6c066609577d910d1a2a077021727af07b6fab0ee22c2f901f22352a", size = 38508060 }, - { url = "https://files.pythonhosted.org/packages/93/0b/b5c99382b839854a71ca9482c684e3472badc62620287cbbdab499b75ce6/scipy-1.16.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5451606823a5e73dfa621a89948096c6528e2896e40b39248295d3a0138d594f", size = 36533717 }, - { url = "https://files.pythonhosted.org/packages/eb/e5/69ab2771062c91e23e07c12e7d5033a6b9b80b0903ee709c3c36b3eb520c/scipy-1.16.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:89728678c5ca5abd610aee148c199ac1afb16e19844401ca97d43dc548a354eb", size = 28570009 }, - { url = "https://files.pythonhosted.org/packages/f4/69/bd75dbfdd3cf524f4d753484d723594aed62cfaac510123e91a6686d520b/scipy-1.16.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e756d688cb03fd07de0fffad475649b03cb89bee696c98ce508b17c11a03f95c", size = 20841942 }, - { url = "https://files.pythonhosted.org/packages/ea/74/add181c87663f178ba7d6144b370243a87af8476664d5435e57d599e6874/scipy-1.16.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5aa2687b9935da3ed89c5dbed5234576589dd28d0bf7cd237501ccfbdf1ad608", size = 23498507 }, - { url = "https://files.pythonhosted.org/packages/1d/74/ece2e582a0d9550cee33e2e416cc96737dce423a994d12bbe59716f47ff1/scipy-1.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0851f6a1e537fe9399f35986897e395a1aa61c574b178c0d456be5b1a0f5ca1f", size = 33286040 }, - { url = "https://files.pythonhosted.org/packages/e4/82/08e4076df538fb56caa1d489588d880ec7c52d8273a606bb54d660528f7c/scipy-1.16.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fedc2cbd1baed37474b1924c331b97bdff611d762c196fac1a9b71e67b813b1b", size = 35176096 }, - { url = "https://files.pythonhosted.org/packages/fa/79/cd710aab8c921375711a8321c6be696e705a120e3011a643efbbcdeeabcc/scipy-1.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2ef500e72f9623a6735769e4b93e9dcb158d40752cdbb077f305487e3e2d1f45", size = 35490328 }, - { url = "https://files.pythonhosted.org/packages/71/73/e9cc3d35ee4526d784520d4494a3e1ca969b071fb5ae5910c036a375ceec/scipy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:978d8311674b05a8f7ff2ea6c6bce5d8b45a0cb09d4c5793e0318f448613ea65", size = 37939921 }, - { url = "https://files.pythonhosted.org/packages/21/12/c0efd2941f01940119b5305c375ae5c0fcb7ec193f806bd8f158b73a1782/scipy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:81929ed0fa7a5713fcdd8b2e6f73697d3b4c4816d090dd34ff937c20fa90e8ab", size = 38479462 }, - { url = "https://files.pythonhosted.org/packages/7a/19/c3d08b675260046a991040e1ea5d65f91f40c7df1045fffff412dcfc6765/scipy-1.16.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:bcc12db731858abda693cecdb3bdc9e6d4bd200213f49d224fe22df82687bdd6", size = 36938832 }, - { url = "https://files.pythonhosted.org/packages/81/f2/ce53db652c033a414a5b34598dba6b95f3d38153a2417c5a3883da429029/scipy-1.16.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:744d977daa4becb9fc59135e75c069f8d301a87d64f88f1e602a9ecf51e77b27", size = 29093084 }, - { url = "https://files.pythonhosted.org/packages/a9/ae/7a10ff04a7dc15f9057d05b33737ade244e4bd195caa3f7cc04d77b9e214/scipy-1.16.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:dc54f76ac18073bcecffb98d93f03ed6b81a92ef91b5d3b135dcc81d55a724c7", size = 21365098 }, - { url = "https://files.pythonhosted.org/packages/36/ac/029ff710959932ad3c2a98721b20b405f05f752f07344622fd61a47c5197/scipy-1.16.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:367d567ee9fc1e9e2047d31f39d9d6a7a04e0710c86e701e053f237d14a9b4f6", size = 23896858 }, - { url = "https://files.pythonhosted.org/packages/71/13/d1ef77b6bd7898720e1f0b6b3743cb945f6c3cafa7718eaac8841035ab60/scipy-1.16.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4cf5785e44e19dcd32a0e4807555e1e9a9b8d475c6afff3d21c3c543a6aa84f4", size = 33438311 }, - { url = "https://files.pythonhosted.org/packages/2d/e0/e64a6821ffbb00b4c5b05169f1c1fddb4800e9307efe3db3788995a82a2c/scipy-1.16.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3d0b80fb26d3e13a794c71d4b837e2a589d839fd574a6bbb4ee1288c213ad4a3", size = 35279542 }, - { url = "https://files.pythonhosted.org/packages/57/59/0dc3c8b43e118f1e4ee2b798dcc96ac21bb20014e5f1f7a8e85cc0653bdb/scipy-1.16.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8503517c44c18d1030d666cb70aaac1cc8913608816e06742498833b128488b7", size = 35667665 }, - { url = "https://files.pythonhosted.org/packages/45/5f/844ee26e34e2f3f9f8febb9343748e72daeaec64fe0c70e9bf1ff84ec955/scipy-1.16.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:30cc4bb81c41831ecfd6dc450baf48ffd80ef5aed0f5cf3ea775740e80f16ecc", size = 38045210 }, - { url = "https://files.pythonhosted.org/packages/8d/d7/210f2b45290f444f1de64bc7353aa598ece9f0e90c384b4a156f9b1a5063/scipy-1.16.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c24fa02f7ed23ae514460a22c57eca8f530dbfa50b1cfdbf4f37c05b5309cc39", size = 38593661 }, - { url = "https://files.pythonhosted.org/packages/81/ea/84d481a5237ed223bd3d32d6e82d7a6a96e34756492666c260cef16011d1/scipy-1.16.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:796a5a9ad36fa3a782375db8f4241ab02a091308eb079746bc0f874c9b998318", size = 36525921 }, - { url = "https://files.pythonhosted.org/packages/4e/9f/d9edbdeff9f3a664807ae3aea383e10afaa247e8e6255e6d2aa4515e8863/scipy-1.16.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:3ea0733a2ff73fd6fdc5fecca54ee9b459f4d74f00b99aced7d9a3adb43fb1cc", size = 28564152 }, - { url = "https://files.pythonhosted.org/packages/3b/95/8125bcb1fe04bc267d103e76516243e8d5e11229e6b306bda1024a5423d1/scipy-1.16.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:85764fb15a2ad994e708258bb4ed8290d1305c62a4e1ef07c414356a24fcfbf8", size = 20836028 }, - { url = "https://files.pythonhosted.org/packages/77/9c/bf92e215701fc70bbcd3d14d86337cf56a9b912a804b9c776a269524a9e9/scipy-1.16.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ca66d980469cb623b1759bdd6e9fd97d4e33a9fad5b33771ced24d0cb24df67e", size = 23489666 }, - { url = "https://files.pythonhosted.org/packages/5e/00/5e941d397d9adac41b02839011594620d54d99488d1be5be755c00cde9ee/scipy-1.16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e7cc1ffcc230f568549fc56670bcf3df1884c30bd652c5da8138199c8c76dae0", size = 33358318 }, - { url = "https://files.pythonhosted.org/packages/0e/87/8db3aa10dde6e3e8e7eb0133f24baa011377d543f5b19c71469cf2648026/scipy-1.16.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ddfb1e8d0b540cb4ee9c53fc3dea3186f97711248fb94b4142a1b27178d8b4b", size = 35185724 }, - { url = "https://files.pythonhosted.org/packages/89/b4/6ab9ae443216807622bcff02690262d8184078ea467efee2f8c93288a3b1/scipy-1.16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4dc0e7be79e95d8ba3435d193e0d8ce372f47f774cffd882f88ea4e1e1ddc731", size = 35554335 }, - { url = "https://files.pythonhosted.org/packages/9c/9a/d0e9dc03c5269a1afb60661118296a32ed5d2c24298af61b676c11e05e56/scipy-1.16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f23634f9e5adb51b2a77766dac217063e764337fbc816aa8ad9aaebcd4397fd3", size = 37960310 }, - { url = "https://files.pythonhosted.org/packages/5e/00/c8f3130a50521a7977874817ca89e0599b1b4ee8e938bad8ae798a0e1f0d/scipy-1.16.1-cp314-cp314-win_amd64.whl", hash = "sha256:57d75524cb1c5a374958a2eae3d84e1929bb971204cc9d52213fb8589183fc19", size = 39319239 }, - { url = "https://files.pythonhosted.org/packages/f2/f2/1ca3eda54c3a7e4c92f6acef7db7b3a057deb135540d23aa6343ef8ad333/scipy-1.16.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:d8da7c3dd67bcd93f15618938f43ed0995982eb38973023d46d4646c4283ad65", size = 36939460 }, - { url = "https://files.pythonhosted.org/packages/80/30/98c2840b293a132400c0940bb9e140171dcb8189588619048f42b2ce7b4f/scipy-1.16.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:cc1d2f2fd48ba1e0620554fe5bc44d3e8f5d4185c8c109c7fbdf5af2792cfad2", size = 29093322 }, - { url = "https://files.pythonhosted.org/packages/c1/e6/1e6e006e850622cf2a039b62d1a6ddc4497d4851e58b68008526f04a9a00/scipy-1.16.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:21a611ced9275cb861bacadbada0b8c0623bc00b05b09eb97f23b370fc2ae56d", size = 21365329 }, - { url = "https://files.pythonhosted.org/packages/8e/02/72a5aa5b820589dda9a25e329ca752842bfbbaf635e36bc7065a9b42216e/scipy-1.16.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dfbb25dffc4c3dd9371d8ab456ca81beeaf6f9e1c2119f179392f0dc1ab7695", size = 23897544 }, - { url = "https://files.pythonhosted.org/packages/2b/dc/7122d806a6f9eb8a33532982234bed91f90272e990f414f2830cfe656e0b/scipy-1.16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f0ebb7204f063fad87fc0a0e4ff4a2ff40b2a226e4ba1b7e34bf4b79bf97cd86", size = 33442112 }, - { url = "https://files.pythonhosted.org/packages/24/39/e383af23564daa1021a5b3afbe0d8d6a68ec639b943661841f44ac92de85/scipy-1.16.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f1b9e5962656f2734c2b285a8745358ecb4e4efbadd00208c80a389227ec61ff", size = 35286594 }, - { url = "https://files.pythonhosted.org/packages/95/47/1a0b0aff40c3056d955f38b0df5d178350c3d74734ec54f9c68d23910be5/scipy-1.16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e1a106f8c023d57a2a903e771228bf5c5b27b5d692088f457acacd3b54511e4", size = 35665080 }, - { url = "https://files.pythonhosted.org/packages/64/df/ce88803e9ed6e27fe9b9abefa157cf2c80e4fa527cf17ee14be41f790ad4/scipy-1.16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:709559a1db68a9abc3b2c8672c4badf1614f3b440b3ab326d86a5c0491eafae3", size = 38050306 }, - { url = "https://files.pythonhosted.org/packages/6e/6c/a76329897a7cae4937d403e623aa6aaea616a0bb5b36588f0b9d1c9a3739/scipy-1.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c0c804d60492a0aad7f5b2bb1862f4548b990049e27e828391ff2bf6f7199998", size = 39427705 }, +sdist = { url = "https://files.pythonhosted.org/packages/f5/4a/b927028464795439faec8eaf0b03b011005c487bb2d07409f28bf30879c4/scipy-1.16.1.tar.gz", hash = "sha256:44c76f9e8b6e8e488a586190ab38016e4ed2f8a038af7cd3defa903c0a2238b3", size = 30580861, upload-time = "2025-07-27T16:33:30.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/91/812adc6f74409b461e3a5fa97f4f74c769016919203138a3bf6fc24ba4c5/scipy-1.16.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:c033fa32bab91dc98ca59d0cf23bb876454e2bb02cbe592d5023138778f70030", size = 36552519, upload-time = "2025-07-27T16:26:29.658Z" }, + { url = "https://files.pythonhosted.org/packages/47/18/8e355edcf3b71418d9e9f9acd2708cc3a6c27e8f98fde0ac34b8a0b45407/scipy-1.16.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6e5c2f74e5df33479b5cd4e97a9104c511518fbd979aa9b8f6aec18b2e9ecae7", size = 28638010, upload-time = "2025-07-27T16:26:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/d9/eb/e931853058607bdfbc11b86df19ae7a08686121c203483f62f1ecae5989c/scipy-1.16.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0a55ffe0ba0f59666e90951971a884d1ff6f4ec3275a48f472cfb64175570f77", size = 20909790, upload-time = "2025-07-27T16:26:43.93Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/be83a271d6e96750cd0be2e000f35ff18880a46f05ce8b5d3465dc0f7a2a/scipy-1.16.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:f8a5d6cd147acecc2603fbd382fed6c46f474cccfcf69ea32582e033fb54dcfe", size = 23513352, upload-time = "2025-07-27T16:26:50.017Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bf/fe6eb47e74f762f933cca962db7f2c7183acfdc4483bd1c3813cfe83e538/scipy-1.16.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb18899127278058bcc09e7b9966d41a5a43740b5bb8dcba401bd983f82e885b", size = 33534643, upload-time = "2025-07-27T16:26:57.503Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ba/63f402e74875486b87ec6506a4f93f6d8a0d94d10467280f3d9d7837ce3a/scipy-1.16.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adccd93a2fa937a27aae826d33e3bfa5edf9aa672376a4852d23a7cd67a2e5b7", size = 35376776, upload-time = "2025-07-27T16:27:06.639Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b4/04eb9d39ec26a1b939689102da23d505ea16cdae3dbb18ffc53d1f831044/scipy-1.16.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:18aca1646a29ee9a0625a1be5637fa798d4d81fdf426481f06d69af828f16958", size = 35698906, upload-time = "2025-07-27T16:27:14.943Z" }, + { url = "https://files.pythonhosted.org/packages/04/d6/bb5468da53321baeb001f6e4e0d9049eadd175a4a497709939128556e3ec/scipy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d85495cef541729a70cdddbbf3e6b903421bc1af3e8e3a9a72a06751f33b7c39", size = 38129275, upload-time = "2025-07-27T16:27:23.873Z" }, + { url = "https://files.pythonhosted.org/packages/c4/94/994369978509f227cba7dfb9e623254d0d5559506fe994aef4bea3ed469c/scipy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:226652fca853008119c03a8ce71ffe1b3f6d2844cc1686e8f9806edafae68596", size = 38644572, upload-time = "2025-07-27T16:27:32.637Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d9/ec4864f5896232133f51382b54a08de91a9d1af7a76dfa372894026dfee2/scipy-1.16.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81b433bbeaf35728dad619afc002db9b189e45eebe2cd676effe1fb93fef2b9c", size = 36575194, upload-time = "2025-07-27T16:27:41.321Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6d/40e81ecfb688e9d25d34a847dca361982a6addf8e31f0957b1a54fbfa994/scipy-1.16.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:886cc81fdb4c6903a3bb0464047c25a6d1016fef77bb97949817d0c0d79f9e04", size = 28594590, upload-time = "2025-07-27T16:27:49.204Z" }, + { url = "https://files.pythonhosted.org/packages/0e/37/9f65178edfcc629377ce9a64fc09baebea18c80a9e57ae09a52edf84880b/scipy-1.16.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:15240c3aac087a522b4eaedb09f0ad061753c5eebf1ea430859e5bf8640d5919", size = 20866458, upload-time = "2025-07-27T16:27:54.98Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7b/749a66766871ea4cb1d1ea10f27004db63023074c22abed51f22f09770e0/scipy-1.16.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f81a25805f3659b48126b5053d9e823d3215e4a63730b5e1671852a1705921", size = 23539318, upload-time = "2025-07-27T16:28:01.604Z" }, + { url = "https://files.pythonhosted.org/packages/c4/db/8d4afec60eb833a666434d4541a3151eedbf2494ea6d4d468cbe877f00cd/scipy-1.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6c62eea7f607f122069b9bad3f99489ddca1a5173bef8a0c75555d7488b6f725", size = 33292899, upload-time = "2025-07-27T16:28:09.147Z" }, + { url = "https://files.pythonhosted.org/packages/51/1e/79023ca3bbb13a015d7d2757ecca3b81293c663694c35d6541b4dca53e98/scipy-1.16.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f965bbf3235b01c776115ab18f092a95aa74c271a52577bcb0563e85738fd618", size = 35162637, upload-time = "2025-07-27T16:28:17.535Z" }, + { url = "https://files.pythonhosted.org/packages/b6/49/0648665f9c29fdaca4c679182eb972935b3b4f5ace41d323c32352f29816/scipy-1.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f006e323874ffd0b0b816d8c6a8e7f9a73d55ab3b8c3f72b752b226d0e3ac83d", size = 35490507, upload-time = "2025-07-27T16:28:25.705Z" }, + { url = "https://files.pythonhosted.org/packages/62/8f/66cbb9d6bbb18d8c658f774904f42a92078707a7c71e5347e8bf2f52bb89/scipy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8fd15fc5085ab4cca74cb91fe0a4263b1f32e4420761ddae531ad60934c2119", size = 37923998, upload-time = "2025-07-27T16:28:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/14/c3/61f273ae550fbf1667675701112e380881905e28448c080b23b5a181df7c/scipy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7b8013c6c066609577d910d1a2a077021727af07b6fab0ee22c2f901f22352a", size = 38508060, upload-time = "2025-07-27T16:28:43.242Z" }, + { url = "https://files.pythonhosted.org/packages/93/0b/b5c99382b839854a71ca9482c684e3472badc62620287cbbdab499b75ce6/scipy-1.16.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5451606823a5e73dfa621a89948096c6528e2896e40b39248295d3a0138d594f", size = 36533717, upload-time = "2025-07-27T16:28:51.706Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e5/69ab2771062c91e23e07c12e7d5033a6b9b80b0903ee709c3c36b3eb520c/scipy-1.16.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:89728678c5ca5abd610aee148c199ac1afb16e19844401ca97d43dc548a354eb", size = 28570009, upload-time = "2025-07-27T16:28:57.017Z" }, + { url = "https://files.pythonhosted.org/packages/f4/69/bd75dbfdd3cf524f4d753484d723594aed62cfaac510123e91a6686d520b/scipy-1.16.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e756d688cb03fd07de0fffad475649b03cb89bee696c98ce508b17c11a03f95c", size = 20841942, upload-time = "2025-07-27T16:29:01.152Z" }, + { url = "https://files.pythonhosted.org/packages/ea/74/add181c87663f178ba7d6144b370243a87af8476664d5435e57d599e6874/scipy-1.16.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5aa2687b9935da3ed89c5dbed5234576589dd28d0bf7cd237501ccfbdf1ad608", size = 23498507, upload-time = "2025-07-27T16:29:05.202Z" }, + { url = "https://files.pythonhosted.org/packages/1d/74/ece2e582a0d9550cee33e2e416cc96737dce423a994d12bbe59716f47ff1/scipy-1.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0851f6a1e537fe9399f35986897e395a1aa61c574b178c0d456be5b1a0f5ca1f", size = 33286040, upload-time = "2025-07-27T16:29:10.201Z" }, + { url = "https://files.pythonhosted.org/packages/e4/82/08e4076df538fb56caa1d489588d880ec7c52d8273a606bb54d660528f7c/scipy-1.16.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fedc2cbd1baed37474b1924c331b97bdff611d762c196fac1a9b71e67b813b1b", size = 35176096, upload-time = "2025-07-27T16:29:17.091Z" }, + { url = "https://files.pythonhosted.org/packages/fa/79/cd710aab8c921375711a8321c6be696e705a120e3011a643efbbcdeeabcc/scipy-1.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2ef500e72f9623a6735769e4b93e9dcb158d40752cdbb077f305487e3e2d1f45", size = 35490328, upload-time = "2025-07-27T16:29:22.928Z" }, + { url = "https://files.pythonhosted.org/packages/71/73/e9cc3d35ee4526d784520d4494a3e1ca969b071fb5ae5910c036a375ceec/scipy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:978d8311674b05a8f7ff2ea6c6bce5d8b45a0cb09d4c5793e0318f448613ea65", size = 37939921, upload-time = "2025-07-27T16:29:29.108Z" }, + { url = "https://files.pythonhosted.org/packages/21/12/c0efd2941f01940119b5305c375ae5c0fcb7ec193f806bd8f158b73a1782/scipy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:81929ed0fa7a5713fcdd8b2e6f73697d3b4c4816d090dd34ff937c20fa90e8ab", size = 38479462, upload-time = "2025-07-27T16:30:24.078Z" }, + { url = "https://files.pythonhosted.org/packages/7a/19/c3d08b675260046a991040e1ea5d65f91f40c7df1045fffff412dcfc6765/scipy-1.16.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:bcc12db731858abda693cecdb3bdc9e6d4bd200213f49d224fe22df82687bdd6", size = 36938832, upload-time = "2025-07-27T16:29:35.057Z" }, + { url = "https://files.pythonhosted.org/packages/81/f2/ce53db652c033a414a5b34598dba6b95f3d38153a2417c5a3883da429029/scipy-1.16.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:744d977daa4becb9fc59135e75c069f8d301a87d64f88f1e602a9ecf51e77b27", size = 29093084, upload-time = "2025-07-27T16:29:40.201Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ae/7a10ff04a7dc15f9057d05b33737ade244e4bd195caa3f7cc04d77b9e214/scipy-1.16.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:dc54f76ac18073bcecffb98d93f03ed6b81a92ef91b5d3b135dcc81d55a724c7", size = 21365098, upload-time = "2025-07-27T16:29:44.295Z" }, + { url = "https://files.pythonhosted.org/packages/36/ac/029ff710959932ad3c2a98721b20b405f05f752f07344622fd61a47c5197/scipy-1.16.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:367d567ee9fc1e9e2047d31f39d9d6a7a04e0710c86e701e053f237d14a9b4f6", size = 23896858, upload-time = "2025-07-27T16:29:48.784Z" }, + { url = "https://files.pythonhosted.org/packages/71/13/d1ef77b6bd7898720e1f0b6b3743cb945f6c3cafa7718eaac8841035ab60/scipy-1.16.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4cf5785e44e19dcd32a0e4807555e1e9a9b8d475c6afff3d21c3c543a6aa84f4", size = 33438311, upload-time = "2025-07-27T16:29:54.164Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e0/e64a6821ffbb00b4c5b05169f1c1fddb4800e9307efe3db3788995a82a2c/scipy-1.16.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3d0b80fb26d3e13a794c71d4b837e2a589d839fd574a6bbb4ee1288c213ad4a3", size = 35279542, upload-time = "2025-07-27T16:30:00.249Z" }, + { url = "https://files.pythonhosted.org/packages/57/59/0dc3c8b43e118f1e4ee2b798dcc96ac21bb20014e5f1f7a8e85cc0653bdb/scipy-1.16.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8503517c44c18d1030d666cb70aaac1cc8913608816e06742498833b128488b7", size = 35667665, upload-time = "2025-07-27T16:30:05.916Z" }, + { url = "https://files.pythonhosted.org/packages/45/5f/844ee26e34e2f3f9f8febb9343748e72daeaec64fe0c70e9bf1ff84ec955/scipy-1.16.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:30cc4bb81c41831ecfd6dc450baf48ffd80ef5aed0f5cf3ea775740e80f16ecc", size = 38045210, upload-time = "2025-07-27T16:30:11.655Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d7/210f2b45290f444f1de64bc7353aa598ece9f0e90c384b4a156f9b1a5063/scipy-1.16.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c24fa02f7ed23ae514460a22c57eca8f530dbfa50b1cfdbf4f37c05b5309cc39", size = 38593661, upload-time = "2025-07-27T16:30:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/81/ea/84d481a5237ed223bd3d32d6e82d7a6a96e34756492666c260cef16011d1/scipy-1.16.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:796a5a9ad36fa3a782375db8f4241ab02a091308eb079746bc0f874c9b998318", size = 36525921, upload-time = "2025-07-27T16:30:30.081Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9f/d9edbdeff9f3a664807ae3aea383e10afaa247e8e6255e6d2aa4515e8863/scipy-1.16.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:3ea0733a2ff73fd6fdc5fecca54ee9b459f4d74f00b99aced7d9a3adb43fb1cc", size = 28564152, upload-time = "2025-07-27T16:30:35.336Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/8125bcb1fe04bc267d103e76516243e8d5e11229e6b306bda1024a5423d1/scipy-1.16.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:85764fb15a2ad994e708258bb4ed8290d1305c62a4e1ef07c414356a24fcfbf8", size = 20836028, upload-time = "2025-07-27T16:30:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/77/9c/bf92e215701fc70bbcd3d14d86337cf56a9b912a804b9c776a269524a9e9/scipy-1.16.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ca66d980469cb623b1759bdd6e9fd97d4e33a9fad5b33771ced24d0cb24df67e", size = 23489666, upload-time = "2025-07-27T16:30:43.663Z" }, + { url = "https://files.pythonhosted.org/packages/5e/00/5e941d397d9adac41b02839011594620d54d99488d1be5be755c00cde9ee/scipy-1.16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e7cc1ffcc230f568549fc56670bcf3df1884c30bd652c5da8138199c8c76dae0", size = 33358318, upload-time = "2025-07-27T16:30:48.982Z" }, + { url = "https://files.pythonhosted.org/packages/0e/87/8db3aa10dde6e3e8e7eb0133f24baa011377d543f5b19c71469cf2648026/scipy-1.16.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ddfb1e8d0b540cb4ee9c53fc3dea3186f97711248fb94b4142a1b27178d8b4b", size = 35185724, upload-time = "2025-07-27T16:30:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/89/b4/6ab9ae443216807622bcff02690262d8184078ea467efee2f8c93288a3b1/scipy-1.16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4dc0e7be79e95d8ba3435d193e0d8ce372f47f774cffd882f88ea4e1e1ddc731", size = 35554335, upload-time = "2025-07-27T16:30:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9a/d0e9dc03c5269a1afb60661118296a32ed5d2c24298af61b676c11e05e56/scipy-1.16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f23634f9e5adb51b2a77766dac217063e764337fbc816aa8ad9aaebcd4397fd3", size = 37960310, upload-time = "2025-07-27T16:31:06.151Z" }, + { url = "https://files.pythonhosted.org/packages/5e/00/c8f3130a50521a7977874817ca89e0599b1b4ee8e938bad8ae798a0e1f0d/scipy-1.16.1-cp314-cp314-win_amd64.whl", hash = "sha256:57d75524cb1c5a374958a2eae3d84e1929bb971204cc9d52213fb8589183fc19", size = 39319239, upload-time = "2025-07-27T16:31:59.942Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f2/1ca3eda54c3a7e4c92f6acef7db7b3a057deb135540d23aa6343ef8ad333/scipy-1.16.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:d8da7c3dd67bcd93f15618938f43ed0995982eb38973023d46d4646c4283ad65", size = 36939460, upload-time = "2025-07-27T16:31:11.865Z" }, + { url = "https://files.pythonhosted.org/packages/80/30/98c2840b293a132400c0940bb9e140171dcb8189588619048f42b2ce7b4f/scipy-1.16.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:cc1d2f2fd48ba1e0620554fe5bc44d3e8f5d4185c8c109c7fbdf5af2792cfad2", size = 29093322, upload-time = "2025-07-27T16:31:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e6/1e6e006e850622cf2a039b62d1a6ddc4497d4851e58b68008526f04a9a00/scipy-1.16.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:21a611ced9275cb861bacadbada0b8c0623bc00b05b09eb97f23b370fc2ae56d", size = 21365329, upload-time = "2025-07-27T16:31:21.188Z" }, + { url = "https://files.pythonhosted.org/packages/8e/02/72a5aa5b820589dda9a25e329ca752842bfbbaf635e36bc7065a9b42216e/scipy-1.16.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dfbb25dffc4c3dd9371d8ab456ca81beeaf6f9e1c2119f179392f0dc1ab7695", size = 23897544, upload-time = "2025-07-27T16:31:25.408Z" }, + { url = "https://files.pythonhosted.org/packages/2b/dc/7122d806a6f9eb8a33532982234bed91f90272e990f414f2830cfe656e0b/scipy-1.16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f0ebb7204f063fad87fc0a0e4ff4a2ff40b2a226e4ba1b7e34bf4b79bf97cd86", size = 33442112, upload-time = "2025-07-27T16:31:30.62Z" }, + { url = "https://files.pythonhosted.org/packages/24/39/e383af23564daa1021a5b3afbe0d8d6a68ec639b943661841f44ac92de85/scipy-1.16.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f1b9e5962656f2734c2b285a8745358ecb4e4efbadd00208c80a389227ec61ff", size = 35286594, upload-time = "2025-07-27T16:31:36.112Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/1a0b0aff40c3056d955f38b0df5d178350c3d74734ec54f9c68d23910be5/scipy-1.16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e1a106f8c023d57a2a903e771228bf5c5b27b5d692088f457acacd3b54511e4", size = 35665080, upload-time = "2025-07-27T16:31:42.025Z" }, + { url = "https://files.pythonhosted.org/packages/64/df/ce88803e9ed6e27fe9b9abefa157cf2c80e4fa527cf17ee14be41f790ad4/scipy-1.16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:709559a1db68a9abc3b2c8672c4badf1614f3b440b3ab326d86a5c0491eafae3", size = 38050306, upload-time = "2025-07-27T16:31:48.109Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6c/a76329897a7cae4937d403e623aa6aaea616a0bb5b36588f0b9d1c9a3739/scipy-1.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c0c804d60492a0aad7f5b2bb1862f4548b990049e27e828391ff2bf6f7199998", size = 39427705, upload-time = "2025-07-27T16:31:53.96Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "snowballstemmer" version = "3.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575 } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274 }, + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, ] [[package]] name = "soupsieve" version = "2.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418 } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677 }, + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" }, ] [[package]] @@ -1736,9 +1738,9 @@ dependencies = [ { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611 } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125 }, + { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125, upload-time = "2024-10-13T20:27:10.448Z" }, ] [[package]] @@ -1767,9 +1769,9 @@ dependencies = [ { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" }, { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876 } +sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741 }, + { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" }, ] [[package]] @@ -1782,9 +1784,9 @@ resolution-markers = [ dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/f0/43c6a5ff3e7b08a8c3b32f81b859f1b518ccc31e45f22e2b41ced38be7b9/sphinx_autodoc_typehints-3.0.1.tar.gz", hash = "sha256:b9b40dd15dee54f6f810c924f863f9cf1c54f9f3265c495140ea01be7f44fa55", size = 36282 } +sdist = { url = "https://files.pythonhosted.org/packages/26/f0/43c6a5ff3e7b08a8c3b32f81b859f1b518ccc31e45f22e2b41ced38be7b9/sphinx_autodoc_typehints-3.0.1.tar.gz", hash = "sha256:b9b40dd15dee54f6f810c924f863f9cf1c54f9f3265c495140ea01be7f44fa55", size = 36282, upload-time = "2025-01-16T18:25:30.958Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/dc/dc46c5c7c566b7ec5e8f860f9c89533bf03c0e6aadc96fb9b337867e4460/sphinx_autodoc_typehints-3.0.1-py3-none-any.whl", hash = "sha256:4b64b676a14b5b79cefb6628a6dc8070e320d4963e8ff640a2f3e9390ae9045a", size = 20245 }, + { url = "https://files.pythonhosted.org/packages/3c/dc/dc46c5c7c566b7ec5e8f860f9c89533bf03c0e6aadc96fb9b337867e4460/sphinx_autodoc_typehints-3.0.1-py3-none-any.whl", hash = "sha256:4b64b676a14b5b79cefb6628a6dc8070e320d4963e8ff640a2f3e9390ae9045a", size = 20245, upload-time = "2025-01-16T18:25:27.394Z" }, ] [[package]] @@ -1797,9 +1799,9 @@ resolution-markers = [ dependencies = [ { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/68/a388a9b8f066cd865d9daa65af589d097efbfab9a8c302d2cb2daa43b52e/sphinx_autodoc_typehints-3.2.0.tar.gz", hash = "sha256:107ac98bc8b4837202c88c0736d59d6da44076e65a0d7d7d543a78631f662a9b", size = 36724 } +sdist = { url = "https://files.pythonhosted.org/packages/93/68/a388a9b8f066cd865d9daa65af589d097efbfab9a8c302d2cb2daa43b52e/sphinx_autodoc_typehints-3.2.0.tar.gz", hash = "sha256:107ac98bc8b4837202c88c0736d59d6da44076e65a0d7d7d543a78631f662a9b", size = 36724, upload-time = "2025-04-25T16:53:25.872Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/c7/8aab362e86cbf887e58be749a78d20ad743e1eb2c73c2b13d4761f39a104/sphinx_autodoc_typehints-3.2.0-py3-none-any.whl", hash = "sha256:884b39be23b1d884dcc825d4680c9c6357a476936e3b381a67ae80091984eb49", size = 20563 }, + { url = "https://files.pythonhosted.org/packages/f7/c7/8aab362e86cbf887e58be749a78d20ad743e1eb2c73c2b13d4761f39a104/sphinx_autodoc_typehints-3.2.0-py3-none-any.whl", hash = "sha256:884b39be23b1d884dcc825d4680c9c6357a476936e3b381a67ae80091984eb49", size = 20563, upload-time = "2025-04-25T16:53:24.492Z" }, ] [[package]] @@ -1811,9 +1813,9 @@ dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/19/d002ed96bdc7738c15847c730e1e88282d738263deac705d5713b4d8fa94/sphinx_book_theme-1.1.4.tar.gz", hash = "sha256:73efe28af871d0a89bd05856d300e61edce0d5b2fbb7984e84454be0fedfe9ed", size = 439188 } +sdist = { url = "https://files.pythonhosted.org/packages/45/19/d002ed96bdc7738c15847c730e1e88282d738263deac705d5713b4d8fa94/sphinx_book_theme-1.1.4.tar.gz", hash = "sha256:73efe28af871d0a89bd05856d300e61edce0d5b2fbb7984e84454be0fedfe9ed", size = 439188, upload-time = "2025-02-20T16:32:32.581Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/9e/c41d68be04eef5b6202b468e0f90faf0c469f3a03353f2a218fd78279710/sphinx_book_theme-1.1.4-py3-none-any.whl", hash = "sha256:843b3f5c8684640f4a2d01abd298beb66452d1b2394cd9ef5be5ebd5640ea0e1", size = 433952 }, + { url = "https://files.pythonhosted.org/packages/51/9e/c41d68be04eef5b6202b468e0f90faf0c469f3a03353f2a218fd78279710/sphinx_book_theme-1.1.4-py3-none-any.whl", hash = "sha256:843b3f5c8684640f4a2d01abd298beb66452d1b2394cd9ef5be5ebd5640ea0e1", size = 433952, upload-time = "2025-02-20T16:32:31.009Z" }, ] [[package]] @@ -1825,45 +1827,45 @@ dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/e5/9ccd6ecd492043123adb465cba504217b9f0a82e2cb5b1d7249c648497c6/sphinx_gallery-0.19.0.tar.gz", hash = "sha256:8400cb5240ad642e28a612fdba0667f725d0505a9be0222d0243de60e8af2eb3", size = 471479 } +sdist = { url = "https://files.pythonhosted.org/packages/cd/e5/9ccd6ecd492043123adb465cba504217b9f0a82e2cb5b1d7249c648497c6/sphinx_gallery-0.19.0.tar.gz", hash = "sha256:8400cb5240ad642e28a612fdba0667f725d0505a9be0222d0243de60e8af2eb3", size = 471479, upload-time = "2025-02-13T03:24:50.081Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/c7/52b48aec16b26c52aba854d03a3a31e0681150301dac1bea2243645a69e7/sphinx_gallery-0.19.0-py3-none-any.whl", hash = "sha256:4c28751973f81769d5bbbf5e4ebaa0dc49dff8c48eb7f11131eb5f6e4aa25f0e", size = 455923 }, + { url = "https://files.pythonhosted.org/packages/77/c7/52b48aec16b26c52aba854d03a3a31e0681150301dac1bea2243645a69e7/sphinx_gallery-0.19.0-py3-none-any.whl", hash = "sha256:4c28751973f81769d5bbbf5e4ebaa0dc49dff8c48eb7f11131eb5f6e4aa25f0e", size = 455923, upload-time = "2025-02-13T03:24:47.697Z" }, ] [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053 } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300 }, + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, ] [[package]] name = "sphinxcontrib-devhelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967 } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 }, + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, ] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617 } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705 }, + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, ] [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 }, + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, ] [[package]] @@ -1875,27 +1877,27 @@ dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/69/bf039237ad260073e8c02f820b3e00dc34f3a2de20aff7861e6b19d2f8c5/sphinxcontrib_mermaid-1.0.0.tar.gz", hash = "sha256:2e8ab67d3e1e2816663f9347d026a8dee4a858acdd4ad32dd1c808893db88146", size = 15153 } +sdist = { url = "https://files.pythonhosted.org/packages/97/69/bf039237ad260073e8c02f820b3e00dc34f3a2de20aff7861e6b19d2f8c5/sphinxcontrib_mermaid-1.0.0.tar.gz", hash = "sha256:2e8ab67d3e1e2816663f9347d026a8dee4a858acdd4ad32dd1c808893db88146", size = 15153, upload-time = "2024-10-12T16:33:03.863Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/c8/784b9ac6ea08aa594c1a4becbd0dbe77186785362e31fd633b8c6ae0197a/sphinxcontrib_mermaid-1.0.0-py3-none-any.whl", hash = "sha256:60b72710ea02087f212028feb09711225fbc2e343a10d34822fe787510e1caa3", size = 9597 }, + { url = "https://files.pythonhosted.org/packages/cd/c8/784b9ac6ea08aa594c1a4becbd0dbe77186785362e31fd633b8c6ae0197a/sphinxcontrib_mermaid-1.0.0-py3-none-any.whl", hash = "sha256:60b72710ea02087f212028feb09711225fbc2e343a10d34822fe787510e1caa3", size = 9597, upload-time = "2024-10-12T16:33:02.303Z" }, ] [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165 } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743 }, + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, ] [[package]] name = "sphinxcontrib-serializinghtml" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] [[package]] @@ -1906,48 +1908,48 @@ dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/16/48/063e167b6e692bc84bbad74df30bcb27e460a7c620af7824729db8dba606/sphinxcontrib_video-0.4.1.tar.gz", hash = "sha256:75a033e71b7de124cc5902430b7ba818a1c6c377be6401d07e9f2329a95d5ca4", size = 11362 } +sdist = { url = "https://files.pythonhosted.org/packages/16/48/063e167b6e692bc84bbad74df30bcb27e460a7c620af7824729db8dba606/sphinxcontrib_video-0.4.1.tar.gz", hash = "sha256:75a033e71b7de124cc5902430b7ba818a1c6c377be6401d07e9f2329a95d5ca4", size = 11362, upload-time = "2025-02-19T17:06:13.632Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/8b/a0271fe65357860ccc52168181891e9fc9d354bfdc9be273e6a77b84f905/sphinxcontrib_video-0.4.1-py3-none-any.whl", hash = "sha256:d63ec68983dac36960557973281a616b5d9e68838369763313fc80533b1ad774", size = 10066 }, + { url = "https://files.pythonhosted.org/packages/5d/8b/a0271fe65357860ccc52168181891e9fc9d354bfdc9be273e6a77b84f905/sphinxcontrib_video-0.4.1-py3-none-any.whl", hash = "sha256:d63ec68983dac36960557973281a616b5d9e68838369763313fc80533b1ad774", size = 10066, upload-time = "2025-02-19T17:06:12.561Z" }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, - { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, - { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, - { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, - { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, - { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, - { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, - { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, - { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, - { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, - { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] [[package]] @@ -1957,27 +1959,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] [[package]] name = "typing-extensions" version = "4.14.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673 } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906 }, + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, ] [[package]] name = "urllib3" version = "2.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185 } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 }, + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] [[package]] @@ -1989,7 +1991,7 @@ dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/60/4f20960df6c7b363a18a55ab034c8f2bcd5d9770d1f94f9370ec104c1855/virtualenv-20.33.1.tar.gz", hash = "sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8", size = 6082160 } +sdist = { url = "https://files.pythonhosted.org/packages/8b/60/4f20960df6c7b363a18a55ab034c8f2bcd5d9770d1f94f9370ec104c1855/virtualenv-20.33.1.tar.gz", hash = "sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8", size = 6082160, upload-time = "2025-08-05T16:10:55.605Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/ff/ded57ac5ff40a09e6e198550bab075d780941e0b0f83cbeabd087c59383a/virtualenv-20.33.1-py3-none-any.whl", hash = "sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67", size = 6060362 }, + { url = "https://files.pythonhosted.org/packages/ca/ff/ded57ac5ff40a09e6e198550bab075d780941e0b0f83cbeabd087c59383a/virtualenv-20.33.1-py3-none-any.whl", hash = "sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67", size = 6060362, upload-time = "2025-08-05T16:10:52.81Z" }, ] From 54960596c0b0734cbc93d1c658d0c20d668dadc6 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Thu, 18 Dec 2025 16:10:38 -0600 Subject: [PATCH 68/85] remove explicit steppers, memory management, and protocol definitions --- .../timestepper/explicit_steppers.py | 321 ---------------- elastica/experimental/timestepper/memory.py | 83 ---- elastica/experimental/timestepper/protocol.py | 111 ------ elastica/rigidbody/data_structures.py | 39 -- elastica/rod/data_structures.py | 360 +----------------- tests/test_math/test_timestepper.py | 79 +--- 6 files changed, 3 insertions(+), 990 deletions(-) delete mode 100644 elastica/experimental/timestepper/explicit_steppers.py delete mode 100644 elastica/experimental/timestepper/memory.py delete mode 100644 elastica/experimental/timestepper/protocol.py diff --git a/elastica/experimental/timestepper/explicit_steppers.py b/elastica/experimental/timestepper/explicit_steppers.py deleted file mode 100644 index a8d96792..00000000 --- a/elastica/experimental/timestepper/explicit_steppers.py +++ /dev/null @@ -1,321 +0,0 @@ -__doc__ = """Explicit timesteppers and concepts""" - -from typing import Any - -import numpy as np -from copy import copy - -from elastica.typing import ( - SystemType, - SystemCollectionType, - StepType, - SteppersOperatorsType, -) -from elastica.experimental.timestepper.protocol import ( - ExplicitSystemProtocol, - ExplicitStepperProtocol, - MemoryProtocol, -) - -from elastica.rod.data_structures import _State as StateType - - -""" -Developer Note --------------- -## Motivation for choosing _Mixin classes below - -The constraint/problem is that we do not know what -`System` we are integrating apriori. For a single -standalone `System` (which defines a `__call__` -operator and has its own states), we should just -step it like a single system. - -Instead if we get a `SystemCollection` made up of -bunch of smaller systems (like Cosserat Rods), we now -need to loop over all these smaller systems and perform -state updates there. Not only that we may also need -to communicate between such smaller systems. - -One way to solve this issue is to give the integrator -two methods: - -- `do_step`, which does the time-stepping for only a -`System` -- `do_system_step` which does the time-stepping for -a `SystemCollection` - -The problem with this approach is that -1. We have more methods than we actually use -(indeed we can only integrate either a `System` or -a `SystemCollection` but not both) -2. From an interface point of view, its ugly and not -graceful (at least IMO). - -The second approach is what I have chosen here, -which is to create two mixin classes : one for -integrating `System` and one for integrating -`SystemCollection`. And then depending upon the runtime -type of the object to be integrated, we can dynamically -mixin the required class. - -This approach overcomes the disadvantages of the -previous approach (as there's only one `do_step` method -associated with a Stepper at any given point of time), -at the expense of being a tad bit harder to understand -(which this documentation will hopefully fix). In essence, -we "smartly" use a mixin class to define the necessary -`do_step` method, which the `integrate` function then uses. -""" - - -class EulerForwardMemory: - def __init__(self, initial_state: StateType) -> None: - self.initial_state = initial_state - - -class RungeKutta4Memory: - """ - Stores all states of Rk within the time-stepper. Works as long as the states - are all one big numpy array, made possible by carefully using views. - - Convenience wrapper around Stateless that provides memory - """ - - def __init__( - self, - initial_state: StateType, - ) -> None: - self.initial_state = initial_state - self.k_1 = initial_state - self.k_2 = initial_state - self.k_3 = initial_state - self.k_4 = initial_state - - -class ExplicitStepperMixin: - """Base class for all explicit steppers - Can also be used as a mixin with optional cls argument below - """ - - def __init__(self: ExplicitStepperProtocol): - self.steps_and_prefactors = self.step_methods() - - def step_methods(self: ExplicitStepperProtocol) -> SteppersOperatorsType: - stages = self.get_stages() - updates = self.get_updates() - - assert len(stages) == len( - updates - ), "Number of stages and updates should be equal to one another" - return tuple(zip(stages, updates)) - - @property - def n_stages(self: ExplicitStepperProtocol) -> int: - return len(self.steps_and_prefactors) - - def step( - self: ExplicitStepperProtocol, - SystemCollection: SystemCollectionType, - time: np.float64, - dt: np.float64, - ) -> np.float64: - if isinstance( - self, EulerForward - ): # TODO: Cleanup - use depedency injection instead - Memory = EulerForwardMemory - elif isinstance(self, RungeKutta4): - Memory = RungeKutta4Memory # type: ignore[assignment] - else: - raise NotImplementedError(f"Memory class not defined for {self}") - memory_collection = tuple( - [Memory(initial_state=system.state) for system in SystemCollection] - ) - return ExplicitStepperMixin.do_step(self, self.steps_and_prefactors, SystemCollection, memory_collection, time, dt) # type: ignore[attr-defined] - - @staticmethod - def do_step( - TimeStepper: ExplicitStepperProtocol, - steps_and_prefactors: SteppersOperatorsType, - SystemCollection: SystemCollectionType, - MemoryCollection: Any, # TODO - time: np.float64, - dt: np.float64, - ) -> np.float64: - for stage, update in steps_and_prefactors: - SystemCollection.synchronize(time) - for system, memory in zip(SystemCollection[:-1], MemoryCollection[:-1]): - stage(system, memory, time, dt) - _ = update(system, memory, time, dt) - - stage(SystemCollection[-1], MemoryCollection[-1], time, dt) - time = update(SystemCollection[-1], MemoryCollection[-1], time, dt) - return time - - def step_single_instance( - self: ExplicitStepperProtocol, - System: SystemType, - Memory: MemoryProtocol, - time: np.float64, - dt: np.float64, - ) -> np.float64: - for stage, update in self.steps_and_prefactors: - stage(System, Memory, time, dt) - time = update(System, Memory, time, dt) - return time - - -class EulerForward(ExplicitStepperMixin): - """ - Classical Euler Forward stepper. Stateless, coordinates operations only. - """ - - def get_stages(self) -> list[StepType]: - return [self._first_stage] - - def get_updates(self) -> list[StepType]: - return [self._first_update] - - def _first_stage( - self, - System: ExplicitSystemProtocol, - Memory: EulerForwardMemory, - time: np.float64, - dt: np.float64, - ) -> None: - pass - - def _first_update( - self, - System: ExplicitSystemProtocol, - Memory: EulerForwardMemory, - time: np.float64, - dt: np.float64, - ) -> np.float64: - System.state += dt * System(time, dt) # type: ignore[arg-type] - return time + dt - - -class RungeKutta4(ExplicitStepperMixin): - """ - Stateless runge-kutta4. coordinates operations only, memory needs - to be externally managed and allocated. - """ - - def get_stages(self) -> list[StepType]: - return [ - self._first_stage, - self._second_stage, - self._third_stage, - self._fourth_stage, - ] - - def get_updates(self) -> list[StepType]: - return [ - self._first_update, - self._second_update, - self._third_update, - self._fourth_update, - ] - - # These methods should be static, but because we need to enable automatic - # discovery in ExplicitStepper, these are bound to the RungeKutta4 class - # For automatic discovery, the order of declaring stages here is very important - def _first_stage( - self, - System: ExplicitSystemProtocol, - Memory: RungeKutta4Memory, - time: np.float64, - dt: np.float64, - ) -> None: - Memory.initial_state = copy(System.state) - Memory.k_1 = dt * System(time, dt) # type: ignore[operator, assignment] - - def _first_update( - self, - System: ExplicitSystemProtocol, - Memory: RungeKutta4Memory, - time: np.float64, - dt: np.float64, - ) -> np.float64: - # prepare for next stage - System.state = Memory.initial_state + 0.5 * Memory.k_1 # type: ignore[operator] - return time + 0.5 * dt - - def _second_stage( - self, - System: ExplicitSystemProtocol, - Memory: RungeKutta4Memory, - time: np.float64, - dt: np.float64, - ) -> None: - Memory.k_2 = dt * System(time, dt) # type: ignore[operator, assignment] - - def _second_update( - self, - System: ExplicitSystemProtocol, - Memory: RungeKutta4Memory, - time: np.float64, - dt: np.float64, - ) -> np.float64: - # prepare for next stage - System.state = Memory.initial_state + 0.5 * Memory.k_2 # type: ignore[operator] - return time - - def _third_stage( - self, - System: ExplicitSystemProtocol, - Memory: RungeKutta4Memory, - time: np.float64, - dt: np.float64, - ) -> None: - Memory.k_3 = dt * System(time, dt) # type: ignore[operator, assignment] - - def _third_update( - self, - System: ExplicitSystemProtocol, - Memory: RungeKutta4Memory, - time: np.float64, - dt: np.float64, - ) -> np.float64: - # prepare for next stage - System.state = Memory.initial_state + Memory.k_3 # type: ignore[operator] - return time + 0.5 * dt - - def _fourth_stage( - self, - System: ExplicitSystemProtocol, - Memory: RungeKutta4Memory, - time: np.float64, - dt: np.float64, - ) -> None: - Memory.k_4 = dt * System(time, dt) # type: ignore[operator, assignment] - - def _fourth_update( - self, - System: ExplicitSystemProtocol, - Memory: RungeKutta4Memory, - time: np.float64, - dt: np.float64, - ) -> np.float64: - # prepare for next stage - System.state = ( - Memory.initial_state - + (Memory.k_1 + 2.0 * Memory.k_2 + 2.0 * Memory.k_3 + Memory.k_4) / 6.0 # type: ignore[operator] - ) - return time - - -# class ExplicitLinearExponentialIntegrator( -# _LinearExponentialIntegratorMixin, ExplicitStepper -# ): -# def __init__(self): -# _LinearExponentialIntegratorMixin.__init__(self) -# ExplicitStepper.__init__(self, _LinearExponentialIntegratorMixin) -# -# -# class StatefulLinearExponentialIntegrator(_StatefulStepper): -# def __init__(self): -# super(StatefulLinearExponentialIntegrator, self).__init__() -# self.stepper = ExplicitLinearExponentialIntegrator() -# self.linear_operator = None diff --git a/elastica/experimental/timestepper/memory.py b/elastica/experimental/timestepper/memory.py deleted file mode 100644 index b63931aa..00000000 --- a/elastica/experimental/timestepper/memory.py +++ /dev/null @@ -1,83 +0,0 @@ -from typing import Iterator, TypeVar, Generic, Type -from elastica.typing import SystemCollectionType -from elastica.experimental.timestepper.explicit_steppers import ( - RungeKutta4, - EulerForward, -) -from elastica.experimental.timestepper.protocol import ExplicitStepperProtocol - -from copy import copy - - -# FIXME: Move memory related functions to separate module or as part of the timestepper -# TODO: Use MemoryProtocol -def make_memory_for_explicit_stepper( - stepper: ExplicitStepperProtocol, system: SystemCollectionType -) -> "MemoryCollection": - # TODO Automated logic (class creation, memory management logic) agnostic of stepper details (RK, AB etc.) - - # is_this_system_a_collection = is_system_a_collection(system) - - memory_cls: Type - if RungeKutta4 in stepper.__class__.mro(): - # Bad way of doing it, introduces tight coupling - # this should rather be taken from the class itself - class MemoryRungeKutta4: - def __init__(self) -> None: - self.initial_state = None - self.k_1 = None - self.k_2 = None - self.k_3 = None - self.k_4 = None - - memory_cls = MemoryRungeKutta4 - elif EulerForward in stepper.__class__.mro(): - - class MemoryEulerForward: - def __init__(self) -> None: - self.initial_state = None - self.k = None - - memory_cls = MemoryEulerForward - else: - raise NotImplementedError("Making memory for other types not supported") - - return MemoryCollection(memory_cls(), len(system)) - - -M = TypeVar("M", bound="MemoryCollection") - - -class MemoryCollection(Generic[M]): - """Slots of memories for timestepper in a cohesive unit. - - A `MemoryCollection` object is meant to be used in conjunction - with a `SystemCollection`, where each independent `System` to - be integrated has its own `Memory`. - - Example - ------- - - A RK4 integrator needs to store k_1, k_2, k_3, k_4 (intermediate - results from four stages) for each `System`. The restriction for - having a memory slot arises because the `Systems` are usually - not independent of one another and may need communication after - every stage. - """ - - def __init__(self, memory: M, n_memory_slots: int): - super(MemoryCollection, self).__init__() - - self.__memories: list[M] = [] - for _ in range(n_memory_slots - 1): - self.__memories.append(copy(memory)) - self.__memories.append(memory) - - def __getitem__(self, idx: int) -> M: - return self.__memories[idx] - - def __len__(self) -> int: - return len(self.__memories) - - def __iter__(self) -> Iterator[M]: - return self.__memories.__iter__() diff --git a/elastica/experimental/timestepper/protocol.py b/elastica/experimental/timestepper/protocol.py deleted file mode 100644 index 0606928a..00000000 --- a/elastica/experimental/timestepper/protocol.py +++ /dev/null @@ -1,111 +0,0 @@ -from typing import Protocol - -from elastica.typing import StepType -from elastica.systems.protocol import SystemProtocol -from elastica.timestepper.protocol import StepperProtocol -from elastica.rod.data_structures import _State as StateType - -import numpy as np -from numpy.typing import NDArray - - -class ExplicitSystemProtocol(SystemProtocol, Protocol): - """ - Protocol defining the required interface for explicit time integration. - - TODO: Temporarily made to handle explicit stepper. - Need to be refactored as the explicit stepper is further developed. - """ - - # Geometry - n_nodes: int - n_elems: int - - # State arrays - position_collection: NDArray[np.float64] - velocity_collection: NDArray[np.float64] - acceleration_collection: NDArray[np.float64] - omega_collection: NDArray[np.float64] - alpha_collection: NDArray[np.float64] - director_collection: NDArray[np.float64] - - # Forces/torques - external_forces: NDArray[np.float64] - external_torques: NDArray[np.float64] - internal_forces: NDArray[np.float64] - internal_torques: NDArray[np.float64] - - def __call__(self, time: np.float64, dt: np.float64) -> np.float64: ... - - @property - def state(self) -> StateType: ... - - @state.setter - def state(self, state: StateType) -> None: ... - - -class MemoryProtocol(Protocol): - @property - def initial_state(self) -> bool: ... - - -class ExplicitStepperProtocol(StepperProtocol, Protocol): - """symplectic stepper protocol.""" - - def get_stages(self) -> list[StepType]: ... - - def get_updates(self) -> list[StepType]: ... - - -# class _LinearExponentialIntegratorMixin: -# """ -# Linear Exponential integrator mixin wrapper. -# """ -# -# def __init__(self): -# pass -# -# def _do_stage(self, System, Memory, time, dt): -# # TODO : Make more general, system should not be calculating what the state -# # transition matrix directly is, but rather it should just give -# Memory.linear_operator = System.get_linear_state_transition_operator(time, dt) -# -# def _do_update(self, System, Memory, time, dt): -# # FIXME What's the right formula when doing update? -# # System.linearly_evolving_state = _batch_matmul( -# # System.linearly_evolving_state, -# # Memory.linear_operator -# # ) -# System.linearly_evolving_state = np.einsum( -# "ijk,ljk->ilk", System.linearly_evolving_state, Memory.linear_operator -# ) -# return time + dt -# -# def _first_prefactor(self, dt): -# """Prefactor call to satisfy interface of SymplecticStepper. Should never -# be used in actual code. -# -# Parameters -# ---------- -# dt : the time step of simulation -# -# Raises -# ------ -# RuntimeError -# """ -# raise RuntimeError( -# "Symplectic prefactor of LinearExponentialIntegrator should not be called!" -# ) -# -# # Code repeat! -# # Easy to avoid, but keep for performance. -# def _do_one_step(self, System, time, prefac): -# System.linearly_evolving_state = np.einsum( -# "ijk,ljk->ilk", -# System.linearly_evolving_state, -# System.get_linear_state_transition_operator(time, prefac), -# ) -# return ( -# time # TODO fix hack that treats time separately here. Shuold be time + dt -# ) -# # return time + dt diff --git a/elastica/rigidbody/data_structures.py b/elastica/rigidbody/data_structures.py index 95944f75..f7e9ae65 100644 --- a/elastica/rigidbody/data_structures.py +++ b/elastica/rigidbody/data_structures.py @@ -2,44 +2,5 @@ from elastica.rod.data_structures import _RodSymplecticStepperMixin -pass -""" -# FIXME : Explicit Stepper doesn't work as States lose the -# views they initially had when working with a timestepper. -class _RigidRodExplicitStepperMixin: - def __init__(self): - ( - self.state, - self.__deriv_state, - self.position_collection, - self.director_collection, - self.velocity_collection, - self.omega_collection, - self.acceleration_collection, - self.alpha_collection, # angular acceleration - ) = _bootstrap_from_data( - "explicit", self.n_elems, self._vector_states, self._matrix_states - ) - - # def __setattr__(self, name, value): - # np.copy(self.__dict__[name], value) - - def __call__(self, time, *args, **kwargs): - self.update_accelerations(time) # Internal, external - - # print("KRC", self.state.kinematic_rate_collection) - # print("DEr", self.__deriv_state.rate_collection) - if np.shares_memory( - self.state.kinematic_rate_collection, - self.velocity_collection - # self.__deriv_state.rate_collection - ): - print("Shares memory") - else: - print("Explicit states does not share memory") - return self.__deriv_state -""" - -# TODO: Temporary solution as the structure for RigidBody is similar to Rod _RigidRodSymplecticStepperMixin = _RodSymplecticStepperMixin diff --git a/elastica/rod/data_structures.py b/elastica/rod/data_structures.py index 92d463fe..70a647ff 100644 --- a/elastica/rod/data_structures.py +++ b/elastica/rod/data_structures.py @@ -1,11 +1,10 @@ __doc__ = "Data structure wrapper for rod components" -from typing import TYPE_CHECKING, Optional -from typing_extensions import Self +from typing import TYPE_CHECKING import numpy as np from numpy.typing import NDArray from numba import njit -from elastica._rotations import _get_rotation_matrix, _rotate +from elastica._rotations import _get_rotation_matrix from elastica._linalg import _batch_matmul if TYPE_CHECKING: @@ -13,41 +12,6 @@ else: SymplecticSystemProtocol = "SymplecticSystemProtocol" -# FIXME : Explicit Stepper doesn't work as States lose the -# views they initially had when working with a timestepper. -# class _RodExplicitStepperMixin: -# def __init__(self) -> None: -# ( -# self.state, -# self.__deriv_state, -# self.position_collection, -# self.director_collection, -# self.velocity_collection, -# self.omega_collection, -# self.acceleration_collection, -# self.alpha_collection, # angular acceleration -# ) = _bootstrap_from_data( -# "explicit", self.n_elems, self._vector_states, self._matrix_states -# ) -# -# # def __setattr__(self, name, value): -# # np.copy(self.__dict__[name], value) -# -# def __call__(self, time, *args, **kwargs): -# self.update_accelerations(time) # Internal, external -# -# # print("KRC", self.state.kinematic_rate_collection) -# # print("DEr", self.__deriv_state.rate_collection) -# if np.shares_memory( -# self.state.kinematic_rate_collection, -# self.velocity_collection -# # self.__deriv_state.rate_collection -# ): -# print("Shares memory") -# else: -# print("Explicit states does not share memory") -# return self.__deriv_state - class _RodSymplecticStepperMixin: @@ -85,326 +49,6 @@ def dynamic_rates( return self.dynamic_states.dynamic_rates(time, prefac) -def _bootstrap_from_data( - stepper_type: str, - n_elems: int, - vector_states: NDArray[np.float64], - matrix_states: NDArray[np.float64], -) -> Optional[ - tuple[ - "_State", - "_DerivativeState", - NDArray[np.float64], - NDArray[np.float64], - NDArray[np.float64], - NDArray[np.float64], - NDArray[np.float64], - NDArray[np.float64], - ] -]: - """Returns states wrapping numpy arrays based on the time-stepping algorithm - - Convenience method that takes in rod internal (raw np.ndarray) data, create views - (references) from it, and outputs State classes that are used in the time-stepping - algorithm. This means that modifying the state modifies the internal data! - - Parameters - ---------- - stepper_type : str (likely to change in future), representing stepper type - Allowed parameters are ['explicit', 'symplectic'] - n_elems : int, number of rod elements - vector_states : np.ndarray of shape (dim, *) with the following structure - `vector_states` = [`position`,`velocity`,`omega`,`acceleration`,`angular acceleration`] - `n_nodes = n_elems + 1` - `position = 0 -> n_nodes , size = n_nodes` - `velocity = n_nodes -> 2 * n_nodes, size = n_nodes` - `omega = 2 * n_nodes -> 2 * n_nodes + nelem, size = nelem` - `acceleration = 2 * n_nodes + nelem -> 3 * n_nodes + nelem, size = n_nodes` - `angular acceleration = 3 * n_nodes + nelem -> 3 * n_nodes + 2 * nelem, size = n_elems` - matrix_states : np.ndarray of shape (dim, dim, n_elems) containing the directors - - Returns - ------- - output : tuple of len 8 containing - (state, derivative_state, position, directors, velocity, omega, acceleration, alpha) - derivative_state carries rate information - - """ - n_nodes = n_elems + 1 - position = np.ndarray.view(vector_states[..., :n_nodes]) - directors = np.ndarray.view(matrix_states) - v_w_dvdt_dwdt = np.ndarray.view(vector_states[..., n_nodes:]) - output: tuple = () - if stepper_type == "explicit": - v_w_states = np.ndarray.view(vector_states[..., n_nodes : 3 * n_nodes - 1]) - output += ( - _State(n_elems, position, directors, v_w_states), - _DerivativeState(n_elems, v_w_dvdt_dwdt), - ) - elif stepper_type == "symplectic": - # TODO: Consider removing. - # output += ( - # _KinematicState(n_elems, position, directors), - # _DynamicState(n_elems, v_w_dvdt_dwdt), - # ) - raise NotImplementedError - else: - return None - - n_velocity_end = n_nodes + n_nodes - velocity = np.ndarray.view(vector_states[..., n_nodes:n_velocity_end]) - - n_omega_end = n_velocity_end + n_elems - omega = np.ndarray.view(vector_states[..., n_velocity_end:n_omega_end]) - - n_acceleration_end = n_omega_end + n_nodes - acceleration = np.ndarray.view(vector_states[..., n_omega_end:n_acceleration_end]) - - n_alpha_end = n_acceleration_end + n_elems - alpha = np.ndarray.view(vector_states[..., n_acceleration_end:n_alpha_end]) - - return output + (position, directors, velocity, omega, acceleration, alpha) - - -""" -Explicit stepper interface -""" - - -class _State: - """State for explicit steppers. - - Wraps data as state, with overloaded methods for explicit steppers - (steppers that integrate all states in one-step/stage). - Allows for separating implementation of stepper from actual - addition/multiplication/other formulae used. - """ - - # TODO : args, kwargs instead of hardcoding types - def __init__( - self, - n_elems: int, - position_collection_view: NDArray[np.float64], - director_collection_view: NDArray[np.float64], - kinematic_rate_collection_view: NDArray[np.float64], - ) -> None: - """ - Parameters - ---------- - n_elems : int, number of rod elements - position_collection_view : view of positions (or) x - director_collection_view : view of directors (or) Q - kinematic_rate_collection_view : view of velocity and omega (or) (v,ω) - """ - super(_State, self).__init__() - self.n_nodes = n_elems + 1 - self.n_kinematic_rates = self.n_nodes + n_elems # start of (v,ω) in (x,Q,v,ω) - self.position_collection = position_collection_view - self.director_collection = director_collection_view - self.kinematic_rate_collection = kinematic_rate_collection_view - - def __iadd__(self, scaled_deriv_array: NDArray[np.float64]) -> Self: - """overloaded += operator - - The add for directors is customized to reflect Rodrigues' rotation - formula. - - Parameters - ---------- - scaled_deriv_array : np.ndarray containing dt * (v, ω, dv/dt, dω/dt) - ,as returned from _DerivativeState's __mul__ method - - Returns - ------- - self : _State with inplace modified data - - """ - # x += v*dt - self.position_collection += scaled_deriv_array[..., : self.n_nodes] - # TODO : Verify the math in this note - r""" - Developer Note - -------------- - Here the overloaded `+=` operator is exploited to perform - matrix multiplication for the directors, which is counter- - intutive at first. While this provides a stable interface - to interact the rod states with the timesteppers and the - rest of the world, the reasons behind including it here also has - a depper mathematical significance. - - Firstly, position lies in the vector space corresponding to R^{3} - and update is done this space (with the + and * operators defined - as usual), hence the `+=` operator (or `__iadd__`) is reflected - as `+=` operator in the position update (line 163 above). - - For directors rather, which lie in a restricteed R^{3} \otimes - R^{3} tensorial space, the space with Q^T.Q = Q.Q^T = I, the + - operator can be thought of as an equivalent `*=` update for a - 'exponential' multiplication with a rotation matrix (e^{At}). - . This does not correspond to the position update. However, if - we view this in a logarithmic space the `*=` becomse the '+=' - operator once again! After performing this `+=` operation, we - bring it back into its original space using the exponential - operator. So we are still indirectly doing the '+=' - update. - - To avoid all this hassle with the operators and spaces, we simply define - '+=' or '__iadd__' in the case of directors as an equivalent - '*=' (matrix multiply) with the RHS below. - """ - # TODO Q *= exp(w*dt) , whats' the formua again? - # TODO the scale factor 1.0 does not seem to be necessary, although - # we perform more work in the present framework (muliply dt to entire vector, then take - # norm) rather than vector norm then multiple by dt (1/3 operation costs) - # TODO optimize (somehow) extra copy away : if we don't make a copy - # its even more slower, maybe due to aliasing effects - np.einsum( - "ijk,jlk->ilk", - _get_rotation_matrix( - 1.0, scaled_deriv_array[..., self.n_nodes : self.n_kinematic_rates] - ), - self.director_collection.copy(), - out=self.director_collection, - ) - # (v,ω) += (dv/dt, dω/dt)*dt - self.kinematic_rate_collection += scaled_deriv_array[ - ..., self.n_kinematic_rates : - ] - return self - - def __add__(self, scaled_derivative_state: NDArray[np.float64]) -> "_State": - """overloaded + operator, useful in state.k1 = state + dt * deriv_state - - The add for directors is customized to reflect Rodrigues' rotation - formula. - - Parameters - ---------- - scaled_derivative_state : np.ndarray with dt * (v, ω, dv/dt, dω/dt) - ,as returned from _DerivativeState's __mul__ method - - Returns - ------- - state : new _State object with modified data (copied) - - Caveats - ------- - Note that the argument is not a `other` _State object but is rather - assumed to be a `np.ndarray` from calling _DerivativeState's __mul__ - method. This reflects the most common use-case in time-steppers - - """ - # x += v*dt - position_collection = ( - self.position_collection + scaled_derivative_state[..., : self.n_nodes] - ) - # Devs : see `_State.__iadd__` for reasons why we do matmul here - director_collection = _rotate( - self.director_collection, - 1.0, - scaled_derivative_state[..., self.n_nodes : self.n_kinematic_rates], - ) - # (v,ω) += (dv/dt, dω/dt)*dt - kinematic_rate_collection = ( - self.kinematic_rate_collection - + scaled_derivative_state[..., self.n_kinematic_rates :] - ) - return _State( - self.n_nodes - 1, - position_collection, - director_collection, - kinematic_rate_collection, - ) - - -class _DerivativeState: - """TimeDerivative of States for explicit steppers. - - Wraps time-derivative data as state, with overloaded methods for - explicit steppers (steppers that integrate all states in one-step/stage). - Allows for separating implementation of stepper from actual addition - /multiplication used. - """ - - def __init__( - self, _unused_n_elems: int, rate_collection_view: NDArray[np.float64] - ) -> None: - """ - Parameters - ---------- - _unused_n_elems : int, number of elements (unused, kept for - compatibility with `_bootstrap_from_data`) - rate_collection_view : np.ndarray containing (v, ω, dv/dt, dω/dt) - """ - super(_DerivativeState, self).__init__() - self.rate_collection = rate_collection_view - - def __rmul__(self, scalar: np.float64) -> NDArray[np.float64]: # type: ignore - """overloaded scalar * self, - - Parameters - ---------- - scalar : float, typically dt (the time-step) - - Returns - ------- - output : np.ndarray containing (v*dt, ω*dt, dv/dt*dt, dω/dt*dt) - - Caveats - ------- - Returns a np.ndarray and not a State object (as one expects). - Returning a State here with (v*dt, ω*dt, dv/dt*dt, dω/dt*dt) as members - is possible but it's less efficient, especially because this is hot - piece of code - """ - """ - Developer Note - -------------- - - Q : Why do we need to overload operators here? - - The Derivative class naturally doesn't have a `mul` overloaded - operator. That means if this method is not present, - doing something like - ``` - ds = _DerivativeState(...) - new_state = 2 * ds - ``` - will throw an error. Note that you can do something like - ``` - ds = _DerivativeState(...) - new_state = 2 * ds.rate_collection - ``` - but this is hacky, as we are exposing the members outside, - in the calling scope (defeats encapsulation and hiding). - The point of having this class is that it works - well with the time-stepper (where we only use `+` and `*` - operations on the State/DerivativeState like above, - i.e. `state = dt * derivative_state` and not something like - `state = dt * derivative_state.rate_collection`). - It also provides an interface for anything outside - the `Rod` system as a whole. - """ - return scalar * self.rate_collection - - def __mul__(self, scalar: np.float64) -> NDArray[np.float64]: - """overloaded self * scalar - - TODO Check if this pattern (forwarding to __mul__) has - any disdvantages apart from extra function call penalty - - Parameters - ---------- - scalar : float, typically dt (the time-step) - - Returns - ------- - output : np.ndarray containing (v*dt, ω*dt, dv/dt*dt, dω/dt*dt) - - """ - return self.__rmul__(scalar) - - """ Symplectic stepper interface """ diff --git a/tests/test_math/test_timestepper.py b/tests/test_math/test_timestepper.py index ace4a22b..3f07b988 100644 --- a/tests/test_math/test_timestepper.py +++ b/tests/test_math/test_timestepper.py @@ -4,11 +4,6 @@ from numpy.testing import assert_allclose from elastica.timestepper import integrate, extend_stepper_interface -from elastica.experimental.timestepper.explicit_steppers import ( - RungeKutta4, - EulerForward, - ExplicitStepperMixin, -) from elastica.timestepper.symplectic_steppers import ( PositionVerlet, PEFRL, @@ -49,20 +44,6 @@ def _kinematic_step(self): def _dynamic_step(self): pass - class MockExplicitStepper(ExplicitStepperMixin): - - def get_stages(self): - return [self._stage] - - def get_updates(self): - return [self._update] - - def _stage(self): - pass - - def _update(self): - pass - # We cannot call a stepper on a system until both the stepper # and system "see" one another (for performance reasons, mostly) # So before "seeing" the system, the stepper should not have @@ -72,7 +53,6 @@ def _update(self): "stepper_module", [ MockSymplecticStepper, - MockExplicitStepper, ], ) def test_symplectic_stepper_interface_for_simple_systems(self, stepper_module): @@ -86,7 +66,7 @@ def test_symplectic_stepper_interface_for_simple_systems(self, stepper_module): @pytest.mark.parametrize( "stepper_module", - [MockSymplecticStepper, MockExplicitStepper], + [MockSymplecticStepper], ) def test_symplectic_stepper_interface_for_collective_systems(self, stepper_module): system = SymplecticUndampedHarmonicOscillatorCollectiveSystem() @@ -122,11 +102,7 @@ def test_integrate_throws_an_assert_for_negative_total_steps(rng): assert "steps is negative" in str(excinfo.value) -# Added automatic discovery of Stateful explicit integrators -# ExplicitSteppers = StatefulExplicitStepper.__subclasses__() # SymplecticSteppers = SymplecticStepper.__subclasses__() -# StatefulExplicitSteppers = [StatefulRungeKutta4, StatefulEulerForward] -ExplicitSteppers = [EulerForward, RungeKutta4] SymplecticSteppers = [PositionVerlet, PEFRL] @@ -148,59 +124,6 @@ def test_symplectic_steppers(self, symplectic_stepper): atol=Tolerance.atol(), ) - @pytest.mark.parametrize("explicit_stepper", ExplicitSteppers) - def test_explicit_steppers(self, explicit_stepper): - collective_system = ScalarExponentialDampedHarmonicOscillatorCollectiveSystem() - final_time = 1.0 - if explicit_stepper == EulerForward: - # Euler requires very small time-steps and in order not to slow down test, - # we are scaling the difference between analytical and numerical solution. - n_steps = 25000 - scale = 1e3 - else: - n_steps = 500 - scale = 1 - - stepper = explicit_stepper() - - dt = np.float64(float(final_time) / n_steps) - time = np.float64(0.0) - tol = Tolerance.atol() - - # Before stepping, let's extend the interface of the stepper - # while providing memory slots - from elastica.experimental.timestepper.memory import ( - make_memory_for_explicit_stepper, - ) - - memory_collection = make_memory_for_explicit_stepper(stepper, collective_system) - from elastica.timestepper import extend_stepper_interface - - do_step, stagets_and_updates = extend_stepper_interface( - stepper, collective_system - ) - - while np.abs(final_time - time) > 1e5 * tol: - time = do_step( - stepper, - stagets_and_updates, - collective_system, - memory_collection, - time, - dt, - ) - - for system in collective_system: - assert_allclose( - system.state, - system.analytical_solution(final_time), - rtol=Tolerance.rtol() * scale, - atol=Tolerance.atol() * scale, - ) - - # @pytest.mark.parametrize("symplectic_stepper", SymplecticSteppers) - # def test_symplectic_against_collective_system(self, symplectic_stepper): - class TestSteppersAgainstRodLikeSystems: """The rods compose specific data-structures that From bc1ca8f337544c738f37214242f141c774452173 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Thu, 18 Dec 2025 16:16:59 -0600 Subject: [PATCH 69/85] update default parameter handling for force classes to avoid mutable defaults --- elastica/external_forces.py | 49 ++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/elastica/external_forces.py b/elastica/external_forces.py index 74d7c6c9..54896a04 100644 --- a/elastica/external_forces.py +++ b/elastica/external_forces.py @@ -86,9 +86,7 @@ class GravityForces(NoForces): def __init__( self, - acc_gravity: NDArray[np.float64] = np.array( - [0.0, -9.80665, 0.0] - ), # FIXME: avoid mutable default + acc_gravity: NDArray[np.float64] | None = None, ) -> None: """ @@ -96,10 +94,14 @@ def __init__( ---------- acc_gravity: numpy.ndarray 1D (dim) array containing data with 'float' type. Gravitational acceleration vector. + Defaults to [0.0, -9.80665, 0.0] if not provided. """ - super(GravityForces, self).__init__() - self.acc_gravity = acc_gravity + super().__init__() + if acc_gravity is None: + acc_gravity = np.array([0.0, -9.80665, 0.0]) + assert len(acc_gravity) == 3, "Gravity acceleration vector must be 3D" + self.acc_gravity = np.array(acc_gravity) def apply_forces( self, system: "RodType | RigidBodyType", time: np.float64 = np.float64(0.0) @@ -228,9 +230,7 @@ class UniformTorques(NoForces): def __init__( self, torque: np.float64, - direction: NDArray[np.float64] = np.array( - [0.0, 0.0, 0.0] - ), # FIXME: avoid mutable default + direction: NDArray[np.float64] | None = None, ) -> None: """ @@ -240,9 +240,11 @@ def __init__( Torque magnitude applied to a rod-like object. direction: numpy.ndarray 1D (dim) array containing data with 'float' type. - Direction in which torque applied. + Direction in which torque applied. Defaults to [0.0, 0.0, 0.0] if not provided. """ super(UniformTorques, self).__init__() + if direction is None: + direction = np.array([0.0, 0.0, 0.0]) self.torque = torque * direction def apply_torques( @@ -270,9 +272,7 @@ class UniformForces(NoForces): def __init__( self, force: np.float64, - direction: NDArray[np.float64] = np.array( - [0.0, 0.0, 0.0] - ), # FIXME: avoid mutable default + direction: NDArray[np.float64] | None = None, ) -> None: """ @@ -282,9 +282,11 @@ def __init__( Force magnitude applied to a rod-like object. direction: numpy.ndarray 1D (dim) array containing data with 'float' type. - Direction in which force applied. + Direction in which force applied. Defaults to [0.0, 0.0, 0.0] if not provided. """ super(UniformForces, self).__init__() + if direction is None: + direction = np.array([0.0, 0.0, 0.0]) self.force = (force * direction).reshape(3, 1) def apply_forces( @@ -325,7 +327,7 @@ class MuscleTorques(NoForces): def __init__( self, - base_length: float, # TODO: Is this necessary? + base_length: float, b_coeff: NDArray[np.float64], period: float, wave_number: float, @@ -340,7 +342,9 @@ def __init__( Parameters ---------- base_length: float - Rest length of the rod-like object. + Rest length of the rod-like object. This parameter is used to + normalize the spatial coordinate along the rod for the traveling + wave calculation. b_coeff: numpy.ndarray 1D array containing data with 'float' type. Beta coefficients for beta-spline. @@ -522,12 +526,8 @@ def __init__( start_force_mag: float, end_force_mag: float, ramp_up_time: float = 0.0, - tangent_direction: NDArray[np.floating] = np.array( - [0.0, 0.0, 1.0] - ), # FIXME: avoid mutable default - normal_direction: NDArray[np.floating] = np.array( - [0.0, 1.0, 0.0] - ), # FIXME: avoid mutable default + tangent_direction: NDArray[np.floating] | None = None, + normal_direction: NDArray[np.floating] | None = None, ) -> None: """ Parameters @@ -541,9 +541,10 @@ def __init__( tangent_direction: numpy.ndarray 1D (3,) array containing data with 'float' type. This is the tangent direction of the system, or normal of the plane that forces applied. + Defaults to [0.0, 0.0, 1.0] if not provided. normal_direction: numpy.ndarray 1D (3,) array containing data with 'float' type. - This is the normal direction of the system. + This is the normal direction of the system. Defaults to [0.0, 1.0, 0.0] if not provided. """ super(EndpointForcesSinusoidal, self).__init__() # Start force @@ -551,6 +552,10 @@ def __init__( self.end_force_mag = np.float64(end_force_mag) # Applied force directions + if normal_direction is None: + normal_direction = np.array([0.0, 1.0, 0.0]) + if tangent_direction is None: + tangent_direction = np.array([0.0, 0.0, 1.0]) self.normal_direction = normal_direction self.roll_direction = np.cross(normal_direction, tangent_direction) From 4126edddea4bb73971aba0aec73764085779e12a Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Thu, 18 Dec 2025 16:18:31 -0600 Subject: [PATCH 70/85] refactor: enhance error handling and type annotations in boundary_conditions and contact_forces modules --- elastica/boundary_conditions.py | 14 +++++++------- elastica/contact_forces.py | 6 +++--- elastica/joint.py | 1 - elastica/transformations.py | 2 -- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/elastica/boundary_conditions.py b/elastica/boundary_conditions.py index 435826d4..f5cbbc3f 100644 --- a/elastica/boundary_conditions.py +++ b/elastica/boundary_conditions.py @@ -284,17 +284,17 @@ def __init__( """ super().__init__(**kwargs) pos, dir = [], [] - for data in fixed_data: + for idx, data in enumerate(fixed_data): if isinstance(data, np.ndarray) and data.shape == (3,): pos.append(data) - elif isinstance(data, np.ndarray) and data.shape == ( - 3, - 3, - ): + elif isinstance(data, np.ndarray) and data.shape == (3, 3): dir.append(data) else: - # TODO: This part is prone to error. - break + raise ValueError( + f"Invalid data at position {idx} in fixed_data. " + f"Expected numpy array with shape (3,) for position or (3, 3) for director, " + f"but got {type(data).__name__} with value: {data}." + ) if len(pos) > 0: # transpose from (blocksize, dim) to (dim, blocksize) diff --git a/elastica/contact_forces.py b/elastica/contact_forces.py index c8f2fe62..3ddad53e 100644 --- a/elastica/contact_forces.py +++ b/elastica/contact_forces.py @@ -3,7 +3,7 @@ """ from typing import TypeVar, Generic, Type -from elastica.typing import RodType, SystemType +from elastica.typing import RodType, SystemType, StaticSystemType from elastica.rod.rod_base import RodBase from elastica.rigidbody.cylinder import Cylinder @@ -27,8 +27,8 @@ from numpy.typing import NDArray -S1 = TypeVar("S1") # TODO: Find bound -S2 = TypeVar("S2") +S1 = TypeVar("S1", bound=StaticSystemType) +S2 = TypeVar("S2", bound=StaticSystemType) class NoContact(Generic[S1, S2]): diff --git a/elastica/joint.py b/elastica/joint.py index 2b147aee..1892d99f 100644 --- a/elastica/joint.py +++ b/elastica/joint.py @@ -194,7 +194,6 @@ class HingeJoint(FreeJoint): 2D (dim, 1) array containing data with 'float' type. Constraint rotation direction. """ - # TODO: IN WRAPPER COMPUTE THE NORMAL DIRECTION OR ASK USER TO GIVE INPUT, IF NOT THROW ERROR def __init__( self, k: float, diff --git a/elastica/transformations.py b/elastica/transformations.py index 3a2e10b5..f98edf4a 100644 --- a/elastica/transformations.py +++ b/elastica/transformations.py @@ -12,8 +12,6 @@ from numpy.typing import NDArray -# TODO Complete, but nicer interface, evolve it eventually - def format_vector_shape( vector_collection: NDArray[np.float64], From 46a573cb11ebd9c79be19d1f5f8c0e7ad3a10508 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Thu, 18 Dec 2025 16:21:57 -0600 Subject: [PATCH 71/85] docs: rotation and damping modules --- elastica/_rotations.py | 151 ++++++++++++++++------ elastica/dissipation.py | 74 +++++++---- elastica/memory_block/memory_block_rod.py | 73 ++++++++++- 3 files changed, 231 insertions(+), 67 deletions(-) diff --git a/elastica/_rotations.py b/elastica/_rotations.py index 0e697c63..0feddabb 100644 --- a/elastica/_rotations.py +++ b/elastica/_rotations.py @@ -17,6 +17,29 @@ def _get_rotation_matrix( scale: np.float64, axis_collection: NDArray[np.float64] ) -> NDArray[np.float64]: + """ + Compute rotation matrices from axis-angle representation using Rodrigues' formula. + + Parameters + ---------- + scale : float + Scale factor applied to rotation angles. The actual rotation angle for each + axis is scale * ||axis||. + axis_collection : numpy.ndarray + 2D array of shape (dim, blocksize) containing rotation axes. Each column + represents an axis of rotation. + + Returns + ------- + rot_mat : numpy.ndarray + 3D array of shape (dim, dim, blocksize) containing rotation matrices computed + using Rodrigues' rotation formula. + + Notes + ----- + The axes are normalized before computing the rotation matrices. A small epsilon + (1e-14) is added to prevent division by zero for zero-length axes. + """ blocksize = axis_collection.shape[1] rot_mat = np.empty((3, 3, blocksize)) @@ -56,19 +79,42 @@ def _rotate( axis_collection: NDArray[np.float64], ) -> NDArray[np.float64]: """ - Does alibi rotations - https://en.wikipedia.org/wiki/Rotation_matrix#Ambiguities + Rotate director collection by specified axes and scale (alibi rotation). + + Performs alibi (active) rotations on a collection of director frames. + Each director frame is rotated around its corresponding axis by an angle + proportional to the scale factor. The rotation is applied using Rodrigues' + rotation formula via `_get_rotation_matrix`. Parameters ---------- - director_collection - scale - axis_collection + director_collection : numpy.ndarray + 3D array of shape (dim, dim, blocksize) containing rotation matrices + (director frames) to be rotated. + scale : float + Scale factor for rotation angles. The actual rotation angle for each + frame is scale * ||axis||, where ||axis|| is the magnitude of the + corresponding axis vector. + axis_collection : numpy.ndarray + 2D array of shape (dim, blocksize) containing rotation axes for each + director frame. Each column represents the axis of rotation for the + corresponding director frame. Returns ------- + rotated_directors : numpy.ndarray + 3D array of shape (dim, dim, blocksize) containing the rotated director + frames. Each frame is rotated around its corresponding axis by the + scaled angle. - TODO Finish documentation + Notes + ----- + This function performs alibi (active) rotations, meaning the coordinate + system is rotated. For more information on rotation matrix ambiguities, see: + https://en.wikipedia.org/wiki/Rotation_matrix#Ambiguities + + The rotation is computed as: R(scale * axis) @ director, where R is the + rotation matrix computed from the axis-angle representation. """ # return _batch_matmul( # director_collection, _get_rotation_matrix(scale, axis_collection) @@ -81,22 +127,30 @@ def _rotate( @njit(cache=True) # type: ignore def _inv_rotate(director_collection: NDArray[np.float64]) -> NDArray[np.float64]: """ - Calculated rate of change using Rodrigues' formula + Compute rotation axes between consecutive director frames using Rodrigues' formula. + + Calculates the rotation axis (in axis-angle representation) that transforms + each director frame to the next one. This is the inverse operation of rotating + directors and is used to extract the relative rotation between consecutive + elements. Parameters ---------- - director_collection : The collection of frames/directors at every element, - numpy.ndarray of shape (dim, dim, n) + director_collection : numpy.ndarray + The collection of frames/directors at every element, of shape (dim, dim, n) + where n is the number of director frames. Returns ------- - vector_collection : The collection of axes around which the body rotates - numpy.ndarray of shape (dim, n) - - Note - ---- - TODO: Benchmark missing + vector_collection : numpy.ndarray + The collection of rotation axes, of shape (dim, n-1). Each column represents + the axis of rotation (scaled by angle) that transforms director[k] to director[k+1]. + Notes + ----- + The output has n-1 elements because it computes the relative rotation between + consecutive pairs of directors. The rotation axis is computed using the trace + of the relative rotation matrix Q_{k+1} @ Q_k^T. """ blocksize = director_collection.shape[2] - 1 vector_collection = np.empty((3, blocksize)) @@ -171,7 +225,22 @@ def _inv_rotate(director_collection: NDArray[np.float64]) -> NDArray[np.float64] # TODO: Below contains numpy-only implementations @functools.lru_cache(maxsize=1) def _generate_skew_map(dim: int) -> list[tuple[int, int, int]]: - # TODO Documentation + """ + Generate mapping indices for converting vectors to skew-symmetric matrices. + + Creates a mapping that defines how vector elements are arranged in a + flattened skew-symmetric matrix representation. This is used for efficient + conversion between vector and matrix forms in dimension-agnostic operations. + + Notes + ----- + The mapping handles the conversion from a vector v = [x, y, z] to a + skew-symmetric matrix M where M[i,j] = -M[j,i] and the off-diagonal + elements correspond to vector components. + + The formula used (dim - (i + j)) works correctly for dimensions 2 and 3, + but may need verification for higher dimensions. + """ # Preallocate mapping_list = [_generate_skew_map_sentinel] * ((dim**2 - dim) // 2) # Indexing (i,j), j is the fastest changing @@ -221,7 +290,19 @@ def _get_skew_map(dim: int) -> tuple[tuple[int, int, int], ...]: @functools.lru_cache(maxsize=1) def _get_inv_skew_map(dim: int) -> tuple[tuple[int, int, int], ...]: - # TODO Documentation + """ + Generate inverse mapping for extracting vectors from skew-symmetric matrices. + + Creates a mapping that defines how to extract vector elements from a + flattened skew-symmetric matrix representation. This is the inverse + operation of `_generate_skew_map`. + + Notes + ----- + This mapping is used to extract vector components from skew-symmetric + matrices. The mapping is generated by inverting the tuple element order + from `_generate_skew_map`. + """ # (vec_src, mat_i, mat_j, sign) mapping_list = _generate_skew_map(dim) @@ -248,15 +329,7 @@ def _get_diag_map(dim: int) -> tuple[int, ...]: def _skew_symmetrize(vector: NDArray[np.float64]) -> NDArray[np.float64]: """ - - Parameters - ---------- - vector : numpy.ndarray of shape (dim, blocksize) - - Returns - ------- - output : numpy.ndarray of shape (dim*dim, blocksize) corresponding to - [0, -z, y, z, 0, -x, -y , x, 0] + Convert vector collection to skew-symmetric matrix collection. Notes ----- @@ -283,16 +356,24 @@ def _skew_symmetrize(vector: NDArray[np.float64]) -> NDArray[np.float64]: # While calculating u^2, use u with einsum instead, as it is tad bit faster def _skew_symmetrize_sq(vector: NDArray[np.float64]) -> NDArray[np.float64]: """ - Generate the square of an orthogonal matrix from vector elements + Generate the square of a skew-symmetric matrix from vector elements. + + Computes u^2 where u is the skew-symmetric matrix corresponding to the input + vector. This is used in Rodrigues' rotation formula. Parameters ---------- - vector : numpy.ndarray of shape (dim, blocksize) + vector : numpy.ndarray + Input vector collection of shape (dim, blocksize). Returns ------- - output : numpy.ndarray of shape (dim*dim, blocksize) corresponding to - [-(y^2+z^2), xy, xz, yx, -(x^2+z^2), yz, zx, zy, -(x^2+y^2)] + output : numpy.ndarray + Square of skew-symmetric matrices of shape (dim, dim, blocksize). + For a 3D vector [x, y, z], the corresponding matrix u^2 is: + [[-(y^2+z^2), xy, xz], + [yx, -(x^2+z^2), yz], + [zx, zy, -(x^2+y^2)]] Note ---- @@ -343,13 +424,11 @@ def _get_skew_symmetric_pair( vector_collection: NDArray[np.float64], ) -> tuple[NDArray[np.float64], NDArray[np.float64]]: """ + Compute both the skew-symmetric matrix and its square from vector collection. - Parameters - ---------- - vector_collection - - Returns - ------- + This is a convenience function that computes both u and u^2 where u is the + skew-symmetric matrix corresponding to the input vectors. These are commonly + used together in Rodrigues' rotation formula. """ u = _skew_symmetrize(vector_collection) diff --git a/elastica/dissipation.py b/elastica/dissipation.py index cc246f61..3d586105 100644 --- a/elastica/dissipation.py +++ b/elastica/dissipation.py @@ -29,13 +29,35 @@ class DamperBase(Generic[T], ABC): Attributes ---------- system : RodBase + """ _system: T - # TODO typing can be made better def __init__(self, *args: Any, **kwargs: Any) -> None: - """Initialize damping module""" + """Initialize damping module + + Parameters + ---------- + *args : Any + Positional arguments (not currently used, reserved for future use). + **kwargs : Any + Keyword arguments. Must include '_system' key containing the system + (rod or rigid body) to be damped. Additional keyword arguments are + passed to derived classes for their specific configuration. + + Raises + ------ + KeyError + If '_system' is not provided in kwargs. This typically indicates + incorrect usage - use simulator.dampen(...).using(...) syntax instead. + + Notes + ----- + The base class extracts the '_system' parameter from kwargs. Derived + damper classes (e.g., AnalyticalLinearDamper, LaplaceDissipationFilter) + may accept additional keyword arguments for their specific configuration. + """ try: self._system = kwargs["_system"] except KeyError: @@ -115,12 +137,11 @@ class AnalyticalLinearDamper(DamperBase): 3. Damping constant: this protocol follows the original algorithm where the damping constants for translational and rotational velocities are assumed to be numerically identical. This leads to dimensional inconsistencies (see - https://github.com/GazzolaLab/PyElastica/issues/354). Hence, this option will be deprecated - in version 0.4.0. + https://github.com/GazzolaLab/PyElastica/issues/354). >>> simulator.dampen(rod).using( ... AnalyticalLinearDamper, - ... damping_constant=0.1, # To be deprecated in 0.4.0 + ... damping_constant=0.1, ... time_step = 1E-4, # Simulation time-step ... ) @@ -135,7 +156,7 @@ class AnalyticalLinearDamper(DamperBase): 1. Set a high value for `damping_constant` to first achieve a stable simulation. 2. If you feel the simulation is overdamped, reduce `damping_constant` until you - feel the simulation is underdamped, and expected dynamics are recovered. + feel the simulation is underdamped, and expected dynamics are recovered. """ def __init__(self, time_step: np.float64, **kwargs: Any) -> None: @@ -148,41 +169,44 @@ def __init__(self, time_step: np.float64, **kwargs: Any) -> None: ) rotational_damping_constant = kwargs.get("rotational_damping_constant", None) + # Count non-None parameters + provided_params = [ + p + for p in [ + damping_constant, + uniform_damping_constant, + translational_damping_constant, + rotational_damping_constant, + ] + if p is not None + ] + self._dampen_rates_protocol: DampenType - if ( - (damping_constant is not None) - and (uniform_damping_constant is None) - and (translational_damping_constant is None) - and (rotational_damping_constant is None) - ): + # Determine which protocol to use based on provided parameters + if len(provided_params) == 1 and damping_constant is not None: + # Deprecated: single damping_constant self._dampen_rates_protocol = self._deprecated_damping_protocol( damping_constant=damping_constant, time_step=time_step ) - - elif ( - (damping_constant is None) - and (uniform_damping_constant is not None) - and (translational_damping_constant is None) - and (rotational_damping_constant is None) - ): + elif len(provided_params) == 1 and uniform_damping_constant is not None: + # Uniform damping: single uniform_damping_constant self._dampen_rates_protocol = self._uniform_damping_protocol( uniform_damping_constant=uniform_damping_constant, time_step=time_step ) - elif ( - (damping_constant is None) - and (uniform_damping_constant is None) - and (translational_damping_constant is not None) - and (rotational_damping_constant is not None) + len(provided_params) == 2 + and translational_damping_constant is not None + and rotational_damping_constant is not None ): + # Physical damping: both translational and rotational constants self._dampen_rates_protocol = self._physical_damping_protocol( translational_damping_constant=translational_damping_constant, rotational_damping_constant=rotational_damping_constant, time_step=time_step, ) - else: + # Invalid parameter combination message = ( "AnalyticalLinearDamper usage:\n" "\tsimulator.dampen(rod).using(\n" diff --git a/elastica/memory_block/memory_block_rod.py b/elastica/memory_block/memory_block_rod.py index a47998ba..bbad5591 100644 --- a/elastica/memory_block/memory_block_rod.py +++ b/elastica/memory_block/memory_block_rod.py @@ -22,12 +22,73 @@ class MemoryBlockCosseratRod(CosseratRod, _RodSymplecticStepperMixin): """ - Memory block class for Cosserat rod equations. This class is derived from Cosserat Rod class in order to inherit - the methods of Cosserat rod class. This class takes the cosserat rod object (systems) and creates big - arrays to store the system data and returns a reference of that data to the systems. - Thus each system is now in contiguous memory, so it is faster to compute Cosserat rod equations. - - TODO: need more documentation! + Memory block class for Cosserat rod equations. + + This class is derived from CosseratRod to inherit all rod methods while providing + a memory-efficient block structure for multiple rod systems. It takes a collection + of CosseratRod objects and creates contiguous memory blocks to store all system data, + allowing for faster computation of Cosserat rod equations through better cache locality + and vectorized operations. + + The class separates rods into straight rods and ring rods (periodic boundary conditions), + and handles ghost nodes, elements, and Voronoi indices for proper boundary conditions. + All rod data is stored in contiguous arrays, with references maintained to the original + rod objects for compatibility. + + Parameters + ---------- + systems : list[RodType] + List of CosseratRod objects to be included in the memory block structure. + Rods are automatically separated into straight rods and ring rods based on + their `ring_rod_flag` attribute. + system_idx_list : list[SystemIdxType] + List of system indices corresponding to each rod in the `systems` list. + These indices are used to map rods back to their original positions in + the simulator's system collection. + + Attributes + ---------- + n_systems : int + Total number of rod systems in the memory block. + n_rods : int + Total number of rods (same as n_systems). + n_elems : int + Total number of elements across all rods in the block structure. + n_nodes : int + Total number of nodes across all rods (n_elems + 1). + n_voronoi : int + Total number of Voronoi points across all rods (n_elems - 1). + ring_rod_flag : bool + Flag indicating if any ring rods are present in the block. + system_idx_list : numpy.ndarray + Array of system indices mapping rods to their original positions. + ghost_nodes_idx : numpy.ndarray + Indices of ghost nodes used for boundary conditions. + ghost_elems_idx : numpy.ndarray + Indices of ghost elements used for boundary conditions. + ghost_voronoi_idx : numpy.ndarray + Indices of ghost Voronoi points used for boundary conditions. + periodic_boundary_nodes_idx : numpy.ndarray + Indices of periodic boundary nodes for ring rods. + periodic_boundary_elems_idx : numpy.ndarray + Indices of periodic boundary elements for ring rods. + periodic_boundary_voronoi_idx : numpy.ndarray + Indices of periodic boundary Voronoi points for ring rods. + + Notes + ----- + - Straight rods are placed first in memory, followed by ring rods. + - Ring rods require additional periodic boundary nodes, elements, and Voronoi points + to maintain compatibility with the block structure implementation. + - Ghost nodes/elements/Voronoi are used to handle boundaries between rods and + periodic boundaries for ring rods. + - All rod data (positions, directors, velocities, etc.) is stored in contiguous + memory blocks for efficient computation. + + See Also + -------- + CosseratRod : Base class for Cosserat rod systems + _RodSymplecticStepperMixin : Mixin providing symplectic stepper interface """ def __init__( From 27d6cb7a379d514fe20902693de5b94d5d16aeec Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Thu, 18 Dec 2025 16:44:39 -0600 Subject: [PATCH 72/85] reorganize memory block handling and enhance system protocol definitions --- elastica/__init__.py | 4 + .../memory_block/memory_block_rod_base.py | 8 - elastica/memory_block/protocol.py | 22 ++- elastica/modules/base_system.py | 104 ++++++++++-- elastica/modules/memory_block.py | 104 ++++++------ elastica/modules/protocol.py | 2 +- elastica/systems/protocol.py | 1 + tests/test_modules/test_base_system.py | 154 +++++++++++++++++- 8 files changed, 325 insertions(+), 74 deletions(-) delete mode 100644 elastica/memory_block/memory_block_rod_base.py diff --git a/elastica/__init__.py b/elastica/__init__.py index 2af0066b..79279d75 100644 --- a/elastica/__init__.py +++ b/elastica/__init__.py @@ -1,3 +1,7 @@ +from elastica.systems.protocol import ( + StaticSystemBase, + SystemProtocol, +) from elastica.rod.knot_theory import ( compute_link, compute_twist, diff --git a/elastica/memory_block/memory_block_rod_base.py b/elastica/memory_block/memory_block_rod_base.py deleted file mode 100644 index 87fcdd6d..00000000 --- a/elastica/memory_block/memory_block_rod_base.py +++ /dev/null @@ -1,8 +0,0 @@ -__doc__ = """Deprecated module. Use memory_blocks.utils instead.""" -import numpy as np -import numpy.typing as npt - -from .utils import ( - make_block_memory_metadata, - make_block_memory_periodic_boundary_metadata, -) diff --git a/elastica/memory_block/protocol.py b/elastica/memory_block/protocol.py index 87bfe554..4b527f77 100644 --- a/elastica/memory_block/protocol.py +++ b/elastica/memory_block/protocol.py @@ -1,12 +1,22 @@ -from typing import Protocol +from typing import Protocol, runtime_checkable +from elastica.typing import StaticSystemType, SystemIdxType from elastica.systems.protocol import SystemProtocol -class BlockProtocol(Protocol): +@runtime_checkable +class BlockSystemProtocol(SystemProtocol, Protocol): + """ + Protocol for block systems. + Block systems are systems that are used to store the data of multiple systems. + """ + + def __init__( + self, systems: list[StaticSystemType], system_idx_list: list[SystemIdxType] + ) -> None: + """ + Block initializer takes the list of systems and the list of system indices. + """ + @property def n_systems(self) -> int: """Number of systems in the block.""" - - -class BlockSystemProtocol(SystemProtocol, BlockProtocol, Protocol): - pass diff --git a/elastica/modules/base_system.py b/elastica/modules/base_system.py index a99c7ad1..1b7e2089 100644 --- a/elastica/modules/base_system.py +++ b/elastica/modules/base_system.py @@ -18,16 +18,24 @@ ) import numpy as np +import warnings from itertools import chain +from collections import defaultdict from collections.abc import MutableSequence from elastica.systems.protocol import StaticSystemBase, SystemProtocol -from elastica.memory_block.protocol import BlockSystemProtocol # noqa: F811 +from elastica.memory_block.protocol import BlockSystemProtocol + +from elastica.memory_block.memory_block_rod import MemoryBlockCosseratRod +from elastica.memory_block.memory_block_rigid_body import MemoryBlockRigidBody from .memory_block import construct_memory_block_structures from .operator_group import OperatorGroupFIFO from .protocol import ModuleProtocol +from ..rod.cosserat_rod import CosseratRod +from ..rigidbody.sphere import Sphere +from ..rigidbody.cylinder import Cylinder class BaseSystemCollection(MutableSequence): @@ -79,11 +87,22 @@ def __init__(self) -> None: super().__init__() # List of system types/bases that are allowed + # By default, any object that is a subclass of StaticSystemBase is allowed. + # (Technically, any object that is conforms StaticSystemProtocol is allowed.) self.allowed_sys_types: tuple[Type, ...] = (StaticSystemBase,) + # Block support for System types. + # If a system type is not in this dictionary, no block will be constructed for it. + # (Note, block support is defined explicitly, without derivation from BaseSystem.) + self._block_supports: dict[Type[BlockSystemType], list[Type[SystemType]]] = ( + defaultdict(list) + ) + self._block_supports[MemoryBlockCosseratRod].append(CosseratRod) + self._block_supports[MemoryBlockRigidBody].extend([Sphere, Cylinder]) + # List of systems to be integrated self.__systems: list[StaticSystemType] = [] - self.__final_blocks: list[BlockSystemType] = [] + self.__final_systems: list[SystemType] = [] # Flag Finalize: Finalizing twice will cause an error, # but the error message is very misleading @@ -110,6 +129,13 @@ def _check_type(self, sys_to_be_added: Any) -> bool: f"The system {sys_to_be_added.__class__} requires the following modules:\n" f"{sys_to_be_added.REQUISITE_MODULES}\n" ) + if id(sys_to_be_added) in [id(system) for system in self.__systems]: + # Warning for duplicate system instance + warnings.warn( + f"System {sys_to_be_added.__class__} is already in the system collection.\n" + "Adding multiple instance is technically allowed, but it is not recommended.", + UserWarning, + ) return True def __len__(self) -> int: @@ -139,18 +165,65 @@ def __str__(self) -> str: """To be readable""" return str(self.__systems) + @final + def append_allowed_types(self, additional_types: Type[SystemType]) -> None: + """ + Append the allowed system types. + In order to add block support, use `enable_block_supports`. + """ + self.allowed_sys_types += (additional_types,) + @final def extend_allowed_types( self, additional_types: tuple[Type[SystemType], ...] ) -> None: + """ + Extend the allowed system types. Typically used for building custom extensions. + In order to add block support, use `enable_block_supports`. + """ self.allowed_sys_types += additional_types @final - def override_allowed_types( + def _override_allowed_types( self, allowed_types: tuple[Type[SystemType], ...] ) -> None: + """ + Override the allowed system types. + Only used for testing purposes. + """ self.allowed_sys_types = allowed_types + @final + def enable_block_supports( + self, + system_type: Type[SystemType], + block_type: Type[BlockSystemType], + ) -> None: + """ + Enable block support for a system type. + If the system type already has block support enabled, it will be overridden. + (In case user wants different implementation of the memory block.) + + + Parameters + ---------- + system_type: Type[SystemType] + System type to enable block support for. + block_type: Type[BlockSystemType] + Block type to enable for the system type. + + Examples + -------- + >>> simulator.append_allowed_types(CustomRod) + >>> simulator.enable_block_supports(CustomRod, CustomMemoryBlock) + """ + for btype in self._block_supports: + if system_type in self._block_supports[btype]: + self._block_supports[btype].append(system_type) + break + else: + self._block_supports[block_type] = [system_type] + @final def get_system_index( self, system: "SystemType | StaticSystemType" @@ -209,11 +282,12 @@ def systems(self) -> Generator[StaticSystemType, None, None]: yield system @final - def block_systems(self) -> Generator[BlockSystemType, None, None]: + def final_systems(self) -> Generator[SystemType, None, None]: """ - Iterate over all block systems in the system collection. + Iterate over all systems in the system collection. + This generator is used to pass the systems to the timestepper. """ - for block in self.__final_blocks: + for block in self.__final_systems: yield block @final @@ -223,16 +297,26 @@ def finalize(self) -> None: all rod-like objects to the simulator as well as all boundary conditions, callbacks, etc., acting on these rod-like objects. After the finalize method called, the user cannot add new features to the simulator class. + + Parameters + ---------- + verbose: bool + If True, will print verbose output. """ assert not self._finalize_flag, "The finalize cannot be called twice." self._finalize_flag = True # Construct memory block - self.__final_blocks = construct_memory_block_structures(self.__systems) - # FIXME: We need this to make ring-rod working. - # But probably need to be refactored - self.__systems.extend(self.__final_blocks) + blocks, non_blocked_systems = construct_memory_block_structures( + self.__systems, + self._block_supports, + ) + self.__systems.extend(blocks) # blocks are also systems + + # Finalize the list of systems to run stepping. + self.__final_systems.extend(blocks) + self.__final_systems.extend(non_blocked_systems) # Recurrent call finalize functions for all components. for finalize in self._feature_group_finalize: diff --git a/elastica/modules/memory_block.py b/elastica/modules/memory_block.py index 10d1e7a0..9936214e 100644 --- a/elastica/modules/memory_block.py +++ b/elastica/modules/memory_block.py @@ -3,64 +3,76 @@ Cosserat Rods, Rigid Body etc. """ -from elastica.typing import ( - RodType, - RigidBodyType, - StaticSystemType, - SystemIdxType, - BlockSystemType, -) +from typing import Type, TYPE_CHECKING +from collections import defaultdict +from elastica.systems.protocol import SystemProtocol -from elastica.rod.rod_base import RodBase -from elastica.rigidbody.rigid_body_base import RigidBodyBase -from elastica.memory_block.memory_block_rod import MemoryBlockCosseratRod -from elastica.memory_block.memory_block_rigid_body import MemoryBlockRigidBody +if TYPE_CHECKING: + from elastica.typing import ( + SystemType, + StaticSystemType, + SystemIdxType, + BlockSystemType, + ) def construct_memory_block_structures( - systems: list[StaticSystemType], -) -> list[BlockSystemType]: + systems: list["StaticSystemType"], + block_supports: dict[Type["BlockSystemType"], list[Type["SystemType"]]], +) -> tuple[list["BlockSystemType"], list["SystemType"]]: """ - This function takes the systems (rod or rigid body) appended to the simulator class and - separates them into lists depending on if system is Cosserat rod or rigid body. Then using - these separated out systems it creates the memory blocks for Cosserat rods and rigid bodies. + This function takes the systems appended to the simulator class and + separates them into groups based on their block support. Then using + these grouped systems it creates the memory blocks. + + Parameters + ---------- + systems : list[StaticSystemType] + List of systems to be grouped into memory blocks. + block_supports : dict[Type[BlockSystemType], list[Type[SystemType]]] + Dictionary mapping block types to the list of system types that support it. Returns ------- + list[BlockSystemType] + List of memory block structures created from the systems. + Notes + ----- + Systems that don't have an associated block type in the dictionary will be + skipped (no block constructed), but they are still allowed to be appended + to the system collection. """ - _memory_blocks: list[BlockSystemType] = [] - temp_list_for_cosserat_rod_systems: list[RodType] = [] - temp_list_for_rigid_body_systems: list[RigidBodyType] = [] - temp_list_for_cosserat_rod_systems_idx: list[SystemIdxType] = [] - temp_list_for_rigid_body_systems_idx: list[SystemIdxType] = [] - - for system_idx, sys_to_be_added in enumerate(systems): - - if isinstance(sys_to_be_added, RodBase): - temp_list_for_cosserat_rod_systems.append(sys_to_be_added) - temp_list_for_cosserat_rod_systems_idx.append(system_idx) + _memory_blocks: list["BlockSystemType"] = [] + _non_blocked_systems: list[SystemProtocol] = [] - elif isinstance(sys_to_be_added, RigidBodyBase): - temp_list_for_rigid_body_systems.append(sys_to_be_added) - temp_list_for_rigid_body_systems_idx.append(system_idx) + # Group systems by their block type + system_list: dict[Type["BlockSystemType"], list["StaticSystemType"]] = defaultdict( + list + ) + index_list: dict[Type["BlockSystemType"], list["SystemIdxType"]] = defaultdict(list) - else: - continue # No error:: any typechecking should be finished by BaseSystemCollection._check_type + for system_idx, system in enumerate(systems): + # Find the matching system type in block_supports + block_type = None + for bt, system_types in block_supports.items(): + if ( + type(system) in system_types + ): # Explicit check for *exact* system type, not subclasses. + block_type = bt + break - if temp_list_for_cosserat_rod_systems: - _memory_blocks.append( - MemoryBlockCosseratRod( - temp_list_for_cosserat_rod_systems, - temp_list_for_cosserat_rod_systems_idx, - ) - ) + if block_type is not None: + # If block type found, group the system + system_list[block_type].append(system) + index_list[block_type].append(system_idx) + elif isinstance(system, SystemProtocol): + _non_blocked_systems.append(system) - if temp_list_for_rigid_body_systems: - _memory_blocks.append( - MemoryBlockRigidBody( - temp_list_for_rigid_body_systems, temp_list_for_rigid_body_systems_idx - ) - ) + # Create blocks for each block type + for block_type, systems_for_block in system_list.items(): + # block_type is a concrete class with constructor (systems, system_idx_list) + block: BlockSystemType = block_type(systems_for_block, index_list[block_type]) + _memory_blocks.append(block) - return list(_memory_blocks) + return _memory_blocks, _non_blocked_systems diff --git a/elastica/modules/protocol.py b/elastica/modules/protocol.py index 7a34fd6d..4011f984 100644 --- a/elastica/modules/protocol.py +++ b/elastica/modules/protocol.py @@ -45,7 +45,7 @@ def __getitem__(self, i: slice | int) -> "list[SystemType] | SystemType": ... def systems(self) -> Generator[StaticSystemType, None, None]: ... - def block_systems(self) -> Generator[BlockSystemType, None, None]: ... + def final_systems(self) -> Generator[SystemType, None, None]: ... def get_system_index( self, sys_to_be_added: "SystemType | StaticSystemType" diff --git a/elastica/systems/protocol.py b/elastica/systems/protocol.py index 7d6ffb7b..5f607b8d 100644 --- a/elastica/systems/protocol.py +++ b/elastica/systems/protocol.py @@ -20,6 +20,7 @@ class StaticSystemBase(Protocol): REQUISITE_MODULES: list[Type] +@runtime_checkable class SystemProtocol(StaticSystemBase, Protocol): """ Protocol for all dynamic elastica system. diff --git a/tests/test_modules/test_base_system.py b/tests/test_modules/test_base_system.py index 6d907879..84c55e58 100644 --- a/tests/test_modules/test_base_system.py +++ b/tests/test_modules/test_base_system.py @@ -1,6 +1,7 @@ __doc__ = """ Test modules for base systems """ import pytest +import warnings import numpy as np from elastica.modules import ( @@ -27,7 +28,8 @@ def load_collection(self): rng = np.random.default_rng(42) # Fixed seed for test reproducibility bsc = BaseSystemCollection() - bsc.extend_allowed_types((int, float, str, np.ndarray)) + bsc.extend_allowed_types((int, float, str)) + bsc.append_allowed_types(np.ndarray) # Bypass check, but its fine for testing bsc.append(3) bsc.append(5.0) @@ -99,7 +101,7 @@ def test_extend_correctness(self, load_collection): def test_override_allowed_types(self, load_collection, mock_rod): bsc = load_collection - bsc.override_allowed_types((int, float, str)) + bsc._override_allowed_types((int, float, str)) # First check that adding a rod object throws an # error as we have replaced rods now it @@ -119,7 +121,7 @@ def test_invalid_idx_in_get_sys_index_throws(self, load_collection): from elastica.rod import RodBase bsc = load_collection - bsc.override_allowed_types((RodBase,)) + bsc._override_allowed_types((RodBase,)) with pytest.raises(AssertionError) as excinfo: bsc.get_system_index(100) assert "exceeds number of" in str(excinfo.value) @@ -141,11 +143,157 @@ def test_unregistered_system_in_get_sys_index_throws( def test_get_sys_index_returns_correct_idx(self, load_collection): assert load_collection.get_system_index(1) == 1 + def test_duplicate_system_warning(self): + """Test that adding the same system instance twice emits a warning.""" + bsc = BaseSystemCollection() + bsc.extend_allowed_types((int,)) + + # Add a system + test_system = 42 + bsc.append(test_system) + + # Try to add the same system again - should emit warning + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + bsc._check_type(test_system) + + # Verify warning was issued + assert len(w) == 1 + assert issubclass(w[0].category, UserWarning) + assert "already in the system collection" in str(w[0].message) + assert "not recommended" in str(w[0].message) + + def test_duplicate_system_warning_with_rod(self, mock_rod): + """Test that adding the same rod instance twice emits a warning.""" + bsc = BaseSystemCollection() + from elastica.rod import RodBase + + bsc.extend_allowed_types((RodBase,)) + + # Add a rod + bsc.append(mock_rod) + + # Try to add the same rod again - should emit warning + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + bsc.append(mock_rod) + + # Verify warning was issued + assert len(w) == 1 + assert issubclass(w[0].category, UserWarning) + assert "already in the system collection" in str(w[0].message) + assert "not recommended" in str(w[0].message) + @pytest.mark.xfail def test_delitem(self, load_collection): del load_collection[0] assert load_collection[0] == 3 + def test_requisite_modules_error(self): + """Test that RuntimeError is raised when system requires modules not present.""" + + class Collection(BaseSystemCollection): + pass + + bsc = Collection() + + # Create a mock system class that requires Constraints module + class SystemWithRequisiteModules: + REQUISITE_MODULES = [int] # Require int module + + system = SystemWithRequisiteModules() + bsc.append_allowed_types( + SystemWithRequisiteModules, + ) + + # Should raise RuntimeError because BaseSystemCollection doesn't have Constraints + # The type check passes (SystemWithRequisiteModules is in allowed_sys_types), + # but REQUISITE_MODULES check fails + with pytest.raises(RuntimeError) as excinfo: + bsc._check_type(system) + assert "requires the following modules" in str(excinfo.value) + assert "int" in str(excinfo.value) + + def test_requisite_modules_success(self): + """Test that system with REQUISITE_MODULES passes when modules are present.""" + + # Create a simulator with Constraints module + class SimulatorInt(BaseSystemCollection, int): + pass + + bsc = SimulatorInt() + + # Create a mock system class that requires Constraints module + class SystemWithRequisiteModules: + REQUISITE_MODULES = [int] + + system = SystemWithRequisiteModules() + bsc.append_allowed_types( + SystemWithRequisiteModules, + ) + + # Should pass because BaseSystemCollection has Constraints + assert bsc._check_type(system) is True + + def test_enable_block_supports_new_system_type(self): + """Test enable_block_supports when system_type is not in any block_supports (else clause).""" + from elastica.rod.cosserat_rod import CosseratRod + + class CustomBlock: + pass + + class DerivedRod(CosseratRod): + def __init__(self): + pass + + derived_rod = DerivedRod() + + bsc = BaseSystemCollection() + + # Initially, CustomRod should not be in block_supports + found = False + for block_type in bsc._block_supports.values(): + if derived_rod in block_type: + found = True + break + assert not found, "CustomRod should not be in block_supports initially" + + # Enable block support for CustomRod (else clause - creates new entry) + bsc.enable_block_supports(derived_rod, CustomBlock) + assert derived_rod in bsc._block_supports[CustomBlock] + + def test_enable_block_supports_existing_system_type(self): + """Test enable_block_supports when system_type is already in block_supports (if branch).""" + from elastica.memory_block.memory_block_rod import MemoryBlockCosseratRod + from elastica.rod.cosserat_rod import CosseratRod + + class CustomBlock: + pass + + bsc = BaseSystemCollection() + + # CosseratRod should already be in block_supports (set in __init__) + assert CosseratRod in bsc._block_supports[MemoryBlockCosseratRod] + + # Get the initial count + bsc.enable_block_supports(CosseratRod, MemoryBlockCosseratRod) + assert bsc._block_supports[MemoryBlockCosseratRod].count(CosseratRod) == 1 + + # Switch block support + bsc.enable_block_supports(CosseratRod, CustomBlock) + assert bsc._block_supports[MemoryBlockCosseratRod].count(CosseratRod) == 0 + assert bsc._block_supports[CustomBlock].count(CosseratRod) == 1 + + # Create no duplicates + bsc.enable_block_supports(CosseratRod, CustomBlock) + assert bsc._block_supports[MemoryBlockCosseratRod].count(CosseratRod) == 0 + assert bsc._block_supports[CustomBlock].count(CosseratRod) == 1 + + # Switch block support back + bsc.enable_block_supports(CosseratRod, MemoryBlockCosseratRod) + assert bsc._block_supports[MemoryBlockCosseratRod].count(CosseratRod) == 1 + assert bsc._block_supports[CustomBlock].count(CosseratRod) == 0 + class GenericSimulatorClass( BaseSystemCollection, Constraints, Forcing, Connections, CallBacks From b51062998b2344cc4818c924ab523dbccbe96e8a Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Thu, 18 Dec 2025 16:45:56 -0600 Subject: [PATCH 73/85] refactor: rigid body implementation, name for block system impl. --- elastica/rigidbody/cylinder.py | 54 +++++++++++---------- elastica/rigidbody/rigid_body_base.py | 2 +- elastica/rod/cosserat_rod.py | 1 - elastica/timestepper/symplectic_steppers.py | 10 ++-- 4 files changed, 35 insertions(+), 32 deletions(-) diff --git a/elastica/rigidbody/cylinder.py b/elastica/rigidbody/cylinder.py index 06cadb02..b4a1adfa 100644 --- a/elastica/rigidbody/cylinder.py +++ b/elastica/rigidbody/cylinder.py @@ -10,6 +10,29 @@ from elastica.rigidbody.rigid_body_base import RigidBodyBase +def _assert_check_array_size( + to_check: NDArray[np.float64], name: str, expected: int = 3 +) -> None: + """ + Validate that an array has the expected size. + """ + array_size = to_check.size + assert array_size == expected, ( + f"Invalid size of '{name}'. " f"Expected: {expected}, but got: {array_size}" + ) + + +def _assert_check_lower_bound( + to_check: float, name: str, lower_bound: float = 0.0 +) -> None: + """ + Validate that a value is greater than a lower bound. + """ + assert ( + to_check > lower_bound + ), f"Value for '{name}' ({to_check}) must be at least {lower_bound}. " + + class Cylinder(RigidBodyBase): def __init__( self, @@ -32,32 +55,13 @@ def __init__( base_radius : float density : float """ + _assert_check_array_size(start, "start") + _assert_check_array_size(direction, "direction") + _assert_check_array_size(normal, "normal") - # FIXME: Refactor - def assert_check_array_size( - to_check: NDArray[np.float64], name: str, expected: int = 3 - ) -> None: - array_size = to_check.size - assert array_size == expected, ( - f"Invalid size of '{name}'. " - f"Expected: {expected}, but got: {array_size}" - ) - - # FIXME: Refactor - def assert_check_lower_bound( - to_check: float, name: str, lower_bound: float = 0.0 - ) -> None: - assert ( - to_check > lower_bound - ), f"Value for '{name}' ({to_check}) must be at least {lower_bound}. " - - assert_check_array_size(start, "start") - assert_check_array_size(direction, "direction") - assert_check_array_size(normal, "normal") - - assert_check_lower_bound(base_length, "base_length") - assert_check_lower_bound(base_radius, "base_radius") - assert_check_lower_bound(density, "density") + _assert_check_lower_bound(base_length, "base_length") + _assert_check_lower_bound(base_radius, "base_radius") + _assert_check_lower_bound(density, "density") super().__init__() diff --git a/elastica/rigidbody/rigid_body_base.py b/elastica/rigidbody/rigid_body_base.py index eca9b1f7..ce890223 100644 --- a/elastica/rigidbody/rigid_body_base.py +++ b/elastica/rigidbody/rigid_body_base.py @@ -25,7 +25,7 @@ class RigidBodyBase(ABC, SystemProtocol): def __init__(self) -> None: # rigid body does not have elements it only has one node. We are setting n_elems to - # make code to work. _bootstrap_from_data requires n_elems to be define + # make code to work. self.n_elems: int = 1 self.n_nodes: int = 1 diff --git a/elastica/rod/cosserat_rod.py b/elastica/rod/cosserat_rod.py index f2c586c0..4801c7da 100644 --- a/elastica/rod/cosserat_rod.py +++ b/elastica/rod/cosserat_rod.py @@ -769,7 +769,6 @@ def _compute_dilatation_rate( """ Update dilatation_rate given position, velocity, length, and rest_length """ - # TODO Use the vector formula rather than separating it out # self.lengths = l_i = |r^{i+1} - r^{i}| r_dot_v = _batch_dot(position_collection, velocity_collection) r_plus_one_dot_v = _batch_dot( diff --git a/elastica/timestepper/symplectic_steppers.py b/elastica/timestepper/symplectic_steppers.py index a13b935a..7cd1d1eb 100644 --- a/elastica/timestepper/symplectic_steppers.py +++ b/elastica/timestepper/symplectic_steppers.py @@ -98,7 +98,7 @@ def do_step( """ for kin_prefactor, kin_step, dyn_step in steps_and_prefactors[:-1]: - for system in SystemCollection.block_systems(): + for system in SystemCollection.final_systems(): kin_step(system, time, dt) time += kin_prefactor(dt) @@ -107,14 +107,14 @@ def do_step( SystemCollection.constrain_values(time) # We need internal forces and torques because they are used by interaction module. - for system in SystemCollection.block_systems(): + for system in SystemCollection.final_systems(): system.compute_internal_forces_and_torques(time) # system.update_internal_forces_and_torques() # Add external forces, controls etc. SystemCollection.synchronize(time) - for system in SystemCollection.block_systems(): + for system in SystemCollection.final_systems(): dyn_step(system, time, dt) # Constrain only rates @@ -124,7 +124,7 @@ def do_step( last_kin_prefactor = steps_and_prefactors[-1][0] last_kin_step = steps_and_prefactors[-1][1] - for system in SystemCollection.block_systems(): + for system in SystemCollection.final_systems(): last_kin_step(system, time, dt) time += last_kin_prefactor(dt) SystemCollection.constrain_values(time) @@ -133,7 +133,7 @@ def do_step( SystemCollection.apply_callbacks(time, round(time / dt)) # Zero out the external forces and torques - for system in SystemCollection.block_systems(): + for system in SystemCollection.final_systems(): system.zeroed_out_external_forces_and_torques(time) return time From 397590ee5f97ee44c01aa22a7bb21b38564fb64f Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Thu, 18 Dec 2025 16:47:07 -0600 Subject: [PATCH 74/85] refactor: update memory block handling and system protocol integration --- elastica/experimental/interaction.py | 4 +- elastica/modules/base_system.py | 5 +-- elastica/modules/constraints.py | 4 +- elastica/restart.py | 6 +-- tests/analytical.py | 22 +++++----- tests/test_modules/test_callbacks.py | 1 - tests/test_modules/test_connections.py | 1 - tests/test_modules/test_constraints.py | 1 - tests/test_modules/test_damping.py | 1 - tests/test_modules/test_forcing.py | 1 - .../test_memory_block_cosserat_rod.py | 40 ++++++++++++++----- .../test_memory_block_rigid_body.py | 5 ++- tests/test_rod/mock_rod.py | 2 +- 13 files changed, 56 insertions(+), 37 deletions(-) diff --git a/elastica/experimental/interaction.py b/elastica/experimental/interaction.py index 293349ee..d6042674 100644 --- a/elastica/experimental/interaction.py +++ b/elastica/experimental/interaction.py @@ -102,7 +102,9 @@ def anisotropic_friction_numba_rigid_body( velocity_collection, external_forces, ) - # FIXME: In future change the below part we should be able to compute the normal + # NOTE: Currently uses director_collection[0] as axial direction. + # If user anticipate the normal direction to be different from the director_collection[0] + # due to the rolling motion, this function cannot be used. axial_direction = director_collection[0] # rigid_body_normal # system.tangents element_velocity = velocity_collection diff --git a/elastica/modules/base_system.py b/elastica/modules/base_system.py index 1b7e2089..e3d9fedf 100644 --- a/elastica/modules/base_system.py +++ b/elastica/modules/base_system.py @@ -219,10 +219,9 @@ def enable_block_supports( """ for btype in self._block_supports: if system_type in self._block_supports[btype]: - self._block_supports[btype].append(system_type) + self._block_supports[btype].remove(system_type) break - else: - self._block_supports[block_type] = [system_type] + self._block_supports[block_type].append(system_type) @final def get_system_index( diff --git a/elastica/modules/constraints.py b/elastica/modules/constraints.py index fbf48cac..f3abfb69 100644 --- a/elastica/modules/constraints.py +++ b/elastica/modules/constraints.py @@ -73,9 +73,9 @@ def _finalize_constraints(self) -> None: constrain will synchronize the only periodic boundaries of position, director, velocity and omega variables. """ - for block in self.block_systems(): + for block in self.final_systems(): # append the memory block to the simulation as a system. Memory block is the final system in the simulation. - if hasattr(block, "ring_rod_flag"): + if hasattr(block, "ring_rod_flag") and block.ring_rod_flag: from elastica._synchronize_periodic_boundary import ( _ConstrainPeriodicBoundaries, ) diff --git a/elastica/restart.py b/elastica/restart.py index fb9ae084..0445d710 100644 --- a/elastica/restart.py +++ b/elastica/restart.py @@ -6,9 +6,9 @@ import json from itertools import groupby -from .memory_block import MemoryBlockCosseratRod, MemoryBlockRigidBody +from .memory_block.protocol import BlockSystemProtocol -from .typing import SystemType, SystemCollectionType +from .typing import SystemCollectionType def all_equal(iterable: Iterable[Any]) -> bool: @@ -96,7 +96,7 @@ def load_state( # Load system state for idx, system in enumerate(simulator.systems()): # TODO: Not exactly sure why this condition is necessary. - if isinstance(system, (MemoryBlockCosseratRod, MemoryBlockRigidBody)): + if isinstance(system, BlockSystemProtocol): continue name = system.__class__.__name__ # type: ignore path = os.path.join(directory, f"{name}_{idx}.npz") diff --git a/tests/analytical.py b/tests/analytical.py index c045105c..b6108087 100644 --- a/tests/analytical.py +++ b/tests/analytical.py @@ -303,22 +303,22 @@ class CollectiveSystem: """This collective system class is to test multiple memory structure blocks.""" def __init__(self): - self._memory_blocks = [] + self._final_systems = [] def systems(self): - return self._memory_blocks + return self._final_systems - def block_systems(self): - return self._memory_blocks + def final_systems(self): + return self._final_systems def __getitem__(self, idx): - return self._memory_blocks[idx] + return self._final_systems[idx] def __len__(self): - return len(self._memory_blocks) + return len(self._final_systems) def __iter__(self): - return self._memory_blocks.__iter__() + return self._final_systems.__iter__() def synchronize(self, time): pass @@ -336,12 +336,12 @@ def apply_callbacks(self, time, current_step: int): class SymplecticUndampedHarmonicOscillatorCollectiveSystem(CollectiveSystem): def __init__(self): super(SymplecticUndampedHarmonicOscillatorCollectiveSystem, self).__init__() - self._memory_blocks.append( + self._final_systems.append( SymplecticUndampedSimpleHarmonicOscillatorSystem( omega=2.0 * np.pi, init_val=np.array([1.0, 0.0]) ) ) - self._memory_blocks.append( + self._final_systems.append( SymplecticUndampedSimpleHarmonicOscillatorSystem( omega=1.0 * np.pi, init_val=np.array([0.0, 0.5]) ) @@ -353,8 +353,8 @@ def __init__(self): super( ScalarExponentialDampedHarmonicOscillatorCollectiveSystem, self ).__init__() - self._memory_blocks.append(ScalarExponentialDecaySystem()) - self._memory_blocks.append(DampedSimpleHarmonicOscillatorSystem()) + self._final_systems.append(ScalarExponentialDecaySystem()) + self._final_systems.append(DampedSimpleHarmonicOscillatorSystem()) def make_simple_system_with_positions_directors( diff --git a/tests/test_modules/test_callbacks.py b/tests/test_modules/test_callbacks.py index 6b79a74d..40c01492 100644 --- a/tests/test_modules/test_callbacks.py +++ b/tests/test_modules/test_callbacks.py @@ -65,7 +65,6 @@ class TestCallBacksMixin: class SystemCollectionWithCallBacksMixedin(BaseSystemCollection, CallBacks): pass - # TODO fix link after new PR from elastica.rod import RodBase class MockRod(RodBase): diff --git a/tests/test_modules/test_connections.py b/tests/test_modules/test_connections.py index 8de57f5a..8c73eff5 100644 --- a/tests/test_modules/test_connections.py +++ b/tests/test_modules/test_connections.py @@ -191,7 +191,6 @@ class TestConnectionsMixin: class SystemCollectionWithConnectionsMixin(BaseSystemCollection, Connections): pass - # TODO fix link after new PR from elastica.rod import RodBase class MockRod(RodBase): diff --git a/tests/test_modules/test_constraints.py b/tests/test_modules/test_constraints.py index c998db84..facc6306 100644 --- a/tests/test_modules/test_constraints.py +++ b/tests/test_modules/test_constraints.py @@ -213,7 +213,6 @@ class TestConstraintsMixin: class SystemCollectionWithConstraintsMixedin(BaseSystemCollection, Constraints): pass - # TODO fix link after new PR from elastica.rod import RodBase class MockRod(RodBase): diff --git a/tests/test_modules/test_damping.py b/tests/test_modules/test_damping.py index 993506d8..0ee541db 100644 --- a/tests/test_modules/test_damping.py +++ b/tests/test_modules/test_damping.py @@ -89,7 +89,6 @@ class TestDampingMixin: class SystemCollectionWithDampingMixedin(BaseSystemCollection, Damping): pass - # TODO fix link after new PR from elastica.rod import RodBase class MockRod(RodBase): diff --git a/tests/test_modules/test_forcing.py b/tests/test_modules/test_forcing.py index 49c074ec..759bdaa5 100644 --- a/tests/test_modules/test_forcing.py +++ b/tests/test_modules/test_forcing.py @@ -72,7 +72,6 @@ class TestForcingMixin: class SystemCollectionWithForcingMixedin(BaseSystemCollection, Forcing): pass - # TODO fix link after new PR from elastica.rod import RodBase class MockRod(RodBase): diff --git a/tests/test_modules/test_memory_block_cosserat_rod.py b/tests/test_modules/test_memory_block_cosserat_rod.py index 0a3c025a..71225b9f 100644 --- a/tests/test_modules/test_memory_block_cosserat_rod.py +++ b/tests/test_modules/test_memory_block_cosserat_rod.py @@ -8,6 +8,7 @@ from elastica.rod import RodBase from elastica.modules.memory_block import construct_memory_block_structures from elastica.memory_block.memory_block_rod import MemoryBlockCosseratRod +from elastica.systems.protocol import SystemProtocol class BaseRodForTesting(RodBase): @@ -74,19 +75,38 @@ def __init__(self, n_elems: np.int64, ring_rod_flag: bool): self.bend_matrix = rng.standard_normal() * np.ones((3, 3, self.n_voronoi)) +class BaseRodForTestingSteppable(SystemProtocol): + def compute_internal_forces_and_torques(self, time: np.float64) -> None: + pass + + def update_accelerations(self, time: np.float64) -> None: + pass + + def zeroed_out_external_forces_and_torques(self, time: np.float64) -> None: + pass + + @pytest.mark.parametrize("n_rods", [1, 2, 5, 6]) -def test_construct_memory_block_structures_for_cosserat_rod(n_rods): +def test_construct_memory_block_structures_for_cosserat_rod_with_non_blocking_systems( + n_rods, +): """ - This test is only testing the validity of created block-structure class, using the + This test is only testing the validity of created block-structure class with non-blocking systems, using the construct_memory_block_structures function. + """ - Parameters - ---------- - n_rods + systems = [BaseRodForTestingSteppable() for _ in range(n_rods)] + _, non_blocking_systems_list = construct_memory_block_structures(systems, {}) + + assert len(non_blocking_systems_list) == n_rods + assert systems == non_blocking_systems_list - Returns - ------- +@pytest.mark.parametrize("n_rods", [1, 2, 5, 6]) +def test_construct_memory_block_structures_for_cosserat_rod(n_rods): + """ + This test is only testing the validity of created block-structure class, using the + construct_memory_block_structures function. """ systems = [ @@ -96,9 +116,11 @@ def test_construct_memory_block_structures_for_cosserat_rod(n_rods): for _ in range(n_rods) ] - memory_block_list = construct_memory_block_structures(systems) + block_supports = {MemoryBlockCosseratRod: [BaseRodForTesting]} + memory_block_list, _ = construct_memory_block_structures(systems, block_supports) - assert issubclass(memory_block_list[0].__class__, MemoryBlockCosseratRod) + assert isinstance(memory_block_list[0], MemoryBlockCosseratRod) + assert len(memory_block_list) == 1 @pytest.mark.parametrize("n_straight_rods", [0, 1, 2, 5]) diff --git a/tests/test_modules/test_memory_block_rigid_body.py b/tests/test_modules/test_memory_block_rigid_body.py index ef43d780..8b3e76fb 100644 --- a/tests/test_modules/test_memory_block_rigid_body.py +++ b/tests/test_modules/test_memory_block_rigid_body.py @@ -60,9 +60,10 @@ def test_construct_memory_block_structures_for_rigid_bodies(n_bodies): systems = [MockRigidBodyForTesting() for _ in range(n_bodies)] - memory_block_list = construct_memory_block_structures(systems) + block_supports = {MemoryBlockRigidBody: [MockRigidBodyForTesting]} + memory_block_list, _ = construct_memory_block_structures(systems, block_supports) - assert issubclass(memory_block_list[0].__class__, MemoryBlockRigidBody) + assert isinstance(memory_block_list[0], MemoryBlockRigidBody) @pytest.mark.parametrize("n_bodies", [1, 2, 5, 6]) diff --git a/tests/test_rod/mock_rod.py b/tests/test_rod/mock_rod.py index a70bf17f..570456b1 100644 --- a/tests/test_rod/mock_rod.py +++ b/tests/test_rod/mock_rod.py @@ -3,7 +3,7 @@ import numpy as np -from elastica.memory_block.memory_block_rod_base import ( +from elastica.memory_block.utils import ( make_block_memory_periodic_boundary_metadata, ) from elastica.utils import MaxDimension From 0f48d1520a8b6adeb34c90fcfefb243319cb8c47 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Thu, 18 Dec 2025 17:36:28 -0600 Subject: [PATCH 75/85] docs: rename StaticSystemBase to StaticSystemProtocol and update related documentations --- docs/advanced/PackageDesign.md | 60 +++++++++++--------------- docs/guide/workflow.md | 2 +- elastica/__init__.py | 2 +- elastica/modules/base_system.py | 6 +-- elastica/surface/plane.py | 3 +- elastica/systems/protocol.py | 4 +- elastica/typing.py | 6 +-- tests/test_modules/test_base_system.py | 4 +- tests/test_modules/test_contact.py | 4 +- 9 files changed, 41 insertions(+), 50 deletions(-) diff --git a/docs/advanced/PackageDesign.md b/docs/advanced/PackageDesign.md index b56e8bd5..afef5aa1 100644 --- a/docs/advanced/PackageDesign.md +++ b/docs/advanced/PackageDesign.md @@ -20,43 +20,35 @@ Elastica package uses [structural subtyping](https://peps.python.org/pep-0544/) direction RL subgraph Systems Protocol direction RL - SLBD(SlenderBodyGeometryProtool) - SymST["SymplecticSystem:\n• KinematicStates/Rates\n• DynamicStates/Rates"] + SymST["SymplecticSystemProtocol
(Necessary to be stepped by the timestepper)"] style SymST text-align:left - ExpST["ExplicitSystem:\n• States (Unused)"] - style ExpST text-align:left - P((position\nvelocity\nacceleration\n..)) --> SLBD - subgraph StaticSystemType - Surface - Mesh - end - subgraph SystemType - direction TB - Rod - RigidBody - end - SLBD --> SymST + StaticSystemType["Static System Type"
• Plane] + SystemType["(Dynamic) System Type
• CosseratRod (Rod)
• Sphere (RigidBody)
• Cylinder (RigidBody)"] + SystemType --> SymST - SLBD --> ExpST - SystemType --> ExpST end + + subgraph System Collection + SysColl["SystemCollectionProtocol"] + end + subgraph Timestepper Protocol - direction TB - StP["StepperProtocol\n• step(SystemCollection, time, dt)"] - style StP text-align:left - SymplecticStepperProtocol["SymplecticStepperProtocol\n• PositionVerlet"] + direction LR + SymplecticStepperProtocol["SymplecticStepperProtocol
• PositionVerlet"] style SymplecticStepperProtocol text-align:left - ExplicitStepperProtocol["ExplicitStepperProtocol\n(Unused)"] end - subgraph SystemCollection + SymST --> SysColl -->|Symplectic systems only| SymplecticStepperProtocol + StaticSystemType --> SysColl +``` - end - SymST --> SystemCollection --> SymplecticStepperProtocol - ExpST --> SystemCollection --> ExplicitStepperProtocol - StaticSystemType --> SystemCollection +#### Key takeaways: + +- Any object that conforms to `StaticSystemProtocol` can be added to the system collection. + - If you want to add custom type to the system, you can use `append_allowed_types` to add it to the system collection. To add associated block support, you can use `enable_block_supports`. +- Among the systems added to the system collection, only objects that conform to `SystemProtocol` will be integrated by the timestepper. +- If block support is available for a system, they will be collected together during the `finalize` step, and passed to the timestepper. -``` ### System Collection (Build memory block) @@ -66,15 +58,15 @@ Elastica package uses [structural subtyping](https://peps.python.org/pep-0544/) St((Stepper)) subgraph SystemCollectionType direction LR - StSys["StaticSystem:\n• Surface\n• Mesh"] + StSys["StaticSystem:
• Plane"] style StSys text-align:left - DynSys["DynamicSystem:\n• Rod\n  • CosseratRod\n• RigidBody\n  • Sphere\n  • Cylinder"] + DynSys["DynamicSystem:
• CosseratRod
• Sphere
• Cylinder"] style DynSys text-align:left - BlDynSys["BlockSystemType:\n• BlockCosseratRod\n• BlockRigidBody"] + BlDynSys["BlockSystem:
• MemoryBlockCosseratRod
• MemoryBlockRigidBody"] style BlDynSys text-align:left - F{{"Feature Group (OperatorGroup):\n• Synchronize\n• Constrain values\n• Constrain rates\n• Callback"}} + F{{"Feature Group (OperatorGroup):
• Synchronize
• Constrain values
• Constrain rates
• Callback"}} style F text-align:left end Sys --> StSys --> F @@ -91,9 +83,9 @@ Elastica package uses [structural subtyping](https://peps.python.org/pep-0544/) St((Stepper)) subgraph SystemCollectionType direction LR - StSys["StaticSystem:\n• Surface\n• Mesh"] + StSys["StaticSystem:
• Plane"] style StSys text-align:left - DynSys["DynamicSystem:\n• Rod\n  • CosseratRod\n• RigidBody\n  • Sphere\n  • Cylinder"] + DynSys["DynamicSystem:
• Rod
  • CosseratRod
• RigidBody
  • Sphere
  • Cylinder"] style DynSys text-align:left subgraph Feature diff --git a/docs/guide/workflow.md b/docs/guide/workflow.md index ff75fb89..68be7e18 100644 --- a/docs/guide/workflow.md +++ b/docs/guide/workflow.md @@ -271,7 +271,7 @@ timestepper = PositionVerlet() final_time = 10 # seconds total_steps = int(final_time / dt) -# Explicit timestepper loop +# Timestepper loop dt = final_time / total_steps time = 0.0 for i in range(total_steps): diff --git a/elastica/__init__.py b/elastica/__init__.py index 79279d75..c745fa3f 100644 --- a/elastica/__init__.py +++ b/elastica/__init__.py @@ -1,5 +1,5 @@ from elastica.systems.protocol import ( - StaticSystemBase, + StaticSystemProtocol, SystemProtocol, ) from elastica.rod.knot_theory import ( diff --git a/elastica/modules/base_system.py b/elastica/modules/base_system.py index e3d9fedf..c325f760 100644 --- a/elastica/modules/base_system.py +++ b/elastica/modules/base_system.py @@ -24,7 +24,7 @@ from collections import defaultdict from collections.abc import MutableSequence -from elastica.systems.protocol import StaticSystemBase, SystemProtocol +from elastica.systems.protocol import StaticSystemProtocol, SystemProtocol from elastica.memory_block.protocol import BlockSystemProtocol from elastica.memory_block.memory_block_rod import MemoryBlockCosseratRod @@ -87,9 +87,9 @@ def __init__(self) -> None: super().__init__() # List of system types/bases that are allowed - # By default, any object that is a subclass of StaticSystemBase is allowed. + # By default, any object that is a subclass of StaticSystemProtocol is allowed. # (Technically, any object that is conforms StaticSystemProtocol is allowed.) - self.allowed_sys_types: tuple[Type, ...] = (StaticSystemBase,) + self.allowed_sys_types: tuple[Type, ...] = (StaticSystemProtocol,) # Block support for System types. # If a system type is not in this dictionary, no block will be constructed for it. diff --git a/elastica/surface/plane.py b/elastica/surface/plane.py index cc317a52..70035710 100644 --- a/elastica/surface/plane.py +++ b/elastica/surface/plane.py @@ -4,10 +4,9 @@ import numpy as np from numpy.typing import NDArray from elastica.utils import Tolerance -from elastica.systems.protocol import StaticSystemBase -class Plane(StaticSystemBase): +class Plane: """ Plane static system. Static system does not change by the timestepping. diff --git a/elastica/systems/protocol.py b/elastica/systems/protocol.py index 5f607b8d..961430c2 100644 --- a/elastica/systems/protocol.py +++ b/elastica/systems/protocol.py @@ -11,7 +11,7 @@ @runtime_checkable -class StaticSystemBase(Protocol): +class StaticSystemProtocol(Protocol): """ Protocol for all static elastica system. Minimal requirement interface to be included in the simulator. @@ -21,7 +21,7 @@ class StaticSystemBase(Protocol): @runtime_checkable -class SystemProtocol(StaticSystemBase, Protocol): +class SystemProtocol(StaticSystemProtocol, Protocol): """ Protocol for all dynamic elastica system. """ diff --git a/elastica/typing.py b/elastica/typing.py index d6beabeb..72fe7a1f 100644 --- a/elastica/typing.py +++ b/elastica/typing.py @@ -20,7 +20,7 @@ from .modules.protocol import SystemCollectionProtocol from .systems.protocol import ( - StaticSystemBase, + StaticSystemProtocol, SystemProtocol, SymplecticSystemProtocol, ) @@ -37,14 +37,14 @@ SystemCollectionProtocol = "SystemCollectionProtocol" SystemProtocol = "SystemProtocol" - StaticSystemBase = "StaticSystemBase" + StaticSystemProtocol = "StaticSystemProtocol" SymplecticSystemProtocol = "SymplecticSystemProtocol" StepperProtocol = "StepperProtocol" SymplecticStepperProtocol = "SymplecticStepperProtocol" BlockSystemProtocol = "BlockSystemProtocol" -StaticSystemType: TypeAlias = "StaticSystemBase" +StaticSystemType: TypeAlias = "StaticSystemProtocol" SystemType: TypeAlias = "SystemProtocol" SystemIdxType: TypeAlias = int BlockSystemType: TypeAlias = "BlockSystemProtocol" diff --git a/tests/test_modules/test_base_system.py b/tests/test_modules/test_base_system.py index 84c55e58..24a444f9 100644 --- a/tests/test_modules/test_base_system.py +++ b/tests/test_modules/test_base_system.py @@ -78,14 +78,14 @@ def test_extend_allowed_types(self, load_collection): from elastica.rod import RodBase from elastica.rigidbody import RigidBodyBase - from elastica.systems.protocol import StaticSystemBase, SystemProtocol + from elastica.systems.protocol import StaticSystemProtocol, SystemProtocol # Types are extended in the fixture assert int in bsc.allowed_sys_types assert float in bsc.allowed_sys_types assert str in bsc.allowed_sys_types assert np.ndarray in bsc.allowed_sys_types - assert StaticSystemBase in bsc.allowed_sys_types # Minimal requirement + assert StaticSystemProtocol in bsc.allowed_sys_types # Minimal requirement def test_extend_correctness(self, load_collection): """ diff --git a/tests/test_modules/test_contact.py b/tests/test_modules/test_contact.py index bb9f2a79..2fe53fb2 100644 --- a/tests/test_modules/test_contact.py +++ b/tests/test_modules/test_contact.py @@ -90,7 +90,7 @@ class SystemCollectionWithContactMixin(BaseSystemCollection, Contact): from elastica.rod import RodBase from elastica.rigidbody import RigidBodyBase - from elastica.systems.protocol import StaticSystemBase + from elastica.systems.protocol import StaticSystemProtocol class MockRod(RodBase): def __init__(self, *args, **kwargs): @@ -113,7 +113,7 @@ class MockRigidBody(RigidBodyBase): def __init__(self, *args, **kwargs): self.n_elems = 1 - class MockSurface(StaticSystemBase): + class MockSurface(StaticSystemProtocol): def __init__(self, *args, **kwargs): self.n_facets = 1 From e3306312cd539bb5cb96c740fed5b499fd702750 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 21 Dec 2025 22:13:48 -0600 Subject: [PATCH 76/85] improve encapsulation and simplifies API for symplectic timestepping --- elastica/rod/data_structures.py | 164 +++++--------------- elastica/systems/protocol.py | 28 +--- elastica/timestepper/symplectic_steppers.py | 65 ++------ 3 files changed, 55 insertions(+), 202 deletions(-) diff --git a/elastica/rod/data_structures.py b/elastica/rod/data_structures.py index 70a647ff..2e5e1eb4 100644 --- a/elastica/rod/data_structures.py +++ b/elastica/rod/data_structures.py @@ -14,73 +14,54 @@ class _RodSymplecticStepperMixin: + """ + Mixin class providing necessary methods for integration of + the kinematic and dynamic equations of the rod. + """ + + n_nodes: int + # Posture state position_collection: NDArray[np.float64] director_collection: NDArray[np.float64] + # Velocity state velocity_collection: NDArray[np.float64] omega_collection: NDArray[np.float64] + v_w_collection: NDArray[np.float64] # Rate collection + # Acceleration state + dvdt_dwdt_collection: NDArray[np.float64] # Second-derivative collection - v_w_collection: NDArray[np.float64] - dvdt_dwdt_collection: NDArray[np.float64] - - def __init__(self) -> None: - self.kinematic_states = _KinematicState( - self.position_collection, self.director_collection - ) - self.dynamic_states = _DynamicState( - self.v_w_collection, - self.dvdt_dwdt_collection, + def update_kinematics( + self, + time: np.float64, + prefac: np.float64, + ) -> None: + overload_operator_kinematic_numba( + self.n_nodes, + prefac, + self.position_collection, + self.director_collection, self.velocity_collection, self.omega_collection, ) - # Expose rate returning functions in the interface - # to be used by the time-stepping algorithm - # dynamic rates needs to call update_accelerations and henc - # is another function - self.kinematic_rates = self.dynamic_states.kinematic_rates - - def dynamic_rates( - self: SymplecticSystemProtocol, + def update_dynamics( + self, time: np.float64, prefac: np.float64, - ) -> NDArray[np.float64]: - self.update_accelerations(time) - return self.dynamic_states.dynamic_rates(time, prefac) + ) -> None: + overload_operator_dynamic_numba( + prefac, + self.rate_collection, + self.dvdt_dwdt_collection, + ) """ -Symplectic stepper interface +Symplectic stepper operation """ -class _KinematicState: - """State storing (x,Q) for symplectic steppers. - Wraps data as state, with overloaded methods for symplectic steppers. - Allows for separating implementation of stepper from actual - addition/multiplication/other formulae used. - - Symplectic steppers rely only on in-place modifications to state and so - only these methods are provided. - """ - - def __init__( - self, - position_collection_view: NDArray[np.float64], - director_collection_view: NDArray[np.float64], - ) -> None: - """ - Parameters - ---------- - position_collection_view : view of positions (or) x - director_collection_view : view of directors (or) Q - """ - # super(_KinematicState, self).__init__() - - self.position_collection = position_collection_view - self.director_collection = director_collection_view - - @njit(cache=True) # type: ignore def overload_operator_kinematic_numba( n_nodes: int, @@ -114,85 +95,17 @@ def overload_operator_kinematic_numba( rotation_matrix = _get_rotation_matrix(1.0, prefac * omega_collection) director_collection[:] = _batch_matmul(rotation_matrix, director_collection) - return - - -class _DynamicState: - """State storing (v,ω, dv/dt, dω/dt) for symplectic steppers. - - Wraps data as state, with overloaded methods for symplectic steppers. - Allows for separating implementation of stepper from actual - addition/multiplication/other formulae used. - Symplectic steppers rely only on in-place modifications to state and so - only these methods are provided. - """ - - def __init__( - self, - v_w_collection: NDArray[np.float64], - dvdt_dwdt_collection: NDArray[np.float64], - velocity_collection: NDArray[np.float64], - omega_collection: NDArray[np.float64], - ) -> None: - """ - Parameters - ---------- - n_elems : int, number of rod elements - rate_collection_view : np.ndarray containing (v, ω, dv/dt, dω/dt) - v_w_collection : numpy.ndarray - - """ - super(_DynamicState, self).__init__() - # Limit at which (v, w) end - # Create views for dynamic state - self.rate_collection = v_w_collection - self.dvdt_dwdt_collection = dvdt_dwdt_collection - self.velocity_collection = velocity_collection - self.omega_collection = omega_collection - - def kinematic_rates( - self, time: np.float64, prefac: np.float64 - ) -> tuple[NDArray[np.float64], NDArray[np.float64]]: - """Yields kinematic rates to interact with _KinematicState - - Returns - ------- - v_and_omega : np.ndarray consisting of (v,ω) - Caveats - ------- - Doesn't return a _KinematicState with (dt*v, dt*w) as members, - as one expects the _Kinematic __add__ operator to interact - with another _KinematicState. This is done for efficiency purposes. - """ - # RHS functino call, gives v,w so that - # Comes from kin_state -> (x,Q) += dt * (v,w) <- First part of dyn_state - return self.velocity_collection, self.omega_collection - - def dynamic_rates( - self, time: np.float64, prefac: np.float64 - ) -> NDArray[np.float64]: - """Yields dynamic rates to add to with _DynamicState - Returns - ------- - acc_and_alpha : np.ndarray consisting of (dv/dt,dω/dt) - Caveats - ------- - Doesn't return a _DynamicState with (dt*v, dt*w) as members, - as one expects the _Dynamic __add__ operator to interact - with another _DynamicState. This is done for efficiency purposes. - """ - return prefac * self.dvdt_dwdt_collection - @njit(cache=True) # type: ignore def overload_operator_dynamic_numba( + prefac: np.float64, rate_collection: NDArray[np.float64], - scaled_second_deriv_array: NDArray[np.float64], + second_deriv_array: NDArray[np.float64], ) -> None: """overloaded += operator, updating dynamic_rates Parameters ---------- - scaled_second_deriv_array : np.ndarray containing dt * (dvdt, dωdt), + second_deriv_array : np.ndarray containing (dvdt, dωdt), as retured from _DynamicState's `dynamic_rates` method Returns ------- @@ -200,15 +113,12 @@ def overload_operator_dynamic_numba( Caveats ------- Takes a np.ndarray and not a _DynamicState object (as one expects). - This is done for efficiency reasons, see `dynamic_rates`. """ # Always goes in LHS : that means the update is on the rates alone - # (v,ω) += dt * (dv/dt, dω/dt) -> self.dynamic_rates - # rate_collection[..., : n_kinematic_rates] += scaled_second_deriv_array - blocksize = scaled_second_deriv_array.shape[1] + # (v,ω) += dt * (dv/dt, dω/dt) + # rate_collection[..., : n_kinematic_rates] += second_deriv_aray + blocksize = second_deriv_array.shape[1] for i in range(2): for k in range(blocksize): - rate_collection[i, k] += scaled_second_deriv_array[i, k] - - return + rate_collection[i, k] += prefac * second_deriv_array[i, k] diff --git a/elastica/systems/protocol.py b/elastica/systems/protocol.py index 961430c2..584f309e 100644 --- a/elastica/systems/protocol.py +++ b/elastica/systems/protocol.py @@ -4,8 +4,6 @@ from abc import abstractmethod -from elastica.rod.data_structures import _KinematicState, _DynamicState - import numpy as np from numpy.typing import NDArray @@ -47,9 +45,7 @@ class SymplecticSystemProtocol(SystemProtocol, Protocol): The symplectic stepper accesses: - ``n_nodes`` - - ``kinematic_states`` (position_collection, director_collection) - - ``dynamic_states`` (velocity_collection, omega_collection, rate_collection) - - ``dynamic_rates(time, prefac)`` to compute acceleration updates + - ``update_kinematics`` and ``update_dynamics``: called by the timestepper See Also -------- @@ -60,24 +56,10 @@ class SymplecticSystemProtocol(SystemProtocol, Protocol): n_nodes: int - @property - def kinematic_states(self) -> _KinematicState: - """Return kinematic state.""" - ... - - @property - def dynamic_states(self) -> _DynamicState: - """Return dynamic state.""" - ... - - def kinematic_rates( - self, time: np.float64, prefac: np.float64 - ) -> tuple[NDArray[np.float64], NDArray[np.float64]]: - """Compute kinematic rates.""" + def update_kinematics(self, time: np.float64, prefac: np.float64) -> None: + """Update kinematic state. Typically called after compute_internal_forces_and_torques.""" ... - def dynamic_rates( - self, time: np.float64, prefac: np.float64 - ) -> NDArray[np.float64]: - """Compute dynamic rates.""" + def update_dynamics(self, time: np.float64, prefac: np.float64) -> None: + """Update dynamic state. Typically called after ``update_accelerations``.""" ... diff --git a/elastica/timestepper/symplectic_steppers.py b/elastica/timestepper/symplectic_steppers.py index 7cd1d1eb..db89e2a0 100644 --- a/elastica/timestepper/symplectic_steppers.py +++ b/elastica/timestepper/symplectic_steppers.py @@ -12,10 +12,6 @@ import numpy as np -from elastica.rod.data_structures import ( - overload_operator_kinematic_numba, - overload_operator_dynamic_numba, -) from elastica.systems.protocol import SymplecticSystemProtocol from .protocol import SymplecticStepperProtocol @@ -109,7 +105,6 @@ def do_step( # We need internal forces and torques because they are used by interaction module. for system in SystemCollection.final_systems(): system.compute_internal_forces_and_torques(time) - # system.update_internal_forces_and_torques() # Add external forces, controls etc. SystemCollection.synchronize(time) @@ -185,22 +180,14 @@ def _first_kinematic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: prefac = self._first_prefactor(dt) - overload_operator_kinematic_numba( - System.n_nodes, - prefac, - System.kinematic_states.position_collection, - System.kinematic_states.director_collection, - System.dynamic_states.velocity_collection, - System.dynamic_states.omega_collection, - ) + System.update_kinematics(time, prefac) def _first_dynamic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: - overload_operator_dynamic_numba( - System.dynamic_states.rate_collection, - System.dynamic_rates(time, dt), - ) + prefac = dt + System.update_accelerations(time) + System.update_dynamics(time, prefac) class PEFRL(SymplecticStepperMixin): @@ -245,25 +232,16 @@ def _first_kinematic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: prefac = self._first_kinematic_prefactor(dt) - overload_operator_kinematic_numba( - System.n_nodes, - prefac, - System.kinematic_states.position_collection, - System.kinematic_states.director_collection, - System.dynamic_states.velocity_collection, - System.dynamic_states.omega_collection, - ) + System.update_kinematics(time, prefac) # System.kinematic_states += prefac * System.kinematic_rates(time, prefac) def _first_dynamic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: - prefac = self.lambda_dash_coeff * dt - overload_operator_dynamic_numba( - System.dynamic_states.rate_collection, - System.dynamic_rates(time, prefac), - ) # System.dynamic_states += prefac * System.dynamic_rates(time, prefac) + prefac = self.lambda_dash_coeff * dt + System.update_accelerations(time) + System.update_dynamics(time, prefac) def _second_kinematic_prefactor(self, dt: np.float64) -> np.float64: return self.χ * dt @@ -272,25 +250,16 @@ def _second_kinematic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: prefac = self._second_kinematic_prefactor(dt) - overload_operator_kinematic_numba( - System.n_nodes, - prefac, - System.kinematic_states.position_collection, - System.kinematic_states.director_collection, - System.dynamic_states.velocity_collection, - System.dynamic_states.omega_collection, - ) + System.update_kinematics(time, prefac) # System.kinematic_states += prefac * System.kinematic_rates(time, prefac) def _second_dynamic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: - prefac = self.λ * dt - overload_operator_dynamic_numba( - System.dynamic_states.rate_collection, - System.dynamic_rates(time, prefac), - ) # System.dynamic_states += prefac * System.dynamic_rates(time, prefac) + prefac = self.λ * dt + System.update_accelerations(time) + System.update_dynamics(time, prefac) def _third_kinematic_prefactor(self, dt: np.float64) -> np.float64: return self.xi_chi_dash_coeff * dt @@ -299,15 +268,7 @@ def _third_kinematic_step( self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 ) -> None: prefac = self._third_kinematic_prefactor(dt) - # Need to fill in - overload_operator_kinematic_numba( - System.n_nodes, - prefac, - System.kinematic_states.position_collection, - System.kinematic_states.director_collection, - System.dynamic_states.velocity_collection, - System.dynamic_states.omega_collection, - ) + System.update_kinematics(time, prefac) # System.kinematic_states += prefac * System.kinematic_rates(time, prefac) From a5f156423e1dc73d8f133375ba7a448a480a8a65 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 21 Dec 2025 22:45:26 -0600 Subject: [PATCH 77/85] refactor: enhance data structures and documentation for rod components --- elastica/rod/data_structures.py | 100 +++++++---- elastica/systems/protocol.py | 3 - tests/analytical.py | 17 +- ...emory_block_with_symplectic_timestepper.py | 164 +----------------- 4 files changed, 84 insertions(+), 200 deletions(-) diff --git a/elastica/rod/data_structures.py b/elastica/rod/data_structures.py index 2e5e1eb4..9ca3247c 100644 --- a/elastica/rod/data_structures.py +++ b/elastica/rod/data_structures.py @@ -1,4 +1,11 @@ -__doc__ = "Data structure wrapper for rod components" +""" +Data structures and Numba-jitted operators for handling rod components +and their integration in a symplectic time-stepping scheme. + +This module provides the `_RodSymplecticStepperMixin` for managing +kinematic and dynamic states of rods, and optimized functions for +their in-place updates. +""" from typing import TYPE_CHECKING import numpy as np @@ -15,8 +22,13 @@ class _RodSymplecticStepperMixin: """ - Mixin class providing necessary methods for integration of - the kinematic and dynamic equations of the rod. + Mixin class providing necessary methods for integration of the kinematic and + dynamic equations of the rod. + + This mixin manages the rod's posture (position and directors), velocity + (linear and angular), and acceleration states. It provides `update_kinematics` + and `update_dynamics` methods to apply updates to these states, typically + called by a symplectic time-stepper. """ n_nodes: int @@ -29,15 +41,28 @@ class _RodSymplecticStepperMixin: omega_collection: NDArray[np.float64] v_w_collection: NDArray[np.float64] # Rate collection # Acceleration state - dvdt_dwdt_collection: NDArray[np.float64] # Second-derivative collection + # acceleration_collection: NDArray[np.float64] + # alpha_collection: NDArray[np.float64] + dvdt_dwdt_collection: NDArray[np.float64] # Second derivative collection def update_kinematics( self, time: np.float64, prefac: np.float64, ) -> None: + """ + Update kinematic state. + + Typically called after velocity and omega (angular velocity) have been updated. + + Parameters + ---------- + time : float + Current time. + prefac : float + Integration prefactor. + """ overload_operator_kinematic_numba( - self.n_nodes, prefac, self.position_collection, self.director_collection, @@ -50,9 +75,21 @@ def update_dynamics( time: np.float64, prefac: np.float64, ) -> None: + """ + Update dynamic state. + + Typically called after acceleration and alpha (angular acceleration) have been updated. + + Parameters + ---------- + time : float + Current time. + prefac : float + Integration prefactor. + """ overload_operator_dynamic_numba( prefac, - self.rate_collection, + self.v_w_collection, self.dvdt_dwdt_collection, ) @@ -64,33 +101,35 @@ def update_dynamics( @njit(cache=True) # type: ignore def overload_operator_kinematic_numba( - n_nodes: int, prefac: np.float64, position_collection: NDArray[np.float64], director_collection: NDArray[np.float64], velocity_collection: NDArray[np.float64], omega_collection: NDArray[np.float64], ) -> None: - """overloaded += operator + """Performs in-place update of kinematic states (position and director) using Numba. - The add for directors is customized to reflect Rodrigues' rotation + This operator updates the position and director collections of a rod based on + its velocity and angular velocity. The director update uses Rodrigues' rotation formula. + Parameters ---------- - scaled_deriv_array : np.ndarray containing dt * (v, ω), - as retured from _DynamicState's `kinematic_rates` method - Returns - ------- - self : _KinematicState instance with inplace modified data - Caveats - ------- - Takes a np.ndarray and not a _KinematicState object (as one expects). - This is done for efficiency reasons, see _DynamicState's `kinematic_rates` - method + prefac : numpy.float64 + Pre-factor (e.g., time step `dt`) to scale the velocity and angular velocity. + position_collection : numpy.ndarray + Position of the rod nodes. Modified in-place. + director_collection : numpy.ndarray + Director (orientation) of the rod elements. Modified in-place. + velocity_collection : numpy.ndarray + Linear velocity of the rod nodes. + omega_collection : numpy.ndarray + Angular velocity of the rod elements. """ # x += v*dt + blocksize = position_collection.shape[1] for i in range(3): - for k in range(n_nodes): + for k in range(blocksize): position_collection[i, k] += prefac * velocity_collection[i, k] rotation_matrix = _get_rotation_matrix(1.0, prefac * omega_collection) director_collection[:] = _batch_matmul(rotation_matrix, director_collection) @@ -102,23 +141,24 @@ def overload_operator_dynamic_numba( rate_collection: NDArray[np.float64], second_deriv_array: NDArray[np.float64], ) -> None: - """overloaded += operator, updating dynamic_rates + """Performs in-place update of dynamic states (linear and angular velocities) using Numba. + + This operator updates the rate collection (which stores linear and angular velocities) + of a rod based on the second derivative array (linear and angular accelerations). + Parameters ---------- - second_deriv_array : np.ndarray containing (dvdt, dωdt), - as retured from _DynamicState's `dynamic_rates` method - Returns - ------- - self : _DynamicState instance with inplace modified data - Caveats - ------- - Takes a np.ndarray and not a _DynamicState object (as one expects). + prefac : numpy.float64 + Pre-factor (e.g., time step `dt`) to scale the second derivative terms. + rate_collection : numpy.ndarray + Collection of linear and angular velocities of the rod. Modified in-place. + second_deriv_array : numpy.ndarray + Collection of linear and angular accelerations (dv/dt, dω/dt) of the rod. """ # Always goes in LHS : that means the update is on the rates alone # (v,ω) += dt * (dv/dt, dω/dt) # rate_collection[..., : n_kinematic_rates] += second_deriv_aray blocksize = second_deriv_array.shape[1] - for i in range(2): for k in range(blocksize): rate_collection[i, k] += prefac * second_deriv_array[i, k] diff --git a/elastica/systems/protocol.py b/elastica/systems/protocol.py index 584f309e..be321417 100644 --- a/elastica/systems/protocol.py +++ b/elastica/systems/protocol.py @@ -44,7 +44,6 @@ class SymplecticSystemProtocol(SystemProtocol, Protocol): (e.g., :class:`PositionVerlet`, :class:`PEFRL`) must satisfy this protocol. The symplectic stepper accesses: - - ``n_nodes`` - ``update_kinematics`` and ``update_dynamics``: called by the timestepper See Also @@ -54,8 +53,6 @@ class SymplecticSystemProtocol(SystemProtocol, Protocol): """ - n_nodes: int - def update_kinematics(self, time: np.float64, prefac: np.float64) -> None: """Update kinematic state. Typically called after compute_internal_forces_and_torques.""" ... diff --git a/tests/analytical.py b/tests/analytical.py index b6108087..a606cba1 100644 --- a/tests/analytical.py +++ b/tests/analytical.py @@ -19,7 +19,7 @@ def state(self, new_state): self._state = new_state -class BaseSymplecticSystem: +class BaseSymplecticSystem(_RodSymplecticStepperMixin): def __init__(self): pass @@ -116,7 +116,7 @@ def analytical_solution(self, time): ) return np.array([analytical_position, analytical_velocity]) - def __call__(self, time, *args, **kwargs): + def __call__(self): return self.A_matrix @ self._state @@ -139,13 +139,15 @@ def __init__(self, omega=2.0 * np.pi, init_val=np.array([1.0, 0.0])): self._kin_state = TestKinematicState(self._state[0:1]) # Create a view instead self._dyn_state = TestDynamicState(self._state[1:2]) # Create a view instead self.n_nodes = self._kin_state.n_nodes + self.position_collection = self._kin_state.position_collection + self.director_collection = self._kin_state.director_collection self.velocity_collection = self._dyn_state.rate_collection[..., 0].reshape(3, 1) self.omega_collection = self._dyn_state.rate_collection[..., 1].reshape(3, 1) + self.v_w_collection = self._dyn_state.rate_collection - def dynamic_rates(self, time, *args, **kwargs): - temp = super(SymplecticUndampedSimpleHarmonicOscillatorSystem, self).__call__( - *args, **kwargs - )[-1] + @property + def dvdt_dwdt_collection(self): + temp = super().__call__()[-1] # Expand rate vector in order to be consistent with time-stepper implementation blocksize = 1 # self._dyn_state.n_kinematic_rates rate = np.zeros((3, blocksize)) @@ -168,6 +170,9 @@ def energy(st): def compute_internal_forces_and_torques(self, time): pass + def update_accelerations(self, time): + pass + def zeroed_out_external_forces_and_torques(self, time): pass diff --git a/tests/test_math/test_memory_block_with_symplectic_timestepper.py b/tests/test_math/test_memory_block_with_symplectic_timestepper.py index a9a40264..18808853 100644 --- a/tests/test_math/test_memory_block_with_symplectic_timestepper.py +++ b/tests/test_math/test_memory_block_with_symplectic_timestepper.py @@ -90,44 +90,6 @@ def update_accelerations(self, time): pass -@pytest.mark.parametrize("n_rods", [1, 2, 5, 6]) -def test_block_structure_kinematic_state_references(n_rods, rng): - """ - This function is testing validity of kinematic state views and compare them - with the block structure vectors. - - Parameters - ---------- - n_rods - - Returns - ------- - - """ - world_rods = [MockRod(rng.randint(10, 30 + 1)) for _ in range(n_rods)] - block_structure = BlockStructureWithSymplecticStepper(world_rods) - - assert_allclose( - block_structure.position_collection, - block_structure.kinematic_states.position_collection, - atol=Tolerance.atol(), - ) - assert np.shares_memory( - block_structure.position_collection, - block_structure.kinematic_states.position_collection, - ) - - assert_allclose( - block_structure.director_collection, - block_structure.kinematic_states.director_collection, - atol=Tolerance.atol(), - ) - assert np.shares_memory( - block_structure.director_collection, - block_structure.kinematic_states.director_collection, - ) - - @pytest.mark.parametrize("n_rods", [1, 2, 5, 6]) def test_block_structure_kinematic_update(n_rods, rng): """ @@ -162,10 +124,7 @@ def test_block_structure_kinematic_update(n_rods, rng): out=correct_director, ) - # block_structure.kinematic_states += block_structure.kinematic_rates(0, prefac) - overload_operator_kinematic_numba( - block_structure.n_nodes, prefac, block_structure.position_collection, block_structure.director_collection, @@ -181,125 +140,6 @@ def test_block_structure_kinematic_update(n_rods, rng): ) -@pytest.mark.parametrize("n_rods", [1, 2, 5, 6]) -def test_block_structure_dynamic_state_references(n_rods, rng): - """ - This function is testing validity of dynamic state views and compare them - with the block structure vectors. - - Parameters - ---------- - n_rods - - Returns - ------- - - """ - world_rods = [MockRod(rng.randint(10, 30 + 1)) for _ in range(n_rods)] - block_structure = BlockStructureWithSymplecticStepper(world_rods) - - assert_allclose( - block_structure.velocity_collection, - block_structure.dynamic_states.velocity_collection, - atol=Tolerance.atol(), - ) - assert np.shares_memory( - block_structure.velocity_collection, - block_structure.dynamic_states.velocity_collection, - ) - - assert_allclose( - block_structure.omega_collection, - block_structure.dynamic_states.omega_collection, - atol=Tolerance.atol(), - ) - assert np.shares_memory( - block_structure.omega_collection, - block_structure.dynamic_states.omega_collection, - ) - - assert_allclose( - block_structure.v_w_collection, - block_structure.dynamic_states.rate_collection, - atol=Tolerance.atol(), - ) - assert np.shares_memory( - block_structure.v_w_collection, block_structure.dynamic_states.rate_collection - ) - - assert_allclose( - block_structure.dvdt_dwdt_collection, - block_structure.dynamic_states.dvdt_dwdt_collection, - atol=Tolerance.atol(), - ) - assert np.shares_memory( - block_structure.dvdt_dwdt_collection, - block_structure.dynamic_states.dvdt_dwdt_collection, - ) - - -@pytest.mark.parametrize("n_rods", [1, 2, 5, 6]) -def test_block_structure_dynamic_state_kinematic_rates(n_rods, rng): - """ - This function is testing validity of dynamic state function and compare them - with the block structure vectors. - - Parameters - ---------- - n_rods - - Returns - ------- - - """ - world_rods = [MockRod(rng.randint(10, 30 + 1)) for _ in range(n_rods)] - block_structure = BlockStructureWithSymplecticStepper(world_rods) - - prefac = 1.0 - - correct_velocity = prefac * block_structure.velocity_collection.copy() - velocity_test = block_structure.kinematic_rates(0, prefac)[0].copy() - - assert_allclose( - correct_velocity, - velocity_test, - atol=Tolerance.atol(), - ) - - correct_omega = prefac * block_structure.omega_collection.copy() - omega_test = block_structure.kinematic_rates(0, prefac)[1].copy() - - assert_allclose( - correct_omega, - omega_test, - atol=Tolerance.atol(), - ) - - -@pytest.mark.parametrize("n_rods", [1, 2, 5, 6]) -def test_block_structure_dynamic_state_dynamic_rates(n_rods, rng): - """ - This function is testing validity of dynamic rates function and compare them - with the block structure vector. - - Parameters - ---------- - n_rods - - Returns - ------- - - """ - world_rods = [MockRod(rng.randint(10, 30 + 1)) for _ in range(n_rods)] - block_structure = BlockStructureWithSymplecticStepper(world_rods) - - assert_allclose( - block_structure.dvdt_dwdt_collection, - block_structure.dynamic_rates(0, prefac=1), - atol=Tolerance.atol(), - ) - - @pytest.mark.parametrize("n_rods", [1, 2, 5, 6]) def test_block_structure_dynamic_update(n_rods, rng): """ @@ -325,7 +165,9 @@ def test_block_structure_dynamic_update(n_rods, rng): correct_v_w = v_w + prefac * dvdt_dwdt overload_operator_dynamic_numba( - block_structure.v_w_collection, block_structure.dynamic_rates(0, prefac) + prefac, + block_structure.v_w_collection, + block_structure.dvdt_dwdt_collection, ) assert_allclose(correct_v_w, block_structure.v_w_collection, atol=Tolerance.atol()) From 3086ae59b5317067771ec17a9fea88e735b2f9df Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 21 Dec 2025 22:56:24 -0600 Subject: [PATCH 78/85] docs: Update PackageDesign.md for new SymplecticSystemProtocol --- docs/advanced/PackageDesign.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced/PackageDesign.md b/docs/advanced/PackageDesign.md index afef5aa1..e885adcd 100644 --- a/docs/advanced/PackageDesign.md +++ b/docs/advanced/PackageDesign.md @@ -20,7 +20,7 @@ Elastica package uses [structural subtyping](https://peps.python.org/pep-0544/) direction RL subgraph Systems Protocol direction RL - SymST["SymplecticSystemProtocol
(Necessary to be stepped by the timestepper)"] + SymST["SymplecticSystemProtocol
(Mixin for timestepper)
• update_kinematics
• update_dynamics"] style SymST text-align:left StaticSystemType["Static System Type"
• Plane] SystemType["(Dynamic) System Type
• CosseratRod (Rod)
• Sphere (RigidBody)
• Cylinder (RigidBody)"] @@ -46,7 +46,7 @@ Elastica package uses [structural subtyping](https://peps.python.org/pep-0544/) - Any object that conforms to `StaticSystemProtocol` can be added to the system collection. - If you want to add custom type to the system, you can use `append_allowed_types` to add it to the system collection. To add associated block support, you can use `enable_block_supports`. -- Among the systems added to the system collection, only objects that conform to `SystemProtocol` will be integrated by the timestepper. +- Among the systems added to the system collection, only objects that conform to `SymplecticSystemProtocol` will be integrated by the symplectic timestepper. This protocol requires `update_kinematics(time, prefac)` and `update_dynamics(time, prefac)` methods to be implemented. - If block support is available for a system, they will be collected together during the `finalize` step, and passed to the timestepper. From fecb75e9b336ea0a4ab1319521d14de8bb5de0f2 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sun, 21 Dec 2025 23:09:50 -0600 Subject: [PATCH 79/85] remove outdated constructor --- elastica/memory_block/memory_block_rod.py | 3 --- .../test_math/test_memory_block_with_symplectic_timestepper.py | 1 - 2 files changed, 4 deletions(-) diff --git a/elastica/memory_block/memory_block_rod.py b/elastica/memory_block/memory_block_rod.py index bbad5591..8940328f 100644 --- a/elastica/memory_block/memory_block_rod.py +++ b/elastica/memory_block/memory_block_rod.py @@ -260,9 +260,6 @@ def __init__( self.rest_kappa, self.periodic_boundary_voronoi_idx ) - # Initialize the mixin class for symplectic time-stepper. - _RodSymplecticStepperMixin.__init__(self) - def _allocate_block_variables_in_nodes(self, systems: list[RodType]) -> None: """ This function takes system collection and allocates the variables on diff --git a/tests/test_math/test_memory_block_with_symplectic_timestepper.py b/tests/test_math/test_memory_block_with_symplectic_timestepper.py index 18808853..78c81cb4 100644 --- a/tests/test_math/test_memory_block_with_symplectic_timestepper.py +++ b/tests/test_math/test_memory_block_with_symplectic_timestepper.py @@ -84,7 +84,6 @@ class BlockStructureWithSymplecticStepper( ): def __init__(self, systems): MemoryBlockCosseratRod.__init__(self, systems, [i for i in range(len(systems))]) - _RodSymplecticStepperMixin.__init__(self) def update_accelerations(self, time): pass From 40a574df60d14bd45790056747ea851d8905757f Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Wed, 24 Dec 2025 01:47:22 -0600 Subject: [PATCH 80/85] preserve: old damping --- elastica/__init__.py | 1 + elastica/dissipation.py | 106 +++++++++++++++++++++++++++++-- elastica/modules/contact.py | 4 -- elastica/timestepper/__init__.py | 4 +- 4 files changed, 104 insertions(+), 11 deletions(-) diff --git a/elastica/__init__.py b/elastica/__init__.py index c745fa3f..9f93e7fb 100644 --- a/elastica/__init__.py +++ b/elastica/__init__.py @@ -56,6 +56,7 @@ DamperBase, AnalyticalLinearDamper, LaplaceDissipationFilter, + RayleighDissipation, ) from elastica.modules.base_system import BaseSystemCollection from elastica.modules.callbacks import CallBacks diff --git a/elastica/dissipation.py b/elastica/dissipation.py index 3d586105..3136a291 100644 --- a/elastica/dissipation.py +++ b/elastica/dissipation.py @@ -14,8 +14,10 @@ import numpy as np from numpy.typing import NDArray +from elastica.typing import SystemType -T = TypeVar("T") + +T = TypeVar("T", bound=SystemType) class DamperBase(Generic[T], ABC): @@ -91,7 +93,6 @@ def dampen_rates(self, system: T, time: np.float64) -> None: The time of simulation. """ - pass DampenType: TypeAlias = Callable[[RodType], None] @@ -142,7 +143,7 @@ class AnalyticalLinearDamper(DamperBase): >>> simulator.dampen(rod).using( ... AnalyticalLinearDamper, ... damping_constant=0.1, - ... time_step = 1E-4, # Simulation time-step + ... time_step=1E-4, ... ) Notes @@ -207,7 +208,7 @@ def __init__(self, time_step: np.float64, **kwargs: Any) -> None: ) else: # Invalid parameter combination - message = ( + raise ValueError( "AnalyticalLinearDamper usage:\n" "\tsimulator.dampen(rod).using(\n" "\t\tAnalyticalLinearDamper,\n" @@ -228,7 +229,6 @@ def __init__(self, time_step: np.float64, **kwargs: Any) -> None: "\t\ttime_step=...,\n" "\t)\n" ) - raise ValueError(message) def _deprecated_damping_protocol( self, damping_constant: np.float64, time_step: np.float64 @@ -298,6 +298,102 @@ def dampen_rates(self, system: RodType, time: np.float64) -> None: self._dampen_rates_protocol(system) +class RayleighDissipation(DamperBase): + """ + Rayleigh dissipation model matching the C++ implementation. + + This class implements the C++ force-based damping model for compatibility. + It is deprecated in favor of :class:`AnalyticalLinearDamper` which provides + better numerical stability and unconditional stability. This implementation + is kept for validation for old cases. + + This class implements force-based damping that matches the C++ nest-simulator + implementation. It adds damping forces and torques proportional to velocities: + + .. math:: + + \\mathbf{F}_{damp} = -\\nu \\mathbf{v} + + \\boldsymbol{\\tau}_{damp} = -\\nu \\boldsymbol{\\omega} + + where the damping coefficient :math:`\\nu` can decay exponentially over time. + + The damping forces are added to external forces and integrated through the + time stepper, which may require smaller time steps for large damping values. + + Parameters + ---------- + damping_constant : float + Damping coefficient :math:`\\nu` (per unit length). Units: [1/s] or [kg/(m·s)] + + Examples + -------- + .. code-block:: python + + simulator.dampen(rod).using( + RayleighDissipation, + damping_constant=0.1, + ) + + See Also + -------- + AnalyticalLinearDamper : Recommended alternative with better stability + LaplaceDissipationFilter : Alternative filtering-based dissipation + """ + + def __init__( + self, + damping_constant: np.float64, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + + if damping_constant < 0.0: + raise ValueError("damping_constant must be non-negative") + + _relaxation_time = 0.0 # relaxation: scale damping by exp(-time/relaxation) + + # Pre-compute average element length for rescaling + rest_lengths = self._system.rest_lengths + n_elems = self._system.n_elems + self._average_element_length = np.sum(rest_lengths) / n_elems + + if _relaxation_time > 0.0: + self.get_nu = lambda time: damping_constant * np.exp( + -time / _relaxation_time + ) + else: + self.get_nu = lambda time: damping_constant + + def dampen_rates(self, system: RodType, time: np.float64) -> None: + """ + Apply Rayleigh dissipation forces and torques. + + Parameters + ---------- + system : RodType + Rod system to apply damping to + time : float + Current simulation time + """ + # Rescale since nu is per unit length + nu_now = self.get_nu(time) * self._average_element_length + + # Apply damping forces: F = -nu * v + # Boundary factor: 0.5 at endpoints, 1.0 otherwise (matches C++) + # dampingForces[i] -= (nuNow * factor) * v[i] + for i in range(system.n_nodes): + factor = 0.5 if (i == 0 or i == system.n_nodes - 1) else 1.0 + damping_force = -(nu_now * factor) * system.velocity_collection[:, i] + system.external_forces[:, i] += damping_force + + # Apply damping torques: T = -nu * w + # dampingTorques[i] -= nuNow * w[i] + for i in range(system.n_elems): + damping_torque = -nu_now * system.omega_collection[:, i] + system.external_torques[:, i] += damping_torque + + class LaplaceDissipationFilter(DamperBase): """ Laplace Dissipation Filter class. This class corresponds qualitatively to a diff --git a/elastica/modules/contact.py b/elastica/modules/contact.py index 5647e472..93c60636 100644 --- a/elastica/modules/contact.py +++ b/elastica/modules/contact.py @@ -15,12 +15,8 @@ ) from .protocol import SystemCollectionProtocol, ModuleProtocol -import logging - from elastica.contact_forces import NoContact -logger = logging.getLogger(__name__) - class Contact(SystemCollectionProtocol): """ diff --git a/elastica/timestepper/__init__.py b/elastica/timestepper/__init__.py index d87e7840..b19e50f4 100644 --- a/elastica/timestepper/__init__.py +++ b/elastica/timestepper/__init__.py @@ -11,8 +11,8 @@ from .protocol import StepperProtocol -# Deprecated: Remove in the future version -# Many script still uses this method to control timestep. Keep it for backward compatibility +# Deprecated: Kept for backward compatibility. +# Many script still uses this method to control timestep. def extend_stepper_interface( stepper: StepperProtocol, system_collection: SystemCollectionType ) -> tuple[ From ee5506c94057ff4c9cca0251e7e2f29d69890eb8 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Wed, 24 Dec 2025 03:56:49 -0600 Subject: [PATCH 81/85] fix hinting --- elastica/dissipation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elastica/dissipation.py b/elastica/dissipation.py index 3136a291..f1a691ec 100644 --- a/elastica/dissipation.py +++ b/elastica/dissipation.py @@ -377,7 +377,7 @@ def dampen_rates(self, system: RodType, time: np.float64) -> None: Current simulation time """ # Rescale since nu is per unit length - nu_now = self.get_nu(time) * self._average_element_length + nu_now = self.get_nu(time) * self._average_element_length # type: ignore # Apply damping forces: F = -nu * v # Boundary factor: 0.5 at endpoints, 1.0 otherwise (matches C++) From aae60a1e6bc53fd83d51e573fb30765eb7bdfa77 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Fri, 26 Dec 2025 14:22:34 -0600 Subject: [PATCH 82/85] remove action to publish --- .github/workflows/publish-to-pypi.yml | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 .github/workflows/publish-to-pypi.yml diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml deleted file mode 100644 index 86f70c90..00000000 --- a/.github/workflows/publish-to-pypi.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: publish - -on: - release: - types: [created] - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - uv-version: latest - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - name: Build - run: | - make build - - name: Publish distribution 📦 to PyPI - if: startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_API_TOKEN }} From f2eea948f1944167115dc9bac5016c26a7b339cb Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sat, 27 Dec 2025 16:20:08 -0600 Subject: [PATCH 83/85] add few extra tools in pre-commit --- .pre-commit-config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f0395a35..deeafcc1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,8 +10,11 @@ repos: - id: trailing-whitespace - id: check-json - id: check-yaml + - id: check-toml - id: end-of-file-fixer exclude: LICENSE + - id: check-added-large-files + - id: mixed-line-ending - repo: local hooks: From ba887198497ef669598384bf27cd1a2abaa03342 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sat, 24 Jan 2026 15:58:02 -0600 Subject: [PATCH 84/85] update: remove layer of stepper protocol structure --- elastica/timestepper/__init__.py | 2 +- elastica/timestepper/protocol.py | 22 +---- elastica/timestepper/symplectic_steppers.py | 90 +++++++++++---------- elastica/typing.py | 6 +- 4 files changed, 52 insertions(+), 68 deletions(-) diff --git a/elastica/timestepper/__init__.py b/elastica/timestepper/__init__.py index b19e50f4..f061967e 100644 --- a/elastica/timestepper/__init__.py +++ b/elastica/timestepper/__init__.py @@ -22,7 +22,7 @@ def extend_stepper_interface( SteppersOperatorsType, ]: try: - stepper_methods: SteppersOperatorsType = stepper.steps_and_prefactors + stepper_methods: SteppersOperatorsType = stepper.steps_and_prefactors # type: ignore do_step_method: Callable = stepper.do_step # type: ignore[attr-defined] except AttributeError as e: raise NotImplementedError(f"{stepper} stepper is not supported.") from e diff --git a/elastica/timestepper/protocol.py b/elastica/timestepper/protocol.py index d228c0ea..b1287b19 100644 --- a/elastica/timestepper/protocol.py +++ b/elastica/timestepper/protocol.py @@ -3,8 +3,7 @@ from typing import Protocol from elastica.typing import ( - SteppersOperatorsType, - StepType, + SystemType, SystemCollectionType, ) from elastica.systems.protocol import SymplecticSystemProtocol @@ -15,15 +14,6 @@ class StepperProtocol(Protocol): """Protocol for all time-steppers""" - steps_and_prefactors: SteppersOperatorsType - - def __init__(self) -> None: ... - - @property - def n_stages(self) -> int: ... - - def step_methods(self) -> SteppersOperatorsType: ... - def step( self, SystemCollection: SystemCollectionType, @@ -32,13 +22,5 @@ def step( ) -> np.float64: ... def step_single_instance( - self, System: SymplecticSystemProtocol, time: np.float64, dt: np.float64 + self, System: SystemType, time: np.float64, dt: np.float64 ) -> np.float64: ... - - -class SymplecticStepperProtocol(StepperProtocol, Protocol): - """symplectic stepper protocol.""" - - def get_steps(self) -> list[StepType]: ... - - def get_prefactors(self) -> list[StepType]: ... diff --git a/elastica/timestepper/symplectic_steppers.py b/elastica/timestepper/symplectic_steppers.py index db89e2a0..98c8bb87 100644 --- a/elastica/timestepper/symplectic_steppers.py +++ b/elastica/timestepper/symplectic_steppers.py @@ -1,11 +1,12 @@ __doc__ = """Symplectic time steppers and concepts for integrating the kinematic and dynamic equations of rod-like objects. """ -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Callable from itertools import zip_longest from elastica.typing import ( SystemCollectionType, + SystemType, StepType, SteppersOperatorsType, ) @@ -13,7 +14,7 @@ import numpy as np from elastica.systems.protocol import SymplecticSystemProtocol -from .protocol import SymplecticStepperProtocol +from .protocol import StepperProtocol """ Developer Note @@ -25,10 +26,13 @@ class SymplecticStepperMixin: - def __init__(self: SymplecticStepperProtocol): + get_steps: Callable[[], list[StepType]] + get_prefactors: Callable[[], list[StepType]] + + def __init__(self) -> None: self.steps_and_prefactors: SteppersOperatorsType = self.step_methods() - def step_methods(self: SymplecticStepperProtocol) -> SteppersOperatorsType: + def step_methods(self) -> SteppersOperatorsType: # Let the total number of steps for the Symplectic method # be (2*n + 1) (for time-symmetry). _steps: list[StepType] = self.get_steps() @@ -56,32 +60,14 @@ def no_operation(*args: Any) -> None: ) @property - def n_stages(self: SymplecticStepperProtocol) -> int: + def n_stages(self) -> int: return len(self.steps_and_prefactors) def step( - self: SymplecticStepperProtocol, + self, SystemCollection: SystemCollectionType, time: np.float64 | float, dt: np.float64 | float, - ) -> np.float64: - return SymplecticStepperMixin.do_step( - self, - self.steps_and_prefactors, - SystemCollection, - np.float64(time), - np.float64(dt), - ) - - # TODO: Merge with .step method in the future. - # DEPRECATED: Use .step instead. - @staticmethod - def do_step( - TimeStepper: SymplecticStepperProtocol, - steps_and_prefactors: SteppersOperatorsType, - SystemCollection: SystemCollectionType, - time: np.float64, - dt: np.float64, ) -> np.float64: """ Function for doing symplectic stepper over the user defined rods (system). @@ -92,53 +78,73 @@ def do_step( The time after the integration step. """ - for kin_prefactor, kin_step, dyn_step in steps_and_prefactors[:-1]: + simulation_time = np.float64(time) + simulation_dt = np.float64(dt) + for kin_prefactor, kin_step, dyn_step in self.steps_and_prefactors[:-1]: for system in SystemCollection.final_systems(): - kin_step(system, time, dt) + kin_step(system, simulation_time, simulation_dt) - time += kin_prefactor(dt) + simulation_time += kin_prefactor(simulation_dt) # Constrain only values - SystemCollection.constrain_values(time) + SystemCollection.constrain_values(simulation_time) # We need internal forces and torques because they are used by interaction module. for system in SystemCollection.final_systems(): - system.compute_internal_forces_and_torques(time) + system.compute_internal_forces_and_torques(simulation_time) # Add external forces, controls etc. - SystemCollection.synchronize(time) + SystemCollection.synchronize(simulation_time) for system in SystemCollection.final_systems(): - dyn_step(system, time, dt) + dyn_step(system, simulation_time, simulation_dt) # Constrain only rates - SystemCollection.constrain_rates(time) + SystemCollection.constrain_rates(simulation_time) # Peel the last kinematic step and prefactor alone - last_kin_prefactor = steps_and_prefactors[-1][0] - last_kin_step = steps_and_prefactors[-1][1] + last_kin_prefactor = self.steps_and_prefactors[-1][0] + last_kin_step = self.steps_and_prefactors[-1][1] for system in SystemCollection.final_systems(): - last_kin_step(system, time, dt) - time += last_kin_prefactor(dt) - SystemCollection.constrain_values(time) + last_kin_step(system, simulation_time, simulation_dt) + simulation_time += last_kin_prefactor(simulation_dt) + SystemCollection.constrain_values(simulation_time) # Call back function, will call the user defined call back functions and store data - SystemCollection.apply_callbacks(time, round(time / dt)) + SystemCollection.apply_callbacks( + simulation_time, round(simulation_time / simulation_dt) + ) # Zero out the external forces and torques for system in SystemCollection.final_systems(): - system.zeroed_out_external_forces_and_torques(time) + system.zeroed_out_external_forces_and_torques(simulation_time) + + return simulation_time + + @staticmethod + def do_step( + TimeStepper: StepperProtocol, + steps_and_prefactors: SteppersOperatorsType, + SystemCollection: SystemCollectionType, + time: np.float64, + dt: np.float64, + ) -> np.float64: # pragma: no cover + from warning import warn - return time + warn("This method is deprecated. Use the instance method .step instead.") + return Timestepper.step(SystemCollection, time, dt) # type: ignore def step_single_instance( - self: SymplecticStepperProtocol, - System: SymplecticSystemProtocol, + self, + System: SystemType, time: np.float64, dt: np.float64, ) -> np.float64: + """ + (The function is used for single system instance, mainly for testing purposes.) + """ for kin_prefactor, kin_step, dyn_step in self.steps_and_prefactors[:-1]: kin_step(System, time, dt) diff --git a/elastica/typing.py b/elastica/typing.py index 72fe7a1f..9e1be93c 100644 --- a/elastica/typing.py +++ b/elastica/typing.py @@ -24,10 +24,7 @@ SystemProtocol, SymplecticSystemProtocol, ) - from .timestepper.protocol import ( - StepperProtocol, - SymplecticStepperProtocol, - ) + from .timestepper.protocol import StepperProtocol from .memory_block.protocol import BlockSystemProtocol else: @@ -40,7 +37,6 @@ StaticSystemProtocol = "StaticSystemProtocol" SymplecticSystemProtocol = "SymplecticSystemProtocol" StepperProtocol = "StepperProtocol" - SymplecticStepperProtocol = "SymplecticStepperProtocol" BlockSystemProtocol = "BlockSystemProtocol" From 7ce516984eefc75fab74fb57113f57b425a4d27a Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Sat, 24 Jan 2026 17:14:19 -0600 Subject: [PATCH 85/85] move out energy equation to rod_base --- elastica/rod/cosserat_rod.py | 70 ------------------------------------ 1 file changed, 70 deletions(-) diff --git a/elastica/rod/cosserat_rod.py b/elastica/rod/cosserat_rod.py index 4801c7da..e035c994 100644 --- a/elastica/rod/cosserat_rod.py +++ b/elastica/rod/cosserat_rod.py @@ -23,7 +23,6 @@ _average, ) from .factory_function import allocate -from .knot_theory import KnotTheory position_difference_kernel = _difference position_average = _average @@ -627,75 +626,6 @@ def zeroed_out_external_forces_and_torques(self, time: np.float64) -> None: self.external_forces, self.external_torques ) - def compute_translational_energy(self) -> NDArray[np.float64]: - """ - Compute total translational energy of the rod at the instance. - """ - return ( - 0.5 - * ( - self.mass - * np.einsum( - "ij, ij-> j", self.velocity_collection, self.velocity_collection - ) - ).sum() - ) - - def compute_rotational_energy(self) -> NDArray[np.float64]: - """ - Compute total rotational energy of the rod at the instance. - """ - J_omega_upon_e = ( - _batch_matvec(self.mass_second_moment_of_inertia, self.omega_collection) - / self.dilatation - ) - return 0.5 * np.einsum("ik,ik->k", self.omega_collection, J_omega_upon_e).sum() - - def compute_velocity_center_of_mass(self) -> NDArray[np.float64]: - """ - Compute velocity center of mass of the rod at the instance. - """ - mass_times_velocity = np.einsum("j,ij->ij", self.mass, self.velocity_collection) - sum_mass_times_velocity = np.einsum("ij->i", mass_times_velocity) - - return sum_mass_times_velocity / self.mass.sum() - - def compute_position_center_of_mass(self) -> NDArray[np.float64]: - """ - Compute position center of mass of the rod at the instance. - """ - mass_times_position = np.einsum("j,ij->ij", self.mass, self.position_collection) - sum_mass_times_position = np.einsum("ij->i", mass_times_position) - - return sum_mass_times_position / self.mass.sum() - - def compute_bending_energy(self) -> NDArray[np.float64]: - """ - Compute total bending energy of the rod at the instance. - """ - kappa_diff = self.kappa - self.rest_kappa - bending_internal_torques = _batch_matvec(self.bend_matrix, kappa_diff) - - return ( - 0.5 - * ( - _batch_dot(kappa_diff, bending_internal_torques) - * self.rest_voronoi_lengths - ).sum() - ) - - def compute_shear_energy(self) -> NDArray[np.float64]: - """ - Compute total shear energy of the rod at the instance. - """ - sigma_diff = self.sigma - self.rest_sigma - shear_internal_forces = _batch_matvec(self.shear_matrix, sigma_diff) - - return ( - 0.5 - * (_batch_dot(sigma_diff, shear_internal_forces) * self.rest_lengths).sum() - ) - # Below is the numba-implementation of Cosserat Rod equations. They don't need to be visible by users.