Source code for gym_jiminy.common.blocks.deformation_estimator
"""Implementation of a stateless kinematic deformation estimator block
compatible with gym_jiminy reinforcement learning pipeline environment design.
"""
from collections.abc import Mapping
from typing import List, Dict, Sequence, Tuple, Optional
import numpy as np
import numba as nb
import gymnasium as gym
import jiminy_py.core as jiminy
from jiminy_py.core import ( # pylint: disable=no-name-in-module
EncoderSensor, ImuSensor, get_frame_indices)
import pinocchio as pin
from ..bases import BaseActT, BaseObsT, BaseObserverBlock, InterfaceJiminyEnv
from ..utils import (DataNested,
matrices_to_quat,
quat_multiply,
compute_tilt_from_quat,
swing_from_vector)
# FIXME: Enabling cache causes segfault on Apple Silicon
@nb.jit(nopython=True, cache=False, inline='always')
def _compute_orientation_error(obs_imu_quats: np.ndarray,
obs_imu_indices: Tuple[int, ...],
inv_obs_imu_quats: np.ndarray,
kin_imu_rots: Tuple[np.ndarray, ...],
kin_imu_quats: np.ndarray,
dev_imu_quats: np.ndarray,
ignore_twist: bool) -> None:
"""Compute total deviation of observed IMU data wrt to theoretical model in
world frame.
"""
# Re-order IMU data
for i, imu_index in enumerate(obs_imu_indices):
inv_obs_imu_quats[:, i] = obs_imu_quats[:, imu_index]
# Compute orientation error
if ignore_twist:
# Compute swing of deviation between observed and kinematic IMU data:
# 1. Extract observed IMU tilt
# 2. Apply kinematic IMU rotation on it
# 3. Reconstruct swing from tilt of deviation
# This approach is equivalent to removing the twist from the exact
# deviation directly, but much faster as it does not require computing
# the observed IMU quaternions nor to compose rotations.
# Extract observed tilt: w_R_obs.T @ e_z
obs_imu_tilts = np.stack(compute_tilt_from_quat(inv_obs_imu_quats), 1)
# Apply theoretical kinematic IMU rotation on observed tilt.
# The result can be interpreted as the tilt error between observed
# and theoretical kinematic IMU orientation in world frame, ie:
# w_tilt_err = (w_R_kin @ w_R_obs.T) @ e_z
for i, kin_imu_rot in enumerate(kin_imu_rots):
obs_imu_tilts[i] = kin_imu_rot @ obs_imu_tilts[i]
# Compute "smallest" rotation that can explain the tilt error.
swing_from_vector(obs_imu_tilts.T, dev_imu_quats)
# Conjugate quaternion of IMU deviation
dev_imu_quats[-1] *= -1
else:
# Convert kinematic IMU rotation matrices to quaternions
matrices_to_quat(kin_imu_rots, kin_imu_quats)
# Conjugate quaternion of IMU orientation
inv_obs_imu_quats[-1] *= -1
# Compute one-by-one difference between observed and theoretical
# kinematic IMU orientations.
quat_multiply(kin_imu_quats,
inv_obs_imu_quats,
dev_imu_quats)
@nb.jit(nopython=True, cache=True, inline='always')
def _compute_deformation_from_deviation(kin_flex_rots: Tuple[np.ndarray, ...],
kin_flex_quats: np.ndarray,
is_flex_flipped: np.ndarray,
is_chain_orphan: Tuple[bool, bool],
dev_imu_quats: np.ndarray,
inv_child_dev_imu_quats: np.ndarray,
parent_dev_imu_quats: np.ndarray,
deformation_flex_quats: np.ndarray
) -> None:
"""Cast observed IMU deviations in world frame into deformations at their
corresponding flexibility frame.
"""
# Convert kinematic flexibility rotation matrices to quaternions
matrices_to_quat(kin_flex_rots, kin_flex_quats)
# Translate observed IMU deviation in world frame to their respective
# parent and child flex frames.
is_flex_parent_orphan, is_flex_child_orphan = is_chain_orphan
if is_flex_parent_orphan:
quat_multiply(
dev_imu_quats, kin_flex_quats[:, 1:], parent_dev_imu_quats[:, 1:])
if is_flex_child_orphan:
quat_multiply(dev_imu_quats,
kin_flex_quats[:, :-1],
inv_child_dev_imu_quats[:, :-1])
else:
quat_multiply(
dev_imu_quats, kin_flex_quats, inv_child_dev_imu_quats)
else:
nflexs = len(kin_flex_rots)
quat_multiply(
dev_imu_quats[:, :nflexs], kin_flex_quats, parent_dev_imu_quats)
if is_flex_child_orphan:
quat_multiply(dev_imu_quats[:, 1:],
kin_flex_quats[:, :-1],
inv_child_dev_imu_quats[:, :-1])
else:
quat_multiply(
dev_imu_quats[:, 1:], kin_flex_quats, inv_child_dev_imu_quats)
if is_flex_parent_orphan:
parent_dev_imu_quats[:, 0] = kin_flex_quats[:, 0]
if is_flex_child_orphan:
inv_child_dev_imu_quats[:, -1] = kin_flex_quats[:, -1]
inv_child_dev_imu_quats[-1] *= -1
# Project observed total deformation on flexibility frames
quat_multiply(inv_child_dev_imu_quats,
parent_dev_imu_quats,
deformation_flex_quats)
# Conjugate deformation quaternion if triplet (parent IMU, flex, child IMU)
# is reversed wrt to the standard joint ordering from URDF.
deformation_flex_quats[-1][is_flex_flipped] *= -1
# FIXME: Enabling cache causes segfault on Apple Silicon
[docs]
@nb.jit(nopython=True, cache=False)
def flexibility_estimator(obs_imu_quats: np.ndarray,
obs_imu_indices: Tuple[int, ...],
inv_obs_imu_quats: np.ndarray,
kin_imu_rots: Tuple[np.ndarray, ...],
kin_imu_quats: np.ndarray,
kin_flex_rots: Tuple[np.ndarray, ...],
kin_flex_quats: np.ndarray,
is_flex_flipped: np.ndarray,
is_chain_orphan: Tuple[bool, bool],
dev_imu_quats: np.ndarray,
inv_child_dev_imu_quats: np.ndarray,
parent_dev_imu_quats: np.ndarray,
deformation_flex_quats: np.ndarray,
ignore_twist: bool) -> None:
"""Compute the local deformation at an arbitrary set of flexibility points
that are presumably responsible for most of the whole deformation of the
mechanical structure.
.. warning::
The local deformation at each flexibility frame must be observable, ie
the flexibility and IMU frames interleave with each others in a unique
and contiguous sub-chain in theoretical kinematic tree of the robot.
:param obs_imu_quats: Orientation estimates of an unordered arbitrary set
of IMUs as a 2D array whose first dimension gathers
the 4 quaternion coordinates [qx, qy, qz, qw] while
the second corresponds to N independent IMU data.
:param obs_imu_indices: M-tuple of ordered IMU indices of interest for
which the total deviation will be computed.
:param inv_obs_imu_quats: Pre-allocated memory for storing the inverse of
the orientation estimates for an ordered subset
of the IMU data `obs_imu_quats` according to
`obs_imu_indices`.
:param kin_imu_rots: Tuple of M kinematic frame orientations corresponding
to the ordered subset of IMUs `obs_imu_indices`, for
the configuration of the theoretical robot model.
:param kin_imu_quats: Pre-allocated memory for storing the kinematic frame
orientations of the ordered subset of IMUs of
interest as a 2D array whose first dimension gathers
the 4 quaternion coordinates while the second
corresponds to the M independent IMUs.
:param kin_flex_rots: Tuple of K kinematic frame orientations for all the
flexibility points that interleaves with the ordered
subset of IMUs of interest in the kinematic tree.
:param kin_flex_quats: Pre-allocated memory for storing the kinematic frame
orientations of the flexibility points that
interleaves with the ordered subset of IMUs of
interest as a 2D array whose first dimension gathers
the 4 quaternion coordinates while the second
corresponds to the K independent flexibility points.
:param is_flex_flipped: Whether local deformation estimates for each
flexibility point must be inverted to be consistent
with standard URDF convention as 1D boolean array.
:param is_chain_orphan: 2-Tuple stating whether first and last flexibility
point is orphan respectively, ie only a single IMU
is available for estimating its local deformation.
:param dev_imu_quats: Total deviation of th observed IMU data wrt to the
theoretical model in world frame for the ordered
subset of IMUs of interest, as a 2D array whose first
dimension gathers the 4 quaternion coordinates while
the second corresponds to the M independent IMUs.
:param inv_child_dev_imu_quats:
Total deviation observed IMU data in child flexibility frame as a 2D
array whose first dimension gathers the 4 quaternion coordinates while
the second corresponds to the K independent flexibility frames.
:param parent_dev_imu_quats:
Total deviation observed IMU data in parent flexibility frame as a 2D
array whose first dimension gathers the 4 quaternion coordinates while
the second corresponds to the K independent flexibility frames.
:param deformation_flex_quats:
Pre-allocated memory for storing the local deformation estimates for
each flexibility point flexibility points as a 2D array whose first
dimension gathers the 4 quaternion coordinates while the second
corresponds to the K independent flexibility points.
:param ignore_twist: Whether to ignore the twist of the orientation
estimates of the ordered subset of IMUs of interest,
and incidentally the twist of deformation at the
flexibility points.
"""
# Compute error between observed and theoretical kinematic IMU orientation
_compute_orientation_error(obs_imu_quats,
obs_imu_indices,
inv_obs_imu_quats,
kin_imu_rots,
kin_imu_quats,
dev_imu_quats,
ignore_twist)
# Project observed total deformation on flexibility frames
_compute_deformation_from_deviation(kin_flex_rots,
kin_flex_quats,
is_flex_flipped,
is_chain_orphan,
dev_imu_quats,
inv_child_dev_imu_quats,
parent_dev_imu_quats,
deformation_flex_quats)
[docs]
def get_flexibility_imu_frame_chains(
pinocchio_model: pin.Model,
flex_joint_names: Sequence[str],
imu_frame_names: Sequence[str]) -> Sequence[Tuple[
Sequence[str], Sequence[Optional[str]], Sequence[bool]]]:
"""Extract the minimal set of contiguous sub-chains in kinematic tree of a
given model that goes through all the specified flexibility and IMU frames.
:param pinocchio_model: Model from which to extract sub-chains.
:param flex_joint_names: Unordered sequence of joint names that must be
considered as associated with flexibility frames.
:param imu_frame_names: Unordered sequence of frame names that must be
considered as associated with IMU frames.
"""
# Determine the leaf joints of the kinematic tree, ie all the joints that
# are not parent of any other joint.
parents = pinocchio_model.parents
leaf_joint_indices = set(range(len(parents))) - set(parents)
# Compute the support of each leaf joint, ie the sub-chain going from each
# leaf to the root joint.
supports = []
for joint_index in leaf_joint_indices:
support = []
while True:
support.append(joint_index)
if joint_index == 0:
break
joint_index = parents[joint_index]
supports.append(support)
# Deduce all the kinematic chains.
# For each support, check if there is a match in any other chain.
# The first match (in order) must be the only one to be considered.
# It always exists, as a root joint is shared by all supports.
chains = []
for i, support_1 in enumerate(supports):
for support_2 in supports[(i + 1):]:
for joint_index in support_1:
if joint_index in support_2:
break
support_left = support_1[:(support_1.index(joint_index) + 1)]
support_right = support_2[:support_2.index(joint_index)]
chains.append([*support_left, *support_right[::-1]])
# Special case when there is a chain in the kinematic tree
if not chains:
chains.append(supports[0])
# Determine the parent joint of all flexibility and IMU frames
flex_joint_map, imu_joint_map = ({
pinocchio_model.frames[index].parent: name
for name, index in zip(
frame_names, get_frame_indices(pinocchio_model, frame_names))}
for frame_names in (flex_joint_names, imu_frame_names))
flex_joint_indices, imu_joint_indices = (
set(joint_map.keys())
for joint_map in (flex_joint_map, imu_joint_map))
# Make sure that the robot has no IMU on its root joint if fixed-based
root_type = jiminy.get_joint_type(pinocchio_model, 1)
if root_type != jiminy.JointModelType.FREE:
if 1 in imu_joint_indices:
raise ValueError(
"There must not be an IMU frame attached to the root joint of "
"the robot if it has a fixed based (no freeflyer).")
if not imu_joint_indices.issuperset(leaf_joint_indices):
raise ValueError(
"There must be an IMU frame attached to all the leaf joints "
"of the robot if it has a fixed based (no freeflyer).")
# Remove all joints not being flex or having an IMU attached
flex_imu_joint_chains_all = []
flex_or_imu_joint_indices = flex_joint_indices | imu_joint_indices
for chain in chains:
flex_imu_chain = list(
i for i in chain if i in flex_or_imu_joint_indices)
if len(flex_imu_chain) > 1:
flex_imu_joint_chains_all.append(flex_imu_chain)
# Remove redundant chains, ie the subchains of some other
flex_imu_joint_chains = []
for i, chain_i in enumerate(flex_imu_joint_chains_all):
for chain_j in flex_imu_joint_chains_all:
is_subchain = False
for i in range(0, len(chain_j) - len(chain_i)):
if chain_i == chain_j[:len(chain_i)]:
is_subchain = True
break
if is_subchain:
break
else:
flex_imu_joint_chains.append(chain_i)
# Duplicate flexibility and IMU frames sharing the same joint indices.
# Go through each chain and check that IMU and flex joints interleaves.
duplicated_flex_imu_joint_chains = []
flex_and_imu_joint_indices = flex_joint_indices & imu_joint_indices
for chain in flex_imu_joint_chains_all:
is_imu_joint = False
duplicated_chain = []
for joint_index in chain:
duplicated_chain.append(joint_index)
if joint_index in flex_and_imu_joint_indices:
duplicated_chain.append(joint_index)
else:
if (is_imu_joint and joint_index in flex_joint_indices) or (
not is_imu_joint and joint_index in imu_joint_indices):
is_imu_joint = not is_imu_joint
continue
raise ValueError("Flexibility and IMU frames must interleave.")
duplicated_flex_imu_joint_chains.append(duplicated_chain)
# Extract triplets (parent IMU, flex, child IMU), where either the parent
# or child IMU is None for orphan flexibility frames.
flex_imu_joint_triplets_all: List[
Tuple[Optional[int], int, Optional[int]]] = []
imu_and_not_flex_joint_indices = imu_joint_indices - flex_joint_indices
for chain in duplicated_flex_imu_joint_chains:
start_index = 0
if chain[0] not in imu_and_not_flex_joint_indices:
flex_imu_joint_triplets_all.append(
(None, *chain[:2])) # type: ignore[arg-type]
i = 2
for j in range(start_index, len(chain) - 2, 2):
flex_imu_joint_triplets_all.append(
chain[j:(j + 3)]) # type: ignore[arg-type]
if chain[-1] not in imu_and_not_flex_joint_indices:
flex_imu_joint_triplets_all.append(
(*chain[-2:], None)) # type: ignore[arg-type]
# Remove redundant triplet, ie associated with the same flexibility frame.
# Complete triplet must be preferred over orphan triplet.
flex_imu_joint_triplets = []
flex_joint_triplets = []
is_orphan_triplets = []
for triplet in flex_imu_joint_triplets_all:
parent_imu_joint, flex_joint, child_imu_joint = triplet
is_orphan = parent_imu_joint is None or child_imu_joint is None
if flex_joint not in flex_joint_triplets:
flex_imu_joint_triplets.append(triplet)
flex_joint_triplets.append(flex_joint)
is_orphan_triplets.append(is_orphan)
elif not is_orphan:
triplet_index = flex_joint_triplets.index(flex_joint)
if is_orphan_triplets[triplet_index]:
flex_imu_joint_triplets[triplet_index] = triplet
flex_joint_triplets[triplet_index] = flex_joint
# Concatenate triplets back in frame chains.
# Note that computations can be vectorized for each independent chain.
flex_imu_name_chains = [] # [(flexs, imus, flipped), ...]
imu_grp: List[Optional[str]] = []
child_joint_imu_prev: Optional[int] = -1
for triplet in flex_imu_joint_triplets:
parent_imu_joint, flex_joint, child_imu_joint = triplet
if parent_imu_joint is None:
assert child_imu_joint is not None
is_flipped = flex_joint >= child_imu_joint
elif child_imu_joint is None:
assert parent_imu_joint is not None
is_flipped = parent_imu_joint >= flex_joint
else:
is_flipped = triplet != sorted(
triplet) # type: ignore[type-var]
if child_joint_imu_prev != parent_imu_joint:
flex_grp: List[str] = []
imu_grp = [imu_joint_map.get(parent_imu_joint, None)]
is_flipped_grp: List[bool] = []
flex_imu_name_chains.append((flex_grp, imu_grp, is_flipped_grp))
child_joint_imu_prev = child_imu_joint
flex_grp.append(flex_joint_map[flex_joint])
imu_grp.append(imu_joint_map.get(child_imu_joint, None))
is_flipped_grp.append(is_flipped)
return flex_imu_name_chains
[docs]
class DeformationEstimator(
BaseObserverBlock[np.ndarray, np.ndarray, BaseObsT, BaseActT]):
"""Compute the local deformation at an arbitrary set of flexibility points
that are presumably responsible for most of the whole deformation of the
mechanical structure.
The number of IMU sensors and flexibility frames must be consistent:
* If the robot has no freeflyer, there must be as many IMU sensors as
flexibility frames (0), ie
*---o---x---o---x---o---x
|
|
x---o---x
* Otherwise, it can either have one more IMU than flexibility frames
(1), the same number (2), or up to one less IMU per branch in the
kinematic tree (3).
(1) x---o---x---o---x---o---x
|
|
x---o---x
(2) +---o---x---o---x---o---x
|
|
x---o---x
(3) +---o---x---o---x---o---+
|
|
x---o---+
*: Fixed base, +: leaf frame, x: IMU frame, o: flexibility frame
(1): The pose of the freeflyer is ignored when estimating the deformation
at the flexibility frames in local frame. Mathematically, it is the
same as (0) when considering a virtual IMU with fixed orientation to
identity for the root joint.
(2): One has to compensate for the missing IMU by providing instead the
configuration of the freeflyer. More precisely, one should ensure that
the orientation of the parent frame of the orphan flexibility frame
matches the reality for the theoretical configuration. This usually
requires making some assumptions to guess to pose of the frame that is
not directly observable. Any discrepancy will be aggregated to the
estimated deformation for the orphan flexibility frame specifically
since both cannot be distinguished. This issue typically happens when
there is no IMUs in the feet of a legged robot. In such a case, there
is no better option than assuming that one of the active contact
bodies remains flat on the ground. If the twist of the IMUs are
ignored, then the twist of the contact body does not matter either,
otherwise it must be set appropriately by the user to get a
meaningless estimate for the twist of the deformation. If it cannot be
observed by some exteroceptive sensor such as vision, then the most
reasonable assumption is to suppose that it matches the twist of the
IMU coming right after in the kinematic tree. This way, they will
cancel out each other without adding bias to the twist of the orphan
flexibility frame.
(3): This case is basically the same as (2), with the addition that only
the deformation of one of the orphan flexibility frames can be
estimated at once, namely the one whose parent frame is declared as
having known orientation. The other ones will be set to identity. For
a legged robot, this corresponds to one of the contact bodies, usually
the one holding most of the total weight.
.. warning::
(2) and (3) are not supported for now, as it requires using one
additional observation layer responsible for estimating the theoretical
configuration of the robot including its freeflyer, along with the name
of the reference frame, ie the one having known orientation.
.. seealso::
Matthieu Vigne, Antonio El Khoury, Marine Pétriaux, Florent Di Meglio,
and Nicolas Petit "MOVIE: a Velocity-aided IMU Attitude Estimator for
Observing and Controlling Multiple Deformations on Legged Robots" IEEE
Robotics and Automation Letters, Institute of Electrical and
Electronics Engineers, 2022, 7 (2):
https://hal.science/hal-03511198/document
"""
def __init__(self,
name: str,
env: InterfaceJiminyEnv[BaseObsT, BaseActT],
*,
imu_frame_names: Sequence[str],
flex_frame_names: Sequence[str],
ignore_twist: bool = True,
nested_imu_key: Sequence[str] = ("features", "mahony_filter"),
# root_frame: str = "root_joint",
update_ratio: int = 1) -> None:
"""
.. warning::
The user-specified ordering of the flexibility frames will not be
honored. Indeed, they are reordered to be consistent with the
kinematic tree according to URDF standard.
:param name: Name of the block.
:param env: Environment to connect with.
:param imu_frame_names: Unordered sequence of IMU frame names that must
be used to estimate the local deformation at
all flexibility frames.
:param flex_frame_names: Unordered sequence of flexibility frame names.
It does not have to match the flexibility
frames of the true dynamic model, which is
only known in simulation. It is up to the user
to choose them appropriately. Ideally, they
must be chosen so as to explain as much as
possible the effect of the deformation on
kinematic quantities, eg the vertical position
of the end effectors, considering the number
and placement of the IMUs at hand.
:param ignore_twist: Whether to ignore the twist of the IMU quaternion
estimate. If so, then the twist of the deformation
will not be estimated either.
:param nested_imu_key: Nested key from environment observation mapping
to the IMU quaternion estimates. Their ordering
must be consistent with the true IMU sensors of
the robot.
:param update_ratio: Ratio between the update period of the observer
and the one of the subsequent observer. -1 to
match the simulation timestep of the environment.
Optional: `1` by default.
"""
# Sanitize user argument(s)
imu_frame_names, flex_frame_names = map(
list, (imu_frame_names, flex_frame_names))
# Backup some of the user-argument(s)
self.ignore_twist = ignore_twist
# Create flexible dynamic model.
# Dummy physical parameters are specified as they have no effect on
# kinematic computations.
model = jiminy.Model()
pinocchio_model_th = env.robot.pinocchio_model_th
if env.robot.has_freeflyer:
pinocchio_model_th = pin.buildReducedModel(
pinocchio_model_th, [1], pin.neutral(pinocchio_model_th))
model.initialize(pinocchio_model_th)
options = model.get_options()
options["dynamics"]["enableFlexibility"] = True
for frame_name in flex_frame_names:
options["dynamics"]["flexibilityConfig"].append(
{
"frameName": frame_name,
"stiffness": np.ones(3),
"damping": np.ones(3),
"inertia": np.ones(3),
}
)
model.set_options(options)
# Backup theoretical pinocchio model without floating base
self.pinocchio_model_th = model.pinocchio_model_th.copy()
self.pinocchio_data_th = model.pinocchio_data_th.copy()
# Extract contiguous chains of flexibility and IMU frames for which
# computations can be vectorized. It also stores the information of
# whether or not the sign of the deformation must be reversed to be
# consistent with standard convention.
flexibility_joint_names = model.flexibility_joint_names
flex_imu_frame_names_chains = get_flexibility_imu_frame_chains(
model.pinocchio_model, flexibility_joint_names, imu_frame_names)
# Replace actual flex joint name by corresponding rigid frame
self.flex_imu_frame_names_chains = []
for flex_frame_names_, imu_frame_names_, is_flipped in (
flex_imu_frame_names_chains):
flex_frame_names_ = [
flex_frame_names[flexibility_joint_names.index(name)]
for name in flex_frame_names_]
self.flex_imu_frame_names_chains.append(
(flex_frame_names_, imu_frame_names_, is_flipped))
# Check if a freeflyer estimator is required
if model.has_freeflyer:
for _, imu_frame_names_, _ in self.flex_imu_frame_names_chains:
if None in imu_frame_names_:
raise NotImplementedError(
"Freeflyer estimator is not supported for now.")
# Backup flexibility frame names
self.flexibility_frame_names = [
name for flex_frame_names, _, _ in self.flex_imu_frame_names_chains
for name in flex_frame_names]
# Define flexibility and IMU frame orientation proxies for fast access.
# Note that they will be initialized in `_setup` method.
self._kin_flex_rots: List[Tuple[np.ndarray, ...]] = []
self._kin_imu_rots: List[Tuple[np.ndarray, ...]] = []
# Pre-allocate memory for intermediary computations
self._is_chain_orphan = []
self._is_flex_flipped = []
self._kin_flex_quats = []
self._inv_obs_imu_quats = []
self._kin_imu_quats = []
self._dev_imu_quats = []
self._dev_parent_imu_quats = []
self._dev_child_imu_quats = []
for flex_frame_names_, imu_frame_names_, is_flipped in (
self.flex_imu_frame_names_chains):
num_flexs = len(flex_frame_names_)
num_imus = len(tuple(filter(None, imu_frame_names_)))
self._kin_flex_quats.append(np.empty((4, num_flexs)))
self._kin_imu_quats.append(np.empty((4, num_imus)))
self._inv_obs_imu_quats.append(np.empty((4, num_imus)))
self._dev_imu_quats.append(np.empty((4, num_imus)))
dev_parent_imu_quats = np.empty((4, num_flexs))
is_parent_orphan = imu_frame_names_[0] is None
is_child_orphan = imu_frame_names_[-1] is None
self._dev_parent_imu_quats.append(dev_parent_imu_quats)
dev_child_imu_quats = np.empty((4, num_flexs))
self._dev_child_imu_quats.append(dev_child_imu_quats)
self._is_flex_flipped.append(np.array(is_flipped))
self._is_chain_orphan.append((is_parent_orphan, is_child_orphan))
# Define IMU and encoder measurement proxy for fast access
obs_imu_quats: DataNested = env.observation
for key in nested_imu_key:
assert isinstance(obs_imu_quats, Mapping)
obs_imu_quats = obs_imu_quats[key]
assert isinstance(obs_imu_quats, np.ndarray)
self._obs_imu_quats = obs_imu_quats
# Get mapping from IMU frame to index
imu_frame_map: Dict[str, int] = {}
for sensor in env.robot.sensors[ImuSensor.type]:
assert isinstance(sensor, ImuSensor)
imu_frame_map[sensor.frame_name] = sensor.index
# Make sure that the robot has one encoder per mechanical joint
encoder_sensors = env.robot.sensors[EncoderSensor.type]
if len(encoder_sensors) < len(model.mechanical_joint_indices):
raise ValueError(
"The robot must have one encoder per mechanical joints.")
# Extract mapping from encoders to theoretical configuration.
# Note that revolute unbounded joints are not supported for now.
self.encoder_to_position_map = [-1,] * env.robot.nmotors
for sensor in env.robot.sensors[EncoderSensor.type]:
assert isinstance(sensor, EncoderSensor)
joint_index = self.pinocchio_model_th.getJointId(sensor.joint_name)
joint = self.pinocchio_model_th.joints[joint_index]
joint_type = jiminy.get_joint_type(joint)
if joint_type == jiminy.JointModelType.ROTARY_UNBOUNDED:
raise ValueError(
"Revolute unbounded joints are not supported for now.")
self.encoder_to_position_map[sensor.index] = joint.idx_q
# Extract measured motor / joint positions for fast access.
# Note that they will be initialized in `_setup` method.
self.encoder_data = np.array([])
# Ratio to translate encoder data to joint side
self.encoder_to_joint_ratio = np.array([])
# Buffer storing the theoretical configuration
self._q_th = pin.neutral(self.pinocchio_model_th)
# Whether the observer has been compiled already
self._is_compiled = False
# Initialize the observer
super().__init__(name, env, update_ratio)
# Define chunks associated with each independent flexibility-imu chain
self._deformation_flex_quats, self._obs_imu_indices = [], []
flex_start_index = 0
for flex_frame_names_, imu_frame_names_, _ in (
self.flex_imu_frame_names_chains):
flex_last_index = flex_start_index + len(flex_frame_names_)
self._deformation_flex_quats.append(
self.observation[:, flex_start_index:flex_last_index])
flex_start_index = flex_last_index
imu_frame_names_filtered = tuple(filter(None, imu_frame_names_))
imu_indices = tuple(
imu_frame_map[name] for name in imu_frame_names_filtered)
self._obs_imu_indices.append(imu_indices)
def _initialize_observation_space(self) -> None:
nflex = sum(
len(flex_frame_names)
for flex_frame_names, _, _ in self.flex_imu_frame_names_chains)
self.observation_space = gym.spaces.Box(
low=np.full((4, nflex), -1.0 - 1e-9),
high=np.full((4, nflex), 1.0 + 1e-9),
dtype=np.float64)
def _setup(self) -> None:
# Call base implementation
super()._setup()
# Refresh the theoretical model of the robot.
# Even if the robot may change, the theoretical model of the robot is
# not supposed to change in a way that would break this observer.
pinocchio_model_th = self.env.robot.pinocchio_model_th
if self.env.robot.has_freeflyer:
pinocchio_model_th = pin.buildReducedModel(
pinocchio_model_th, [1], pin.neutral(pinocchio_model_th))
self.pinocchio_model_th = pinocchio_model_th
self.pinocchio_data_th = self.pinocchio_model_th.createData()
# Fix initialization of the observation to be valid quaternions
self.observation[-1] = 1.0
# Refresh flexibility and IMU frame orientation proxies
self._kin_flex_rots.clear()
self._kin_imu_rots.clear()
for flex_frame_names, imu_frame_names_, _ in (
self.flex_imu_frame_names_chains):
imu_frame_names = list(filter(None, imu_frame_names_))
kin_flex_rots, kin_imu_rots = (tuple(
self.pinocchio_data_th.oMf[frame_index].rotation
for frame_index in get_frame_indices(
self.pinocchio_model_th, frame_names))
for frame_names in (flex_frame_names, imu_frame_names))
self._kin_flex_rots.append(kin_flex_rots)
self._kin_imu_rots.append(kin_imu_rots)
# Refresh measured motor position proxy
self.encoder_data, _ = self.env.sensor_measurements[EncoderSensor.type]
# Refresh mechanical reduction ratio
encoder_to_joint_ratio = []
for sensor in self.env.robot.sensors[EncoderSensor.type]:
try:
motor = self.env.robot.motors[sensor.motor_index]
motor_options = motor.get_options()
mechanical_reduction = motor_options["mechanicalReduction"]
encoder_to_joint_ratio.append(1.0 / mechanical_reduction)
except IndexError:
encoder_to_joint_ratio.append(1.0)
self.encoder_to_joint_ratio = np.array(encoder_to_joint_ratio)
# Call `refresh_observation` manually to pre-compile it if necessary
if not self._is_compiled:
self.refresh_observation(self.env.observation)
self._is_compiled = True
@property
def fieldnames(self) -> List[List[str]]:
return [[f"{name}.Quat{e}" for name in self.flexibility_frame_names]
for e in ("x", "y", "z", "w")]
[docs]
def refresh_observation(self, measurement: BaseObsT) -> None:
# Translate encoder data at joint level
joint_positions = self.encoder_to_joint_ratio * self.encoder_data
# Update the configuration of the theoretical model of the robot
self._q_th[self.encoder_to_position_map] = joint_positions
# Update kinematic quantities according to the estimated configuration.
# FIXME: Compute frame placement only for relevant IMUs.
pin.framesForwardKinematics(
self.pinocchio_model_th, self.pinocchio_data_th, self._q_th)
# Estimate all the deformations in their local frame.
# It loops over each flexibility-imu chain independently.
for args in zip(
self._obs_imu_indices,
self._inv_obs_imu_quats,
self._kin_imu_rots,
self._kin_imu_quats,
self._kin_flex_rots,
self._kin_flex_quats,
self._is_flex_flipped,
self._is_chain_orphan,
self._dev_imu_quats,
self._dev_child_imu_quats,
self._dev_parent_imu_quats,
self._deformation_flex_quats):
flexibility_estimator(
self._obs_imu_quats, *args, self.ignore_twist)