import numpy as np
import xml.etree.ElementTree as xml
import blueprints as blue
[docs]
class BaseJoint(blue.JointType, blue.MoveableThing, blue.thing.NodeThing):
"""
The Joint class represents Mujoco joint objects. All Joints inherit from this base class.
.. note::
Joints enable movement along the degrees of freedom their parent Bodies and not to Bodies
attached to the Joints (one cannot attach Bodies to Joints). This might be counterintuitive
since real joints move objects which are attached to them.
Most attribute descriptions are partially taken from `Mujoco <https://mujoco.readthedocs.io/en/latest/XMLreference.html#body-joint>`__.
"""
[docs]
@blue.restrict
def __init__(self,
pos: np.ndarray|list[int|float] = [0., 0., 0.],
alpha: int|float = 0.,
beta: int|float = 0.,
gamma: int|float = 0.,
springdamper: np.ndarray|list[int|float] = [0., 0.],
actuatorforcerange: np.ndarray|list[int|float] = [0., 0.],
stiffness: int|float = 0.,
springref: int|float = 0.,
armature: int|float = 0.,
damping: int|float = 0.,
sensors: blue.SensorType|list[blue.SensorType]|None = None,
actuators: blue.ActuatorType|list[blue.ActuatorType]|None = None,
name: str|None = None,
copy: bool = True,
x: int|float|np.int32|np.int64|np.float32|np.float64|None = None,
y: int|float|np.int32|np.int64|np.float32|np.float64|None = None,
z: int|float|np.int32|np.int64|np.float32|np.float64|None = None,
**kwargs):
"""
Parameters
----------
pos : np.ndarray | list[int | float], optional
Represents the position of the object. Changing this attribute also changes the properties
:attr:`x`, :attr:`y` and :attr:`z`.
alpha : int | float, optional
(Improper) euler angle of rotation around the x-axis in radian. Changing this value also
changes the :attr:`euler` property.
beta : int | float, optional
(Improper) euler angle of rotation around the y-axis in radian. Changing this value also
changes the :attr:`euler` property.
gamma : int | float, optional
(Improper) euler angle of rotation around the z-axis in radian. Changing this value also
changes the :attr:`euler` property.
springdamper : np.ndarray | list[int | float], optional
When both numbers are positive, the compiler will override any stiffness and damping
values specified with the attributes below, and will instead set them automatically so
that the resulting mass-spring-damper for this joint has the desired time constant
(first value) and damping ratio (second value).
actuatorforcerange : np.ndarray | list[int | float], optional
This attribute specifies whether actuator forces acting on the Joint should be clamped.
stiffness : int | float, optional
Joint stiffness. If this value is positive, a spring will be created with equilibrium
position given by springref below. The spring force is computed along with the other
passive forces.
springref : int | float, optional
The Joint position or angle in which the joint spring (if any) achieves equilibrium.
armature : int | float, optional
Armature inertia (or rotor inertia, or reflected inertia) of all degrees of freedom
created by this Joint. These are constants added to the diagonal of the inertia matrix
in generalized coordinates. They make the simulation more stable, and often increase
physical realism. This is because when a motor is attached to the system with a transmission
that amplifies the motor force by :math:`c`, the inertia of the rotor (i.e., the moving part
of the motor) is amplified by :math:`c\\cdot c`. The same holds for gears in the early
stages of planetary gear boxes. These extra inertias often dominate the inertias of the
robot parts that are represented explicitly in the model, and the armature attribute is the
way to model them.
damping : int | float, optional
Damping applied to all degrees of freedom created by this joint. Unlike friction loss which
is computed by the constraint solver, damping is simply a force linear in velocity. It is
included in the passive forces. Despite this simplicity, larger damping values can make
numerical integrators unstable, which is why our Euler integrator handles damping implicitly.
sensors : blue.SensorType | list[blue.SensorType] | None, optional
The Sensors are attached to the actuator on initialization.
actuators : blue.ActuatorType | list[blue.ActuatorType] | None, optional
The Actuators are attached to the actuator on initialization.
name : str | None, optional
The user specified name. In the case of a naming conflict the name will be altered by an
enumeration scheme.
copy : bool, optional
If True, the Sensors and Actuators passed during init will be copied before attachment,
otherwise the original Sensor is attached.
**kwargs
Keyword arguments are passed to ``super().__init__``.
"""
pack = lambda x: x if isinstance(x, list) else [x]
sensors = pack(sensors) if sensors else []
actuators = pack(actuators) if actuators else []
self._sensors = []
self._tendons = []
self._actuators = []
self._CHILDREN = {'tendons': {'type': blue.TendonType,
'children': self._tendons},
'sensors': {'type': blue.SensorType,
'children': self._sensors},
'actuators': {'type': blue.ActuatorType,
'children': self._actuators}}
self.springdamper = springdamper
self.actuatorforcerange = actuatorforcerange
self.stiffness = stiffness
self.springref = springref
self.armature = armature
self.damping = damping
super().__init__(pos=pos,
alpha=alpha,
beta=beta,
gamma=gamma,
name=name,
x=x,
y=y,
z=z,
**kwargs)
self.attach(*sensors,
*actuators,
copy=copy)
for actuator in self.actuators:
actuator.joint = self
self._check_children_types()
@blue.restrict
def _build(self,
parent: xml.Element,
world,
indicies,
**kwargs) -> xml.Element:
"""
This method is called to build the xml.
Parameters
----------
parent : xml.etree.ElementTree.Element
The xml element of its parent
world : WorldType
The World from which the build method was called initially
Returns
-------
xml.etree.ElementTree.Element
The builded xml element of the Joint.
"""
self._index = indicies['jnt']
indicies['jnt'] += 1
if hasattr(self, 'axis'):
if 'axis' in kwargs:
axis = kwargs.pop('axis')
else:
axis = self.axis
kwargs['axis'] = self.rotation_matrix @ axis
return super()._build(parent, world, indicies, **kwargs)
@blue.restrict
@classmethod
def _from_xml_element(cls,
xml_element: xml.Element,
sensors: list = None,
actuators: list = []) -> blue.ThingType:
"""
This method reconstructs a Joint from an xml element.
Parameters
----------
xml_element : xml.Element
The xml element from which a Geom is reconstructed.
sensors : blue.SensorType | list[blue.SensorType] | None, optional
The Sensors are attached to the actuator on initialization.
actuators : blue.ActuatorType | list[blue.ActuatorType] | None, optional
The Actuators are attached to the actuator on initialization.
Returns
-------
blue.ThingType
The reconstructed Joint.
"""
init_args, post_args, rest_args = cls._xml_element_args(xml_element)
init_args['sensors'] = sensors
init_args['copy'] = False
if 'type' in rest_args:
joint_type = rest_args.pop('type')
else:
joint_type = None
joint = object.__new__(blue.REGISTER.JOINT_THINGS[joint_type])
joint.__init__(**init_args)
for key, val in post_args.items():
setattr(joint, key, val)
for actuator in actuators:
joint.attach(actuator, copy=False)
actuator.joint = joint
return joint
# MUJOCO PROPERTIES
@property
def type(self) -> str:
"""
This is a derived attribute, which is used to specify the ``type`` attribute in the
mujoco ``joint`` tag.
Returns
-------
str
The type is the lower case name of the Joint class.
"""
return self.__class__.__name__.lower()
@property
def springdamper(self) -> np.ndarray:
"""
When both numbers are positive, the compiler will override any stiffness and damping
values specified with the attributes below, and will instead set them automatically so
that the resulting mass-spring-damper for this joint has the desired time constant
(first value) and damping ratio (second value).
Returns
-------
np.ndarray
"""
return self._springdamper
@springdamper.setter
@blue.restrict
def springdamper(self, springdamper: np.ndarray|list[int|float]) -> None:
"""
When both numbers are positive, the compiler will override any stiffness and damping
values specified with the attributes below, and will instead set them automatically so
that the resulting mass-spring-damper for this joint has the desired time constant
(first value) and damping ratio (second value).
Parameters
----------
springdamper : np.ndarray | list[int | float]
The springdamer which is assigned.
"""
if isinstance(springdamper, list):
if len(springdamper) != 2:
raise ValueError(f'springdamper must have a length of 2, got {len(springdamper)} instead.')
elif isinstance(springdamper, np.ndarray):
if springdamper.shape != (2,):
raise ValueError(f'springdamper must have a shape of (2,), got {springdamper.shape} instead.')
self._springdamper = np.array(springdamper, dtype=np.float32)
@property
def actuatorforcerange(self) -> np.ndarray:
"""
actuatorforcerange : np.ndarray | list[int | float], optional
This attribute specifies whether actuator forces acting on the Joint should be clamped.
Returns
-------
np.ndarray
"""
return self._actuatorforcerange
@actuatorforcerange.setter
@blue.restrict
def actuatorforcerange(self, actuatorforcerange: np.ndarray|list[int|float]) -> None:
"""
This attribute specifies whether actuator forces acting on the Joint should be clamped.
Parameters
----------
actuatorforcerange : np.ndarray | list[int | float], optional
The actuator force range which is assigned.
"""
if isinstance(actuatorforcerange, list):
if len(actuatorforcerange) != 2:
raise ValueError(f'actuatorforcerange must have a length of 2, got {len(actuatorforcerange)} instead.')
elif isinstance(actuatorforcerange, np.ndarray):
if actuatorforcerange.shape != (2,):
raise ValueError(f'actuatorforcerange must have a shape of (2,), got {actuatorforcerange.shape} instead.')
self._actuatorforcerange = np.array(actuatorforcerange, dtype=np.float32)
@property
def stiffness(self) -> float:
"""
Joint stiffness. If this value is positive, a spring will be created with equilibrium
position given by springref below. The spring force is computed along with the other
passive forces.
Returns
-------
float
"""
return self._stiffness
@stiffness.setter
@blue.restrict
def stiffness(self, stiffness: int|float) -> None:
"""
Joint stiffness. If this value is positive, a spring will be created with equilibrium
position given by springref below. The spring force is computed along with the other
passive forces.
Parameters
----------
stiffness : int | float, optional
The stiffness which is assigned
"""
self._stiffness = float(stiffness)
@property
def springref(self) -> float:
"""
The Joint position or angle in which the joint spring (if any) achieves equilibrium.
Returns
-------
float
"""
return self._springref
@springref.setter
@blue.restrict
def springref(self, springref: int|float) -> None:
"""
The Joint position or angle in which the joint spring (if any) achieves equilibrium.
Parameters
----------
springref : int | float, optional
The springref which is assigned
"""
self._springref = float(springref)
@property
def armature(self) -> float:
"""
Armature inertia (or rotor inertia, or reflected inertia) of all degrees of freedom
created by this Joint.
Returns
-------
float
"""
return self._armature
@armature.setter
@blue.restrict
def armature(self, armature: int|float) -> None:
"""
Armature inertia (or rotor inertia, or reflected inertia) of all degrees of freedom created
by this Joint.
Parameters
----------
armature : int | float
The armature which is assigned
"""
self._armature = float(armature)
@property
def damping(self) -> float:
"""
Damping applied to all degrees of freedom created by this joint. Unlike friction loss which
is computed by the constraint solver, damping is simply a force linear in velocity. It is
included in the passive forces. Despite this simplicity, larger damping values can make
numerical integrators unstable, which is why our Euler integrator handles damping implicitly.
Returns
-------
float
"""
return self._damping
@damping.setter
@blue.restrict
def damping(self, damping: int|float) -> None:
"""
Damping applied to all degrees of freedom created by this joint. Unlike friction loss which is
computed by the constraint solver, damping is simply a force linear in velocity. It is included
in the passive forces. Despite this simplicity, larger damping values can make numerical
integrators unstable, which is why our Euler integrator handles damping implicitly.
Parameters
----------
damping : int | float
The damping which is assigned
"""
self._damping = float(damping)
[docs]
class Hinge(blue.HingeType, BaseJoint):
"""
Most attribute descriptions are partially taken from `Mujoco <https://mujoco.readthedocs.io/en/latest/XMLreference.html#body-joint>`__.
"""
[docs]
@blue.restrict
def __init__(self,
pos: np.ndarray|list[int|float] = [0., 0., 0.],
axis: np.ndarray|list[int|float]|str = [0., 0., 1.],
range: np.ndarray|list[int|float] = [0., 0.],
ref: int|float = 0.,
frictionloss: int|float = 0.,
name: str|None = None,
x: int|float|np.int32|np.int64|np.float32|np.float64|None = None,
y: int|float|np.int32|np.int64|np.float32|np.float64|None = None,
z: int|float|np.int32|np.int64|np.float32|np.float64|None = None,
**kwargs):
"""
Parameters
----------
pos : np.ndarray | list[int | float], optional
Represents the position of the object. Changing this attribute also changes the properties
:attr:`x`, :attr:`y` and :attr:`z`.
axis : np.ndarray | list[int | float] | str, optional
This attribute specifies the axis of rotation for hinge Joints and the direction of
translation for Slides.
range : np.ndarray | list[int | float], optional
The reference position or angle of the Joint. It defines the joint value corresponding
to the initial model configuration.
ref : int | float, optional
The reference position or angle of the Joint. It defines the joint value corresponding
to the initial model configuration.
frictionloss : int | float, optional
Friction loss due to dry friction. This value is the same for all degrees of freedom
created by this Joint.
name : str | None, optional
The user specified name. In the case of a naming conflict the name will be altered by
an enumeration scheme.
**kwargs
Keyword arguments are passed to ``super().__init__``.
"""
axis = blue.utils.geometry.Vector.get_axis(axis)
self.axis = axis
self.range = range
self.ref = ref
self.frictionloss = frictionloss
super().__init__(pos=pos,
name=name,
x=x,
y=y,
z=z,
**kwargs)
@property
def axis(self) -> np.ndarray:
"""
This attribute specifies the axis of rotation for hinge Joints and the direction of translation for Slides.
Returns
-------
np.ndarray
"""
return self._axis
@axis.setter
@blue.restrict
def axis(self, axis: np.ndarray|list[int|float]) -> None:
"""
This attribute specifies the axis of rotation for hinge Joints and the direction of translation for Slides.
Parameters
----------
axis : np.ndarray | list[int | float]
The axis which is assigned
"""
self._axis = np.array(axis, np.float32)
@property
def range(self) -> np.ndarray:
"""
The reference position or angle of the Joint. It defines the joint value corresponding to the initial model configuration.
Returns
-------
np.ndarray
"""
return self._range
@range.setter
@blue.restrict
def range(self, range: np.ndarray|list[int|float]) -> None:
"""
The reference position or angle of the Joint. It defines the joint value corresponding to the initial model configuration.
Parameters
----------
range : np.ndarray | list[int | float]
The range which is assigned
"""
self._range = np.array(range, np.float32)
@property
def ref(self) -> float:
"""
The reference position or angle of the Joint. It defines the joint value corresponding to the initial model configuration.
Returns
-------
float
"""
return self._ref
@ref.setter
@blue.restrict
def ref(self, ref: int|float) -> None:
"""
The reference position or angle of the Joint. It defines the joint value corresponding to the initial model configuration.
Parameters
----------
ref : int | float
The ref which is assigned
"""
self._ref = float(ref)
@property
def frictionloss(self) -> float:
"""
Friction loss due to dry friction. This value is the same for all degrees of freedom created by this Joint.
Returns
-------
float
"""
return self._frictionloss
@frictionloss.setter
@blue.restrict
def frictionloss(self, frictionloss: int|float) -> None:
"""
Friction loss due to dry friction. This value is the same for all degrees of freedom created by this Joint.
Parameters
----------
frictionloss : int | float
The xxx wfrictionlossh is assigned
"""
self._frictionloss = float(frictionloss)
[docs]
class Slide(blue.SlideType, BaseJoint):
"""
Most attribute descriptions are partially taken from `Mujoco <https://mujoco.readthedocs.io/en/latest/XMLreference.html#body-joint>`__.
"""
[docs]
@blue.restrict
def __init__(self,
pos: np.ndarray|list[int|float] = [0., 0., 0.],
axis: np.ndarray|list[int|float]|str = [0., 0., 1.],
range: np.ndarray|list[int|float] = [0., 0.],
ref: int|float = 0,
frictionloss: int|float = 0.,
name: str|None = None,
x: int|float|np.int32|np.int64|np.float32|np.float64|None = None,
y: int|float|np.int32|np.int64|np.float32|np.float64|None = None,
z: int|float|np.int32|np.int64|np.float32|np.float64|None = None,
**kwargs):
"""
Parameters
----------
pos : np.ndarray | list[int | float], optional
Represents the position of the object. Changing this attribute also changes the properties
:attr:`x`, :attr:`y` and :attr:`z`.
axis : np.ndarray | list[int | float] | str, optional
This attribute specifies the axis of rotation for hinge Joints and the direction of translation for Slides.
range : np.ndarray | list[int | float], optional
The reference position or angle of the Joint. It defines the joint value corresponding to the initial model configuration.
ref : int | float, optional
The reference position or angle of the Joint. It defines the joint value corresponding to the initial model configuration.
frictionloss : int | float, optional
Friction loss due to dry friction. This value is the same for all degrees of freedom created by this Joint.
name : str | None, optional
The user specified name. In the case of a naming conflict the name will be altered by an
enumeration scheme.
**kwargs
Keyword arguments are passed to ``super().__init__``.
"""
axis = blue.utils.geometry.Vector.get_axis(axis)
self.axis = axis
self.range = range
self.ref = ref
self.frictionloss = frictionloss
super().__init__(pos=pos,
name=name,
x=x,
y=y,
z=z,
**kwargs)
@property
def axis(self) -> np.ndarray:
"""
This attribute specifies the axis of rotation for hinge Joints and the direction of translation for Slides.
Returns
-------
np.ndarray
"""
return self._axis
@axis.setter
@blue.restrict
def axis(self, axis: np.ndarray|list[int|float]) -> None:
"""
This attribute specifies the axis of rotation for hinge Joints and the direction of translation for Slides.
Parameters
----------
axis : np.ndarray | list[int | float]
The axis which is assigned
"""
self._axis = np.array(axis, np.float32)
@property
def range(self) -> np.ndarray:
"""
The reference position or angle of the Joint. It defines the joint value corresponding to the initial model configuration.
Returns
-------
np.ndarray
"""
return self._range
@range.setter
@blue.restrict
def range(self, range: np.ndarray|list[int|float]) -> None:
"""
The reference position or angle of the Joint. It defines the joint value corresponding to the initial model configuration.
Parameters
----------
range : np.ndarray | list[int | float]
The range which is assigned
"""
self._range = np.array(range, np.float32)
@property
def ref(self) -> float:
"""
The reference position or angle of the Joint. It defines the joint value corresponding to the initial model configuration.
Returns
-------
float
"""
return self._ref
@ref.setter
@blue.restrict
def ref(self, ref: int|float) -> None:
"""
The reference position or angle of the Joint. It defines the joint value corresponding to the initial model configuration.
Parameters
----------
ref : int | float
The ref which is assigned
"""
self._ref = float(ref)
@property
def frictionloss(self) -> float:
"""
Friction loss due to dry friction. This value is the same for all degrees of freedom created by this Joint.
Returns
-------
float
"""
return self._frictionloss
@frictionloss.setter
@blue.restrict
def frictionloss(self, frictionloss: int|float) -> None:
"""
Friction loss due to dry friction. This value is the same for all degrees of freedom created by this Joint.
Parameters
----------
frictionloss : int | float
The xxx wfrictionlossh is assigned
"""
self._frictionloss = float(frictionloss)
[docs]
class Ball(blue.BallType, BaseJoint):
"""
Most attribute descriptions are partially taken from `Mujoco <https://mujoco.readthedocs.io/en/latest/XMLreference.html#body-joint>`__.
"""
[docs]
@blue.restrict
def __init__(self,
pos: np.ndarray|list[int|float] = [0., 0., 0.],
range: np.ndarray|list[int|float] = [0., 0.],
frictionloss: int|float = 0.,
name: str|None = None,
x: int|float|np.int32|np.int64|np.float32|np.float64|None = None,
y: int|float|np.int32|np.int64|np.float32|np.float64|None = None,
z: int|float|np.int32|np.int64|np.float32|np.float64|None = None,
**kwargs):
"""
Parameters
----------
pos : np.ndarray | list[int | float], optional
Represents the position of the object. Changing this attribute also changes the properties
:attr:`x`, :attr:`y` and :attr:`z`.
range : np.ndarray | list[int | float], optional
The reference position or angle of the Joint. It defines the joint value corresponding to the initial model configuration.
frictionloss : int | float, optional
Friction loss due to dry friction. This value is the same for all degrees of freedom created by this Joint.
name : str | None, optional
The user specified name. In the case of a naming conflict the name will be altered by an
enumeration scheme.
x : int | float |np.int32 | np.int64 | np.float32 | np.float64 | None, optional
If `pos` is not specified, this argument sets the X position coordinate.
y : int | float |np.int32 | np.int64 | np.float32 | np.float64 | None, optional
If `pos` is not specified, this argument sets the Y position coordinate.
z : int | float |np.int32 | np.int64 | np.float32 | np.float64 | None, optional
If `pos` is not specified, this argument sets the Z position coordinate.
**kwargs
Keyword arguments are passed to ``super().__init__``.
"""
self.range = range
self.frictionloss = frictionloss
super().__init__(pos=pos,
name=name,
x=x,
y=y,
z=z,
**kwargs)
@property
def range(self) -> np.ndarray:
"""
The reference position or angle of the Joint. It defines the joint value corresponding to the initial model configuration.
Returns
-------
np.ndarray
"""
return self._range
@range.setter
@blue.restrict
def range(self, range: np.ndarray|list[int|float]) -> None:
"""
The reference position or angle of the Joint. It defines the joint value corresponding to the initial model configuration.
Parameters
----------
range : np.ndarray | list[int | float]
The range which is assigned
"""
self._range = np.array(range, np.float32)
@property
def frictionloss(self) -> float:
"""
Friction loss due to dry friction. This value is the same for all degrees of freedom created by this Joint.
Returns
-------
float
"""
return self._frictionloss
@frictionloss.setter
@blue.restrict
def frictionloss(self, frictionloss: int|float) -> None:
"""
Friction loss due to dry friction. This value is the same for all degrees of freedom created by this Joint.
Parameters
----------
frictionloss : int | float
The xxx which is assigned
"""
self._frictionloss = float(frictionloss)
[docs]
class Free(blue.FreeType, BaseJoint):
"""
Most attribute descriptions are partially taken from `Mujoco <https://mujoco.readthedocs.io/en/latest/XMLreference.html#body-joint>`__.
"""
[docs]
def __init__(self,
**kwargs):
"""
Parameters
----------
**kwargs
Keyword arguments are passed to ``super().__init__``.
"""
super().__init__(**kwargs)
[docs]
class Joint(Hinge):
"""
Dummy for :class:`Hinge`
"""
@property
def type(self) -> None:
"""
Dummy for :class:`Hinge`
Returns
-------
str
``'hinge'``
"""
return None
JOINT_THINGS = {None: Joint,
'hinge': Hinge,
'slide': Slide,
'ball': Ball,
'free': Free}