"""
Tendons
=======
:class:`Tendons <blueprints.tendon.Tendon>` can be used to string together multiple Things. The can provide additional
structural stability or connect otherwise kinetically distinct Things. In Mujoco,
Tendon can be bound to Sites, Geoms and Joints according to certain constraints
limiting which Things can be bound and which Tendon attributes are passed into the
simulation. Tendons introduce cycles to the kinematic hierarchy and are therefore
treated specially with the :meth:`Path.bind <blueprints.tendon.Path.bind>` method replacing :meth:`NodeThing.attach <blueprints.thing.node.NodeThing.attach>` within
a special context manager.
Joint (Fixed) Tendons
---------------------
Joint Tendons are the simplest usecase since they only use a reduced set of
attributes and do not support spliting paths into pulleys. The attributes allowed
for Joint Tendons are:
* :attr:`name <blueprints.tendon.Tendon.name>`
* :attr:`limited <blueprints.tendon.Tendon.limited>`
* :attr:`range <blueprints.tendon.Tendon.range>`
* :attr:`frictionloss <blueprints.tendon.Tendon.frictionloss>`
* :attr:`margin <blueprints.tendon.Tendon.margin>`
* :attr:`springlength <blueprints.tendon.Tendon.springlength>`
* :attr:`stiffness <blueprints.tendon.Tendon.stiffness>`
* :attr:`damping <blueprints.tendon.Tendon.damping>`
We first create some example Things.
.. code-block:: py
:caption: Creating Example Things
>>> world = blue.World()
>>> body = blue.Body(geoms=blue.geoms.Sphere(), # introduces mass to Body
>>> joints=blue.joints.Hinge())
>>> world.attach(body.shift(x=0, name='A'),
>>> body.shift(x=3, name='B'),
>>> body.shift(x=6, name='C'))
>>> tendon = blue.Tendon()
To bind the Joints to the tendon we first grab them from the world and then bind
them to the tendon path within the Tendons context. When we bind a Joint to a Tendon,
a coefficient must be specified weighing the interaction strength between Tendon and
Joint.
.. code-block:: py
:caption: Tendon Context
>>> jA = world.bodies['A'].joints[0]
>>> jB = world.bodies['B'].joints[0]
>>> jC = world.bodies['C'].joints[0]
>>> with tendon as path:
>>> path.bind(jA, coef=1.)
>>> path.bind(jB, coef=2.)
>>> path.bind(jC, coef=1.)
.. code-block:: py
:caption: Alternatively
>>> with tendon as path:
>>> path.bind(jA, jB, jC, coef=[1.0, 2.0, 1.0])
The resulting XML is split into the normal kinematic tree and the Tendon referencing
its bound Things via name attribute.
.. code-block:: mxml
:caption: XML Structure Tendon
<tendon>
<fixed name="anonymous_tendon">
<joint joint="anonymous_hinge_(0)" coef="1.0" />
<joint joint="anonymous_hinge_(1)" coef="2.0" />
<joint joint="anonymous_hinge_(2)" coef="1.0" />
</fixed>
</tendon>
.. note::
Keep in mind that Fixed Joint Tendons do not support :attr:`width <blueprints.tendon.Tendon.width>` and are hence invisible.
.. code-block:: mxml
:caption: XML Structure World
<worldbody>
<body name="A">
<joint type="hinge" name="anonymous_hinge_(0)" axis="0.0 0.0 1.0" />
<geom size="1.0" type="sphere" name="anonymous_sphere_(0)" />
</body>
<body pos="3.0 0.0 0.0" name="B">
<joint type="hinge" name="anonymous_hinge_(1)" axis="0.0 0.0 1.0" />
<geom size="1.0" type="sphere" name="anonymous_sphere_(1)"/>
</body>
<body pos="6.0 0.0 0.0" name="C">
<joint type="hinge" name="anonymous_hinge_(2)" axis="0.0 0.0 1.0" />
<geom size="1.0" type="sphere" name="anonymous_sphere_(2)"/>
</body>
</worldbody>
Site/Geom (Spatial) Tendons
---------------------------
Spatial Tendons are bind to :class:`Sites <blueprints.sites.BaseSite>` and :class:`Geoms <blueprints.geoms.BaseGeom>` and support path splitting. The following rules
must be satisfied for mujoco to accept the build.
* Every path must start and end with a :class:`Site <blueprints.sites.BaseSite>`.
* Every :class:`Geom <blueprints.geoms.BaseGeom>` must be sandwiched between two :class:`Sites <blueprints.sites.BaseSite>`.
* Every Tendon binding Sites/Geoms must not also bind Joints.
Lets build some example Things and bind them to a Tendon.
.. code-block:: py
:caption: Creating Example Things
>>> body_A = blue.Body(sites=blue.sites.Sphere(radius=0.5)).shift(x=-3, name='A')
>>> body_B = blue.Body(geoms=blue.geoms.Sphere(radius=0.5),
>>> sites=blue.sites.Sphere(radius=0.5).shift(z=3), name='B')
>>> body_C = blue.Body(sites=blue.sites.Sphere(radius=0.5)).shift(x=3, name='C')
>>> world.attach(body_A, body_C, body_B)
>>> tendon = blue.Tendon(width=0.1)
When binding a :class:`Geom <blueprints.geoms.BaseGeom>` to a Tendon, an optional ``side_site`` argument may be
specified. If the Tendon runs through the Geom at the begining of the Mujoco simulation,
it is snapped out on the side of the specified ``side_site``.
.. code-block:: py
:caption: Binding Sites and Geoms
>>> sA = world.bodies['A'].sites[0]
>>> sB = world.bodies['B'].sites[0]
>>> gB = world.bodies['B'].geoms[0]
>>> sC = world.bodies['C'].sites[0]
>>> with tendon as path:
>>> path.bind(sA)
>>> path.bind(gB, side_site=sB)
>>> path.bind(sC)
.. image:: /_static/tendon_side_site.png
The resulting XML structure are separated into the cyclical references in the header of the Mujoco XML
and its components in the normal kinematic hierarchy.
.. code-block:: mxml
:caption: XML Structure Tendon
<tendon>
<spatial width="0.1" name="anonymous_tendon">
<site site="anonymous_sphere_(0)" />
<geom geom="anonymous_sphere" sidesite="anonymous_sphere_(2)" />
<site site="anonymous_sphere_(1)" />
</spatial>
</tendon>
.. code-block:: mxml
:caption: XML Structure World
<worldbody>
<body pos="-3.0 0.0 0.0" name="A">
<site size="0.5 0.0 0.0" type="sphere" name="anonymous_sphere_(0)" />
</body>
<body pos="3.0 0.0 0.0" name="C">
<site size="0.5 0.0 0.0" type="sphere" name="anonymous_sphere_(1)" />
</body>
<body name="B">
<geom size="0.5" type="sphere" name="anonymous_sphere" />
<site size="0.5 0.0 0.0" type="sphere" pos="0.0 0.0 3.0" name="anonymous_sphere_(2)" />
</body>
</worldbody>
Path Splitting
--------------
Mujoco supports pullies which split a path into multiple subpaths. The forces acting on each subpath
are distributed proportionally. The begining of a new path is not necessarily directly connected to
end of the parent path.
.. code-block:: py
:caption: Creating Example Things
>>> world = blue.World()
>>> body_A = blue.Body(pos=[0, 0, 0], sites=blue.sites.Sphere(radius=0.5))
>>> body_B = blue.Body(pos=[2, 0, 0], sites=blue.sites.Sphere(radius=0.5))
>>> body_C1 = blue.Body(pos=[4,-2, 0], sites=blue.sites.Sphere(radius=0.5))
>>> body_C2 = blue.Body(pos=[4, 2, 0], sites=blue.sites.Sphere(radius=0.5))
>>> body_D1 = blue.Body(pos=[6,-2, 0], sites=blue.sites.Sphere(radius=0.5))
>>> body_D2 = blue.Body(pos=[6, 0, 0], sites=blue.sites.Sphere(radius=0.5))
>>> body_D3 = blue.Body(pos=[6, 4, 0], sites=blue.sites.Sphere(radius=0.5))
>>> tendon = blue.Tendon(width=0.1)
Using the method :meth:`split <blueprints.tendon.Path.split>` a pulley is introduced that splits the path into `n`
branches. A branch path can also be split.
.. code-block:: py
:caption: Binding with Multiple Paths
>>> with tendon as path:
>>> path.bind(body_A.sites)
>>> path.bind(body_B.sites)
>>> path_1, path_2, path_3 = path.split(3)
>>> # PATH ONE
>>> path_1.bind(body_B.sites)
>>> path_1.bind(body_C1.sites)
>>> path_1.bind(body_D1.sites)
>>> # PATH TWO
>>> path_2.bind(body_C2.sites)
>>> path_2.bind(body_D2.sites)
>>> # PATH THREE
>>> path_3.bind(body_C2.sites)
>>> path_3.bind(body_D3.sites)
The example above introduces a pulley with the ``divisor`` 3. The path bindings can be seen in the
following image from left to right with ``path_1`` on the bottom and ``path_2`` and ``path_3`` ontop both
sharing the same initial Site.
.. image:: /_static/tendon_example.png
The resulting XML structures are:
.. code-block:: mxml
:caption: XML Structure Tendon
<tendon>
<spatial width="0.1" name="anonymous_tendon">
<site site="anonymous_sphere_(0)" />
<site site="anonymous_sphere_(1)" />
<pulley divisor="3" />
<site site="anonymous_sphere_(3)" />
<site site="anonymous_sphere_(6)" />
<pulley divisor="3" />
<site site="anonymous_sphere_(3)" />
<site site="anonymous_sphere_(5)" />
<pulley divisor="3" />
<site site="anonymous_sphere_(1)" />
<site site="anonymous_sphere_(2)" />
<site site="anonymous_sphere_(4)" />
</spatial>
</tendon>
.. code-block:: mxml
:caption: XML Structure World
<worldbody>
<body name="anonymous_body_(0)">
<site size="0.5 0.0 0.0" type="sphere" name="anonymous_sphere_(0)" />
</body>
<body pos="2.0 0.0 0.0" name="anonymous_body_(1)">
<site size="0.5 0.0 0.0" type="sphere" name="anonymous_sphere_(1)" />
</body>
<body pos="4.0 -2.0 0.0" name="anonymous_body_(2)">
<site size="0.5 0.0 0.0" type="sphere" name="anonymous_sphere_(2)" />
</body>
<body pos="4.0 2.0 0.0" name="anonymous_body_(3)">
<site size="0.5 0.0 0.0" type="sphere" name="anonymous_sphere_(3)" />
</body>
<body pos="6.0 -2.0 0.0" name="anonymous_body_(4)">
<site size="0.5 0.0 0.0" type="sphere" name="anonymous_sphere_(4)" />
</body>
<body pos="6.0 0.0 0.0" name="anonymous_body_(5)">
<site size="0.5 0.0 0.0" type="sphere" name="anonymous_sphere_(5)" />
</body>
<body pos="6.0 4.0 0.0" name="anonymous_body_(6)">
<site size="0.5 0.0 0.0" type="sphere" name="anonymous_sphere_(6)" />
</body>
</worldbody>
Copies of Tendons
-----------------
Copying Tendons takes special attention since Tendons introduce cyclical references (i.e. multiple children of a :class:`NodeThing <blueprints.node.NodeThing>`
can be bound to the same Tendon). Therefore Tendons only get copied when the (root) Thing copied by the user has as its descendants all references
of the Tendon.
Let's see an example of this:
.. code-block:: py
:caption: Creating and Binding Example Things
>>> tendon = blue.Tendon(width=0.1)
>>> root = blue.Body(name='root')
>>> body_site = blue.Body(sites=blue.sites.Sphere(radius=0.5))
>>> root.attach(body_site.shift(x=0, name='A'),
>>> body_site.shift(x=2, name='B'))
>>> A = root.bodies['A']
>>> B = root.bodies['B']
>>> C = body_site.shift(x=4, name='C')
>>> with tendon as path:
>>> path.bind(A.sites)
>>> path.bind(B.sites)
>>> path.bind(C.sites)
We have created three bodies with sites (``A``, ``B``, ``C``) that are bound together with a Tendon.
But only ``A`` and ``B`` are attached to root, while ``C`` is not. If both are attached to the World
without creating a copy, they world will contain the tendon.
.. code-block:: py
:caption: World Attachment without Copy
>>> world.attach(root, C, copy=False)
.. code-block:: mxml
:caption: XML Structure Tendon
<tendon>
<spatial width="0.1" name="anonymous_tendon">
<site site="anonymous_sphere_(0)" />
<site site="anonymous_sphere_(1)" />
<site site="anonymous_sphere_(2)" />
</spatial>
</tendon>
.. code-block:: mxml
:caption: XML Structure World
<worldbody>
<body name="root">
<body name="A">
<site size="0.5 0.0 0.0" type="sphere" name="anonymous_sphere_(0)" />
</body>
<body pos="2.0 0.0 0.0" name="B">
<site size="0.5 0.0 0.0" type="sphere" name="anonymous_sphere_(1)" />
</body>
</body>
<body pos="4.0 0.0 0.0" name="C">
<site size="0.5 0.0 0.0" type="sphere" name="anonymous_sphere_(2)" />
</body>
</worldbody>
If we however create a copy of ``root`` and ``C`` separately the Tendon will be discarded, since neither
``root`` nor ``C`` contain all refferences of the Tendon as their descendants (the references of the Tendon
are the bound Sites ``"anonymous_sphere_(0)"``, ``"anonymous_sphere_(1)"`` and ``"anonymous_sphere_(2)"``).
.. code-block:: py
:caption: World Attachment without Copy
>>> world.attach(root, C, copy=True)
.. code-block:: mxml
:caption: XML Structure Tendon
</tendon>
.. code-block:: mxml
:caption: XML Structure World
<worldbody>
<body name="root">
<body name="A">
<site size="0.5 0.0 0.0" type="sphere" name="anonymous_sphere_(0)" />
</body>
<body pos="2.0 0.0 0.0" name="B">
<site size="0.5 0.0 0.0" type="sphere" name="anonymous_sphere_(1)" />
</body>
</body>
<body pos="4.0 0.0 0.0" name="C">
<site size="0.5 0.0 0.0" type="sphere" name="anonymous_sphere_(2)" />
</body>
</worldbody>
"""
from collections import defaultdict
import numpy as np
import xml.etree.ElementTree as xml
import blueprints as blue
[docs]
class Tendon(blue.TendonType, blue.NodeThing, blue.ColoredThing, blue.FocalThing):
[docs]
@blue.restrict
def __init__(self,
name: str|None = None,
limited: bool|None = None,
act_force_limited: bool|None = None,
min_length: int|float = 0.,
max_length: int|float = 0.,
min_act_force: int|float = 0.,
max_act_force: int|float = 0.,
frictionloss: int|float = 0.,
width: int|float = 0.003,
color: object|None = None,
stiffness: int|float = 0.,
damping: int|float = 0.,
armature: int|float = 0.,
**kwargs) -> None:
"""
Some attribute descriptions are taken from `Mujoco <https://mujoco.readthedocs.io/en/latest/XMLreference.html#tendon>`__.
Parameters
----------
limited : bool | None
Sets whether length limits constraints are imposed on the mujoco
solver. Leaving this argument as None (recommended) enables autolimits
derived from the other attributes.
act_force_limited : bool | None
This argument is the equivalent to `limited` for actuator forces.
min_length : int | float
Minimal length of Tendon
max_length : int | float
Maximal length of Tendon
min_act_force : int | float
Minimal force allowed to act on Tendon
max_act_force : int | float
Maximal force allowed to act on Tendon
frictionloss : int | float
Friction loss caused by dry friction. This value should be positive.
width : int | float
Crosssection radius of the Tendon
stiffness : int | float
If set to a positive value it acts as a Spring coefficient.
damping : int | float
If set to a positive value it acts as a Damping coefficient.
armature : int | float
Inertia associated with changes in tendon length
"""
self._built = False
self._ACTIVE = False
# MIGRATION ATTRIBUTES
self._ADDRESS_BOOK = defaultdict(list)
self._MIGRATED = False
self._MIGRATIONS = defaultdict(int)
# PATH ATTRIBUTES
self._branches = []
# CHILDREN
pack = lambda x: x if isinstance(x, list) else [x]
self._sites = []
self._geoms = []
self._side_sites = []
self._joints = []
self._CHILDREN = {}
self._PSEUDO_CHILDREN = {'sites': {'type': blue.SiteType,
'children': self._sites},
'geoms': {'type': blue.GeomType,
'children': self._geoms},
'side_sites': {'type': blue.SiteType,
'children': self._side_sites},
'joints': {'type': blue.JointType,
'children': self._joints}}
# MUJOCO ATTRIBUTES
self.limited = limited
self.act_force_limited = act_force_limited
self.min_length = min_length
self.max_length = max_length
self.min_act_force = min_act_force
self.max_act_force = max_act_force
self.frictionloss = frictionloss
self.width = width
self.stiffness = stiffness
self.damping = damping
self.armature = armature
# SETTING COLOR BLUEPRINT COLOR DEFAULT FOR GEOMS
color = color if color is not None else 'grey'
super().__init__(name=name,
color=color,
**kwargs)
def __enter__(self):
self._ACTIVE = True
self._branches = [Path(self)]
return self._branches[0]
def __exit__(self, exc_type, error, trace):
self._ACTIVE = False
@property
def _MIGRATION_DONE(self):
return all(v == 0 for v in self._MIGRATIONS.values())
def _get_path(self, address):
path = self
for i in address:
path = path._branches[i]
return path
def _migrate(self, old, new):
if not self._MIGRATED:
self._start_migration()
if self._VALID:
addresses = self._ADDRESS_BOOK[old]
for address, i_path, other in addresses:
path = self._COPY._get_path(address)
if isinstance(old, blue.SiteType):
path._path[i_path][other] = new
if other == 0:
self._COPY._sites.append(new)
new._tendons.append(self._COPY)
else:
self._COPY._side_sites.append(new)
new._side_tendons.append(self._COPY)
elif isinstance(old, blue.GeomType):
path._path[i_path][other] = new
self._COPY._geoms.append(new)
new._tendons.append(self._COPY)
elif isinstance(old, blue.JointType):
path._path[i_path][0] = new
path._path[i_path][1] = other
self._COPY._joints.append(new)
new._tendons.append(self._COPY)
self._MIGRATIONS[old] -= 1
if self._MIGRATION_DONE:
self._finalize_migration()
def _start_migration(self):
# CHECK LEGALITY
self._MIGRATED = True
self._VALID = True
things = self._sites + self._geoms + self._side_sites + self._joints
INVALID = []
for thing in things:
if blue.REGISTER.copy_root in thing.path:
self._MIGRATIONS[thing] += 1
else:
self._VALID = False
INVALID.append(thing)
# CREATE COPY
self._COPY = super().copy()
# CREATE MIGRATION OBJECTS
self._ADDRESS_BOOK = defaultdict(list)
# CHECK VALIDITY
if not self._VALID:
print(f'WARNING: {repr(self)} is discarded. The Things ({', '.join(map(repr, INVALID))}) bound by the Tendon are not descendants of the copied Thing {repr(blue.REGISTER.copy_root)}.')
return
# BREADTH SEARCH
queue = [([i], branch) for i, branch in enumerate(self._branches)]
while queue:
address, path = queue.pop(0)
path_parent = self._COPY._get_path(address[:-1])
path_copy = Path(self._COPY)
path_parent._branches.append(path_copy)
for i_path, (thing, other) in enumerate(path):
self._ADDRESS_BOOK[thing].append((address, i_path, 0))
if other is not None and isinstance(other, blue.SiteType):
self._ADDRESS_BOOK[other].append((address, i_path, 1))
path_copy._path.append([None, None])
for i_branch, branch in enumerate(path._branches):
queue.append((address + [i_branch], branch))
def _finalize_migration(self):
self._MIGRATED = False
self._VALID = True
self._MIGRATIONS = defaultdict(int)
del self._COPY
del self._ADDRESS_BOOK
[docs]
def attach(self, *items, copy=False) -> None:
"""
Attachements are not allowed for Tendons. Use :meth:`Path.bind` instead.
"""
raise Exception('Attachement is not allowed for Tendons, use Path.bind instead. Refer to the Documentation for details.')
def _build(self,
parent,
world,
indicies,
**kwargs):
if self._built:
return
mujoco_specs = self._mujoco_specs(**kwargs)
if self._joints:
INVALID = []
for attr in mujoco_specs.keys():
if attr not in self._FIXED_ATTR:
INVALID.append(attr)
for attr in INVALID:
del mujoco_specs[attr]
if INVALID:
print(f'WARNING: Tendons with Joints have a reduced set of attributes. Ignoring attributes ({', '.join(map(str, INVALID))}).')
tendon = xml.SubElement(world._xml_tendon,
'fixed' if self._joints else 'spatial',
**mujoco_specs)
queue = [(len(self._branches), path) for path in self._branches]
while queue:
idx, path = queue.pop()
if idx > 1:
pulley = xml.SubElement(tendon, 'pulley', divisor=str(idx))
for thing, other in path:
if isinstance(thing, blue.SiteType):
xml.SubElement(tendon, 'site', site=thing.name)
elif isinstance(thing, blue.GeomType):
if not isinstance(other, blue.SiteType):
xml.SubElement(tendon, 'geom', geom=thing.name)
else:
xml.SubElement(tendon, 'geom', geom=thing.name, sidesite=other.name)
elif isinstance(thing, blue.JointType):
if not isinstance(other, (int, float)):
xml.SubElement(tendon, 'joint', joint=thing.name)
else:
xml.SubElement(tendon, 'joint', joint=thing.name, coef=str(float(other)))
for branch in path._branches[::-1]:
queue.insert(0, (len(path._branches), branch))
self._built = True
# DERIVED ATTRIBUTES
@property
def actuatorfrclimited(self) -> bool|None:
"""
Indicates whether the Actuator forces applied are limited.
Returns
-------
bool | None
In the attribute is None, the value is set to ``auto``.
"""
return self._act_force_limited
@property
def range(self) -> np.ndarray:
"""
The length range of the Tendon.
Returns
-------
np.ndarray
The two entries indicate minimum and maximum Tendon length.
"""
return np.array([self.min_length,
self.max_length], dtype=np.float32)
@property
def actuatorfrcrange(self) -> np.ndarray:
"""
The Actuator force range of the Tendon.
Returns
-------
np.ndarray
The two entries indicate minimum and maximum Actuator force applied to the Tendon.
"""
return np.array([self.min_act_force,
self.max_act_force], dtype=np.float32)
# BLUEPRINTS ATTRIBUTES
@property
def max_length(self) -> float:
"""
Changing this value results in a change in the first component of ``range``.
The user should make sure, that the initial length of the tendon as
computed by the distance between its bound Things does not exeed this attribute.
Returns
-------
float
Maximum length of the Tendon
"""
return self._max_length
@max_length.setter
@blue.restrict
def max_length(self, max_length: int|float) -> None:
self._max_length = float(max_length)
@property
def min_length(self) -> float:
"""
Changing this value results in a change in the second component of ``range``.
Returns
-------
float
Minimum length of the Tendon
"""
return self._min_length
@min_length.setter
@blue.restrict
def min_length(self, min_length: int|float) -> None:
self._min_length = float(min_length)
@property
def max_act_force(self) -> float:
"""
Changing this value results in a change in the first component of ``actuatorfrcrange``.
Returns
-------
float
Maximum force applicable by an :class:`Actuator <blueprints.actuator.BaseActuator>`
"""
return self._max_act_force
@max_act_force.setter
@blue.restrict
def max_act_force(self, max_act_force: int|float) -> None:
self._max_act_force = float(max_act_force)
@property
def min_act_force(self) -> float:
"""
Changing this value results in a change in the second component of ``actuatorfrcrange``.
Returns
-------
float
Minimum force applicable by an :class:`Actuator <blueprints.actuator.BaseActuator>`
"""
return self._min_act_force
@min_act_force.setter
@blue.restrict
def min_act_force(self, min_act_force: int|float) -> None:
self._min_act_force = float(min_act_force)
# MUJOCO ATTRIBUTES
@property
def limited(self) -> bool|None:
"""
Indicates whether the Tendon length is limited by ``range``.
Returns
-------
bool | None
In the attribute is None, the value is set to ``auto``.
"""
return self._limited
@limited.setter
@blue.restrict
def limited(self, limited: bool|None) -> None:
self._limited = limited
@property
def act_force_limited(self) -> bool|None:
"""
Indicates whether the Actuator forces applied are limited.
Returns
-------
bool | None
In the attribute is None, the value is set to ``auto``.
"""
return self._act_force_limited
@act_force_limited.setter
@blue.restrict
def act_force_limited(self, act_force_limited: bool|None) -> None:
self._act_force_limited = act_force_limited
@property
def frictionloss(self) -> float:
"""
Friction loss caused by dry friction.
Returns
-------
float
To enable friction loss, set this attribute to a positive value.
"""
return self._frictionloss
@frictionloss.setter
@blue.restrict
def frictionloss(self, frictionloss: int|float) -> None:
self._frictionloss = float(frictionloss)
@property
def width(self) -> float:
"""
This attribute is purely cosmetical.
Returns
-------
föoat
Width of the Tendon.
"""
return self._width
@width.setter
@blue.restrict
def width(self, width: int|float) -> None:
self._width = float(width)
@property
def stiffness(self) -> float:
"""
A positive value generates a spring force (linear in position) acting along the Tendon.
Returns
-------
float
Stiffness of the Tendon
"""
return self._stiffness
@stiffness.setter
@blue.restrict
def stiffness(self, stiffness: int|float) -> None:
self._stiffness = float(stiffness)
@property
def damping(self) -> float:
"""
A positive value generates a damping force (linear in velocity) acting along the tendon.
If possible, :attr:`Joint.damping <blueprints.joints.BaseJoint.damping>` should be used
for stability.
Returns
-------
float
Tendon dampin.
"""
return self._damping
@damping.setter
@blue.restrict
def damping(self, damping: int|float) -> None:
self._damping = float(damping)
@property
def armature(self) -> float:
"""
Setting this attribute to a positive value :math:`m` adds a kinetic
energy term :math:`\\frac{1}{2}mv^2`, where :math:`v` is the tendon velocity.
Returns
-------
float
Inertia associated with changes in Tendon length
"""
return self._armature
@armature.setter
@blue.restrict
def armature(self, armature: int|float) -> None:
self._armature = float(armature)
[docs]
class Path(blue.PathType):
"""
Paths are used to specify the ``<pulley>`` element in Mujoco.
A path can be strung along multiple :class:`Sites <blueprints.sites.BaseSite>`,
:class:`Joints <blueprints.joints.BaseJoint>` and :class:`Geoms <blueprints.geoms.BaseGeom>`.
Every path must start and end with a Site and every Geom must be sandwitched between two Sites.
"""
[docs]
def __init__(self, tendon):
"""
.. warning::
This class is not to be instantiated by the user directly. Use the contextmanager
:meth:`Tendon.__enter__` instead (see above).
"""
self.tendon = tendon
self._path = []
self._branches = []
self._split = False
def __iter__(self):
for thing, side_site in self._path:
yield thing, side_site
def __getitem__(self, idx):
if isinstance(idx, int):
if idx >= len(self._path):
raise IndexError(f'This Path has only {len(self._path)} entries but entry {idx} was requested!')
return self._path[idx][0]
elif isinstance(idx, slice):
if idx.start >= len(self._path):
raise IndexError(f'This Path has only {len(self._path)} entries but entry {idx.start} was requested!')
elif idx.stop >= len(self._path):
raise IndexError(f'This Path has only {len(self._path)} entries but entry {idx.stop} was requested!')
return [thing for thing, side_site in self._path[idx]]
else:
raise NotImplemented
[docs]
@blue.restrict
def bind(self,
*things: list[blue.ThingType],
side_site: blue.SiteType|None = None,
coef: int|float|list[int|float]|None = None) -> None:
"""
Binding is performed in order. If a ``side_site`` is specified only
one Geom to be bound can be passed.
Parameters
----------
*things : list [ blue.ThingType ]
The Sites and Geoms to be bound to a Tendon path
side_site: blue.SiteType | None
A side Site for a Geom specifies to which side of the geom
the Tendon should snap out if passing through the Geom initially.
coef : int | float | list [ int | float] | None
Numeric coefficients for Joints. The argument must have the same shape as *things.
"""
if self._split:
raise Exception(f'Paths can not bind Things after they were split. ')
if not self.tendon._ACTIVE:
raise Exception('Tendons can only be bound to Things within the context manager. Use "with tendon as path:" and refer to the Documentation.')
if side_site is not None:
if len(things) != 1 or not isinstance(things[0], blue.GeomType):
raise ValueError('If a side_site is specified, precisely one Geom must be given.')
# FLATTENING VIEWS
flattened_things = []
for thing in things:
if isinstance(thing, blue.ViewType):
flattened_things.extend(list(thing))
else:
flattened_things.append(thing)
# CHECKING COEFFICIENTS
if coef is not None:
if not all(isinstance(thing, blue.JointType) for thing in flattened_things):
raise ValueError(f'Coefficients can only be specified for Joints.')
coef = [coef] if not isinstance(coef, list) else coef
if len(coef) != len(things):
raise ValueError(f'Every Coefficient must correspond to a Joint. Got {len(coef)} Coefficients and {len(things)} Joints.')
# BINDING THINGS
for i, thing in enumerate(flattened_things):
if isinstance(thing, blue.SiteType):
if self.tendon._joints:
raise ValueError('Tendons can either bind Joints or Sites and Geoms, not both!')
self.tendon._sites.append(thing)
elif isinstance(thing, blue.GeomType):
if isinstance(self._path[-1], blue.GeomType):
raise ValueError('Geoms bound to a Tendon must be sandwiched by Sites!')
if self.tendon._joints:
raise ValueError('Tendons can either bind Joints or Sites and Geoms, not both!')
if not isinstance(thing, (blue.SphereGeomType, blue.CylinderGeomType)):
raise ValueError(f'The only geom types allowed for Tendon binding are Cylinders and Spheres. Got {type(thing)}.')
self.tendon._geoms.append(thing)
if side_site is not None:
self.tendon._side_sites.append(side_site)
side_site._side_tendons.append(self.tendon)
elif isinstance(thing, blue.JointType):
if self.tendon._geoms or self.tendon._sites:
raise ValueError('Tendons can either bind Joints or Sites and Geoms, not both!')
self.tendon._joints.append(thing)
else:
raise ValueError(f'Tendon.bind expects Things of Type Geom, Site or Joint. Got {', '.join(map(lambda x: str(repr(x)), flattened_things))}')
other = coef[i] if coef is not None and isinstance(thing, blue.JointType) else side_site
self._path.append([thing, other])
thing._tendons.append(self.tendon)
[docs]
def split(self, number: int) -> tuple:
"""
If a pulley should split the Tendon into :math:`n` :class:`Paths <blueprints.tendon.Path>`, this method
handles the split. A split Path does not necessarily start at the last element of its parent Path.
Once split, a Path can no longer bind Things.
Parameters
----------
number : int
The number of new Paths
Returns
-------
tuple [ blue.PathType ]
The new Paths connected through a pulley
"""
if not self.tendon._ACTIVE:
raise Exception('Paths can only be split within the context manager. Use "with tendon as path:" and refer to the Documentation.')
if self.tendon._joints:
raise ValueError('Tendons binding Joints cannot be split into pulleys!')
self._branches = list(Path(self.tendon) for _ in range(number))
self._split = True
return tuple(iter(self._branches))