Spaces:
Sleeping
Sleeping
"""Implementations of pathways for use by actuators.""" | |
from abc import ABC, abstractmethod | |
from sympy.core.singleton import S | |
from sympy.physics.mechanics.loads import Force | |
from sympy.physics.mechanics.wrapping_geometry import WrappingGeometryBase | |
from sympy.physics.vector import Point, dynamicsymbols | |
__all__ = ['PathwayBase', 'LinearPathway', 'ObstacleSetPathway', | |
'WrappingPathway'] | |
class PathwayBase(ABC): | |
"""Abstract base class for all pathway classes to inherit from. | |
Notes | |
===== | |
Instances of this class cannot be directly instantiated by users. However, | |
it can be used to created custom pathway types through subclassing. | |
""" | |
def __init__(self, *attachments): | |
"""Initializer for ``PathwayBase``.""" | |
self.attachments = attachments | |
def attachments(self): | |
"""The pair of points defining a pathway's ends.""" | |
return self._attachments | |
def attachments(self, attachments): | |
if hasattr(self, '_attachments'): | |
msg = ( | |
f'Can\'t set attribute `attachments` to {repr(attachments)} ' | |
f'as it is immutable.' | |
) | |
raise AttributeError(msg) | |
if len(attachments) != 2: | |
msg = ( | |
f'Value {repr(attachments)} passed to `attachments` was an ' | |
f'iterable of length {len(attachments)}, must be an iterable ' | |
f'of length 2.' | |
) | |
raise ValueError(msg) | |
for i, point in enumerate(attachments): | |
if not isinstance(point, Point): | |
msg = ( | |
f'Value {repr(point)} passed to `attachments` at index ' | |
f'{i} was of type {type(point)}, must be {Point}.' | |
) | |
raise TypeError(msg) | |
self._attachments = tuple(attachments) | |
def length(self): | |
"""An expression representing the pathway's length.""" | |
pass | |
def extension_velocity(self): | |
"""An expression representing the pathway's extension velocity.""" | |
pass | |
def to_loads(self, force): | |
"""Loads required by the equations of motion method classes. | |
Explanation | |
=========== | |
``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be | |
passed to the ``loads`` parameters of its ``kanes_equations`` method | |
when constructing the equations of motion. This method acts as a | |
utility to produce the correctly-structred pairs of points and vectors | |
required so that these can be easily concatenated with other items in | |
the list of loads and passed to ``KanesMethod.kanes_equations``. These | |
loads are also in the correct form to also be passed to the other | |
equations of motion method classes, e.g. ``LagrangesMethod``. | |
""" | |
pass | |
def __repr__(self): | |
"""Default representation of a pathway.""" | |
attachments = ', '.join(str(a) for a in self.attachments) | |
return f'{self.__class__.__name__}({attachments})' | |
class LinearPathway(PathwayBase): | |
"""Linear pathway between a pair of attachment points. | |
Explanation | |
=========== | |
A linear pathway forms a straight-line segment between two points and is | |
the simplest pathway that can be formed. It will not interact with any | |
other objects in the system, i.e. a ``LinearPathway`` will intersect other | |
objects to ensure that the path between its two ends (its attachments) is | |
the shortest possible. | |
A linear pathway is made up of two points that can move relative to each | |
other, and a pair of equal and opposite forces acting on the points. If the | |
positive time-varying Euclidean distance between the two points is defined, | |
then the "extension velocity" is the time derivative of this distance. The | |
extension velocity is positive when the two points are moving away from | |
each other and negative when moving closer to each other. The direction for | |
the force acting on either point is determined by constructing a unit | |
vector directed from the other point to this point. This establishes a sign | |
convention such that a positive force magnitude tends to push the points | |
apart. The following diagram shows the positive force sense and the | |
distance between the points:: | |
P Q | |
o<--- F --->o | |
| | | |
|<--l(t)--->| | |
Examples | |
======== | |
>>> from sympy.physics.mechanics import LinearPathway | |
To construct a pathway, two points are required to be passed to the | |
``attachments`` parameter as a ``tuple``. | |
>>> from sympy.physics.mechanics import Point | |
>>> pA, pB = Point('pA'), Point('pB') | |
>>> linear_pathway = LinearPathway(pA, pB) | |
>>> linear_pathway | |
LinearPathway(pA, pB) | |
The pathway created above isn't very interesting without the positions and | |
velocities of its attachment points being described. Without this its not | |
possible to describe how the pathway moves, i.e. its length or its | |
extension velocity. | |
>>> from sympy.physics.mechanics import ReferenceFrame | |
>>> from sympy.physics.vector import dynamicsymbols | |
>>> N = ReferenceFrame('N') | |
>>> q = dynamicsymbols('q') | |
>>> pB.set_pos(pA, q*N.x) | |
>>> pB.pos_from(pA) | |
q(t)*N.x | |
A pathway's length can be accessed via its ``length`` attribute. | |
>>> linear_pathway.length | |
sqrt(q(t)**2) | |
Note how what appears to be an overly-complex expression is returned. This | |
is actually required as it ensures that a pathway's length is always | |
positive. | |
A pathway's extension velocity can be accessed similarly via its | |
``extension_velocity`` attribute. | |
>>> linear_pathway.extension_velocity | |
sqrt(q(t)**2)*Derivative(q(t), t)/q(t) | |
Parameters | |
========== | |
attachments : tuple[Point, Point] | |
Pair of ``Point`` objects between which the linear pathway spans. | |
Constructor expects two points to be passed, e.g. | |
``LinearPathway(Point('pA'), Point('pB'))``. More or fewer points will | |
cause an error to be thrown. | |
""" | |
def __init__(self, *attachments): | |
"""Initializer for ``LinearPathway``. | |
Parameters | |
========== | |
attachments : Point | |
Pair of ``Point`` objects between which the linear pathway spans. | |
Constructor expects two points to be passed, e.g. | |
``LinearPathway(Point('pA'), Point('pB'))``. More or fewer points | |
will cause an error to be thrown. | |
""" | |
super().__init__(*attachments) | |
def length(self): | |
"""Exact analytical expression for the pathway's length.""" | |
return _point_pair_length(*self.attachments) | |
def extension_velocity(self): | |
"""Exact analytical expression for the pathway's extension velocity.""" | |
return _point_pair_extension_velocity(*self.attachments) | |
def to_loads(self, force): | |
"""Loads required by the equations of motion method classes. | |
Explanation | |
=========== | |
``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be | |
passed to the ``loads`` parameters of its ``kanes_equations`` method | |
when constructing the equations of motion. This method acts as a | |
utility to produce the correctly-structred pairs of points and vectors | |
required so that these can be easily concatenated with other items in | |
the list of loads and passed to ``KanesMethod.kanes_equations``. These | |
loads are also in the correct form to also be passed to the other | |
equations of motion method classes, e.g. ``LagrangesMethod``. | |
Examples | |
======== | |
The below example shows how to generate the loads produced in a linear | |
actuator that produces an expansile force ``F``. First, create a linear | |
actuator between two points separated by the coordinate ``q`` in the | |
``x`` direction of the global frame ``N``. | |
>>> from sympy.physics.mechanics import (LinearPathway, Point, | |
... ReferenceFrame) | |
>>> from sympy.physics.vector import dynamicsymbols | |
>>> q = dynamicsymbols('q') | |
>>> N = ReferenceFrame('N') | |
>>> pA, pB = Point('pA'), Point('pB') | |
>>> pB.set_pos(pA, q*N.x) | |
>>> linear_pathway = LinearPathway(pA, pB) | |
Now create a symbol ``F`` to describe the magnitude of the (expansile) | |
force that will be produced along the pathway. The list of loads that | |
``KanesMethod`` requires can be produced by calling the pathway's | |
``to_loads`` method with ``F`` passed as the only argument. | |
>>> from sympy import symbols | |
>>> F = symbols('F') | |
>>> linear_pathway.to_loads(F) | |
[(pA, - F*q(t)/sqrt(q(t)**2)*N.x), (pB, F*q(t)/sqrt(q(t)**2)*N.x)] | |
Parameters | |
========== | |
force : Expr | |
Magnitude of the force acting along the length of the pathway. As | |
per the sign conventions for the pathway length, pathway extension | |
velocity, and pair of point forces, if this ``Expr`` is positive | |
then the force will act to push the pair of points away from one | |
another (it is expansile). | |
""" | |
relative_position = _point_pair_relative_position(*self.attachments) | |
loads = [ | |
Force(self.attachments[0], -force*relative_position/self.length), | |
Force(self.attachments[-1], force*relative_position/self.length), | |
] | |
return loads | |
class ObstacleSetPathway(PathwayBase): | |
"""Obstacle-set pathway between a set of attachment points. | |
Explanation | |
=========== | |
An obstacle-set pathway forms a series of straight-line segment between | |
pairs of consecutive points in a set of points. It is similiar to multiple | |
linear pathways joined end-to-end. It will not interact with any other | |
objects in the system, i.e. an ``ObstacleSetPathway`` will intersect other | |
objects to ensure that the path between its pairs of points (its | |
attachments) is the shortest possible. | |
Examples | |
======== | |
To construct an obstacle-set pathway, three or more points are required to | |
be passed to the ``attachments`` parameter as a ``tuple``. | |
>>> from sympy.physics.mechanics import ObstacleSetPathway, Point | |
>>> pA, pB, pC, pD = Point('pA'), Point('pB'), Point('pC'), Point('pD') | |
>>> obstacle_set_pathway = ObstacleSetPathway(pA, pB, pC, pD) | |
>>> obstacle_set_pathway | |
ObstacleSetPathway(pA, pB, pC, pD) | |
The pathway created above isn't very interesting without the positions and | |
velocities of its attachment points being described. Without this its not | |
possible to describe how the pathway moves, i.e. its length or its | |
extension velocity. | |
>>> from sympy import cos, sin | |
>>> from sympy.physics.mechanics import ReferenceFrame | |
>>> from sympy.physics.vector import dynamicsymbols | |
>>> N = ReferenceFrame('N') | |
>>> q = dynamicsymbols('q') | |
>>> pO = Point('pO') | |
>>> pA.set_pos(pO, N.y) | |
>>> pB.set_pos(pO, -N.x) | |
>>> pC.set_pos(pA, cos(q) * N.x - (sin(q) + 1) * N.y) | |
>>> pD.set_pos(pA, sin(q) * N.x + (cos(q) - 1) * N.y) | |
>>> pB.pos_from(pA) | |
- N.x - N.y | |
>>> pC.pos_from(pA) | |
cos(q(t))*N.x + (-sin(q(t)) - 1)*N.y | |
>>> pD.pos_from(pA) | |
sin(q(t))*N.x + (cos(q(t)) - 1)*N.y | |
A pathway's length can be accessed via its ``length`` attribute. | |
>>> obstacle_set_pathway.length.simplify() | |
sqrt(2)*(sqrt(cos(q(t)) + 1) + 2) | |
A pathway's extension velocity can be accessed similarly via its | |
``extension_velocity`` attribute. | |
>>> obstacle_set_pathway.extension_velocity.simplify() | |
-sqrt(2)*sin(q(t))*Derivative(q(t), t)/(2*sqrt(cos(q(t)) + 1)) | |
Parameters | |
========== | |
attachments : tuple[Point, Point] | |
The set of ``Point`` objects that define the segmented obstacle-set | |
pathway. | |
""" | |
def __init__(self, *attachments): | |
"""Initializer for ``ObstacleSetPathway``. | |
Parameters | |
========== | |
attachments : tuple[Point, ...] | |
The set of ``Point`` objects that define the segmented obstacle-set | |
pathway. | |
""" | |
super().__init__(*attachments) | |
def attachments(self): | |
"""The set of points defining a pathway's segmented path.""" | |
return self._attachments | |
def attachments(self, attachments): | |
if hasattr(self, '_attachments'): | |
msg = ( | |
f'Can\'t set attribute `attachments` to {repr(attachments)} ' | |
f'as it is immutable.' | |
) | |
raise AttributeError(msg) | |
if len(attachments) <= 2: | |
msg = ( | |
f'Value {repr(attachments)} passed to `attachments` was an ' | |
f'iterable of length {len(attachments)}, must be an iterable ' | |
f'of length 3 or greater.' | |
) | |
raise ValueError(msg) | |
for i, point in enumerate(attachments): | |
if not isinstance(point, Point): | |
msg = ( | |
f'Value {repr(point)} passed to `attachments` at index ' | |
f'{i} was of type {type(point)}, must be {Point}.' | |
) | |
raise TypeError(msg) | |
self._attachments = tuple(attachments) | |
def length(self): | |
"""Exact analytical expression for the pathway's length.""" | |
length = S.Zero | |
attachment_pairs = zip(self.attachments[:-1], self.attachments[1:]) | |
for attachment_pair in attachment_pairs: | |
length += _point_pair_length(*attachment_pair) | |
return length | |
def extension_velocity(self): | |
"""Exact analytical expression for the pathway's extension velocity.""" | |
extension_velocity = S.Zero | |
attachment_pairs = zip(self.attachments[:-1], self.attachments[1:]) | |
for attachment_pair in attachment_pairs: | |
extension_velocity += _point_pair_extension_velocity(*attachment_pair) | |
return extension_velocity | |
def to_loads(self, force): | |
"""Loads required by the equations of motion method classes. | |
Explanation | |
=========== | |
``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be | |
passed to the ``loads`` parameters of its ``kanes_equations`` method | |
when constructing the equations of motion. This method acts as a | |
utility to produce the correctly-structred pairs of points and vectors | |
required so that these can be easily concatenated with other items in | |
the list of loads and passed to ``KanesMethod.kanes_equations``. These | |
loads are also in the correct form to also be passed to the other | |
equations of motion method classes, e.g. ``LagrangesMethod``. | |
Examples | |
======== | |
The below example shows how to generate the loads produced in an | |
actuator that follows an obstacle-set pathway between four points and | |
produces an expansile force ``F``. First, create a pair of reference | |
frames, ``A`` and ``B``, in which the four points ``pA``, ``pB``, | |
``pC``, and ``pD`` will be located. The first two points in frame ``A`` | |
and the second two in frame ``B``. Frame ``B`` will also be oriented | |
such that it relates to ``A`` via a rotation of ``q`` about an axis | |
``N.z`` in a global frame (``N.z``, ``A.z``, and ``B.z`` are parallel). | |
>>> from sympy.physics.mechanics import (ObstacleSetPathway, Point, | |
... ReferenceFrame) | |
>>> from sympy.physics.vector import dynamicsymbols | |
>>> q = dynamicsymbols('q') | |
>>> N = ReferenceFrame('N') | |
>>> N = ReferenceFrame('N') | |
>>> A = N.orientnew('A', 'axis', (0, N.x)) | |
>>> B = A.orientnew('B', 'axis', (q, N.z)) | |
>>> pO = Point('pO') | |
>>> pA, pB, pC, pD = Point('pA'), Point('pB'), Point('pC'), Point('pD') | |
>>> pA.set_pos(pO, A.x) | |
>>> pB.set_pos(pO, -A.y) | |
>>> pC.set_pos(pO, B.y) | |
>>> pD.set_pos(pO, B.x) | |
>>> obstacle_set_pathway = ObstacleSetPathway(pA, pB, pC, pD) | |
Now create a symbol ``F`` to describe the magnitude of the (expansile) | |
force that will be produced along the pathway. The list of loads that | |
``KanesMethod`` requires can be produced by calling the pathway's | |
``to_loads`` method with ``F`` passed as the only argument. | |
>>> from sympy import Symbol | |
>>> F = Symbol('F') | |
>>> obstacle_set_pathway.to_loads(F) | |
[(pA, sqrt(2)*F/2*A.x + sqrt(2)*F/2*A.y), | |
(pB, - sqrt(2)*F/2*A.x - sqrt(2)*F/2*A.y), | |
(pB, - F/sqrt(2*cos(q(t)) + 2)*A.y - F/sqrt(2*cos(q(t)) + 2)*B.y), | |
(pC, F/sqrt(2*cos(q(t)) + 2)*A.y + F/sqrt(2*cos(q(t)) + 2)*B.y), | |
(pC, - sqrt(2)*F/2*B.x + sqrt(2)*F/2*B.y), | |
(pD, sqrt(2)*F/2*B.x - sqrt(2)*F/2*B.y)] | |
Parameters | |
========== | |
force : Expr | |
The force acting along the length of the pathway. It is assumed | |
that this ``Expr`` represents an expansile force. | |
""" | |
loads = [] | |
attachment_pairs = zip(self.attachments[:-1], self.attachments[1:]) | |
for attachment_pair in attachment_pairs: | |
relative_position = _point_pair_relative_position(*attachment_pair) | |
length = _point_pair_length(*attachment_pair) | |
loads.extend([ | |
Force(attachment_pair[0], -force*relative_position/length), | |
Force(attachment_pair[1], force*relative_position/length), | |
]) | |
return loads | |
class WrappingPathway(PathwayBase): | |
"""Pathway that wraps a geometry object. | |
Explanation | |
=========== | |
A wrapping pathway interacts with a geometry object and forms a path that | |
wraps smoothly along its surface. The wrapping pathway along the geometry | |
object will be the geodesic that the geometry object defines based on the | |
two points. It will not interact with any other objects in the system, i.e. | |
a ``WrappingPathway`` will intersect other objects to ensure that the path | |
between its two ends (its attachments) is the shortest possible. | |
To explain the sign conventions used for pathway length, extension | |
velocity, and direction of applied forces, we can ignore the geometry with | |
which the wrapping pathway interacts. A wrapping pathway is made up of two | |
points that can move relative to each other, and a pair of equal and | |
opposite forces acting on the points. If the positive time-varying | |
Euclidean distance between the two points is defined, then the "extension | |
velocity" is the time derivative of this distance. The extension velocity | |
is positive when the two points are moving away from each other and | |
negative when moving closer to each other. The direction for the force | |
acting on either point is determined by constructing a unit vector directed | |
from the other point to this point. This establishes a sign convention such | |
that a positive force magnitude tends to push the points apart. The | |
following diagram shows the positive force sense and the distance between | |
the points:: | |
P Q | |
o<--- F --->o | |
| | | |
|<--l(t)--->| | |
Examples | |
======== | |
>>> from sympy.physics.mechanics import WrappingPathway | |
To construct a wrapping pathway, like other pathways, a pair of points must | |
be passed, followed by an instance of a wrapping geometry class as a | |
keyword argument. We'll use a cylinder with radius ``r`` and its axis | |
parallel to ``N.x`` passing through a point ``pO``. | |
>>> from sympy import symbols | |
>>> from sympy.physics.mechanics import Point, ReferenceFrame, WrappingCylinder | |
>>> r = symbols('r') | |
>>> N = ReferenceFrame('N') | |
>>> pA, pB, pO = Point('pA'), Point('pB'), Point('pO') | |
>>> cylinder = WrappingCylinder(r, pO, N.x) | |
>>> wrapping_pathway = WrappingPathway(pA, pB, cylinder) | |
>>> wrapping_pathway | |
WrappingPathway(pA, pB, geometry=WrappingCylinder(radius=r, point=pO, | |
axis=N.x)) | |
Parameters | |
========== | |
attachment_1 : Point | |
First of the pair of ``Point`` objects between which the wrapping | |
pathway spans. | |
attachment_2 : Point | |
Second of the pair of ``Point`` objects between which the wrapping | |
pathway spans. | |
geometry : WrappingGeometryBase | |
Geometry about which the pathway wraps. | |
""" | |
def __init__(self, attachment_1, attachment_2, geometry): | |
"""Initializer for ``WrappingPathway``. | |
Parameters | |
========== | |
attachment_1 : Point | |
First of the pair of ``Point`` objects between which the wrapping | |
pathway spans. | |
attachment_2 : Point | |
Second of the pair of ``Point`` objects between which the wrapping | |
pathway spans. | |
geometry : WrappingGeometryBase | |
Geometry about which the pathway wraps. | |
The geometry about which the pathway wraps. | |
""" | |
super().__init__(attachment_1, attachment_2) | |
self.geometry = geometry | |
def geometry(self): | |
"""Geometry around which the pathway wraps.""" | |
return self._geometry | |
def geometry(self, geometry): | |
if hasattr(self, '_geometry'): | |
msg = ( | |
f'Can\'t set attribute `geometry` to {repr(geometry)} as it ' | |
f'is immutable.' | |
) | |
raise AttributeError(msg) | |
if not isinstance(geometry, WrappingGeometryBase): | |
msg = ( | |
f'Value {repr(geometry)} passed to `geometry` was of type ' | |
f'{type(geometry)}, must be {WrappingGeometryBase}.' | |
) | |
raise TypeError(msg) | |
self._geometry = geometry | |
def length(self): | |
"""Exact analytical expression for the pathway's length.""" | |
return self.geometry.geodesic_length(*self.attachments) | |
def extension_velocity(self): | |
"""Exact analytical expression for the pathway's extension velocity.""" | |
return self.length.diff(dynamicsymbols._t) | |
def to_loads(self, force): | |
"""Loads required by the equations of motion method classes. | |
Explanation | |
=========== | |
``KanesMethod`` requires a list of ``Point``-``Vector`` tuples to be | |
passed to the ``loads`` parameters of its ``kanes_equations`` method | |
when constructing the equations of motion. This method acts as a | |
utility to produce the correctly-structred pairs of points and vectors | |
required so that these can be easily concatenated with other items in | |
the list of loads and passed to ``KanesMethod.kanes_equations``. These | |
loads are also in the correct form to also be passed to the other | |
equations of motion method classes, e.g. ``LagrangesMethod``. | |
Examples | |
======== | |
The below example shows how to generate the loads produced in an | |
actuator that produces an expansile force ``F`` while wrapping around a | |
cylinder. First, create a cylinder with radius ``r`` and an axis | |
parallel to the ``N.z`` direction of the global frame ``N`` that also | |
passes through a point ``pO``. | |
>>> from sympy import symbols | |
>>> from sympy.physics.mechanics import (Point, ReferenceFrame, | |
... WrappingCylinder) | |
>>> N = ReferenceFrame('N') | |
>>> r = symbols('r', positive=True) | |
>>> pO = Point('pO') | |
>>> cylinder = WrappingCylinder(r, pO, N.z) | |
Create the pathway of the actuator using the ``WrappingPathway`` class, | |
defined to span between two points ``pA`` and ``pB``. Both points lie | |
on the surface of the cylinder and the location of ``pB`` is defined | |
relative to ``pA`` by the dynamics symbol ``q``. | |
>>> from sympy import cos, sin | |
>>> from sympy.physics.mechanics import WrappingPathway, dynamicsymbols | |
>>> q = dynamicsymbols('q') | |
>>> pA = Point('pA') | |
>>> pB = Point('pB') | |
>>> pA.set_pos(pO, r*N.x) | |
>>> pB.set_pos(pO, r*(cos(q)*N.x + sin(q)*N.y)) | |
>>> pB.pos_from(pA) | |
(r*cos(q(t)) - r)*N.x + r*sin(q(t))*N.y | |
>>> pathway = WrappingPathway(pA, pB, cylinder) | |
Now create a symbol ``F`` to describe the magnitude of the (expansile) | |
force that will be produced along the pathway. The list of loads that | |
``KanesMethod`` requires can be produced by calling the pathway's | |
``to_loads`` method with ``F`` passed as the only argument. | |
>>> F = symbols('F') | |
>>> loads = pathway.to_loads(F) | |
>>> [load.__class__(load.location, load.vector.simplify()) for load in loads] | |
[(pA, F*N.y), (pB, F*sin(q(t))*N.x - F*cos(q(t))*N.y), | |
(pO, - F*sin(q(t))*N.x + F*(cos(q(t)) - 1)*N.y)] | |
Parameters | |
========== | |
force : Expr | |
Magnitude of the force acting along the length of the pathway. It | |
is assumed that this ``Expr`` represents an expansile force. | |
""" | |
pA, pB = self.attachments | |
pO = self.geometry.point | |
pA_force, pB_force = self.geometry.geodesic_end_vectors(pA, pB) | |
pO_force = -(pA_force + pB_force) | |
loads = [ | |
Force(pA, force * pA_force), | |
Force(pB, force * pB_force), | |
Force(pO, force * pO_force), | |
] | |
return loads | |
def __repr__(self): | |
"""Representation of a ``WrappingPathway``.""" | |
attachments = ', '.join(str(a) for a in self.attachments) | |
return ( | |
f'{self.__class__.__name__}({attachments}, ' | |
f'geometry={self.geometry})' | |
) | |
def _point_pair_relative_position(point_1, point_2): | |
"""The relative position between a pair of points.""" | |
return point_2.pos_from(point_1) | |
def _point_pair_length(point_1, point_2): | |
"""The length of the direct linear path between two points.""" | |
return _point_pair_relative_position(point_1, point_2).magnitude() | |
def _point_pair_extension_velocity(point_1, point_2): | |
"""The extension velocity of the direct linear path between two points.""" | |
return _point_pair_length(point_1, point_2).diff(dynamicsymbols._t) | |