counterbalance_analysis.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 #
00003 # Software License Agreement (BSD License)
00004 #
00005 # Copyright (c) 2010, Willow Garage, Inc.
00006 # All rights reserved.
00007 #
00008 # Redistribution and use in source and binary forms, with or without
00009 # modification, are permitted provided that the following conditions
00010 # are met:
00011 #
00012 #  * Redistributions of source code must retain the above copyright
00013 #    notice, this list of conditions and the following disclaimer.
00014 #  * Redistributions in binary form must reproduce the above
00015 #    copyright notice, this list of conditions and the following
00016 #    disclaimer in the documentation and/or other materials provided
00017 #    with the distribution.
00018 #  * Neither the name of the Willow Garage nor the names of its
00019 #    contributors may be used to endorse or promote products derived
00020 #    from this software without specific prior written permission.
00021 #
00022 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
00023 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
00024 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
00025 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
00026 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
00027 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
00028 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00029 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
00030 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
00031 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00032 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00033 # POSSIBILITY OF SUCH DAMAGE.
00034 
00035 ##\author Kevin Watts
00036 ##\brief Analyzes counterbalance data
00037 
00038 PKG = 'pr2_counterbalance_check'
00039 import roslib; roslib.load_manifest(PKG)
00040 
00041 import numpy
00042 import math
00043 import sys
00044 
00045 
00046 import matplotlib
00047 matplotlib.use('Agg')
00048 import matplotlib.pyplot as plt
00049 from StringIO import StringIO
00050 
00051 from pr2_self_test_msgs.msg import Plot, TestValue, TestParam
00052 
00053 ok_dict = { False: 'FAIL', True: 'OK' }
00054 
00055 def str_to_bytes(s):
00056        return map(lambda x: x if x < 128 else x-256, map(ord, s))
00057 
00058 class JointPositionAnalysisData(object):
00059     def __init__(self, msg):
00060         self.time     = numpy.array(msg.time)
00061         self.position = numpy.array(msg.position)
00062         self.velocity = numpy.array(msg.velocity)
00063         self.effort   = numpy.array(msg.effort)
00064 
00065         self.position_avg = numpy.average(self.position)
00066         self.position_sd  = numpy.std(self.position)
00067         self.effort_avg   = numpy.average(self.effort)
00068         self.effort_sd    = numpy.std(self.effort)
00069 
00070 class CBPositionAnalysisData(object):
00071     def __init__(self, msg):
00072         self.flex_position = msg.flex_position
00073         self.lift_hold = JointPositionAnalysisData(msg.lift_hold)
00074         self.flex_hold = JointPositionAnalysisData(msg.flex_hold)
00075 
00076 class CBRunAnalysisData(object):
00077     def __init__(self, msg):
00078         self.lift_position = msg.lift_position
00079         self.flex_data = []
00080         for fd in msg.flex_data:
00081             self.flex_data.append(CBPositionAnalysisData(fd))
00082 
00083 class CounterbalanceAnalysisData(object):
00084     ##\param msg CounterbalanceTestData : Message from controller
00085     def __init__(self, msg):
00086         self.lift_data = []
00087         for ld in msg.lift_data:
00088             self.lift_data.append(CBRunAnalysisData(ld))
00089 
00090 ##\brief Stores parameters from CB analysis test
00091 class CounterbalanceAnalysisParams(object):
00092     def __init__(self, msg):
00093         self.lift_dither  = msg.lift_amplitude
00094         self.flex_dither  = msg.flex_amplitude
00095         
00096         self.lift_joint   = msg.lift_joint
00097         self.flex_joint   = msg.flex_joint
00098         
00099         self.timeout_hit  = msg.timeout_hit
00100         self.flex_test    = msg.flex_test
00101 
00102         self.lift_mse     = msg.arg_value[9]
00103         self.lift_avg_abs = msg.arg_value[10]
00104         self.lift_avg_eff = msg.arg_value[11]
00105         self.flex_mse     = msg.arg_value[12]
00106         self.flex_avg_abs = msg.arg_value[13]
00107         self.flex_avg_eff = msg.arg_value[14]
00108 
00109         self.lift_p       = msg.arg_value[15]
00110         self.lift_i       = msg.arg_value[16]
00111         self.lift_d       = msg.arg_value[17]
00112         self.lift_i_clamp = msg.arg_value[18]
00113 
00114         self.flex_p       = msg.arg_value[19]
00115         self.flex_i       = msg.arg_value[20]
00116         self.flex_d       = msg.arg_value[21]
00117         self.flex_i_clamp = msg.arg_value[22]
00118 
00119         if len(msg.arg_value) > 24:
00120             self.screw_tol = msg.arg_value[23]
00121             self.bar_tol   = msg.arg_value[24]
00122         else:
00123             # For backwards compatibility
00124             self.screw_tol = 2.0
00125             self.bar_tol   = 0.8
00126 
00127         self.num_flexes = len(msg.lift_data[0].flex_data)
00128         self.num_lifts  = len(msg.lift_data)
00129         
00130         self.min_lift = msg.lift_data[0].lift_position
00131         self.max_lift = msg.lift_data[-1].lift_position
00132 
00133         self.min_flex = msg.lift_data[0].flex_data[0].flex_position
00134         self.max_flex = msg.lift_data[0].flex_data[-1].flex_position
00135 
00136         self.named_params = {}
00137         for i in range(0, 9):
00138             self.named_params[msg.arg_name[i]] = msg.arg_value[i]
00139 
00140     def get_test_params(self):
00141         params = []
00142         params.append(TestParam(key='Lift Dither', value=str(self.lift_dither)))
00143         params.append(TestParam(key='Flex Dither', value=str(self.flex_dither)))
00144         params.append(TestParam(key='Lift Joint', value=self.lift_joint))
00145         params.append(TestParam(key='Flex Joint', value=self.flex_joint))
00146         params.append(TestParam(key='Timeout Hit', value=ok_dict[not self.timeout_hit]))
00147         params.append(TestParam(key='Flex Tested', value=str(self.flex_test)))
00148 
00149         params.append(TestParam(key='Lift MSE', value=str(self.lift_mse)))
00150         params.append(TestParam(key='Lift Avg Abs', value=str(self.lift_avg_abs)))
00151         params.append(TestParam(key='Lift Avg Effort', value=str(self.lift_avg_eff)))
00152 
00153 
00154         params.append(TestParam(key='Lift P Gain', value=str(self.lift_p)))
00155         params.append(TestParam(key='Lift I Gain', value=str(self.lift_i)))
00156         params.append(TestParam(key='Lift D Gain', value=str(self.lift_d)))
00157         params.append(TestParam(key='Lift I Clamp', value=str(self.lift_i_clamp)))
00158 
00159         params.append(TestParam(key='Num Lifts', value=str(self.num_lifts)))
00160         params.append(TestParam(key='Min Lift', value="%.2f" % self.min_lift))
00161         params.append(TestParam(key='Max Lift', value="%.2f" % self.max_lift))
00162 
00163         if self.flex_test:
00164             params.append(TestParam(key='Flex MSE', value=str(self.flex_mse)))
00165             params.append(TestParam(key='Flex Avg Abs', value=str(self.flex_avg_abs)))
00166             params.append(TestParam(key='Flex Avg Effort', value=str(self.flex_avg_eff)))
00167             params.append(TestParam(key='Flex P Gain', value=str(self.flex_p)))
00168             params.append(TestParam(key='Flex I Gain', value=str(self.flex_i)))
00169             params.append(TestParam(key='Flex D Gain', value=str(self.flex_d)))
00170             params.append(TestParam(key='Flex I Clamp', value=str(self.flex_i_clamp)))
00171             params.append(TestParam(key='Num Flexes', value=str(self.num_flexes)))
00172             params.append(TestParam(key='Min Flex', value="%.2f" % self.min_flex))
00173             params.append(TestParam(key='Max Flex', value="%.2f" % self.max_flex))
00174 
00175         for key, val in self.named_params.iteritems():
00176             params.append(TestParam(key=key, value=str(val)))
00177             
00178 
00179         return params
00180 
00181 class CounterbalanceAnalysisResult:
00182     __slots__ = ['html', 'summary', 'result', 'values']
00183     def __init__(self):
00184         self.html = ''
00185         self.summary = ''
00186         self.result = False
00187         self.values = []
00188 
00189 ##\brief Get average efforts for CB test as a list
00190 ##
00191 ##\param lift_calc bool : Lift or flex efforts
00192 def get_efforts(data, lift_calc):
00193     avg_effort_list = []
00194     for ld in data.lift_data:
00195         for fd in ld.flex_data:
00196             if lift_calc:
00197                 avg_effort_list.append(fd.lift_hold.effort_avg)
00198             else:
00199                 avg_effort_list.append(fd.flex_hold.effort_avg)
00200 
00201     return avg_effort_list
00202 
00203 def _get_mean_sq_effort(avg_effort_array):
00204     sq_array = avg_effort_array * avg_effort_array
00205     return numpy.average(sq_array)
00206 
00207 def _get_mean_abs_effort(avg_effort_array):
00208     abs_array = abs(avg_effort_array)
00209     return numpy.average(abs_array)
00210 
00211 def _get_mean_effort(avg_effort_array):
00212     return numpy.average(avg_effort_array)
00213 
00214 ##\brief Returns a list of lift positions, efforts for a given flex position (vary by lift)
00215 def _get_const_flex_effort(data, flex_index = 0, lift_calc = True):
00216     effort_list = []
00217     lift_list = []
00218     for ld in data.lift_data:
00219         fd = ld.flex_data[flex_index]
00220         lift_list.append(ld.lift_position)
00221         if lift_calc:
00222             effort_list.append(fd.lift_hold.effort_avg)
00223         else:
00224             effort_list.append(fd.flex_hold.effort_avg)
00225 
00226     return lift_list, effort_list
00227     
00228 def _get_const_lift_effort(data, lift_index = 0, lift_calc = True):
00229     ld = data.lift_data[lift_index]
00230         
00231     effort_list = []
00232     flex_list = []
00233 
00234     for fd in ld.flex_data:
00235         flex_list.append(fd.flex_position)
00236         if lift_calc:
00237             effort_list.append(fd.lift_hold.effort_avg)
00238         else:
00239             effort_list.append(fd.flex_hold.effort_avg)
00240 
00241     return flex_list, effort_list
00242 
00243 
00244 
00245 def _get_flex_positions(data):
00246     flex_list = []
00247     for fd in data.lift_data[0].flex_data:
00248         flex_list.append(fd.flex_position)
00249 
00250     return flex_list
00251 
00252 def _get_lift_positions(data):
00253     lifts = []
00254     for ld in data.lift_data:
00255         lifts.append(ld.lift_position)
00256     return lifts
00257 
00258 
00259 ##\brief Gives effort contour plot of efforts by lift, flex position
00260 ##
00261 ##\param params CounterbalanceAnalysisParams : Input params
00262 ##\param data CounterbalanceAnalysisData : Test Data
00263 ##\return qualification.msg.Plot : Plot message with contour
00264 def plot_effort_contour(params, data, lift_calc = True):
00265     effort_list = []
00266     for i in range(0, params.num_lifts):
00267         flexes, efforts = _get_const_lift_effort(data, i, lift_calc)
00268         effort_list.append(efforts)
00269     flexes = _get_flex_positions(data)
00270     lifts = _get_lift_positions(data)
00271 
00272     flex_grid, lift_grid = numpy.meshgrid(numpy.array(flexes), numpy.array(lifts))
00273     effort_grid = numpy.array(effort_list)
00274 
00275     CS = plt.contour(flex_grid, lift_grid, effort_grid)
00276     plt.clabel(CS, inline=0, fontsize=10)
00277     
00278     plt.xlabel('Flex')
00279     plt.ylabel('Lift')
00280         
00281     stream = StringIO()
00282     plt.savefig(stream, format = 'png')
00283     image = stream.getvalue()
00284     p = Plot()
00285     if lift_calc:
00286         p.title = 'lift_effort_contour'
00287     else:
00288         p.title = 'flex_effort_contour'
00289     p.image = str_to_bytes(image)
00290     p.image_format = 'png'
00291     
00292     plt.close()
00293 
00294     return p
00295 
00296 ##\brief Plots CB efforts against shoulder lift position
00297 ##
00298 ##\param flex_index int : Index of flex data to plot against
00299 ##\param lift_calc bool : Lift efforts or flex efforts
00300 def plot_efforts_by_lift_position(params, data, flex_index = -1, lift_calc = True):
00301     lift_position, effort = _get_const_flex_effort(data, flex_index, lift_calc)
00302     
00303     flex_position = data.lift_data[0].flex_data[flex_index].flex_position
00304 
00305     plt.plot(numpy.array(lift_position), numpy.array(effort))
00306     if lift_calc:
00307         plt.title('Shoulder Lift Effort at Flex Position %.2f' % (flex_position))
00308     else:
00309         plt.title('Shoulder Flex Effort at Flex Position %.2f' % (flex_position))
00310     plt.axes()
00311     plt.xlabel('Lift Position')
00312     plt.ylabel('Effort')
00313     plt.axhline(y = 0, color = 'r', label='_nolegend_')
00314 
00315     stream = StringIO()
00316     plt.savefig(stream, format = 'png')
00317     image = stream.getvalue()
00318     p = Plot()
00319     if lift_calc:
00320         p.title = 'lift_effort_const_flex_%d' % flex_index
00321     else:
00322         p.title = 'flex_effort_const_flex_%d' % flex_index
00323     p.image = str_to_bytes(image)
00324     p.image_format = 'png'
00325     
00326     plt.close()
00327 
00328     return p
00329 
00330 ##\brief Checks shoulder lift efforts against params
00331 ##
00332 ##\return CounterbalanceAnalysisResult
00333 def analyze_lift_efforts(params, data):
00334     result = CounterbalanceAnalysisResult()
00335     
00336     avg_efforts = numpy.array(get_efforts(data, True))
00337     mse = _get_mean_sq_effort(avg_efforts)
00338     avg_abs = _get_mean_abs_effort(avg_efforts)
00339     avg_eff = _get_mean_effort(avg_efforts)
00340 
00341     mse_ok = mse < params.lift_mse
00342     avg_abs_ok = avg_abs < params.lift_avg_abs
00343     avg_eff_ok = abs(avg_eff) < abs(params.lift_avg_eff)
00344 
00345     if mse_ok and avg_abs_ok:
00346         result.summary = 'Lift efforts OK'
00347     elif avg_eff < 0:
00348         result.summary = 'Counterbalance is too weak. Requires adjustment'
00349     else:
00350         result.summary = 'Counterbalance is too strong. Requires adjustment'
00351 
00352     html = ['<p>%s</p>' % result.summary]
00353     
00354     html.append('<table border="1" cellpadding="2" cellspacing="0">')
00355     html.append('<tr><td><b>Parameter</b></td><td><b>Value</b></td><td><b>Maximum</b></td><td><b>Status</b></td></tr>')
00356     html.append('<tr><td><b>Mean Sq. Effort</b></td><td>%.2f</td><td>%.2f</td><td>%s</td></tr>' % (mse, params.lift_mse, ok_dict[mse_ok]))
00357     html.append('<tr><td><b>Average Abs. Effort</b></td><td>%.2f</td><td>%.2f</td><td>%s</td></tr>' % (avg_abs, params.lift_avg_abs, ok_dict[avg_abs_ok]))
00358     html.append('<tr><td><b>Average Effort</b></td><td>%.2f</td><td>%.2f</td><td>%s</td></tr>' % (avg_eff, params.lift_avg_eff, ok_dict[avg_eff_ok]))
00359     html.append('</table>')
00360 
00361     result.html = '\n'.join(html)
00362 
00363     result.result = mse_ok and avg_abs_ok
00364     
00365     result.values = []
00366     result.values.append(TestValue('Lift MSE', str(mse), '', str(params.lift_mse)))
00367     result.values.append(TestValue('Lift Avg. Abs. Effort', str(avg_abs), '', str(params.lift_avg_abs)))
00368     result.values.append(TestValue('Lift Avg.  Effort', str(avg_eff), '', str(params.lift_avg_eff)))
00369 
00370     return result
00371     
00372 ##\brief Checks shoulder flex efforts against params
00373 ##
00374 ##\return CounterbalanceAnalysisResult
00375 def analyze_flex_efforts(params, data):
00376     result = CounterbalanceAnalysisResult()
00377     
00378     avg_efforts = numpy.array(get_efforts(data, False))
00379     mse = _get_mean_sq_effort(avg_efforts)
00380     avg_abs = _get_mean_abs_effort(avg_efforts)
00381     avg_eff = _get_mean_effort(avg_efforts)
00382 
00383     mse_ok = mse < params.flex_mse
00384     avg_abs_ok = avg_abs < params.flex_avg_abs
00385     avg_eff_ok = abs(avg_eff) < abs(params.flex_avg_eff)
00386 
00387     if mse_ok and avg_abs_ok:
00388         result.summary = 'Flex efforts OK'
00389     else:
00390         result.summary = 'Flex MSE/Avg. Absolute effort too high'
00391 
00392     html = ['<p>%s</p>' % result.summary]
00393     
00394     html.append('<table border="1" cellpadding="2" cellspacing="0">')
00395     html.append('<tr><td><b>Parameter</b></td><td><b>Value</b></td><td><b>Maximum</b></td><td><b>Status</b></td></tr>')
00396     html.append('<tr><td><b>Mean Sq. Effort</b></td><td>%.2f</td><td>%.2f</td><td>%s</td></tr>' % (mse, params.flex_mse, ok_dict[mse_ok]))
00397     html.append('<tr><td><b>Average Abs. Effort</b></td><td>%.2f</td><td>%.2f</td><td>%s</td></tr>' % (avg_abs, params.flex_avg_abs, ok_dict[avg_abs_ok]))
00398     html.append('<tr><td><b>Average Effort</b></td><td>%.2f</td><td>%.2f</td><td>%s</td></tr>' % (avg_eff, params.flex_avg_eff, ok_dict[avg_eff_ok]))
00399     html.append('</table>')
00400 
00401     result.html = '\n'.join(html)
00402 
00403     result.result = mse_ok and avg_abs_ok
00404     
00405     result.values = []
00406     result.values.append(TestValue('Flex MSE', str(mse), '', str(params.flex_mse)))
00407     result.values.append(TestValue('Flex Avg. Abs. Effort', str(avg_abs), '', str(params.flex_avg_abs)))
00408     result.values.append(TestValue('Flex Avg.  Effort', str(avg_eff), '', str(params.flex_avg_eff)))
00409 
00410     return result
00411 
00412 ##\brief Calculates CB adjustment 
00413 ##
00414 ##\return (secondary, arm_gimbal) : Turns CW
00415 def calc_cb_adjust(data, model_file):
00416     try:
00417         # Use this to tune to last known "good" position
00418         # A = numpy.load(model_file).transpose()
00419         
00420         # This uses minimum of total torque
00421         A = numpy.load(model_file)[:-1].transpose() 
00422         B = numpy.array(get_efforts(data, True) + get_efforts(data, False))
00423         X = numpy.linalg.lstsq(A,B)
00424     except:
00425         print >> sys.stderr, "Unable to calculate CB adjustment. May have incorrect model data"
00426         import traceback
00427         traceback.print_exc()
00428         return (100, 100)
00429 
00430     # Recommended adjustments
00431     secondary = -X[0][0]
00432     cb_bar = X[0][1] # CCW increases force
00433 
00434     return (secondary, cb_bar)
00435 
00436 ##\brief Return CB adjustments to minimize total torque
00437 ##
00438 ## Uses CB adjustment/torque value from the model file. 
00439 ## Model file is generated by 'test_pr2_self_test/counterbalance_training.py'
00440 ##\param params CounterbalanceAnalysisParams
00441 ##\param data CounterbalanceAnalysisData
00442 ##\param str : Filename of model file
00443 def check_cb_adjustment(params, data, model_file):
00444     result = CounterbalanceAnalysisResult()
00445 
00446     (secondary, cb_bar) = calc_cb_adjust(data, model_file)
00447 
00448     if (abs(secondary) > 25 or abs(cb_bar) > 25):
00449         result.result = False
00450         result.summary = 'Unable to calculate CB adjustment. Data may be corrupt or CB may be too far off'
00451         result.html = '<p>Unable to calculate CB adjustment.</p>' 
00452         return result
00453 
00454     secondary_dir = 'CW' if secondary > 0 else 'CCW' 
00455     cb_bar_dir = 'CW' if cb_bar > 0 else 'CCW'
00456     
00457     adjust_msg = '<table border="1" cellpadding="2" cellspacing="0">'
00458     adjust_msg += '<tr><td><b>Adjustment</b></td><td><b>Turns</b></td><td><b>Direction</b></td><td><b>Allowable Tolerance</b></td></tr>\n'
00459     adjust_msg += '<tr><td>Secondary Spring</td><td>%.1f</td><td>%s</td><td>%.1f</td></tr>\n' % (abs(secondary), secondary_dir, params.screw_tol)
00460     adjust_msg += '<tr><td>Arm Gimbal Shaft</td><td>%.1f</td><td>%s</td><td>%.1f</td></tr>\n' % (abs(cb_bar), cb_bar_dir, params.bar_tol)
00461     adjust_msg += '</table>\n'
00462 
00463     
00464     if abs(secondary) > params.screw_tol or abs(cb_bar) > params.bar_tol:
00465         result.result = False
00466         result.summary = 'CB adjustment recommended. Follow instructions below to tune CB. '
00467         result.html = '<p>CB adjustment recommended. Adjusting the counterbalance will increase performance of the arm. (Note: CW = "Clockwise")</p>\n<p>%s</p>\n' % adjust_msg
00468     else:
00469         result.result = True
00470         result.summary = ''
00471         result.html = '<p>No CB adjustment recommended. You may choose to adjust the CB using the following instructions, but this is within tolerance.</p>\n<p>%s</p>' % adjust_msg
00472 
00473 
00474     result.values = [TestValue('Secondary Spring Adjustment', str(secondary), '', str(params.screw_tol)),
00475                      TestValue('CB Bar Adjustment', str(cb_bar), '', str(params.bar_tol))]
00476 
00477     return result


pr2_counterbalance_check
Author(s): Kevin Watts
autogenerated on Mon Sep 14 2015 14:39:10