"""This module deals with physical interactions between objects."""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Tuple
import numpy as np
[docs]def get_distance_components(
point1: np.ndarray, point2: np.ndarray
) -> Tuple[np.ndarray, float]:
"""
Returns the direction and magnitude components of a vector that connects two points.
Parameters
----------
point1: np.ndarray
The coordinates of the first point.
point2:np.ndarray
The coordinates of the second point.
Returns
-------
np.ndarray
The unit vector that defines the direction between the two points.
float
The distance between the two points.
"""
distance_vector = point1 - point2
norm = np.linalg.norm(distance_vector)
if norm < 0.0000001:
norm = 0.0000001
unit_vector = distance_vector / norm
return unit_vector, norm
[docs]def normalize_vector(vector: np.ndarray) -> np.ndarray:
"""
Returns the input array normalized to a unit vector.
Parameters
----------
vector
The array to be normalized.
Returns
-------
np.ndarray
The normalized vector.
"""
return vector / np.linalg.norm(vector)
[docs]def get_sphere_overlap(radius1: float, radius2: float, distance: float) -> float:
"""
Returns the overlap between two objects represented as spheres.
Parameters
----------
radius1: float
The radius of the first object (represented as a sphere).
radius2: float
The radius of the second object (represented as a sphere).
distance: float
The distance between the centres of the two objects.
Returns
-------
float
The overlap between the two objects.
"""
overlap = radius1 + radius2 - distance
if overlap < 0.00001:
return 0.0
return overlap
[docs]def get_sphere_cylinder_intersection(
center: np.ndarray, base: np.ndarray, top: np.ndarray
) -> np.ndarray:
"""
Returns the closest point on the cylinder axis to the sphere.
The intersection is given by
the dot product. Taking the dot product between the cylinder axis and the axis that connects
the cylinder base to the sphere, the dot product gives us the projection of the cylinder-sphere
axis on the cylinder axis. For dot products between 0 and 1, the closest point is on the
cylinder axis. For dot products below 0 or above 1, the closest points are the base and the
extremity of the cylinder, respectively.
Parameters
----------
center: np.ndarray
The coordinates for the center of the sphere object.
base: np.ndarray
The coordinates for the base of the cylinder object.
top: np.ndarray
The coordinates for the extremity of the cylinder object.
Returns
-------
np.ndarray
The coordinates of the closest point to the sphere on the cylinder axis
"""
base_to_sphere_axis = np.subtract(center, base)
cylinder_axis = np.subtract(top, base)
dot_product = np.dot(base_to_sphere_axis, cylinder_axis)
projection_fraction = dot_product / np.linalg.norm(cylinder_axis) ** 2
if projection_fraction <= 0:
return base
if projection_fraction >= 1:
return top
return base + cylinder_axis * projection_fraction
[docs]def get_cylinder_intersection(
base_1: np.ndarray, top_1: np.ndarray, base_2: np.ndarray, top_2: np.ndarray
) -> Tuple[np.ndarray, np.ndarray]:
"""
Returns the closest point between two cylinders.
The cross product is used to evaluate
if the cylinder axes are parallel. If they are, the closest points between the two objects
are considered to be their middle points. If not, the projection of each axis on the other
axis is computed to find the closest point on each axis.
Parameters
----------
base_1: np.ndarray
The coordinates for the base of the first cylinder object.
top_1: np.ndarray
The coordinates for the extremity of the first cylinder object.
base_2: np.ndarray
The coordinates for the base of the second cylinder object.
top_2: np.ndarray
The coordinates for the extremity of the second cylinder object.
Returns
-------
np.ndarray
The closest point on the axis of the first cylinder.
np.ndarray
The closest point on the axis of the second cylinder.
"""
cylinder_axis_2 = np.subtract(top_2, base_2)
cylinder_axis_1 = np.subtract(top_1, base_1)
unit_vector_2, norm2 = get_distance_components(top_2, base_2)
unit_vector_1, norm1 = get_distance_components(top_1, base_1)
cross = np.cross(unit_vector_1, unit_vector_2)
denominator = np.linalg.norm(cross) ** 2
# If cylinder axes are parallel, set the closest points as the middle points
if denominator < 0.000001:
d0 = np.dot(unit_vector_1, (base_2 - base_1))
d1 = np.dot(unit_vector_1, (top_2 - base_1))
if d0 <= 0 >= d1:
if np.absolute(d0) < np.absolute(d1):
return base_1, base_2
return base_1, top_2
elif d0 >= norm1 <= d1:
if np.absolute(d0) < np.absolute(d1):
return top_1, base_2
return top_1, top_2
p1 = base_1 + 0.5 * cylinder_axis_1
p2 = base_2 + 0.5 * cylinder_axis_2
return p1, p2
t = base_2 - base_1
detA = np.linalg.det([t, unit_vector_2, cross])
detB = np.linalg.det([t, unit_vector_1, cross])
t0 = detA / denominator
t1 = detB / denominator
# Closest point on segment 1
pA = base_1 + (unit_vector_1 * t0)
# Closest point on segment B 2
pB = base_2 + (unit_vector_2 * t1)
# Clamp projections
if t0 < 0:
pA = base_1
elif t0 > norm1:
pA = top_1
if t1 < 0:
pB = base_2
elif t1 > norm2:
pB = top_2
# Clamp projection A
if (t0 < 0) or (t0 > norm1):
dot = np.dot(unit_vector_2, (pA - base_2))
if dot < 0:
dot = 0
elif dot > norm2:
dot = norm2
pB = base_2 + (unit_vector_2 * dot)
# Clamp projection B
if (t1 < 0) or (t1 > norm2):
dot = np.dot(unit_vector_1, (pB - base_1))
if dot < 0:
dot = 0
elif dot > norm1:
dot = norm1
pA = base_1 + (unit_vector_1 * dot)
return pA, pB
[docs]class PhysicalProperties:
"""
Class with the mechanical properties of a physical object.
Parameters
----------
radius
The radius of the physical object.
interaction_factor
The factor used to calculate the radius of interaction.
"""
def __init__(self, radius: float, interaction_factor: float) -> None:
self.radius = radius
self.interaction_factor = interaction_factor
@property
def radius(self):
"""
Returns the radius of the object.
Raises an error if a negative or non-numerical value is set.
"""
return self._radius
@radius.setter
def radius(self, radius: float):
if not isinstance(radius, (int, float)):
raise TypeError("Object radius should be a numerical value.")
if radius < 0.0:
raise ValueError("Object radius should be positive.")
self._radius = radius
@property
def interaction_factor(self):
"""
Returns the interaction factor of the object.
Raises an error if a negative or non-numerical value is set.
"""
return self._interaction_factor
@interaction_factor.setter
def interaction_factor(self, interaction_factor: float):
if not isinstance(interaction_factor, (int, float)):
raise TypeError("Interaction factor should be a numerical value.")
if interaction_factor < 0.0:
raise ValueError("Interaction factor should be positive.")
self._interaction_factor = interaction_factor
@property
def interaction_radius(self) -> float:
"""Returns the radius of interaction of the physical object."""
return self.interaction_factor * self.radius
[docs]class SphereProperties(PhysicalProperties):
"""
Class with the mechanical properties of a sphere.
Parameters
----------
radius
The radius of the physical object.
interaction_factor
The factor used to calculate the radius of interaction.
"""
def __init__(self, radius: float, interaction_factor: float):
super().__init__(radius, interaction_factor)
[docs]class CylinderProperties(PhysicalProperties):
"""
Class with the mechanical properties of a cylinder with a spring axis.
Parameters
----------
radius
The radius of the physical object.
interaction_factor
The factor used to calculate the radius of interaction.
"""
def __init__(
self,
radius: float,
interaction_factor: float,
spring_constant: float,
default_length: float,
) -> None:
super().__init__(radius, interaction_factor)
self.spring_constant = spring_constant
self.default_length = default_length
[docs] def get_spring_tension(self, cylinder_length: float) -> float:
"""Returns the tension in the spring for a given spring length."""
length_difference = cylinder_length - self.default_length
return self.spring_constant * length_difference / self.default_length
[docs]@dataclass
class SimpleFactory(ContactFactory):
sphere_sphere_adhesion: float
sphere_sphere_repulsion: float
sphere_cylinder_adhesion: float
sphere_cylinder_repulsion: float
cylinder_cylinder_adhesion: float
cylinder_cylinder_repulsion: float
[docs] def get_sphere_sphere_interactions(self) -> ContactForces:
return SimpleContact(self.sphere_sphere_adhesion, self.sphere_sphere_repulsion)
[docs] def get_sphere_cylinder_interactions(self) -> ContactForces:
return SimpleContact(
self.sphere_cylinder_adhesion, self.sphere_cylinder_repulsion
)
[docs] def get_cylinder_cylinder_interactions(self) -> ContactForces:
return SimpleContact(
self.cylinder_cylinder_adhesion, self.cylinder_cylinder_repulsion
)
[docs]@dataclass
class PotentialsFactory(ContactFactory):
sphere_sphere_adhesion: float
sphere_sphere_repulsion: float
sphere_sphere_smoothness: int
sphere_cylinder_adhesion: float
sphere_cylinder_repulsion: float
sphere_cylinder_smoothness: int
cylinder_cylinder_adhesion: float
cylinder_cylinder_repulsion: float
cylinder_cylinder_smoothness: int
[docs] def get_sphere_sphere_interactions(self) -> PotentialsContact:
return PotentialsContact(
self.sphere_sphere_adhesion,
self.sphere_sphere_repulsion,
self.sphere_sphere_smoothness,
)
[docs] def get_sphere_cylinder_interactions(self) -> PotentialsContact:
return PotentialsContact(
self.sphere_cylinder_adhesion,
self.sphere_cylinder_repulsion,
self.sphere_cylinder_smoothness,
)
[docs] def get_cylinder_cylinder_interactions(self) -> PotentialsContact:
return PotentialsContact(
self.cylinder_cylinder_adhesion,
self.cylinder_cylinder_repulsion,
self.cylinder_cylinder_smoothness,
)