vulp  2.3.0
BulletInterface.cpp
Go to the documentation of this file.
1 // SPDX-License-Identifier: Apache-2.0
2 // Copyright 2022 Stéphane Caron
3 
5 
6 #include <algorithm>
7 #include <memory>
8 #include <string>
9 
10 #include "tools/cpp/runfiles/runfiles.h"
12 
13 using bazel::tools::cpp::runfiles::Runfiles;
14 
15 namespace vulp::actuation {
16 
17 std::string find_plane_urdf(const std::string argv0) {
18  std::string error;
19  std::unique_ptr<Runfiles> runfiles(Runfiles::Create(argv0, &error));
20  if (runfiles == nullptr) {
21  throw std::runtime_error(
22  "Could not retrieve the package path to plane.urdf: " + error);
23  }
24  return runfiles->Rlocation("vulp/vulp/actuation/bullet/plane/plane.urdf");
25 }
26 
28  const Parameters& params)
29  : Interface(layout), params_(params) {
30  // Start simulator
31  auto flag = (params.gui ? eCONNECT_GUI : eCONNECT_DIRECT);
32  bool is_connected = bullet_.connect(flag);
33  if (!is_connected) {
34  throw std::runtime_error("Could not connect to the Bullet GUI");
35  }
36 
37  // Setup simulation scene
38  bullet_.configureDebugVisualizer(COV_ENABLE_GUI, 0);
39  bullet_.configureDebugVisualizer(COV_ENABLE_RENDERING, 0);
40  bullet_.configureDebugVisualizer(COV_ENABLE_SHADOWS, 0);
41  if (params.gravity) {
42  bullet_.setGravity(btVector3(0, 0, -9.81));
43  }
44  bullet_.setRealTimeSimulation(false); // making sure
45 
46  // Load robot model
47  robot_ = bullet_.loadURDF(params.robot_urdf_path);
48  imu_link_index_ = get_link_index("imu");
49  if (imu_link_index_ < 0) {
50  throw std::runtime_error("Robot does not have a link named \"imu\"");
51  }
52 
53  // Read servo layout
54  for (const auto& id_joint : servo_joint_map()) {
55  const auto& servo_id = id_joint.first;
56  const std::string& joint_name = id_joint.second;
57  moteus::ServoReply reply;
58  reply.id = servo_id;
59  reply.result.temperature = 20.0; // ['C], simulation room temperature
60  reply.result.voltage = 18.0; // [V], nominal voltage of a RYOBI battery
61  joint_index_map_.try_emplace(joint_name, -1);
62  servo_reply_.try_emplace(joint_name, reply);
63  }
64 
65  // Map servo layout to Bullet
66  b3JointInfo joint_info;
67  const int nb_joints = bullet_.getNumJoints(robot_);
68  for (int joint_index = 0; joint_index < nb_joints; ++joint_index) {
69  bullet_.getJointInfo(robot_, joint_index, &joint_info);
70  std::string joint_name = joint_info.m_jointName;
71  if (joint_index_map_.find(joint_name) != joint_index_map_.end()) {
72  joint_index_map_[joint_name] = joint_index;
73 
75  props.maximum_torque = joint_info.m_jointMaxForce;
76  joint_properties_.try_emplace(joint_name, props);
77  }
78  }
79 
80  // Load plane URDF
81  if (params.floor) {
82  if (bullet_.loadURDF(find_plane_urdf(params.argv0)) < 0) {
83  throw std::runtime_error("Could not load the plane URDF!");
84  }
85  } else {
86  spdlog::info("Not loading the plane URDF");
87  if (params.gravity) {
88  spdlog::warn(
89  "No ground plane was loaded, but gravity is enabled. The robot will "
90  "fall!");
91  }
92  }
93 
94  // Load environment URDFs
95  for (const auto& urdf_path : params.env_urdf_paths) {
96  spdlog::info("Loading environment URDF: ", urdf_path);
97  if (bullet_.loadURDF(urdf_path) < 0) {
98  throw std::runtime_error("Could not load the environment URDF: " +
99  urdf_path);
100  }
101  }
102 
103  // Start visualizer and configure simulation
104  bullet_.configureDebugVisualizer(COV_ENABLE_RENDERING, 1);
105  reset(Dictionary{});
106 }
107 
108 BulletInterface::~BulletInterface() { bullet_.disconnect(); }
109 
110 void BulletInterface::reset(const Dictionary& config) {
111  params_.configure(config);
112  bullet_.setTimeStep(params_.dt);
120 }
121 
123  const Eigen::Vector3d& position_base_in_world,
124  const Eigen::Quaterniond& orientation_base_in_world,
125  const Eigen::Vector3d& linear_velocity_base_to_world_in_world,
126  const Eigen::Vector3d& angular_velocity_base_in_base) {
127  bullet_.resetBasePositionAndOrientation(
128  robot_, bullet_from_eigen(position_base_in_world),
129  bullet_from_eigen(orientation_base_in_world));
130 
131  const auto& rotation_base_to_world =
132  orientation_base_in_world; // transforms are coordinates
133  const Eigen::Vector3d angular_velocity_base_in_world =
134  rotation_base_to_world * angular_velocity_base_in_base;
135  bullet_.resetBaseVelocity(
137  bullet_from_eigen(angular_velocity_base_in_world));
138 }
139 
141  for (const auto& link_name : params_.monitor_contacts) {
142  contact_data_.try_emplace(link_name, BulletContactData());
143  }
144 }
145 
147  const int nb_joints = bullet_.getNumJoints(robot_);
148  for (int joint_index = 0; joint_index < nb_joints; ++joint_index) {
149  bullet_.resetJointState(robot_, joint_index, 0.0);
150  }
151 }
152 
154  b3JointInfo joint_info;
155  const int nb_joints = bullet_.getNumJoints(robot_);
156  for (int joint_index = 0; joint_index < nb_joints; ++joint_index) {
157  bullet_.getJointInfo(robot_, joint_index, &joint_info);
158  std::string joint_name = joint_info.m_jointName;
159  if (joint_index_map_.find(joint_name) != joint_index_map_.end()) {
160  const auto friction_it = params_.joint_friction.find(joint_name);
161  if (friction_it != params_.joint_friction.end()) {
162  joint_properties_[joint_name].friction = friction_it->second;
163  } else {
164  joint_properties_[joint_name].friction = 0.0;
165  }
166  }
167  }
168 }
169 
170 void BulletInterface::observe(Dictionary& observation) const {
171  // Eigen quaternions are serialized as [w, x, y, z]
172  // See include/palimpsest/mpack/eigen.h in palimpsest
173  observation("imu")("orientation") = imu_data_.orientation_imu_in_ars;
174  observation("imu")("angular_velocity") =
175  imu_data_.angular_velocity_imu_in_imu;
176  observation("imu")("linear_acceleration") =
178 
179  Dictionary& monitor = observation("bullet");
180  for (const auto& link_name : params_.monitor_contacts) {
181  monitor("contact")(link_name)("num_contact_points") =
182  contact_data_.at(link_name).num_contact_points;
183  }
184 }
185 
187  const moteus::Data& data,
188  std::function<void(const moteus::Output&)> callback) {
189  assert(data.commands.size() == data.replies.size());
190  assert(!std::isnan(params_.dt));
191  if (!bullet_.isConnected()) {
192  throw std::runtime_error("simulator is not running any more");
193  }
194 
195  read_joint_sensors();
196  read_imu_data(imu_data_, bullet_, robot_, imu_link_index_, params_.dt);
197  read_contacts();
198  send_commands(data);
199  bullet_.stepSimulation();
200 
201  if (params_.follower_camera) {
202  translate_camera_to_robot();
203  }
204 
205  moteus::Output output;
206  for (size_t i = 0; i < data.replies.size(); ++i) {
207  const auto servo_id = data.commands[i].id;
208  const std::string& joint_name = servo_layout().joint_name(servo_id);
209  data.replies[i].id = servo_id;
210  data.replies[i].result = servo_reply_[joint_name].result;
211  output.query_result_size = i + 1;
212  }
213  callback(output);
214 }
215 
216 void BulletInterface::read_contacts() {
217  b3ContactInformation contact_info;
218  b3RobotSimulatorGetContactPointsArgs contact_args;
219  for (const auto& link_name : params_.monitor_contacts) {
220  contact_args.m_bodyUniqueIdA = robot_;
221  contact_args.m_linkIndexA = get_link_index(link_name);
222  bullet_.getContactPoints(contact_args, &contact_info);
223  contact_data_[link_name].num_contact_points =
224  contact_info.m_numContactPoints;
225  }
226 }
227 
228 void BulletInterface::read_joint_sensors() {
229  b3JointSensorState sensor_state;
230  for (const auto& name_index : joint_index_map_) {
231  const auto& joint_name = name_index.first;
232  const auto joint_index = name_index.second;
233  bullet_.getJointState(robot_, joint_index, &sensor_state);
234  auto& result = servo_reply_[joint_name].result;
235  result.position = sensor_state.m_jointPosition / (2.0 * M_PI);
236  result.velocity = sensor_state.m_jointVelocity / (2.0 * M_PI);
237  result.torque = sensor_state.m_jointMotorTorque;
238  }
239 }
240 
241 void BulletInterface::send_commands(const moteus::Data& data) {
242  b3RobotSimulatorJointMotorArgs motor_args(CONTROL_MODE_VELOCITY);
243 
244  for (const auto& command : data.commands) {
245  const auto servo_id = command.id;
246  const std::string& joint_name = servo_layout().joint_name(servo_id);
247  const int joint_index = joint_index_map_[joint_name];
248 
249  const auto previous_mode = servo_reply_[joint_name].result.mode;
250  if (previous_mode == moteus::Mode::kStopped &&
251  command.mode != moteus::Mode::kStopped) {
252  // disable velocity controllers to enable torque control
253  motor_args.m_controlMode = CONTROL_MODE_VELOCITY;
254  motor_args.m_maxTorqueValue = 0.; // [N m]
255  bullet_.setJointMotorControl(robot_, joint_index, motor_args);
256  }
257  servo_reply_[joint_name].result.mode = command.mode;
258 
259  if (command.mode == moteus::Mode::kStopped) {
260  motor_args.m_controlMode = CONTROL_MODE_VELOCITY;
261  motor_args.m_maxTorqueValue = 100.; // [N m]
262  motor_args.m_targetVelocity = 0.; // [rad] / [s]
263  bullet_.setJointMotorControl(robot_, joint_index, motor_args);
264  continue;
265  }
266 
267  if (command.mode != moteus::Mode::kPosition) {
268  throw std::runtime_error(
269  "Bullet interface does not support command mode " +
270  std::to_string(static_cast<unsigned>(command.mode)));
271  }
272 
273  const double target_position = command.position.position * (2.0 * M_PI);
274  const double target_velocity = command.position.velocity * (2.0 * M_PI);
275  const double feedforward_torque = command.position.feedforward_torque;
276  const double kp_scale = command.position.kp_scale;
277  const double kd_scale = command.position.kd_scale;
278  const double maximum_torque = command.position.maximum_torque;
279  const double joint_torque = compute_joint_torque(
280  joint_name, feedforward_torque, target_position, target_velocity,
281  kp_scale, kd_scale, maximum_torque);
282  motor_args.m_controlMode = CONTROL_MODE_TORQUE;
283  motor_args.m_maxTorqueValue = joint_torque;
284  servo_reply_[joint_name].result.torque = joint_torque;
285  bullet_.setJointMotorControl(robot_, joint_index, motor_args);
286  }
287 }
288 
290  const std::string& joint_name, const double feedforward_torque,
291  const double target_position, const double target_velocity,
292  const double kp_scale, const double kd_scale, const double maximum_torque) {
293  assert(!std::isnan(target_velocity));
294  const BulletJointProperties& joint_props = joint_properties_[joint_name];
295  const auto& measurements = servo_reply_[joint_name].result;
296  const double measured_position = measurements.position * (2.0 * M_PI);
297  const double measured_velocity = measurements.velocity * (2.0 * M_PI);
298  const double kp = kp_scale * params_.torque_control_kp;
299  const double kd = kd_scale * params_.torque_control_kd;
300  const double tau_max = std::min(maximum_torque, joint_props.maximum_torque);
301  double torque = feedforward_torque;
302  torque += kd * (target_velocity - measured_velocity);
303  if (!std::isnan(target_position)) {
304  torque += kp * (target_position - measured_position);
305  }
306  constexpr double kMaxStictionVelocity = 1e-3; // rad/s
307  if (std::abs(measured_velocity) > kMaxStictionVelocity) {
308  torque += joint_props.friction * ((measured_velocity > 0.0) ? -1.0 : +1.0);
309  }
310  torque = std::max(std::min(torque, tau_max), -tau_max);
311  return torque;
312 }
313 
314 Eigen::Matrix4d BulletInterface::transform_base_to_world() const noexcept {
315  btVector3 position_base_in_world;
316  btQuaternion orientation_base_in_world;
317  bullet_.getBasePositionAndOrientation(robot_, position_base_in_world,
318  orientation_base_in_world);
319  auto quat = eigen_from_bullet(orientation_base_in_world);
320  Eigen::Matrix4d T = Eigen::Matrix4d::Identity();
321  T.block<3, 3>(0, 0) = quat.normalized().toRotationMatrix();
322  T.block<3, 1>(0, 3) = eigen_from_bullet(position_base_in_world);
323  return T;
324 }
325 
327  const noexcept {
329  btVector3 _; // anonymous, we only get the linear velocity
330  bullet_.getBaseVelocity(robot_, linear_velocity_base_to_world_in_world, _);
332 }
333 
335  const noexcept {
336  btVector3 angular_velocity_base_to_world_in_world;
337  btVector3 _; // anonymous, we only get the angular velocity
338  bullet_.getBaseVelocity(robot_, _, angular_velocity_base_to_world_in_world);
339  Eigen::Matrix4d T = transform_base_to_world();
340  Eigen::Matrix3d rotation_base_to_world = T.block<3, 3>(0, 0);
341  return rotation_base_to_world.transpose() *
342  eigen_from_bullet(angular_velocity_base_to_world_in_world);
343 }
344 
345 void BulletInterface::translate_camera_to_robot() {
346  b3OpenGLVisualizerCameraInfo camera_info;
347  btVector3 position_base_in_world;
348  btQuaternion orientation_base_in_world;
349  bullet_.getBasePositionAndOrientation(robot_, position_base_in_world,
350  orientation_base_in_world);
351  bullet_.getDebugVisualizerCamera(&camera_info);
352  bullet_.resetDebugVisualizerCamera(camera_info.m_dist, camera_info.m_pitch,
353  camera_info.m_yaw, position_base_in_world);
354 }
355 
356 int BulletInterface::get_link_index(const std::string& link_name) {
357  if (link_index_.find(link_name) != link_index_.end()) {
358  return link_index_[link_name];
359  }
360  int link_index = find_link_index(bullet_, robot_, link_name);
361  link_index_[link_name] = link_index;
362  return link_index;
363 }
364 
365 } // namespace vulp::actuation
void cycle(const moteus::Data &data, std::function< void(const moteus::Output &)> callback) final
Spin a new communication cycle.
void reset(const Dictionary &config) override
Reset interface.
Eigen::Vector3d angular_velocity_base_in_base() const noexcept
Get the groundtruth floating base angular velocity.
void reset_joint_angles()
Reset joint angles to zero.
void reset_joint_properties()
Reset joint properties to defaults.
~BulletInterface()
Disconnect interface.
void reset_contact_data()
Reset contact data.
double compute_joint_torque(const std::string &joint_name, const double feedforward_torque, const double target_position, const double target_velocity, const double kp_scale, const double kd_scale, const double maximum_torque)
Reproduce the moteus position controller in Bullet.
void observe(Dictionary &observation) const override
Write actuation-interface observations to dictionary.
Eigen::Matrix4d transform_base_to_world() const noexcept
Get the groundtruth floating base transform.
Eigen::Vector3d linear_velocity_base_to_world_in_world() const noexcept
Get the groundtruth floating base linear velocity.
BulletInterface(const ServoLayout &layout, const Parameters &params)
Initialize interface.
void reset_base_state(const Eigen::Vector3d &position_base_in_world, const Eigen::Quaterniond &orientation_base_in_world, const Eigen::Vector3d &linear_velocity_base_to_world_in_world, const Eigen::Vector3d &angular_velocity_base_in_base)
Reset the pose and velocity of the floating base in the world frame.
Base class for actuation interfaces.
Definition: Interface.h:24
moteus::Data & data()
Get joint command-reply data.
Definition: Interface.h:97
const ServoLayout & servo_layout() const noexcept
Get servo layout.
Definition: Interface.h:74
const std::map< int, std::string > & servo_joint_map() const noexcept
Map from servo ID to joint name.
Definition: Interface.h:82
Map between servos, their busses and the joints they actuate.
Definition: ServoLayout.h:12
const std::string & joint_name(const int servo_id) const
Get the name of the joint a servo actuates.
Definition: ServoLayout.h:44
Send actions to actuators or simulators.
Definition: bullet_utils.h:13
void read_imu_data(BulletImuData &imu_data, b3RobotSimulatorClientAPI &bullet, int robot, const int imu_link_index, double dt)
Compute IMU readings from the IMU link state.
Definition: bullet_utils.h:84
Eigen::Quaterniond eigen_from_bullet(const btQuaternion &quat)
Convert a Bullet quaternion to an Eigen one.
Definition: bullet_utils.h:41
std::string find_plane_urdf(const std::string argv0)
btQuaternion bullet_from_eigen(const Eigen::Quaterniond &quat)
Convert an Eigen quaternion to a Bullet one.
Definition: bullet_utils.h:21
int find_link_index(b3RobotSimulatorClientAPI &bullet, int robot, const std::string &link_name) noexcept
Find the index of a link.
Definition: bullet_utils.h:63
Contact information for a single link.
std::string robot_urdf_path
Path to the URDF model of the robot.
bool gravity
If true, set gravity to -9.81 m/s².
double dt
Simulation timestep in [s].
bool gui
If true, fire up the graphical user interface.
double torque_control_kd
Gain for joint velocity control feedback.
bool floor
If true, load a floor plane.
std::map< std::string, double > joint_friction
Joint friction parameters.
std::vector< std::string > monitor_contacts
Contacts to monitor and report along with observations.
Eigen::Vector3d linear_velocity_base_to_world_in_world
Linear velocity of the base in the world frame upon reset.
Eigen::Quaterniond orientation_base_in_world
Orientation of the base in the world frame upon reset.
Eigen::Vector3d angular_velocity_base_in_base
Body angular velocity of the base upon reset.
Eigen::Vector3d position_base_in_world
Position of the base in the world frame upon reset.
std::vector< std::string > env_urdf_paths
Paths to environment URDFs to load.
std::string argv0
Value of argv[0] used to locate runfiles (e.g.
void configure(const Dictionary &config)
Configure from dictionary.
double torque_control_kp
Gain for joint position control feedback.
bool follower_camera
Translate the camera to follow the robot.
Properties for robot joints in the Bullet simulation.
double maximum_torque
Maximum torque, in N.m.
double friction
Kinetic friction, in N.m.
Eigen::Quaterniond orientation_imu_in_ars
Orientation from the IMU frame to the attitude reference system (ARS) frame.
Definition: ImuData.h:20
Eigen::Vector3d linear_acceleration_imu_in_imu
Body linear acceleration of the IMU, in [m] / [s]².
Definition: ImuData.h:39
Eigen::Vector3d angular_velocity_imu_in_imu
Body angular velocity of the IMU frame in [rad] / [s].
Definition: ImuData.h:30