thruster_manager.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 
16 import numpy
17 import rospy
18 import tf
19 from tf_quaternion import transformations
20 import tf2_ros
21 from os.path import isdir, join
22 import yaml
23 from time import sleep
24 from uuv_gazebo_ros_plugins_msgs.msg import FloatStamped
25 from geometry_msgs.msg import Wrench
26 import xml.etree.ElementTree as etree
27 
28 from .models import Thruster
29 
30 
32  """
33  The thruster manager generates the thruster allocation matrix using the
34  TF information and publishes the thruster forces assuming the the thruster
35  topics are named in the following pattern
36 
37  <thruster_topic_prefix>/<index>/<thruster_topic_suffix>
38 
39  Thruster frames should also be named as follows
40 
41  <thruster_frame_base>_<index>
42  """
43 
44  MAX_THRUSTERS = 16
45 
46  def __init__(self):
47  """Class constructor."""
48  # This flag will be set to true once the thruster allocation matrix is
49  # available
50  self._ready = False
51 
52  # Acquiring the namespace of the vehicle
53  self.namespace = rospy.get_namespace()
54  if self.namespace != '':
55  if self.namespace[-1] != '/':
56  self.namespace += '/'
57 
58  if self.namespace[0] != '/':
59  self.namespace = '/' + self.namespace
60 
61  if not rospy.has_param('thruster_manager'):
62  raise rospy.ROSException('Thruster manager parameters not '
63  'initialized for uuv_name=' +
64  self.namespace)
65 
66  # Load all parameters
67  self.config = rospy.get_param('thruster_manager')
68 
69  robot_description_param = self.namespace + 'robot_description'
70  self.use_robot_descr = False
71  self.axes = {}
72  if rospy.has_param(robot_description_param):
73  self.use_robot_descr = True
74  self.parse_urdf(rospy.get_param(robot_description_param))
75 
76  if self.config['update_rate'] < 0:
77  self.config['update_rate'] = 50
78 
80 
81  tf_buffer = tf2_ros.Buffer()
82  listener = tf2_ros.TransformListener(tf_buffer)
83  tf_trans_ned_to_enu = None
84 
85  try:
86  if self.namespace != '':
87  target = '{}base_link'.format(self.namespace)
88  target = target[1::]
89  source = '{}base_link_ned'.format(self.namespace)
90  else:
91  target = 'base_link'
92  source = 'base_link_ned'
93  source = source[1::]
94  tf_trans_ned_to_enu = tf_buffer.lookup_transform(
95  target, source, rospy.Time(), rospy.Duration(1))
96  except Exception as e:
97  rospy.loginfo('No transform found between base_link and base_link_ned'
98  ' for vehicle {}, message={}'.format(self.namespace, e))
99  self.base_link_ned_to_enu = None
100 
101  if tf_trans_ned_to_enu is not None:
102  self.base_link_ned_to_enu = transformations.quaternion_matrix(
103  (tf_trans_ned_to_enu.transform.rotation.x,
104  tf_trans_ned_to_enu.transform.rotation.y,
105  tf_trans_ned_to_enu.transform.rotation.z,
106  tf_trans_ned_to_enu.transform.rotation.w))[0:3, 0:3]
107 
108  rospy.loginfo('base_link transform NED to ENU=\n' + str(self.base_link_ned_to_enu))
109 
110  rospy.loginfo(
111  'ThrusterManager::update_rate=' + str(self.config['update_rate']))
112 
113  # Set the tf_prefix parameter
114  rospy.set_param('thruster_manager/tf_prefix', self.namespace)
115 
116  # Retrieve the output file path to store the TAM
117  # matrix for future use
118  self.output_dir = None
119  if rospy.has_param('~output_dir'):
120  self.output_dir = rospy.get_param('~output_dir')
121  if not isdir(self.output_dir):
122  raise rospy.ROSException(
123  'Invalid output directory, output_dir=' + self.output_dir)
124  rospy.loginfo('output_dir=' + self.output_dir)
125 
126  # Number of thrusters
127  self.n_thrusters = 0
128 
129  # Thruster objects used to calculate the right angular velocity command
130  self.thrusters = list()
131 
132  # Thrust forces vector
133  self.thrust = None
134 
135  # Thruster allocation matrix: transform thruster inputs to force/torque
137  if rospy.has_param('~tam'):
138  tam = rospy.get_param('~tam')
139  self.configuration_matrix = numpy.array(tam)
140  # Set number of thrusters from the number of columns
141  self.n_thrusters = self.configuration_matrix.shape[1]
142  # Create publishing topics to each thruster
143  params = self.config['conversion_fcn_params']
144  conv_fcn = self.config['conversion_fcn']
145  if type(params) == list and type(conv_fcn) == list:
146  if len(params) != self.n_thrusters or len(conv_fcn) != self.n_thrusters:
147  raise rospy.ROSException('Lists conversion_fcn and '
148  'conversion_fcn_params must have '
149  'the same number of items as of '
150  'thrusters')
151  for i in range(self.n_thrusters):
152  topic = self.config['thruster_topic_prefix'] + str(i) + \
153  self.config['thruster_topic_suffix']
154  if list not in [type(params), type(conv_fcn)]:
155  thruster = Thruster.create_thruster(
156  conv_fcn, i, topic, None, None,
157  **params)
158  else:
159  thruster = Thruster.create_thruster(
160  conv_fcn[i], i, topic, None, None,
161  **params[i])
162 
163  if thruster is None:
164  rospy.ROSException('Invalid thruster conversion '
165  'function=%s'
166  % self.config['conversion_fcn'])
167  self.thrusters.append(thruster)
168  rospy.loginfo('Thruster allocation matrix provided!')
169  rospy.loginfo('TAM=')
170  rospy.loginfo(self.configuration_matrix)
171  self.thrust = numpy.zeros(self.n_thrusters)
172 
173  if not self.update_tam():
174  raise rospy.ROSException('No thrusters found')
175 
176  # (pseudo) inverse: force/torque to thruster inputs
178  if self.configuration_matrix is not None:
179  self.inverse_configuration_matrix = numpy.linalg.pinv(
181 
182  # If an output directory was provided, store matrix for further use
183  if self.output_dir is not None:
184  with open(join(self.output_dir, 'TAM.yaml'), 'w') as yaml_file:
185  yaml_file.write(
186  yaml.safe_dump(
187  dict(tam=self.configuration_matrix.tolist())))
188  else:
189  rospy.loginfo('Invalid output directory for the TAM matrix, dir=' + str(self.output_dir))
190 
191  self.ready = True
192  rospy.loginfo('ThrusterManager: ready')
193 
194  def parse_urdf(self, urdf_str):
195  root = etree.fromstring(urdf_str)
196  for joint in root.findall('joint'):
197  if joint.get('type') == 'fixed':
198  continue
199  axis_str_list = joint.find('axis').get('xyz').split()
200  child = joint.find('child').get('link')
201  if child[0]!='/':
202  child = '/'+child
203 
204  self.axes[child] = numpy.array([float(axis_str_list[0]),
205  float(axis_str_list[1]),
206  float(axis_str_list[2]), 0.0])
207 
208 
209  def update_tam(self, recalculate=False):
210  """Calculate the thruster allocation matrix, if one is not given."""
211  if self.configuration_matrix is not None and not recalculate:
212  self.ready = True
213  rospy.loginfo('TAM provided, skipping...')
214  rospy.loginfo('ThrusterManager: ready')
215  return True
216 
217  self.ready = False
218  rospy.loginfo('ThrusterManager: updating thruster poses')
219  # Small margin to make sure we get thruster frames via tf
220  now = rospy.Time.now() + rospy.Duration(0.2)
221 
222  base = self.namespace + self.config['base_link']
223 
224  self.thrusters = list()
225 
226  equal_thrusters = True
227  idx_thruster_model = 0
228 
229  if type(self.config['conversion_fcn_params']) == list and \
230  type(self.config['conversion_fcn']) == list:
231  if len(self.config['conversion_fcn_params']) != len(
232  self.config['conversion_fcn']):
233  raise rospy.ROSException(
234  'Lists of conversion_fcn_params and conversion_fcn'
235  ' must have equal length')
236  equal_thrusters = False
237 
238  rospy.loginfo('conversion_fcn=' + str(self.config['conversion_fcn']))
239  rospy.loginfo('conversion_fcn_params=' + str(self.config['conversion_fcn_params']))
240 
241  listener = tf.TransformListener()
242  sleep(0.1)
243 
244  for i in range(self.MAX_THRUSTERS):
245  frame = self.namespace + \
246  self.config['thruster_frame_base'] + str(i)
247  try:
248  # try to get thruster pose with respect to base frame via tf
249  rospy.loginfo('transform: ' + base + ' -> ' + frame)
250  now = rospy.Time.now() + rospy.Duration(0.2)
251  listener.waitForTransform(base, frame,
252  now, rospy.Duration(1.0))
253  [pos, quat] = listener.lookupTransform(base, frame, now)
254 
255  topic = self.config['thruster_topic_prefix'] + str(i) + \
256  self.config['thruster_topic_suffix']
257 
258  # If not using robot_description, thrust_axis=None which will
259  # result in the thrust axis being the x-axis,i.e. (1,0,0)
260  thrust_axis = None if not self.use_robot_descr else self.axes[frame]
261 
262  if equal_thrusters:
263  params = self.config['conversion_fcn_params']
264  thruster = Thruster.create_thruster(
265  self.config['conversion_fcn'],
266  i, topic, pos, quat, self.axes[frame], **params)
267  else:
268  if idx_thruster_model >= len(self.config['conversion_fcn']):
269  raise rospy.ROSException('Number of thrusters found and '
270  'conversion_fcn are different')
271  params = self.config['conversion_fcn_params'][idx_thruster_model]
272  conv_fcn = self.config['conversion_fcn'][idx_thruster_model]
273  thruster = Thruster.create_thruster(
274  conv_fcn,
275  i, topic, pos, quat, self.axes[frame],
276  **params)
277  idx_thruster_model += 1
278  if thruster is None:
279  rospy.ROSException('Invalid thruster conversion '
280  'function=%s'
281  % self.config['conversion_fcn'])
282  self.thrusters.append(thruster)
283  except tf.Exception:
284  rospy.loginfo('could not get transform from: ' + base)
285  rospy.loginfo('to: ' + frame)
286  break
287 
288  rospy.loginfo(str(self.thrusters))
289  if len(self.thrusters) == 0:
290  return False
291 
292  # Set the number of thrusters found
293  self.n_thrusters = len(self.thrusters)
294 
295  # Fill the thrust vector
296  self.thrust = numpy.zeros(self.n_thrusters)
297 
298  # Fill the thruster allocation matrix
299  self.configuration_matrix = numpy.zeros((6, self.n_thrusters))
300 
301  for i in range(self.n_thrusters):
302  self.configuration_matrix[:, i] = self.thrusters[i].tam_column
303 
304  # Eliminate small values
305  self.configuration_matrix[numpy.abs(
306  self.configuration_matrix) < 1e-3] = 0.0
307 
308  rospy.loginfo('TAM= %s', str(self.configuration_matrix))
309 
310  # Once we know the configuration matrix we can compute its
311  # (pseudo-)inverse:
312  self.inverse_configuration_matrix = numpy.linalg.pinv(
314 
315  # If an output directory was provided, store matrix for further use
316  if self.output_dir is not None and not recalculate:
317  with open(join(self.output_dir, 'TAM.yaml'), 'w') as yaml_file:
318  yaml_file.write(
319  yaml.safe_dump(
320  dict(tam=self.configuration_matrix.tolist())))
321  rospy.loginfo('TAM saved in <{}>'.format(join(self.output_dir, 'TAM.yaml')))
322  elif recalculate:
323  rospy.loginfo('Recalculate flag on, matrix will not be stored in TAM.yaml')
324  else:
325  rospy.logerr('Invalid output directory for the TAM matrix, dir='.format(
326  self.output_dir))
327 
328  self.ready = True
329  rospy.loginfo('ThrusterManager: ready')
330  return True
331 
332  def command_thrusters(self):
333  """Publish the thruster input into their specific topic."""
334  if self.thrust is None:
335  return
336  for i in range(self.n_thrusters):
337  self.thrusters[i].publish_command(self.thrust[i])
338 
339  def publish_thrust_forces(self, control_forces, control_torques,
340  frame_id=None):
341  if not self.ready:
342  return
343 
344  if frame_id is not None:
345  if self.config['base_link'] != frame_id:
346  assert self.base_link_ned_to_enu is not None, 'Transform from'
347  ' base_link_ned to base_link could not be found'
348  if 'base_link_ned' not in self.config['base_link']:
349  control_forces = numpy.dot(self.base_link_ned_to_enu,
350  control_forces)
351  control_torques = numpy.dot(self.base_link_ned_to_enu,
352  control_torques)
353  else:
354  control_forces = numpy.dot(self.base_link_ned_to_enu.T,
355  control_forces)
356  control_torques = numpy.dot(self.base_link_ned_to_enu.T,
357  control_torques)
358 
359  gen_forces = numpy.hstack(
360  (control_forces, control_torques)).transpose()
361  self.thrust = self.compute_thruster_forces(gen_forces)
362  self.command_thrusters()
363 
364  def compute_thruster_forces(self, gen_forces):
365  """Compute desired thruster forces using the inverse configuration
366  matrix.
367  """
368  # Calculate individual thrust forces
369  thrust = self.inverse_configuration_matrix.dot(gen_forces)
370  # Obey limit on max thrust by applying a constant scaling factor to all
371  # thrust forces
372  limitation_factor = 1.0
373  if type(self.config['max_thrust']) == list:
374  if len(self.config['max_thrust']) != self.n_thrusters:
375  raise rospy.ROSException('max_thrust list must have the length'
376  ' equal to the number of thrusters')
377  max_thrust = self.config['max_thrust']
378  else:
379  max_thrust = [self.config['max_thrust'] for _ in range(self.n_thrusters)]
380  for i in range(self.n_thrusters):
381  if abs(thrust[i]) > max_thrust[i]:
382  thrust[i] = numpy.sign(thrust[i]) * max_thrust[i]
383  return thrust
def publish_thrust_forces(self, control_forces, control_torques, frame_id=None)


uuv_thruster_manager
Author(s): Musa Morena Marcusso Manhaes , Sebastian Scherer , Luiz Ricardo Douat
autogenerated on Thu Jun 18 2020 03:28:39