Skip to content

Commit f66a525

Browse files
authored
Merge pull request #513 from skim0119/cleanup-contact
[Bug] Fix sphere-rod contact
2 parents bbdaba5 + efa4405 commit f66a525

File tree

13 files changed

+444
-980
lines changed

13 files changed

+444
-980
lines changed

docs/api/contact.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ Contact
88
Description
99
-----------
1010

11+
.. note::
12+
(CAUTION)
13+
The contact is recommended to be added at last. This is because contact forces often includes
14+
friction that may depend on other normal forces and contraints to be calculated accurately.
15+
Be careful on the order of adding interactions.
16+
1117
.. rubric:: Available Contact Classes
1218

1319
.. autosummary::

elastica/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@
2828
EndpointForcesSinusoidal,
2929
)
3030
from elastica.interaction import (
31-
AnisotropicFrictionalPlane,
32-
InteractionPlane,
3331
SlenderBodyTheory,
3432
)
3533
from elastica.joint import (

elastica/_contact_functions.py

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
_dot_product,
55
_norm,
66
_find_min_dist,
7+
_find_min_dist_cylinder_sphere,
78
_find_slipping_elements,
89
_node_to_element_mass_or_force,
910
_elements_to_nodes_inplace,
@@ -49,6 +50,13 @@ def _calculate_contact_forces_rod_cylinder(
4950
velocity_damping_coefficient: np.float64,
5051
friction_coefficient: np.float64,
5152
) -> None:
53+
"""
54+
Calculates the contact forces between a rod and a cylinder.
55+
56+
This function computes the linear and angular contact forces acting on both the rod and the cylinder
57+
when they come into contact. It considers spring-damper-based contact forces as well as friction.
58+
The forces are applied to the nodes of the rod and the center of mass of the cylinder.
59+
"""
5260
# We already pass in only the first n_elem x
5361
n_points = x_collection_rod.shape[1]
5462
cylinder_total_contact_forces = np.zeros((3))
@@ -174,6 +182,9 @@ def _calculate_contact_forces_rod_rod(
174182
contact_k: np.float64,
175183
contact_nu: np.float64,
176184
) -> None:
185+
"""
186+
Calculates the contact forces between two rods.
187+
"""
177188
# We already pass in only the first n_elem x
178189
n_points_rod_one = x_collection_rod_one.shape[1]
179190
n_points_rod_two = x_collection_rod_two.shape[1]
@@ -283,6 +294,12 @@ def _calculate_contact_forces_self_rod(
283294
contact_k: np.float64,
284295
contact_nu: np.float64,
285296
) -> None:
297+
"""
298+
Calculates the self-contact forces within a single rod.
299+
300+
This function prevents self-penetration of a rod by calculating contact forces between different elements
301+
of the same rod. A skip parameter is used to avoid checking adjacent elements.
302+
"""
286303
# We already pass in only the first n_elem x
287304
n_points_rod = x_collection_rod.shape[1]
288305
edge_collection_rod_one = _batch_product_k_ik_to_ik(length_rod, tangent_rod)
@@ -364,41 +381,44 @@ def _calculate_contact_forces_self_rod(
364381
def _calculate_contact_forces_rod_sphere(
365382
x_collection_rod: NDArray[np.float64],
366383
edge_collection_rod: NDArray[np.float64],
367-
x_sphere_center: NDArray[np.float64],
368-
x_sphere_tip: NDArray[np.float64],
369-
edge_sphere: NDArray[np.float64],
384+
x_sphere: NDArray[np.float64],
370385
radii_sum: NDArray[np.float64],
371386
length_sum: NDArray[np.float64],
372-
internal_forces_rod: NDArray[np.float64],
373387
external_forces_rod: NDArray[np.float64],
374388
external_forces_sphere: NDArray[np.float64],
375-
external_torques_sphere: NDArray[np.float64],
376-
sphere_director_collection: NDArray[np.float64],
377389
velocity_rod: NDArray[np.float64],
378390
velocity_sphere: NDArray[np.float64],
379391
contact_k: np.float64,
380392
contact_nu: np.float64,
381393
velocity_damping_coefficient: np.float64,
382394
friction_coefficient: np.float64,
383395
) -> None:
396+
"""
397+
Calculates the contact forces between a rod and a sphere.
398+
399+
This function computes the linear and angular contact forces acting on both the rod and the sphere
400+
when they come into contact. It considers spring-damper-based contact forces as well as friction.
401+
The forces are applied to the nodes of the rod and the center of mass of the sphere.
402+
"""
384403
# We already pass in only the first n_elem x
385404
n_points = x_collection_rod.shape[1]
386405
sphere_total_contact_forces = np.zeros((3))
387-
sphere_total_contact_torques = np.zeros((3))
388406
for i in range(n_points):
389407
# Element-wise bounding box
390408
x_selected = x_collection_rod[..., i]
391409
# x_sphere is already a (,) array from outside
392-
del_x = x_selected - x_sphere_tip
410+
del_x = x_selected - x_sphere
393411
norm_del_x = _norm(del_x)
394412

395413
# If outside then don't process
396414
if norm_del_x >= (radii_sum[i] + length_sum[i]):
397415
continue
398416

399417
# find the shortest line segment between the two centerline
400-
distance_vector, x_sphere_contact_point, _ = _find_min_dist(
401-
x_selected, edge_collection_rod[..., i], x_sphere_tip, edge_sphere
418+
distance_vector, _, _ = _find_min_dist_cylinder_sphere(
419+
x_selected,
420+
edge_collection_rod[..., i],
421+
x_sphere,
402422
)
403423
distance_vector_length = _norm(distance_vector)
404424
distance_vector /= distance_vector_length
@@ -453,37 +473,22 @@ def _calculate_contact_forces_rod_sphere(
453473
# Update contact force
454474
net_contact_force += friction_force
455475

456-
# Torques acting on the cylinder
457-
moment_arm = x_sphere_contact_point - x_sphere_center
458-
459476
# Add it to the rods at the end of the day
460477
if i == 0:
461478
external_forces_rod[..., i] -= 2 / 3 * net_contact_force
462479
external_forces_rod[..., i + 1] -= 4 / 3 * net_contact_force
463480
sphere_total_contact_forces += 2.0 * net_contact_force
464-
sphere_total_contact_torques += np.cross(
465-
moment_arm, 2.0 * net_contact_force
466-
)
467481
elif i == n_points - 1:
468482
external_forces_rod[..., i] -= 4 / 3 * net_contact_force
469483
external_forces_rod[..., i + 1] -= 2 / 3 * net_contact_force
470484
sphere_total_contact_forces += 2.0 * net_contact_force
471-
sphere_total_contact_torques += np.cross(
472-
moment_arm, 2.0 * net_contact_force
473-
)
474485
else:
475486
external_forces_rod[..., i] -= net_contact_force
476487
external_forces_rod[..., i + 1] -= net_contact_force
477488
sphere_total_contact_forces += 2.0 * net_contact_force
478-
sphere_total_contact_torques += np.cross(
479-
moment_arm, 2.0 * net_contact_force
480-
)
481489

482490
# Update the cylinder external forces and torques
483491
external_forces_sphere[..., 0] += sphere_total_contact_forces
484-
external_torques_sphere[..., 0] += (
485-
sphere_director_collection @ sphere_total_contact_torques
486-
)
487492

488493

489494
@njit(cache=True) # type: ignore
@@ -501,17 +506,16 @@ def _calculate_contact_forces_rod_plane(
501506
external_forces: NDArray[np.float64],
502507
) -> tuple[NDArray[np.float64], NDArray[np.intp]]:
503508
"""
504-
This function computes the plane force response on the element, in the
505-
case of contact. Contact model given in Eqn 4.8 Gazzola et. al. RSoS 2018 paper
506-
is used.
509+
Calculates the contact forces between a rod and a plane.
510+
511+
This function computes the contact force exerted by a flat plane on a rod.
512+
It includes a linear spring-damper model for the normal force to handle penetration
513+
and a response to prevent the rod from passing through the plane.
514+
It returns the magnitude of the plane response force and indices of elements not in contact.
507515
508-
Parameters
509-
----------
510-
system
516+
Contact model given in Eqn 4.8 Gazzola et. al. RSoS 2018 paper
517+
is used.
511518
512-
Returns
513-
-------
514-
magnitude of the plane response
515519
"""
516520

517521
# Compute plane response force
@@ -597,6 +601,9 @@ def _calculate_contact_forces_rod_plane_with_anisotropic_friction(
597601
internal_torques: NDArray[np.float64],
598602
external_torques: NDArray[np.float64],
599603
) -> None:
604+
"""
605+
Calculates contact forces between a rod and a plane with anisotropic friction.
606+
"""
600607
(
601608
plane_response_force_mag,
602609
no_contact_point_idx,
@@ -796,6 +803,9 @@ def _calculate_contact_forces_cylinder_plane(
796803
velocity_collection: NDArray[np.float64],
797804
external_forces: NDArray[np.float64],
798805
) -> tuple[NDArray[np.float64], NDArray[np.intp]]:
806+
"""
807+
Calculates the contact forces between a cylinder and a plane.
808+
"""
799809

800810
# Compute plane response force
801811
# total_forces = system.internal_forces + system.external_forces

elastica/contact_forces.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -433,10 +433,7 @@ def apply_contact(
433433
):
434434
return
435435

436-
x_sph = (
437-
system_two.position_collection[..., 0]
438-
- system_two.radius * system_two.director_collection[2, :, 0]
439-
)
436+
sphere_position = system_two.position_collection[..., 0]
440437

441438
rod_element_position = 0.5 * (
442439
system_one.position_collection[..., 1:]
@@ -445,16 +442,11 @@ def apply_contact(
445442
_calculate_contact_forces_rod_sphere(
446443
rod_element_position,
447444
system_one.lengths * system_one.tangents,
448-
system_two.position_collection[..., 0],
449-
x_sph,
450-
system_two.radius * system_two.director_collection[2, :, 0],
445+
sphere_position,
451446
system_one.radius + system_two.radius,
452447
system_one.lengths + 2 * system_two.radius,
453-
system_one.internal_forces,
454448
system_one.external_forces,
455449
system_two.external_forces,
456-
system_two.external_torques,
457-
system_two.director_collection[:, :, 0],
458450
system_one.velocity_collection,
459451
system_two.velocity_collection,
460452
self.k,

elastica/contact_utils.py

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ def _find_min_dist(
4141
x2: NDArray[np.float64],
4242
e2: NDArray[np.float64],
4343
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
44+
"""
45+
Find the minimum distance between two centerline segments: (x1, edge1) and (x2, edge2).
46+
"""
4447
e1e1 = _dot_product(e1, e1) # type: ignore
4548
e1e2 = _dot_product(e1, e2) # type: ignore
4649
e2e2 = _dot_product(e2, e2) # type: ignore
@@ -105,11 +108,32 @@ def _find_min_dist(
105108
return x2 + s * e2 - x1 - t * e1, x2 + s * e2, x1 - t * e1
106109

107110

111+
@numba.njit(cache=True) # type: ignore
112+
def _find_min_dist_cylinder_sphere(
113+
x1: NDArray[np.float64],
114+
e1: NDArray[np.float64],
115+
x2: NDArray[np.float64],
116+
) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
117+
"""
118+
Find the minimum distance between centerline segment and point: (x1, edge1) and (x2).
119+
"""
120+
e1e1 = _dot_product(e1, e1) # type: ignore
121+
x1e1 = _dot_product(x1, e1) # type: ignore
122+
x2e1 = _dot_product(e1, x2) # type: ignore
123+
124+
# Parametrization
125+
t = (x2e1 - x1e1) / e1e1 # Comes from taking dot of e1 with a normal
126+
t = _clip(t, 0.0, 1.0)
127+
128+
# Return distance, contact point of system 2, contact point of system 1
129+
return x2 - x1 - t * e1, x2, x1 - t * e1
130+
131+
108132
@numba.njit(cache=True) # type: ignore
109133
def _aabbs_not_intersecting(
110134
aabb_one: NDArray[np.float64], aabb_two: NDArray[np.float64]
111135
) -> Literal[1, 0]:
112-
"""Returns true if not intersecting else false"""
136+
"""Checks if two axis-aligned bounding boxes (AABBs) are not intersecting."""
113137
if (aabb_one[0, 1] < aabb_two[0, 0]) | (aabb_one[0, 0] > aabb_two[0, 1]):
114138
return 1
115139
if (aabb_one[1, 1] < aabb_two[1, 0]) | (aabb_one[1, 0] > aabb_two[1, 1]):
@@ -130,6 +154,13 @@ def _prune_using_aabbs_rod_cylinder(
130154
cylinder_radius: NDArray[np.float64],
131155
cylinder_length: NDArray[np.float64],
132156
) -> Literal[1, 0]:
157+
"""
158+
Prunes broad-phase collision detection between a rod and a cylinder using AABBs.
159+
160+
This function checks for intersection between the axis-aligned bounding boxes (AABBs)
161+
of a rod and a cylinder. It's a quick way to rule out collision without performing
162+
a more detailed and expensive check.
163+
"""
133164
max_possible_dimension = np.zeros((3,))
134165
aabb_rod = np.empty((3, 2))
135166
aabb_cylinder = np.empty((3, 2))
@@ -171,6 +202,13 @@ def _prune_using_aabbs_rod_rod(
171202
rod_two_radius_collection: NDArray[np.float64],
172203
rod_two_length_collection: NDArray[np.float64],
173204
) -> Literal[1, 0]:
205+
"""
206+
Prunes broad-phase collision detection between two rods using AABBs.
207+
208+
This function checks for intersection between the axis-aligned bounding boxes (AABBs)
209+
of two rods. It's a quick way to rule out collision without performing
210+
a more detailed and expensive check.
211+
"""
174212
max_possible_dimension = np.zeros((3,))
175213
aabb_rod_one = np.empty((3, 2))
176214
aabb_rod_two = np.empty((3, 2))
@@ -209,33 +247,29 @@ def _prune_using_aabbs_rod_sphere(
209247
sphere_director: NDArray[np.float64],
210248
sphere_radius: NDArray[np.float64],
211249
) -> Literal[1, 0]:
212-
max_possible_dimension = np.zeros((3,))
250+
"""
251+
Prunes broad-phase collision detection between a rod and a sphere using AABBs.
252+
253+
This function checks for intersection between the axis-aligned bounding boxes (AABBs)
254+
of a rod and a sphere. It's a quick way to rule out collision without performing
255+
a more detailed and expensive check.
256+
"""
257+
# AABB for rod
213258
aabb_rod = np.empty((3, 2))
214-
aabb_sphere = np.empty((3, 2))
215-
max_possible_dimension[...] = np.max(rod_one_radius_collection) + np.max(
216-
rod_one_length_collection
217-
)
259+
# Taking max radius of rod
260+
max_rod_radius = np.max(rod_one_radius_collection)
218261
for i in range(3):
219-
aabb_rod[i, 0] = (
220-
np.min(rod_one_position_collection[i]) - max_possible_dimension[i]
221-
)
222-
aabb_rod[i, 1] = (
223-
np.max(rod_one_position_collection[i]) + max_possible_dimension[i]
224-
)
262+
aabb_rod[i, 0] = np.min(rod_one_position_collection[i]) - max_rod_radius
263+
aabb_rod[i, 1] = np.max(rod_one_position_collection[i]) + max_rod_radius
225264

226-
sphere_dimensions_in_local_FOR = np.array(
227-
[sphere_radius, sphere_radius, sphere_radius]
228-
)
229-
sphere_dimensions_in_world_FOR = np.zeros_like(sphere_dimensions_in_local_FOR)
265+
# AABB for sphere
266+
aabb_sphere = np.empty((3, 2))
267+
# A sphere is symmetrical, so its AABB is easy to compute.
268+
# The director is not needed.
230269
for i in range(3):
231-
for j in range(3):
232-
sphere_dimensions_in_world_FOR[i] += (
233-
sphere_director[j, i, 0] * sphere_dimensions_in_local_FOR[j]
234-
)
270+
aabb_sphere[i, 0] = sphere_position[i, 0] - sphere_radius
271+
aabb_sphere[i, 1] = sphere_position[i, 0] + sphere_radius
235272

236-
max_possible_dimension = np.abs(sphere_dimensions_in_world_FOR)
237-
aabb_sphere[..., 0] = sphere_position[..., 0] - max_possible_dimension
238-
aabb_sphere[..., 1] = sphere_position[..., 0] + max_possible_dimension
239273
return _aabbs_not_intersecting(aabb_sphere, aabb_rod)
240274

241275

0 commit comments

Comments
 (0)