| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688 |
- """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
- @property
- def attachments(self):
- """The pair of points defining a pathway's ends."""
- return self._attachments
- @attachments.setter
- 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)
- @property
- @abstractmethod
- def length(self):
- """An expression representing the pathway's length."""
- pass
- @property
- @abstractmethod
- def extension_velocity(self):
- """An expression representing the pathway's extension velocity."""
- pass
- @abstractmethod
- 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)
- @property
- def length(self):
- """Exact analytical expression for the pathway's length."""
- return _point_pair_length(*self.attachments)
- @property
- 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 similar 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, ...]
- 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)
- @property
- def attachments(self):
- """The set of points defining a pathway's segmented path."""
- return self._attachments
- @attachments.setter
- 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)
- @property
- 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
- @property
- 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
- @property
- def geometry(self):
- """Geometry around which the pathway wraps."""
- return self._geometry
- @geometry.setter
- 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
- @property
- def length(self):
- """Exact analytical expression for the pathway's length."""
- return self.geometry.geodesic_length(*self.attachments)
- @property
- 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)
|