Skip to content

Commit 8ffea7c

Browse files
committed
Add backbone for F/T sensor payload ID
1 parent 99e0760 commit 8ffea7c

File tree

9 files changed

+824
-33
lines changed

9 files changed

+824
-33
lines changed

robot_payload_id/data/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .data_matrix_numeric import (
22
compute_base_param_mapping,
3+
construct_ft_data_matrix,
34
extract_numeric_data_matrix_autodiff,
45
)
56
from .data_matrix_symbolic import (
@@ -15,5 +16,6 @@
1516
from .joint_data import (
1617
compute_autodiff_joint_data_from_fourier_series_traj_params1,
1718
compute_autodiff_joint_data_from_simple_sinusoidal_traj_params,
19+
compute_ft_sensor_measurements,
1820
generate_random_joint_data,
1921
)

robot_payload_id/data/data_matrix_numeric.py

+176
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616
DecomposeLumpedParameters,
1717
Evaluate,
1818
Expression,
19+
JacobianWrtVariable,
1920
MathematicalProgram,
2021
MultibodyForces_,
2122
MultibodyPlant_,
23+
SpatialAcceleration,
24+
SpatialForce,
2225
)
2326
from tqdm import tqdm
2427

@@ -317,6 +320,179 @@ def extract_numeric_data_matrix_autodiff(
317320
return W_data, w0, tau_data
318321

319322

323+
def construct_ft_matrix(
324+
linear_gravity: np.ndarray,
325+
angular_velocity: np.ndarray,
326+
linear_acceleration: np.ndarray,
327+
angular_acceleration: np.ndarray,
328+
) -> np.ndarray:
329+
"""
330+
Constructs the 6x10 matrix that maps the inertial parameters to the force-torque
331+
sensor measurements. The equations take the form [f, tau] = mat * alpha, where f
332+
and tau are the force and torque measurements, and alpha are the lumped inertial
333+
parameters of form [m, hx, hy, hz, Ixx, Ixy, Ixz, Iyy, Iyz, Izz].
334+
All parameters and measurements should be expressed in the sensor frame S.
335+
336+
See equation 5 in "Improving Force Control Performance by Computational Elimination
337+
of Non-Contact Forces/Torques" by Kubus et al.
338+
339+
Args:
340+
linear_gravity (np.ndarray): The linear gravity vector of shape (3,). Expressed
341+
in the sensor frame. g_W_S.
342+
angular_velocity (np.ndarray): The angular velocity vector of shape (3,).
343+
Expressed in the sensor frame. omega_WS_S.
344+
linear_acceleration (np.ndarray): The linear acceleration vector of shape (3,).
345+
Expressed in the sensor frame. a_WS_S.
346+
angular_acceleration (np.ndarray): The angular acceleration vector of shape
347+
(3,). Expressed in the sensor frame. alpha_WS_S.
348+
349+
Returns:
350+
np.ndarray: The 6x10 matrix that maps the inertial parameters to the
351+
force-torque sensor measurements.
352+
"""
353+
mat = np.zeros((6, 10))
354+
mat[0, 0:4] = [
355+
linear_acceleration[0] - linear_gravity[0],
356+
-angular_velocity[1] ** 2 - angular_velocity[2] ** 2,
357+
angular_velocity[0] * angular_velocity[1] - angular_acceleration[2],
358+
angular_velocity[0] * angular_velocity[2] + angular_acceleration[1],
359+
]
360+
mat[1, 0:4] = [
361+
linear_acceleration[1] - linear_gravity[1],
362+
angular_velocity[0] * angular_velocity[1] + angular_acceleration[2],
363+
-angular_velocity[0] ** 2 - angular_velocity[2] ** 2,
364+
angular_velocity[1] * angular_velocity[2] - angular_acceleration[0],
365+
]
366+
mat[2, 0:4] = [
367+
linear_acceleration[2] - linear_gravity[2],
368+
angular_velocity[0] * angular_velocity[2] - angular_acceleration[1],
369+
angular_velocity[1] * angular_velocity[2] + angular_acceleration[0],
370+
-angular_velocity[1] ** 2 - angular_velocity[0] ** 2,
371+
]
372+
mat[3, 0:4] = [
373+
0.0,
374+
0.0,
375+
linear_acceleration[2] - linear_gravity[2],
376+
linear_gravity[1] - linear_acceleration[1],
377+
]
378+
mat[4, 0:4] = [
379+
0.0,
380+
linear_gravity[2] - linear_acceleration[2],
381+
0.0,
382+
linear_acceleration[0] - linear_gravity[0],
383+
]
384+
mat[5, 0:4] = [
385+
0.0,
386+
linear_acceleration[1] - linear_gravity[1],
387+
linear_gravity[0] - linear_acceleration[0],
388+
0.0,
389+
]
390+
mat[3, 4:10] = [
391+
angular_acceleration[0],
392+
angular_acceleration[1] - angular_velocity[0] * angular_velocity[2],
393+
angular_acceleration[2] + angular_velocity[0] * angular_velocity[1],
394+
-angular_velocity[1] * angular_velocity[2],
395+
angular_velocity[1] ** 2 - angular_velocity[2] ** 2,
396+
angular_velocity[1] * angular_velocity[2],
397+
]
398+
mat[4, 4:10] = [
399+
angular_velocity[0] * angular_velocity[2],
400+
angular_acceleration[0] + angular_velocity[1] * angular_velocity[2],
401+
angular_velocity[2] ** 2 - angular_velocity[0] ** 2,
402+
angular_acceleration[1],
403+
angular_acceleration[2] - angular_velocity[0] * angular_velocity[1],
404+
-angular_velocity[0] * angular_velocity[2],
405+
]
406+
mat[5, 4:10] = [
407+
-angular_velocity[0] * angular_velocity[1],
408+
angular_velocity[0] ** 2 - angular_velocity[1] ** 2,
409+
angular_acceleration[0] - angular_velocity[1] * angular_velocity[2],
410+
angular_velocity[0] * angular_velocity[1],
411+
angular_acceleration[1] + angular_velocity[0] * angular_velocity[2],
412+
angular_acceleration[2],
413+
]
414+
return mat
415+
416+
417+
def construct_ft_data_matrix(
418+
plant_components: ArmPlantComponents,
419+
ft_body_name: str,
420+
ft_sensor_frame_name: str,
421+
joint_data: JointData,
422+
use_progress_bar: bool = True,
423+
) -> np.ndarray:
424+
"""Constructs the numeric data matrix for the force-torque sensor measurements.
425+
See `construct_ft_matrix` for more details.
426+
427+
Args:
428+
plant_components (ArmPlantComponents): This must contain the plant. Optionally,
429+
it can also contain the plant's context.
430+
body_name: The name of the body to which the force-torque sensor is welded.
431+
This could be the F/T sensor body if it is modelled as such.
432+
ft_sensor_frame_name (str): The name of the frame that the F/T sensor is
433+
measuring in.
434+
joint_data (JointData): The joint data. Only the joint positions, velocities,
435+
and accelerations are used for the data matrix computation.
436+
use_progress_bar (bool, optional): Whether to use a progress bar.
437+
438+
Returns:
439+
np.ndarray: The numeric data matrix of shape (num_joints * num_timesteps, 10).
440+
"""
441+
plant = plant_components.plant
442+
context = plant_components.plant_context
443+
444+
ft_sensor_body = plant.GetBodyByName(ft_body_name)
445+
ft_sensor_body_frame = ft_sensor_body.body_frame()
446+
# Transform from sensor body frame S to measurement frame F
447+
measurement_frame = plant.GetFrameByName(ft_sensor_frame_name)
448+
X_SF = measurement_frame.CalcPose(context, ft_sensor_body_frame)
449+
450+
# Transform from the world frame to the sensor frame S
451+
X_WS = plant.EvalBodyPoseInWorld(context=context, body=ft_sensor_body)
452+
X_WF = X_WS @ X_SF
453+
X_FW = X_WF.inverse()
454+
R_FW = X_FW.rotation()
455+
456+
num_timesteps = len(joint_data.sample_times_s)
457+
data_matrix = np.zeros((num_timesteps * 6, 10))
458+
for i in tqdm(
459+
range(num_timesteps),
460+
desc="Extracting data matrix",
461+
disable=not use_progress_bar,
462+
):
463+
# Set joint data
464+
plant.SetPositions(context, joint_data.joint_positions[i])
465+
plant.SetVelocities(context, joint_data.joint_velocities[i])
466+
467+
# Compute forces due to gravity
468+
g_W = [0.0, 0.0, -9.81]
469+
g_W_F = R_FW @ g_W
470+
471+
# Compute spatial velocity
472+
V_WS = plant.EvalBodySpatialVelocityInWorld(
473+
context=context,
474+
body=ft_sensor_body,
475+
)
476+
omega_WS_F = R_FW @ V_WS.rotational()
477+
478+
# Compute spatial accelerations
479+
A_WS = plant.EvalBodySpatialAccelerationInWorld(
480+
context=context,
481+
body=ft_sensor_body,
482+
)
483+
a_WS_F = R_FW @ A_WS.translational()
484+
alpha_WS_F = R_FW @ A_WS.rotational()
485+
486+
data_matrix[i * 6 : (i + 1) * 6, :] = construct_ft_matrix(
487+
linear_gravity=g_W_F,
488+
angular_velocity=omega_WS_F,
489+
linear_acceleration=a_WS_F,
490+
angular_acceleration=alpha_WS_F,
491+
)
492+
493+
return data_matrix
494+
495+
320496
def compute_base_param_mapping(
321497
W_data: np.ndarray, scale_by_singular_values: bool = False, tol: float = 1e-6
322498
) -> np.ndarray:

robot_payload_id/data/joint_data.py

+88-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,20 @@
22

33
import numpy as np
44

5-
from pydrake.all import AutoDiffXd, MultibodyForces_, MultibodyPlant
5+
from pydrake.all import (
6+
AutoDiffXd,
7+
JacobianWrtVariable,
8+
MultibodyForces_,
9+
MultibodyPlant,
10+
SpatialForce,
11+
)
612
from tqdm import tqdm
713

8-
from robot_payload_id.utils import FourierSeriesTrajectoryAttributes, JointData
14+
from robot_payload_id.utils import (
15+
ArmPlantComponents,
16+
FourierSeriesTrajectoryAttributes,
17+
JointData,
18+
)
919

1020

1121
def generate_random_joint_data(
@@ -332,3 +342,79 @@ def compute_autodiff_joint_data_from_fourier_series_traj_params1(
332342
sample_times_s=np.arange(num_timesteps) * sample_delta,
333343
)
334344
return joint_data
345+
346+
347+
def compute_ft_sensor_measurements(
348+
arm_plant_components: ArmPlantComponents,
349+
joint_data: JointData,
350+
ft_sensor_frame_name: str,
351+
) -> JointData:
352+
""""""
353+
plant = arm_plant_components.plant
354+
context = arm_plant_components.plant_context
355+
356+
measurement_frame = plant.GetFrameByName(ft_sensor_frame_name)
357+
X_WF = measurement_frame.CalcPoseInWorld(context)
358+
X_FW = X_WF.inverse()
359+
R_FW = X_FW.rotation()
360+
361+
# spatial_jacobian = plant.CalcJacobianSpatialVelocity(
362+
# context=context,
363+
# with_respect_to=JacobianWrtVariable.kQDot,
364+
# frame_B=plant.GetFrameByName(ft_sensor_frame_name),
365+
# p_BoBp_B=np.zeros(3),
366+
# frame_A=plant.world_frame(),
367+
# frame_E=plant.world_frame(),
368+
# )
369+
# spatial_jacobian_transpose_inv = np.linalg.pinv(spatial_jacobian.T)
370+
371+
ft_sensor_measurements = np.empty((joint_data.joint_positions.shape[0], 6))
372+
for i in range(joint_data.joint_positions.shape[0]):
373+
# Set joint data
374+
plant.SetPositions(context, joint_data.joint_positions[i])
375+
plant.SetVelocities(context, joint_data.joint_velocities[i])
376+
377+
# # Compute inverse dynamics
378+
# forces = MultibodyForces_(plant)
379+
# plant.CalcForceElementsContribution(context, forces)
380+
# joint_torques = plant.CalcInverseDynamics(
381+
# context=context,
382+
# known_vdot=joint_data.joint_accelerations[i],
383+
# external_forces=forces,
384+
# )
385+
386+
# # Convert generalized forces into spatial forces
387+
# spatial_force = spatial_jacobian_transpose_inv @ joint_torques
388+
389+
# ft_sensor_measurement = np.concatenate(
390+
# [spatial_force.translational(), spatial_force.rotational()]
391+
# )
392+
393+
# TODO: Figure out who to ask about this. Doesn't make sense without this
394+
# taking accelerations but inverting Jacobian bad as not full rank.
395+
396+
# Compute the reaction force F_CJc_Jc on the child body C at the last joint's
397+
# child frame Jc. For the iiwa, Jc is 'iiwa_link_7'.
398+
# TODO: See https://drakedevelopers.slack.com/archives/C2WBPQDB7/p1710888837360519?thread_ts=1710863581.014389&cid=C2WBPQDB7
399+
reaction_force: SpatialForce = plant.get_reaction_forces_output_port().Eval(
400+
context
401+
)[-1]
402+
# Invert using action-reaction principle.
403+
ft_sensor_measurement = -np.concatenate(
404+
[reaction_force.translational(), reaction_force.rotational()]
405+
)
406+
407+
# Transform into measurement frame. This works as there is no lever arm effect.
408+
ft_sensor_measurement_in_F = np.concatenate(
409+
[R_FW @ ft_sensor_measurement[:3], R_FW @ ft_sensor_measurement[3:]]
410+
)
411+
ft_sensor_measurements[i] = ft_sensor_measurement_in_F
412+
413+
return JointData(
414+
joint_positions=joint_data.joint_positions,
415+
joint_velocities=joint_data.joint_velocities,
416+
joint_accelerations=joint_data.joint_accelerations,
417+
joint_torques=joint_data.joint_torques,
418+
ft_sensor_measurements=ft_sensor_measurements,
419+
sample_times_s=joint_data.sample_times_s,
420+
)

robot_payload_id/environment/arm.py

+29-29
Original file line numberDiff line numberDiff line change
@@ -50,31 +50,31 @@ def create_arm(
5050
)
5151

5252
# Add Controller
53-
controller_plant = MultibodyPlant(time_step)
54-
controller_parser = get_parser(controller_plant)
55-
controller_parser.AddModels(arm_file_path)
56-
controller_plant.Finalize()
57-
arm_controller = builder.AddSystem(
58-
InverseDynamicsController(
59-
controller_plant,
60-
kp=[100] * num_joints,
61-
kd=[10] * num_joints,
62-
ki=[1] * num_joints,
63-
has_reference_acceleration=False,
64-
)
65-
)
66-
arm_controller.set_name("arm_controller")
67-
builder.Connect(
68-
plant.get_state_output_port(arm),
69-
arm_controller.get_input_port_estimated_state(),
70-
)
71-
builder.Connect(
72-
arm_controller.get_output_port_control(), plant.get_actuation_input_port(arm)
73-
)
74-
builder.Connect(
75-
trajectory_source.get_output_port(),
76-
arm_controller.get_input_port_desired_state(),
77-
)
53+
# controller_plant = MultibodyPlant(time_step)
54+
# controller_parser = get_parser(controller_plant)
55+
# controller_parser.AddModels(arm_file_path)
56+
# controller_plant.Finalize()
57+
# arm_controller = builder.AddSystem(
58+
# InverseDynamicsController(
59+
# controller_plant,
60+
# kp=[100] * num_joints,
61+
# kd=[10] * num_joints,
62+
# ki=[1] * num_joints,
63+
# has_reference_acceleration=False,
64+
# )
65+
# )
66+
# arm_controller.set_name("arm_controller")
67+
# builder.Connect(
68+
# plant.get_state_output_port(arm),
69+
# arm_controller.get_input_port_estimated_state(),
70+
# )
71+
# builder.Connect(
72+
# arm_controller.get_output_port_control(), plant.get_actuation_input_port(arm)
73+
# )
74+
# builder.Connect(
75+
# trajectory_source.get_output_port(),
76+
# arm_controller.get_input_port_desired_state(),
77+
# )
7878

7979
# Meshcat
8080
if use_meshcat:
@@ -89,9 +89,9 @@ def create_arm(
8989
meshcat_visualizer = None
9090

9191
state_logger = LogVectorOutput(plant.get_state_output_port(), builder)
92-
commanded_torque_logger = LogVectorOutput(
93-
arm_controller.get_output_port_control(), builder
94-
)
92+
# commanded_torque_logger = LogVectorOutput(
93+
# arm_controller.get_output_port_control(), builder
94+
# )
9595

9696
diagram = builder.Build()
9797

@@ -101,7 +101,7 @@ def create_arm(
101101
plant=plant,
102102
trajectory_source=trajectory_source,
103103
state_logger=state_logger,
104-
commanded_torque_logger=commanded_torque_logger,
104+
commanded_torque_logger=None,
105105
meshcat=meshcat,
106106
meshcat_visualizer=meshcat_visualizer,
107107
)

robot_payload_id/optimization/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .inertial_param_sdp import solve_inertial_param_sdp
1+
from .inertial_param_sdp import solve_ft_payload_sdp, solve_inertial_param_sdp
22
from .nevergrad_augmented_lagrangian import NevergradAugmentedLagrangian
33
from .optimal_experiment_design_b_spline import (
44
ExcitationTrajectoryOptimizerBsplineBlackBoxALNumeric,

0 commit comments

Comments
 (0)