dp_controller_base.py
Go to the documentation of this file.
1 # Copyright (c) 2016-2019 The UUV Simulator Authors.
2 # All rights reserved.
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 from copy import deepcopy
16 import numpy as np
17 import rospy
18 import logging
19 import sys
20 import tf
21 
22 from rospy.numpy_msg import numpy_msg
23 from geometry_msgs.msg import WrenchStamped, PoseStamped, TwistStamped, \
24  Vector3, Quaternion, Pose
25 from std_msgs.msg import Time
26 from nav_msgs.msg import Odometry
27 from uuv_control_interfaces.vehicle import Vehicle
28 from tf_quaternion.transformations import euler_from_quaternion, \
29  quaternion_multiply, quaternion_matrix, quaternion_conjugate, \
30  quaternion_inverse
31 from uuv_control_msgs.msg import Trajectory, TrajectoryPoint
32 from uuv_control_msgs.srv import *
33 from uuv_auv_control_allocator.msg import AUVCommand
34 
35 from .dp_controller_local_planner import DPControllerLocalPlanner as LocalPlanner
36 from ._log import get_logger
37 
38 
39 class DPControllerBase(object):
40  """General abstract class for DP controllers for underwater vehicles.
41  This is an abstract class, must be inherited by a controller module that
42  overrides the update_controller method. If the controller is set to be
43  model based (is_model_based=True), than the vehicle parameters are going
44  to be read from the ROS parameter server.
45 
46  > *Input arguments*
47 
48  * `is_model_based` (*type:* `bool`, *default:* `False`): If `True`, the
49  controller uses a model of the vehicle, `False` if it is non-model-based.
50  * `list_odometry_callbacks` (*type:* `list`, *default:* `None`): List of
51  function handles or `lambda` functions that will be called after each
52  odometry update.
53  * `planner_full_dof` (*type:* `bool`, *default:* `False`): Set the local
54  planner to generate 6 DoF trajectories. Otherwise, the planner will only
55  generate 4 DoF trajectories `(x, y, z, yaw)`.
56 
57  > *ROS parameters*
58 
59  * `use_stamped_poses_only` (*type:* `bool`, *default:* `False`): If `True`,
60  the reference path will be generated with stamped poses only, velocity
61  and acceleration references are set to zero.
62  * `thrusters_only` (*type:* `bool`, *default:* `True`): If `True`, the
63  vehicle only has thrusters as actuators and a thruster manager node is
64  running and the control output will be published as `geometry_msgs/WrenchStamped`.
65  If `False`, the AUV force allocation should be used to compute
66  the control output for each actuator and the output control will be
67  generated as a `uuv_auv_control_allocator.AUVCommand` message.
68  * `saturation` (*type:* `float`, *default:* `5000`): Absolute saturation
69  of the control signal.
70  * `use_auv_control_allocator` (*type:* `bool`, *default:* `False`): If `True`,
71  the output control will be `AUVCommand` message.
72  * `min_thrust` (*type:* `float`, *default:* `40`): Min. thrust set-point output,
73  (parameter only valid for AUVs).
74 
75  > *ROS publishers*
76 
77  * `thruster_output` (*message:* `geometry_msgs/WrenchStamped`): Control set-point
78  for the thruster manager node (requirement: ROS parameters must be `thrusters_only`
79  must be set as `True` and a thruster manager from `uuv_thruster_manager` node must
80  be running).
81  * `auv_command_output` (*message:* `uuv_auv_control_allocator.AUVCommand`): Control
82  set-point for the AUV allocation node (requirement: ROS parameters must be
83  `thrusters_only` must be set as `False` and a AUV control allocation node from
84  `uuv_auv_control_allocator` node must be running).
85  * `reference` (*message:* `uuv_control_msgs/TrajectoryPoint`): Current reference
86  trajectory point.
87  * `error` (*message:* `uuv_control_msgs/TrajectoryPoint`): Current trajectory error.
88 
89  > *ROS services*
90 
91  * `reset_controller` (*service:* `uuv_control_msgs/ResetController`): Reset all
92  variables, including error and reference signals.
93  """
94 
95  _LABEL = ''
96 
97  def __init__(self, is_model_based=False, list_odometry_callbacks=None,
98  planner_full_dof=False):
99  # Flag will be set to true when all parameters are initialized correctly
100  self._is_init = False
102 
103  # Reading current namespace
104  self._namespace = rospy.get_namespace()
105 
106  # Configuration for the vehicle dynamic model
107  self._is_model_based = is_model_based
108 
109  if self._is_model_based:
110  self._logger.info('Setting controller as model-based')
111  else:
112  self._logger.info('Setting controller as non-model-based')
113 
115  if rospy.has_param('~use_stamped_poses_only'):
116  self._use_stamped_poses_only = rospy.get_param('~use_stamped_poses_only')
117 
118  # Flag indicating if the vehicle has only thrusters, otherwise
119  # the AUV allocation node will be used
120  self.thrusters_only = rospy.get_param('~thrusters_only', True)
121 
122  # Instance of the local planner for local trajectory generation
123  self._local_planner = LocalPlanner(
124  full_dof=planner_full_dof,
125  stamped_pose_only=self._use_stamped_poses_only,
126  thrusters_only=self.thrusters_only)
127 
129  # TODO: Fix the saturation term and how it is applied
130  if rospy.has_param('~saturation'):
131  self._thrust_saturation = rospy.get_param('~saturation')
132  if self._control_saturation <= 0:
133  raise rospy.ROSException('Invalid control saturation forces')
134 
135  # Flag indicating either use of the AUV control allocator or
136  # direct command of fins and thruster
138  if not self.thrusters_only:
139  self.use_auv_control_allocator = rospy.get_param(
140  '~use_auv_control_allocator', False)
141 
142  # Remap the following topics, if needed
143  # Publisher for thruster allocator
144  if self.thrusters_only:
145  self._thrust_pub = rospy.Publisher(
146  'thruster_output', WrenchStamped, queue_size=1)
147  else:
148  self._thrust_pub = None
149 
150  if not self.thrusters_only:
151  self._auv_command_pub = rospy.Publisher(
152  'auv_command_output', AUVCommand, queue_size=1)
153  else:
154  self._auv_command_pub = None
155 
156  self._min_thrust = rospy.get_param('~min_thrust', 40.0)
157 
158  self._reference_pub = rospy.Publisher('reference',
159  TrajectoryPoint,
160  queue_size=1)
161  # Publish error (for debugging)
162  self._error_pub = rospy.Publisher('error',
163  TrajectoryPoint, queue_size=1)
164 
165  self._init_reference = False
166 
167  # Reference with relation to the INERTIAL frame
168  self._reference = dict(pos=np.zeros(3),
169  rot=np.zeros(4),
170  vel=np.zeros(6),
171  acc=np.zeros(6))
172 
173  # Errors wih relation to the BODY frame
174  self._errors = dict(pos=np.zeros(3),
175  rot=np.zeros(4),
176  vel=np.zeros(6))
177 
178  # Time step
179  self._dt = 0
180  self._prev_time = rospy.get_time()
181 
182  self._services = dict()
183  self._services['reset'] = rospy.Service('reset_controller',
184  ResetController,
186 
187  # Time stamp for the received trajectory
188  self._stamp_trajectory_received = rospy.get_time()
189 
190  # Instance of the vehicle model
191  self._vehicle_model = None
192  # If list of callbacks is empty, set the default
193  if list_odometry_callbacks is not None and \
194  isinstance(list_odometry_callbacks, list):
195  self._odometry_callbacks = list_odometry_callbacks
196  else:
197  self._odometry_callbacks = [self.update_errors,
198  self.update_controller]
199  # Initialize vehicle, if model based
200  self._create_vehicle_model()
201  # Flag to indicate that odometry topic is receiving data
202  self._init_odom = False
203 
204  # Subscribe to odometry topic
205  self._odom_topic_sub = rospy.Subscriber(
206  'odom', numpy_msg(Odometry), self._odometry_callback)
207 
208  # Stores last simulation time
209  self._prev_t = -1.0
210  self._logger.info('DP controller successfully initialized')
211 
212  def __del__(self):
213  # Removing logging message handlers
214  while self._logger.handlers:
215  self._logger.handlers.pop()
216 
217  @staticmethod
218  def get_controller(name, *args):
219  """Create instance of a specific DP controller."""
220  for controller in DPControllerBase.__subclasses__():
221  if name == controller.__name__:
222  self._logger.info('Creating controller={}'.format(name))
223  return controller(*args)
224 
225  @staticmethod
227  """Return list of DP controllers using this interface."""
228  return [controller.__name__ for controller in
229  DPControllerBase.__subclasses__()]
230 
231  @property
232  def label(self):
233  """`str`: Identifier name of the controller"""
234  return self._LABEL
235 
236  @property
237  def odom_is_init(self):
238  """`bool`: `True` if the first odometry message was received"""
239  return self._init_odom
240 
241  @property
242  def error_pos_world(self):
243  """`numpy.array`: Position error wrt world frame"""
244  return np.dot(self._vehicle_model.rotBtoI, self._errors['pos'])
245 
246  @property
248  """`numpy.array`: Orientation error"""
249  return deepcopy(self._errors['rot'][0:3])
250 
251  @property
253  """`numpy.array`: Orientation error in Euler angles."""
254  e1 = self._errors['rot'][0]
255  e2 = self._errors['rot'][1]
256  e3 = self._errors['rot'][2]
257  eta = self._errors['rot'][3]
258  rot = np.array([[1 - 2 * (e2**2 + e3**2),
259  2 * (e1 * e2 - e3 * eta),
260  2 * (e1 * e3 + e2 * eta)],
261  [2 * (e1 * e2 + e3 * eta),
262  1 - 2 * (e1**2 + e3**2),
263  2 * (e2 * e3 - e1 * eta)],
264  [2 * (e1 * e3 - e2 * eta),
265  2 * (e2 * e3 + e1 * eta),
266  1 - 2 * (e1**2 + e2**2)]])
267  # Roll
268  roll = np.arctan2(rot[2, 1], rot[2, 2])
269  # Pitch, treating singularity cases
270  den = np.sqrt(1 - rot[2, 1]**2)
271  pitch = - np.arctan(rot[2, 1] / max(0.001, den))
272  # Yaw
273  yaw = np.arctan2(rot[1, 0], rot[0, 0])
274  return np.array([roll, pitch, yaw])
275 
276  @property
277  def error_pose_euler(self):
278  """`numpy.array`: Pose error with orientation represented in Euler angles."""
279  return np.hstack((self._errors['pos'], self.error_orientation_rpy))
280 
281  @property
282  def error_vel_world(self):
283  """`numpy.array`: Linear velocity error"""
284  return np.dot(self._vehicle_model.rotBtoI, self._errors['vel'])
285 
286  def __str__(self):
287  msg = 'Dynamic positioning controller\n'
288  msg += 'Controller= ' + self._LABEL + '\n'
289  msg += 'Is model based? ' + str(self._is_model_based) + '\n'
290  msg += 'Vehicle namespace= ' + self._namespace
291  return msg
292 
294  """Create a new instance of a vehicle model. If controller is not model
295  based, this model will have its parameters set to 0 and will be used
296  to receive and transform the odometry data.
297  """
298  if self._vehicle_model is not None:
299  del self._vehicle_model
300  self._vehicle_model = Vehicle(
301  inertial_frame_id=self._local_planner.inertial_frame_id)
302 
303  def _update_reference(self):
304  """Call the local planner interpolator to retrieve a trajectory
305  point and publish the reference message as `uuv_control_msgs/TrajectoryPoint`.
306  """
307  # Update the local planner's information about the vehicle's pose
308  self._local_planner.update_vehicle_pose(
309  self._vehicle_model.pos, self._vehicle_model.quat)
310 
311  t = rospy.get_time()
312  reference = self._local_planner.interpolate(t)
313 
314  if reference is not None:
315  self._reference['pos'] = reference.p
316  self._reference['rot'] = reference.q
317  self._reference['vel'] = np.hstack((reference.v, reference.w))
318  self._reference['acc'] = np.hstack((reference.a, reference.alpha))
319  if reference is not None and self._reference_pub.get_num_connections() > 0:
320  # Publish current reference
321  msg = TrajectoryPoint()
322  msg.header.stamp = rospy.Time.now()
323  msg.header.frame_id = self._local_planner.inertial_frame_id
324  msg.pose.position = Vector3(*self._reference['pos'])
325  msg.pose.orientation = Quaternion(*self._reference['rot'])
326  msg.velocity.linear = Vector3(*self._reference['vel'][0:3])
327  msg.velocity.angular = Vector3(*self._reference['vel'][3:6])
328  msg.acceleration.linear = Vector3(*self._reference['acc'][0:3])
329  msg.acceleration.angular = Vector3(*self._reference['acc'][3:6])
330  self._reference_pub.publish(msg)
331  return True
332 
333  def _update_time_step(self):
334  """Update time step."""
335  t = rospy.get_time()
336  self._dt = t - self._prev_time
337  self._prev_time = t
338 
339  def _reset_controller(self):
340  """Reset reference and and error vectors."""
341  self._init_reference = False
342 
343  # Reference with relation to the INERTIAL frame
344  self._reference = dict(pos=np.zeros(3),
345  rot=np.zeros(4),
346  vel=np.zeros(6),
347  acc=np.zeros(6))
348 
349  # Errors wih relation to the BODY frame
350  self._errors = dict(pos=np.zeros(3),
351  rot=np.zeros(4),
352  vel=np.zeros(6))
353 
354  def reset_controller_callback(self, request):
355  """Service handler function."""
356  self._reset_controller()
357  return ResetControllerResponse(True)
358 
359  def update_controller(self):
360  """This function must be implemented by derived classes
361  with the implementation of the control algorithm.
362  """
363  # Does nothing, must be overloaded
364  raise NotImplementedError()
365 
366  def update_errors(self):
367  """Update error vectors."""
368  if not self.odom_is_init:
369  self._logger.warning('Odometry topic has not been update yet')
370  return
371  self._update_reference()
372  # Calculate error in the BODY frame
373  self._update_time_step()
374  # Rotation matrix from INERTIAL to BODY frame
375  rotItoB = self._vehicle_model.rotItoB
376  rotBtoI = self._vehicle_model.rotBtoI
377  if self._dt > 0:
378  # Update position error with respect to the the BODY frame
379  pos = self._vehicle_model.pos
380  vel = self._vehicle_model.vel
381  quat = self._vehicle_model.quat
382  self._errors['pos'] = np.dot(
383  rotItoB, self._reference['pos'] - pos)
384 
385  # Update orientation error
386  self._errors['rot'] = quaternion_multiply(
387  quaternion_inverse(quat), self._reference['rot'])
388 
389  # Velocity error with respect to the the BODY frame
390  self._errors['vel'] = np.hstack((
391  np.dot(rotItoB, self._reference['vel'][0:3]) - vel[0:3],
392  np.dot(rotItoB, self._reference['vel'][3:6]) - vel[3:6]))
393 
394  if self._error_pub.get_num_connections() > 0:
395  stamp = rospy.Time.now()
396  msg = TrajectoryPoint()
397  msg.header.stamp = stamp
398  msg.header.frame_id = self._local_planner.inertial_frame_id
399  # Publish pose error
400  msg.pose.position = Vector3(*np.dot(rotBtoI, self._errors['pos']))
401  msg.pose.orientation = Quaternion(*self._errors['rot'])
402  # Publish velocity errors in INERTIAL frame
403  msg.velocity.linear = Vector3(*np.dot(rotBtoI, self._errors['vel'][0:3]))
404  msg.velocity.angular = Vector3(*np.dot(rotBtoI, self._errors['vel'][3:6]))
405  self._error_pub.publish(msg)
406 
407  def publish_control_wrench(self, force):
408  """Publish the thruster manager control set-point.
409 
410  > *Input arguments*
411 
412  * `force` (*type:* `numpy.array`): 6 DoF control
413  set-point wrench vector
414  """
415  if not self.odom_is_init:
416  return
417 
418  # Apply saturation
419  for i in range(6):
420  if force[i] < -self._control_saturation:
421  force[i] = -self._control_saturation
422  elif force[i] > self._control_saturation:
423  force[i] = self._control_saturation
424 
425  if not self.thrusters_only:
426  surge_speed = self._vehicle_model.vel[0]
427  self.publish_auv_command(surge_speed, force)
428  return
429 
430  force_msg = WrenchStamped()
431  force_msg.header.stamp = rospy.Time.now()
432  force_msg.header.frame_id = '%s/%s' % (self._namespace, self._vehicle_model.body_frame_id)
433  force_msg.wrench.force.x = force[0]
434  force_msg.wrench.force.y = force[1]
435  force_msg.wrench.force.z = force[2]
436 
437  force_msg.wrench.torque.x = force[3]
438  force_msg.wrench.torque.y = force[4]
439  force_msg.wrench.torque.z = force[5]
440 
441  self._thrust_pub.publish(force_msg)
442 
443  def publish_auv_command(self, surge_speed, wrench):
444  """Publish the AUV control command message
445 
446  > *Input arguments*
447 
448  * `surge_speed` (*type:* `float`): Reference surge speed
449  * `wrench` (*type:* `numpy.array`): 6 DoF wrench vector
450  """
451  if not self.odom_is_init:
452  return
453 
454  surge_speed = max(0, surge_speed)
455 
456  msg = AUVCommand()
457  msg.header.stamp = rospy.Time.now()
458  msg.header.frame_id = '%s/%s' % (self._namespace, self._vehicle_model.body_frame_id)
459  msg.surge_speed = surge_speed
460  msg.command.force.x = max(self._min_thrust, wrench[0])
461  msg.command.force.y = wrench[1]
462  msg.command.force.z = wrench[2]
463  msg.command.torque.x = wrench[3]
464  msg.command.torque.y = wrench[4]
465  msg.command.torque.z = wrench[5]
466 
467  self._auv_command_pub.publish(msg)
468 
469  def _odometry_callback(self, msg):
470  """Odometry topic subscriber callback function.
471 
472  > *Input arguments*
473 
474  * `msg` (*type:* `nav_msgs/Odometry`): Input odometry
475  message
476  """
477  self._vehicle_model.update_odometry(msg)
478 
479  if not self._init_odom:
480  self._init_odom = True
481 
482  if len(self._odometry_callbacks):
483  for func in self._odometry_callbacks:
484  func()
def __init__(self, is_model_based=False, list_odometry_callbacks=None, planner_full_dof=False)


uuv_trajectory_control
Author(s):
autogenerated on Thu Jun 18 2020 03:28:42