Skip to content

Commit e60b67e

Browse files
committed
Add inertial ellipsoid visualization script
1 parent b211139 commit e60b67e

File tree

2 files changed

+279
-3
lines changed

2 files changed

+279
-3
lines changed

robot_payload_id/utils/plant_param_setters.py

+71-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import logging
22

3-
from typing import Dict
3+
from typing import Dict, Optional
44

55
import numpy as np
66

77
from pydrake.all import (
8+
Context,
89
JointActuator,
10+
MultibodyPlant,
911
RevoluteJoint,
1012
RigidBody,
1113
SpatialInertia,
@@ -18,12 +20,16 @@
1820
def write_parameters_to_plant(
1921
arm_components: ArmComponents,
2022
var_name_param_dict: Dict[str, float],
23+
plant_context: Optional[Context] = None,
2124
) -> ArmPlantComponents:
2225
"""
23-
Create a plant context that has the parameters written to it.
26+
Create a plant context that has the parameters written to it. The plant is assumed
27+
to contain a robot arm.
2428
"""
2529
plant = arm_components.plant
26-
plant_context = plant.CreateDefaultContext()
30+
plant_context = (
31+
plant.CreateDefaultContext() if plant_context is None else plant_context
32+
)
2733
# Check if only contains payload parameters
2834
payload_only = len(var_name_param_dict) == 10
2935

@@ -88,3 +94,65 @@ def write_parameters_to_plant(
8894
)
8995

9096
return ArmPlantComponents(plant=plant, plant_context=plant_context)
97+
98+
99+
def write_parameters_to_rigid_body(
100+
plant: MultibodyPlant,
101+
var_name_param_dict: Dict[str, float],
102+
body_name: str,
103+
plant_context: Optional[Context] = None,
104+
) -> ArmComponents:
105+
"""
106+
Create a plant context that has the parameters written to it. The parameters are
107+
of a single rigid body in the plant.
108+
109+
`var_name_param_dict` is a dictionary that contains the 10 inertial parameters.
110+
"""
111+
# Create a plant context
112+
plant_context = (
113+
plant.CreateDefaultContext() if plant_context is None else plant_context
114+
)
115+
body: RigidBody = plant.GetBodyByName(body_name)
116+
117+
# Extract the parameters
118+
for name, value in var_name_param_dict.items():
119+
if "m" in name:
120+
mass = abs(value)
121+
if "hx" in name:
122+
hx = value
123+
if "hy" in name:
124+
hy = value
125+
if "hz" in name:
126+
hz = value
127+
if "Ixx" in name:
128+
Ixx = value
129+
if "Iyy" in name:
130+
Iyy = value
131+
if "Izz" in name:
132+
Izz = value
133+
if "Ixy" in name:
134+
Ixy = value
135+
if "Ixz" in name:
136+
Ixz = value
137+
if "Iyz" in name:
138+
Iyz = value
139+
140+
# TODO: Project the inertia to the closest valid one
141+
body.SetSpatialInertiaInBodyFrame(
142+
plant_context,
143+
SpatialInertia(
144+
mass=mass,
145+
p_PScm_E=np.array([hx, hy, hz]) / mass,
146+
G_SP_E=UnitInertia(
147+
Ixx=Ixx / mass,
148+
Iyy=Iyy / mass,
149+
Izz=Izz / mass,
150+
Ixy=Ixy / mass,
151+
Ixz=Ixz / mass,
152+
Iyz=Iyz / mass,
153+
),
154+
skip_validity_check=True,
155+
),
156+
)
157+
158+
return ArmPlantComponents(plant=plant, plant_context=plant_context)
+208
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
## Similar to `InertiaVisualizer` in drake but allows editing the plant's inertial
2+
## parameters.
3+
4+
import argparse
5+
import logging
6+
7+
from pathlib import Path
8+
9+
import numpy as np
10+
11+
from pydrake.all import (
12+
AddMultibodyPlantSceneGraph,
13+
BodyIndex,
14+
DiagramBuilder,
15+
Ellipsoid,
16+
IllustrationProperties,
17+
MeshcatVisualizer,
18+
MultibodyPlant,
19+
Rgba,
20+
RoleAssign,
21+
Simulator,
22+
SpatialInertia,
23+
StartMeshcat,
24+
)
25+
26+
from robot_payload_id.utils import (
27+
ArmComponents,
28+
ArmPlantComponents,
29+
get_parser,
30+
write_parameters_to_plant,
31+
write_parameters_to_rigid_body,
32+
)
33+
34+
35+
def main():
36+
parser = argparse.ArgumentParser()
37+
parser.add_argument(
38+
"--initial_param_path",
39+
type=Path,
40+
help="Path to the initial parameter `.npy` file. If not provided, the initial "
41+
+ "parameters are set to the model's parameters.",
42+
)
43+
parser.add_argument(
44+
"--model_path",
45+
type=Path,
46+
default=Path("./models/iiwa.dmd.yaml"),
47+
help="Path to the model file to visualize the ellipsoids for.",
48+
)
49+
parser.add_argument(
50+
"--num_joints",
51+
type=int,
52+
default=7,
53+
help="Number of joints in the model. If zero, then the model is assumed to be "
54+
+ "a rigid body.",
55+
)
56+
parser.add_argument(
57+
"--existing_model_alpha",
58+
type=float,
59+
default=0.5,
60+
help="Alpha value for the existing model's visual geometries.",
61+
)
62+
parser.add_argument(
63+
"--log_level",
64+
type=str,
65+
default="INFO",
66+
choices=["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
67+
help="Log level.",
68+
)
69+
70+
args = parser.parse_args()
71+
initial_param_path = args.initial_param_path
72+
model_path: Path = args.model_path
73+
num_joints = args.num_joints
74+
existing_model_alpha = args.existing_model_alpha
75+
76+
logging.basicConfig(level=args.log_level)
77+
78+
# Create plant
79+
builder = DiagramBuilder()
80+
plant: MultibodyPlant
81+
plant, scene_graph = AddMultibodyPlantSceneGraph(builder, time_step=0.0)
82+
parser = get_parser(plant)
83+
models = parser.AddModels(model_path.as_posix())
84+
85+
# Disable gravity to prevent robot from falling down
86+
for model in models:
87+
plant.set_gravity_enabled(model, False)
88+
89+
plant.Finalize()
90+
91+
# Make existing visual geometries transparent
92+
for i in range(plant.num_bodies()):
93+
body = plant.get_body(BodyIndex(i))
94+
visual_geometry_ids = plant.GetVisualGeometriesForBody(body)
95+
for geometry_id in visual_geometry_ids:
96+
old_props = scene_graph.model_inspector().GetIllustrationProperties(
97+
geometry_id
98+
)
99+
new_props = IllustrationProperties(old_props)
100+
old_rgba = old_props.GetProperty("phong", "diffuse")
101+
new_props.UpdateProperty(
102+
group_name="phong",
103+
name="diffuse",
104+
value=Rgba(
105+
r=old_rgba.r(),
106+
g=old_rgba.g(),
107+
b=old_rgba.b(),
108+
a=existing_model_alpha,
109+
),
110+
)
111+
scene_graph.AssignRole(
112+
source_id=plant.get_source_id(),
113+
geometry_id=geometry_id,
114+
properties=new_props,
115+
assign=RoleAssign.kReplace,
116+
)
117+
118+
# Create meshcat
119+
meshcat = StartMeshcat()
120+
_ = MeshcatVisualizer.AddToBuilder(builder, scene_graph, meshcat)
121+
diagram = builder.Build()
122+
context = diagram.CreateDefaultContext()
123+
plant_context = plant.GetMyMutableContextFromRoot(context)
124+
125+
# Load parameters
126+
if initial_param_path is not None:
127+
logging.info(f"Loading initial parameters from {initial_param_path}.")
128+
var_sol_dict = np.load(initial_param_path, allow_pickle=True).item()
129+
130+
if num_joints == 0:
131+
arm_plant_components = write_parameters_to_rigid_body(
132+
plant, var_sol_dict, plant_context
133+
)
134+
else:
135+
arm_components = ArmComponents(
136+
num_joints=num_joints,
137+
diagram=None,
138+
plant=plant,
139+
trajectory_source=None,
140+
state_logger=None,
141+
commanded_torque_logger=None,
142+
meshcat=None,
143+
meshcat_visualizer=None,
144+
)
145+
arm_plant_components = write_parameters_to_plant(
146+
arm_components, var_sol_dict, plant_context
147+
)
148+
else:
149+
logging.info(f"Using default parameters from {model_path}.")
150+
arm_plant_components = ArmPlantComponents(
151+
plant=plant,
152+
plant_context=plant_context,
153+
)
154+
155+
# Create and visualize ellipsoids
156+
for i in range(plant.num_bodies()):
157+
body = plant.get_body(BodyIndex(i))
158+
M_BBo_B: SpatialInertia = body.CalcSpatialInertiaInBodyFrame(
159+
arm_plant_components.plant_context
160+
)
161+
mass = M_BBo_B.get_mass()
162+
if np.isnan(mass):
163+
continue
164+
165+
(
166+
radii,
167+
X_BE,
168+
) = M_BBo_B.CalcPrincipalSemiDiametersAndPoseForSolidEllipsoid()
169+
170+
# Clip for improved visualization. See CalculateInertiaGeometry in drake
171+
# internal.
172+
new_radii = np.clip(radii, a_min=1e-2 * radii.max(), a_max=None)
173+
if np.any(new_radii != radii):
174+
logging.warning(
175+
f"Radii for body {body.name()} were clipped to {new_radii}."
176+
)
177+
178+
# Assuming the ~density of water for the visualization ellipsoid, scale up
179+
# the ellipsoid representation of the unit inertia to have a volume that
180+
# matches the body's actual mass, so that our ellipsoid actually has the
181+
# same inertia as M_BBo_B. (We're illustrating M_BBo, not G_BBo.)
182+
density = 1000.0
183+
unit_inertia_ellipsoid_mass = density * 4.0 / 3.0 * np.pi * np.prod(new_radii)
184+
volume_scale = mass / unit_inertia_ellipsoid_mass
185+
abc = new_radii * np.cbrt(volume_scale)
186+
ellipsoid = Ellipsoid(abc)
187+
188+
# Compute transform
189+
X_WB = plant.EvalBodyPoseInWorld(plant_context, body)
190+
X_WE = X_WB.multiply(X_BE)
191+
192+
# Visualize
193+
color = np.random.choice(range(255), size=3) / 255
194+
meshcat.SetObject(
195+
path=f"ellipsoid_{i}",
196+
shape=ellipsoid,
197+
rgba=Rgba(r=color[0], g=color[1], b=color[2], a=1.0),
198+
)
199+
meshcat.SetTransform(path=f"ellipsoid_{i}", X_ParentPath=X_WE)
200+
201+
# Simulate
202+
simulator = Simulator(diagram, context=context)
203+
simulator.set_target_realtime_rate(1.0)
204+
simulator.AdvanceTo(5.0)
205+
206+
207+
if __name__ == "__main__":
208+
main()

0 commit comments

Comments
 (0)