hysteresis_analysis.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 #
00003 # Copyright (c) 2009, Willow Garage, Inc.
00004 # All rights reserved.
00005 #
00006 # Redistribution and use in source and binary forms, with or without
00007 # modification, are permitted provided that the following conditions are met:
00008 #
00009 #     * Redistributions of source code must retain the above copyright
00010 #       notice, this list of conditions and the following disclaimer.
00011 #     * Redistributions in binary form must reproduce the above copyright
00012 #       notice, this list of conditions and the following disclaimer in the
00013 #       documentation and/or other materials provided with the distribution.
00014 #     * Neither the name of the Willow Garage, Inc. nor the names of its
00015 #       contributors may be used to endorse or promote products derived from
00016 #       this software without specific prior written permission.
00017 #
00018 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
00019 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
00020 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
00021 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
00022 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
00023 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
00024 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
00025 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
00026 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
00027 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00028 # POSSIBILITY OF SUCH DAMAGE.
00029 
00030 ##\author Kevin Watts
00031 ##\brief Analyzes test data from joint hysteresis tests
00032 
00033 PKG = "qualification"
00034 import roslib; roslib.load_manifest(PKG)
00035 
00036 import numpy
00037 import math
00038 
00039 import sys
00040 import os
00041 import string
00042 from time import sleep
00043 
00044 # Matplotlib can behave funny, using "Agg" backend is probably the best
00045 import matplotlib
00046 matplotlib.use('Agg')
00047 import matplotlib.pyplot as plt
00048 from StringIO import StringIO
00049 
00050 from pr2_self_test_msgs.msg import Plot, TestParam, TestValue
00051 
00052 class HysteresisParameters(object):
00053     """
00054     Stores Hysteresis test parameters. Can output them as TestParams
00055     """
00056     def __init__(self):
00057         self.joint_name = None
00058         
00059         self.pos_effort = None
00060         self.neg_effort = None
00061         self.range_max  = None
00062         self.range_min  = None
00063         self.slope      = None
00064         
00065         self.p_gain     = None
00066         self.i_gain     = None
00067         self.d_gain     = None
00068         self.i_clamp    = None
00069         self.max_effort = None
00070         
00071         self.timeout    = None
00072         self.velocity   = None
00073         self.tolerance  = None
00074         self.sd_max     = None
00075 
00076     def validate(self):
00077         if self.joint_name is None: return False
00078         if self.pos_effort is None: return False
00079         if self.neg_effort is None: return False
00080         if self.range_max  is None: return False
00081         if self.range_min  is None: return False
00082         if self.slope      is None: return False
00083         if self.timeout    is None: return False
00084         if self.velocity   is None: return False
00085         if self.tolerance  is None: return False
00086         if self.sd_max     is None: return False
00087 
00088         # Don't need to validate PID gains
00089 
00090         return True
00091 
00092     def get_test_params(self):
00093         """
00094         \return [ TestParam ] : 
00095         """
00096         test_params = []
00097         
00098         test_params.append(TestParam("Joint Name", self.joint_name))
00099         test_params.append(TestParam("Positive Effort", str(self.neg_effort)))
00100         test_params.append(TestParam("Negative Effort", str(self.pos_effort)))
00101         test_params.append(TestParam("Max Range", str(self.range_max)))
00102         test_params.append(TestParam("Min Range", str(self.range_min)))
00103         test_params.append(TestParam("Slope", str(self.slope)))
00104         test_params.append(TestParam("Timeout", str(self.timeout)))
00105         test_params.append(TestParam("Velocity", str(self.velocity)))
00106         test_params.append(TestParam("Effort SD Max", str(self.sd_max)))
00107         test_params.append(TestParam("Effort Tolerance", str(self.tolerance)))
00108 
00109         # Need to write PID gains
00110         if self.p_gain is not None:
00111             test_params.append(TestParam("P Gain", str(self.p_gain)))
00112         if self.i_gain is not None:
00113             test_params.append(TestParam("I Gain", str(self.i_gain)))
00114         if self.d_gain is not None:
00115             test_params.append(TestParam("D Gain", str(self.d_gain)))
00116         if self.i_clamp is not None:
00117             test_params.append(TestParam("I Clamp", str(self.i_clamp)))
00118 
00119         return test_params
00120 
00121 def get_test_value(name, value, minv, maxv):
00122     """
00123     Convert values to TestValue. Force converts all arguments to string.
00124     """
00125     return TestValue(str(name), str(value), str(minv), str(maxv))
00126     
00127     
00128 class HysteresisDirectionData(object):
00129     """
00130     Data for one direction in hysteresis test
00131     """
00132     def __init__(self, position, effort, velocity):
00133         self.range_max = max(position)
00134         self.range_min = min(position)
00135 
00136         min_index = int(0.05 * len(position))
00137         max_index = int(0.95 * len(position))
00138 
00139         self.position = numpy.array(position)[min_index: max_index]
00140         self.effort   = numpy.array(effort)  [min_index: max_index]
00141         self.velocity = numpy.array(velocity)[min_index: max_index]
00142 
00143 class HysteresisTestData(object):
00144     """
00145     Data for hysteresis test
00146     """
00147     def __init__(self, positive_data, negative_data):
00148         self.positive = positive_data
00149         self.negative = negative_data
00150 
00151         self.range_max = max(self.positive.range_max, self.negative.range_max)
00152         self.range_min = min(self.positive.range_min, self.negative.range_min)
00153 
00154 class HysteresisTestData2(object):
00155     """
00156     Data for hysteresis test
00157     """
00158     def __init__(self, data):
00159         self.data = data
00160 
00161         self.range_max = max(d.range_max for d in self.data)
00162         self.range_min = min(d.range_min for d in self.data) 
00163 
00164 
00165 class HysteresisAnalysisResult(object):
00166     """Struct to store result data"""
00167     __slots__ = ['html', 'summary', 'result', 'values' ]
00168     def __init__(self):
00169         self.html = ''
00170         self.summary = ''
00171         self.result = False
00172         self.values = []
00173 
00174 ##\brief Checks range of test
00175 ##
00176 ## To pass, we must be a continuous joint, or must have exceeded our min
00177 ## and max ranges.
00178 ##\return  HysteresisAnalysisResult : Result of test
00179 def range_analysis(params, data):
00180     result = HysteresisAnalysisResult()
00181     if (params.range_max == 0 and params.range_min == 0):
00182         result.summary = 'Range: Continuous.'
00183         result.html = '<p>Continuous joint, no range data.</p>\n'
00184         result.result = True
00185         return result
00186 
00187     min_obs = data.range_min 
00188     max_obs = data.range_max 
00189 
00190     fail = '<div class="error">FAIL</div>'
00191     ok = '<div class="pass">OK</div>'
00192 
00193     max_ok = True
00194     min_ok = True
00195     min_status = ok
00196     max_status = ok
00197     # Check to range against expected values
00198     if (min_obs > params.range_min):
00199       min_ok = False
00200       min_status = fail
00201 
00202     if (max_obs < params.range_max):
00203       max_ok = False
00204       max_status = fail
00205 
00206     result.result = min_ok and max_ok
00207 
00208     result.summary = 'Range: FAIL.'
00209     if result.result:
00210       result.summary = 'Range: OK.'
00211 
00212     if result.result:
00213       result.html = '<p>Range of motion: OK.</p>\n'
00214     elif max_ok and not min_ok:
00215       result.html = "<p>Mechanism did not reach expected minimum range.</p>\n"
00216     elif min_ok and not max_ok:
00217       result.html = "<p>Mechanism did not reach expected maximum range.</p>\n"
00218     else:
00219       result.html = "<p>Mechanism did not reach expected minimum or maximum range.</p>\n"
00220 
00221     table = ['<table border="1" cellpadding="2" cellspacing="0">\n']
00222     table.append('<tr><td></td><td><b>Observed</b></td><td><b>Expected</b></td><td><b>Status</b></td></tr>\n')
00223     table.append('<tr><td>Maximum</td><td>%.3f</td><td>%.3f</td><td>%s</td></tr>\n' % (max_obs, params.range_max, max_status))
00224     table.append('<tr><td>Minimum</td><td>%.3f</td><td>%.3f</td><td>%s</td></tr>\n' % (min_obs, params.range_min, min_status))
00225     table.append('</table>\n')
00226 
00227     result.html += ''.join(table)
00228 
00229     # Write measurements
00230     result.values.append(get_test_value('Max Range', max_obs, params.range_max, ''))
00231     result.values.append(get_test_value('Min Range', min_obs, '', params.range_min))
00232     
00233     return result
00234 
00235 ##\brief Analyzes test effort
00236 ## 
00237 ## To pass, our average efforts must lie within the tolerances of the min/max efforts
00238 ## and must have their std. dev. below the threshold (sd_max). All percentage quantities 
00239 ## are calculated in percent of difference in effort:
00240 ## sd_value = numpy.std(effort) / (numpy.average(positive) - numpy.average(negative))
00241 ## 
00242 ## Effort tolerances is given by "tolerance". SD tolerance given by "sd_max". Both tolerances
00243 ## given in percent of effort.
00244 ##
00245 ##\return  HysteresisAnalysisResult : Result of test
00246 def effort_analysis(params, data):
00247     result = HysteresisAnalysisResult()
00248     
00249     if isinstance(data, HysteresisTestData):
00250       max_avg = numpy.average(data.positive.effort)
00251       min_avg = numpy.average(data.negative.effort)
00252       max_sd = numpy.std(data.positive.effort)
00253       min_sd = numpy.std(data.negative.effort)
00254     elif isinstance(data, HysteresisTestData2):
00255       pos = numpy.array([])
00256       neg = numpy.array([])
00257 
00258       for i in range(len(data.data)):
00259         if i % 2 == 0:
00260           pos = numpy.append(pos, data.data[i].effort)
00261         else:
00262           neg = numpy.append(neg, data.data[i].effort)
00263 
00264       max_avg = numpy.average(pos)
00265       min_avg = numpy.average(neg)
00266       max_sd = numpy.std(pos)
00267       min_sd = numpy.std(neg)
00268     else:
00269       result.summary = 'Wrong Data Type'
00270       result.html = ["<p>Hysteresis Data has a bad data type: %s</p>\n"%data.__class__ ]
00271       result.result = False
00272       return
00273 
00274     
00275     effort_diff = abs(params.pos_effort - params.neg_effort)
00276     tolerance = params.tolerance * effort_diff
00277     sd_max = params.sd_max * effort_diff
00278 
00279     max_ok = abs(max_avg - params.pos_effort) < tolerance
00280     min_ok = abs(min_avg - params.neg_effort) < tolerance
00281 
00282     max_even = max_sd < sd_max
00283     min_even = min_sd < sd_max
00284     
00285     summary = 'Effort: FAIL.'
00286     if max_ok and min_ok and max_even and min_even:
00287       html = ['<p>Efforts OK.</p>\n']
00288       summary = 'Effort: OK.'
00289       result.result = True
00290     elif max_ok and min_ok:
00291       html = ['<p>Efforts have acceptable average, but are uneven.</p>\n']
00292     elif max_avg - params.pos_effort > tolerance and params.neg_effort - min_avg > tolerance:
00293       html = ['<p>Effort is too high.</p>\n']
00294     elif max_avg - params.pos_effort < tolerance and params.neg_effort - min_avg < tolerance:
00295       html = ['<p>Effort is too low.</p>\n']
00296     else:
00297       html = ['<p>Efforts are outside acceptable parameters. See graphs.</p>\n']
00298       
00299     # Standard deviations in percent of value
00300     sd_min_percent = abs(min_sd / effort_diff) * 100
00301     sd_max_percent = abs(max_sd / effort_diff) * 100
00302 
00303     if max_even:
00304       positive_sd_msg = '<div class="pass">OK</div>'
00305     else:
00306       positive_sd_msg = '<div class="warning">UNEVEN</div>'
00307 
00308     if min_even:
00309       negative_sd_msg = '<div class="pass">OK</div>'
00310     else:
00311       negative_sd_msg = '<div class="warning">UNEVEN</div>'
00312       
00313     if max_ok:
00314       positive_msg = '<div class="pass">OK</div>'
00315     else:
00316       positive_msg = '<div class="error">FAIL</div>'
00317 
00318     if min_ok:
00319       negative_msg = '<div class="pass">OK</div>'
00320     else:
00321       negative_msg = '<div class="error">FAIL</div>'
00322 
00323     html.append('<table border="1" cellpadding="2" cellspacing="0">\n')
00324     html.append('<tr><td><b>Name</b></td><td><b>Average</b></td><td><b>Expected</b></td><td><b>Status</b></td><td><b>SD %</b></td><td><b>Status</b></td></tr>\n')
00325     html.append('<tr><td>Positive</td><td>%.2f</td><td>%.2f</td><td>%s</td><td>%.2f</td><td>%s</td></tr>\n' % (max_avg, params.pos_effort, positive_msg, sd_max_percent, positive_sd_msg))
00326     html.append('<tr><td>Negative</td><td>%.2f</td><td>%.2f</td><td>%s</td><td>%.2f</td><td>%s</td></tr>\n' % (min_avg, params.neg_effort, negative_msg, sd_min_percent, negative_sd_msg)) 
00327     html.append('</table><br>\n')
00328     html.append('<p>Effort tolerance: %.2f. SD tolerance: %.1f percent</p>\n' % (tolerance, params.sd_max * 100))
00329 
00330     result.html = ''.join(html)
00331     result.summary = summary
00332 
00333     result.values = []
00334     result.values.append(get_test_value('Positive Effort Avg', max_avg, params.pos_effort - tolerance, params.pos_effort + tolerance))
00335     result.values.append(get_test_value('Positive Effort SD', max_sd, '', sd_max))
00336     result.values.append(get_test_value('Positive Effort SD Percent', sd_max_percent, '', params.sd_max))
00337 
00338     result.values.append(get_test_value('Negative Effort Avg', min_avg, params.neg_effort - tolerance, params.neg_effort + tolerance))
00339     result.values.append(get_test_value('Negative Effort SD', min_sd, '', sd_max))
00340     result.values.append(get_test_value('Positive Effort SD Percent', sd_min_percent, '', params.sd_max))
00341 
00342     return result
00343 
00344 ##\brief Analyzes velocity data 
00345 ## 
00346 ## Reports velocity RMS, Avg and STD, but doesn't enforce any pass/fail standard
00347 ##\return  HysteresisAnalysisResult : Result of test
00348 def velocity_analysis(params, data):
00349     html = ['<p>Search velocity: %.2f.</p><br>\n' % abs(params.velocity)]
00350 
00351     if isinstance(data, HysteresisTestData):
00352       pos_avg = numpy.average(data.positive.velocity)
00353       pos_sd = numpy.std(data.positive.velocity)
00354       pos_rms = math.sqrt(numpy.average( (data.positive.velocity - params.velocity) * (data.positive.velocity - params.velocity) ))
00355   
00356       neg_avg = numpy.average(data.negative.velocity)
00357       neg_sd = numpy.std(data.negative.velocity)
00358       neg_rms = math.sqrt(numpy.average( (data.negative.velocity - params.velocity) * (data.negative.velocity - params.velocity) ))
00359     elif isinstance(data, HysteresisTestData2):
00360       pos = numpy.array([])
00361       neg = numpy.array([])
00362 
00363       for i in range(len(data.data)):
00364         if i % 2 == 0:
00365           pos = numpy.append(pos, data.data[i].effort)
00366         else:
00367           neg = numpy.append(neg, data.data[i].effort)
00368 
00369       pos_avg = numpy.average(pos)
00370       pos_sd = numpy.std(pos)
00371       pos_rms = math.sqrt(numpy.average( (pos - params.velocity) * (pos - params.velocity) ))
00372 
00373       neg_avg = numpy.average(neg)
00374       neg_sd = numpy.std(neg)
00375       neg_rms = math.sqrt(numpy.average( (neg - params.velocity) * (neg - params.velocity) ))
00376     else:
00377       result.summary = 'Wrong Data Type'
00378       result.html = ["<p>Hysteresis Data has a bad data type: %s</p>\n"%data.__class__ ]
00379       result.result = False
00380       return
00381  
00382     html.append('<table border="1" cellpadding="2" cellspacing="0">\n')
00383     html.append('<tr><td></td><td><b>Average</b></td><td><b>Std. Dev.</b></td><td><b>RMS</b></td></tr>\n')
00384     html.append('<tr><td>Positive</td><td>%.2f</td><td>%.3f</td><td>%.3f</td></tr>\n' % (pos_avg, pos_sd, pos_rms))
00385     html.append('<tr><td>Negative</td><td>%.2f</td><td>%.3f</td><td>%.3f</td></tr>\n' % (neg_avg, neg_sd, neg_rms))
00386     html.append('</table>\n')
00387     
00388     result = HysteresisAnalysisResult()
00389     result.result = True
00390     result.html = ''.join(html)
00391 
00392     result.values.append(get_test_value('Positive Velocity Avg', pos_avg, '', ''))
00393     result.values.append(get_test_value('Positive Velocity SD', pos_sd, '', ''))
00394     result.values.append(get_test_value('Positive Velocity RMS', pos_rms, '', ''))
00395     result.values.append(get_test_value('Negative Velocity Avg',neg_avg, '', ''))
00396     result.values.append(get_test_value('Negative Velocity SD', neg_sd, '', ''))
00397     result.values.append(get_test_value('Negative Velocity RMS', neg_rms, '', ''))
00398 
00399     return result
00400 
00401 ##\brief Checks slope of test data (effort v. position)
00402 ##
00403 ## Some components have a "slope" to their position/effort curves. This checks 
00404 ## that the slope is within range and the intercepts are OK.
00405 ## Slope value is given by "slope" field of params. Tolerance from "sd_max"
00406 ## Intercepts are "pos_effort" and "neg_effort" fields. Tolerance from "tolerance".
00407 ##\return  HysteresisAnalysisResult : Result of test
00408 def regression_analysis(params, data):
00409     result = HysteresisAnalysisResult()    
00410 
00411     A_pos = numpy.vstack([data.positive.position, numpy.ones(len(data.positive.position))]).T
00412     A_neg = numpy.vstack([data.negative.position, numpy.ones(len(data.negative.position))]).T
00413     
00414     # y = ax + b
00415     a_max, b_max = numpy.linalg.lstsq(A_pos, data.positive.effort)[0]
00416     a_min, b_min = numpy.linalg.lstsq(A_neg, data.negative.effort)[0]
00417 
00418     # Check min/max slopes close together
00419     slope_avg = 0.5 * (a_max + a_min)
00420     slope_diff = abs((a_max - a_min)) # / slope_avg)
00421     slope_sd_tol = abs(params.slope * params.sd_max)
00422 
00423     diff_ok = slope_diff < slope_sd_tol
00424     pos_ok = abs(params.slope - a_max) < slope_sd_tol
00425     neg_ok = abs(params.slope - a_min) < slope_sd_tol
00426 
00427     tol_intercept = params.tolerance * (params.pos_effort - params.neg_effort)
00428     pos_int_ok = abs(b_max - params.pos_effort) < tol_intercept
00429     neg_int_ok = abs(b_min - params.neg_effort) < tol_intercept
00430 
00431     slope_ok = diff_ok and pos_ok and neg_ok and pos_int_ok and neg_int_ok
00432 
00433     ok_dict = {True: 'OK', False: 'FAIL'}
00434 
00435     if slope_ok:
00436       result.summary = "Effort: OK"
00437       result.result = True
00438     else:
00439       result.summary = "Effort: FAIL"
00440     
00441     ##\todo Check if slope is significant....
00442     html = ['<p>Simple linear regression on the effort and position plot data. See tables below for slopes, intercepts of effort plot. Both the positive and negative slopes must be in tolerance, and the difference between slopes must be acceptable.</p>\n']
00443     html.append('<table border="1" cellpadding="2" cellspacing="0">\n')
00444     html.append('<tr><td><b>Name</b></td><td><b>Slope</b></td><td><b>Expected</b><td><b>Tolerance</b></td><td><b>Passed</b></td>\n')
00445     html.append('<tr><td>Positive</td><td>%.3f</td><td>%.3f</td><td>%.3f</td><td>%s</td></tr>\n' % (a_max, params.slope, slope_sd_tol, ok_dict[pos_ok]))
00446     html.append('<tr><td>Negative</td><td>%.3f</td><td>%.3f</td><td>%.3f</td><td>%s</td></tr>\n' % (a_min, params.slope, slope_sd_tol, ok_dict[neg_ok]))
00447     html.append('<tr><td>Difference</td><td>%.3f</td><td>%.3f</td><td>%.3f</td><td>%s</td></tr>\n' % (a_max - a_min, 0, slope_sd_tol, ok_dict[diff_ok]))
00448     html.append('</table>\n')
00449 
00450     html.append('<p>Slope intercepts must be within tolerance of expected values, or the average effort is incorrect.</p>\n')
00451     html.append('<table border="1" cellpadding="2" cellspacing="0">\n')
00452     html.append('<tr><td><b>Name</b></td><td><b>Intercept</b></td><td><b>Expected</b><td><b>Tolerance</b></td><td><b>Passed</b></td>\n')
00453     html.append('<tr><td>Positive</td><td>%.3f</td><td>%.3f</td><td>%.3f</td><td>%s</td></tr>\n' % (b_max, params.pos_effort, tol_intercept, ok_dict[pos_int_ok]))
00454     html.append('<tr><td>Negative</td><td>%.3f</td><td>%.3f</td><td>%.3f</td><td>%s</td></tr>\n' % (b_min, params.neg_effort, tol_intercept, ok_dict[neg_int_ok]))
00455     html.append('</table>\n')
00456     
00457     result.html = ''.join(html)
00458 
00459     result.values.append(get_test_value('Slope Positive', a_max, params.slope - slope_sd_tol, params.slope + slope_sd_tol))
00460     result.values.append(get_test_value('Slope Negative', a_min, params.slope - slope_sd_tol, params.slope + slope_sd_tol))
00461     result.values.append(get_test_value('Intercept Positive', b_max, params.pos_effort - tol_intercept, params.pos_effort + tol_intercept))
00462     result.values.append(get_test_value('Intercept Negative', b_min, params.neg_effort - tol_intercept, params.neg_effort + tol_intercept))
00463 
00464     return result
00465 
00466 ## Plot effort
00467 ##\return pr2_self_test_msgs/Plot :
00468 def plot_effort(params, data):
00469     # Plot the analyzed data
00470     fig = plt.figure(1)
00471     axes1 = fig.add_subplot(211)
00472     axes2 = fig.add_subplot(212)
00473     axes2.set_xlabel('Position (Radians or Meters)')
00474     axes1.set_ylabel('Effort (Nm or N)')
00475     axes2.set_ylabel('Effort (Nm or N)')
00476     axes1.plot(data.positive.position, data.positive.effort, 'b--', label='_nolegend_')
00477     axes2.plot(data.negative.position, data.negative.effort, 'b--', label='_nolegend_')
00478 
00479     max_avg = numpy.average(data.positive.effort)
00480     min_avg = numpy.average(data.negative.effort)
00481     max_sd = numpy.std(data.positive.effort)
00482     min_sd = numpy.std(data.negative.effort)
00483 
00484     axes1.axhline(y = max_avg, color = 'r', label='Average')
00485     axes1.axhline(y = max_avg + max_sd, color = 'y', label='Error bars')
00486     axes1.axhline(y = max_avg - max_sd, color = 'y', label='_nolegend_')
00487     
00488     axes2.axhline(y = min_avg, color = 'r', label='Average')
00489     axes2.axhline(y = min_avg - min_sd, color = 'y', label='Error bars')
00490     axes2.axhline(y = min_avg + min_sd, color = 'y', label='_nolegend_')
00491             
00492     # Add expected efforts to both plots
00493     axes1.axhline(y = params.pos_effort, color = 'g', label='Expected')
00494     axes2.axhline(y = params.neg_effort, color = 'g', label='Expected')
00495     
00496     fig.text(.3, .95, params.joint_name + ' Hysteresis Efforts')
00497     #axes1.legend(shadow=True)
00498 
00499     stream = StringIO()
00500     plt.savefig(stream, format = "png")
00501     image = stream.getvalue()
00502     plt.close()
00503     
00504     p = Plot()
00505     p.title = params.joint_name + "_hysteresis1"
00506     p.image = image
00507     p.image_format = "png"
00508 
00509     return p
00510 
00511 ## Plot effort
00512 ##\return pr2_self_test_msgs/Plot[] :
00513 def plot_efforts(params, data):
00514     plots = []
00515     for i, d in enumerate(data.data):
00516       # Plot the analyzed data
00517       fig = plt.figure(2)
00518       plt.xlabel('Position (Radians or Meters)')
00519       plt.ylabel('Effort (Nm or N)')
00520       plt.plot(d.position, d.effort, 'b--', label='_nolegend_')
00521   
00522       avg = numpy.average(d.effort)
00523       sd = numpy.std(d.effort)
00524   
00525       plt.axhline(y = avg, color = 'r', label='Average')
00526       plt.axhline(y = avg + sd, color = 'y', label='Error bars')
00527       plt.axhline(y = avg - sd, color = 'y', label='_nolegend_')
00528       
00529       # Add expected efforts to both plots
00530       if i % 2 == 0:
00531         plt.axhline(y = params.pos_effort, color = 'g', label='Expected')
00532       else:
00533         plt.axhline(y = params.neg_effort, color = 'g', label='Expected')
00534       
00535       fig.text(.3, .95, params.joint_name + ' Hysteresis Effort %d'%i)
00536   
00537       stream = StringIO()
00538       plt.savefig(stream, format = "png")
00539       image = stream.getvalue()
00540       plt.close()
00541       
00542       p = Plot()
00543       p.title = params.joint_name + "_hysteresis1_%d"%i
00544       p.image = image
00545       p.image_format = "png"
00546       plots.append(p)
00547 
00548     return plots
00549 
00550 ## Plot velocity
00551 ##\return pr2_self_test_msgs/Plot :
00552 def plot_velocity(params, data):
00553     fig = plt.figure(2)
00554     plt.ylabel('Velocity')
00555     plt.xlabel('Position')
00556     plt.plot(numpy.array(data.positive.position), numpy.array(data.positive.velocity), 'b--', label='Data')
00557     plt.plot(numpy.array(data.negative.position), numpy.array(data.negative.velocity), 'b--', label='_nolegened_')
00558     plt.axhline(y = params.velocity, color = 'g', label = 'Command')
00559     plt.axhline(y = -1 * params.velocity, color = 'g', label = '_nolegend_')
00560     
00561     fig.text(.3, .95, params.joint_name + ' Hysteresis Velocity')
00562     plt.legend(shadow=True)
00563     
00564     stream = StringIO()
00565     plt.savefig(stream, format = "png")
00566     image = stream.getvalue()
00567     
00568     p = Plot()
00569     p.title = params.joint_name + "_hysteresis2"
00570     p.image = image
00571     p.image_format = "png"
00572     
00573     plt.close()
00574     
00575     return p
00576 
00577 ## Plot velocities
00578 ##\return pr2_self_test_msgs/Plot[] :
00579 def plot_velocities(params, data):
00580     plots = []
00581     for i, d in enumerate(data.data):
00582       fig = plt.figure(2)
00583       plt.ylabel('Velocity')
00584       plt.xlabel('Position')
00585       plt.plot(numpy.array(d.position), numpy.array(d.velocity), 'b--', label='Data')
00586       plt.axhline(y = params.velocity, color = 'g', label = 'Command')
00587       plt.axhline(y = -1 * params.velocity, color = 'g', label = '_nolegend_')
00588       
00589       fig.text(.3, .95, params.joint_name + ' Hysteresis Velocity')
00590       plt.legend(shadow=True)
00591       
00592       stream = StringIO()
00593       plt.savefig(stream, format = "png")
00594       image = stream.getvalue()
00595       
00596       p = Plot()
00597       p.title = params.joint_name + "_hysteresis2_%d"%i
00598       p.image = image
00599       p.image_format = "png"
00600       
00601       plt.close()
00602       plots.append(p)
00603     
00604     return plots
00605 
00606 # Wrist analysis starts here
00607 
00608 class WristRollHysteresisTestData(HysteresisTestData):
00609     """Struct to hold wrist test data"""
00610     def __init__(self, msg):
00611         left_min = int(0.05 * len(msg.left_turn.roll_position))
00612         left_max = int(0.95 * len(msg.left_turn.roll_position))
00613         right_min = int(0.05 * len(msg.right_turn.roll_position))
00614         right_max = int(0.95 * len(msg.right_turn.roll_position))
00615 
00616         self.pos_flex_effort = numpy.array(msg.left_turn.flex_effort) [left_min: left_max]
00617         self.neg_flex_effort = numpy.array(msg.right_turn.flex_effort)[right_min: right_max]
00618 
00619         self.positive = HysteresisDirectionData(msg.left_turn.roll_position, msg.left_turn.roll_effort, msg.left_turn.roll_velocity)
00620         self.negative = HysteresisDirectionData(msg.right_turn.roll_position, msg.right_turn.roll_effort, msg.right_turn.roll_velocity)
00621 
00622 class WristRollHysteresisParams(HysteresisParameters):
00623     """Wrist test parameters"""
00624     def __init__(self, msg):
00625         self.joint_name  = msg.roll_joint
00626         self.p_gain      = msg.roll_pid[0]
00627         self.i_gain      = msg.roll_pid[1]
00628         self.d_gain      = msg.roll_pid[2]
00629         self.i_clamp     = msg.roll_pid[3]
00630 
00631         self.velocity    = msg.arg_value[1]
00632         self.tolerance   = msg.arg_value[2]
00633         self.sd_max      = msg.arg_value[3]
00634 
00635         self.timeout     = msg.arg_value[4]
00636         self.pos_effort  = msg.arg_value[5]
00637         self.neg_effort  = msg.arg_value[6]
00638 
00639         self.range_max   = 0
00640         self.range_min   = 0
00641         self.slope       = 0
00642 
00643         # Subclass params only
00644         self.flex_joint   = msg.flex_joint
00645         self.flex_tol     = msg.arg_value[7]
00646         self.flex_max     = msg.arg_value[8]
00647         self.flex_sd      = msg.arg_value[9]
00648         self.flex_p_gain  = msg.flex_pid[0]
00649         self.flex_i_gain  = msg.flex_pid[1]
00650         self.flex_d_gain  = msg.flex_pid[2]
00651         self.flex_i_clamp = msg.flex_pid[3]
00652 
00653     def get_test_params(self):
00654         test_params = HysteresisParameters.get_test_params(self)
00655 
00656         test_params.append(TestParam("Flex Joint", self.flex_joint))
00657         test_params.append(TestParam("Flex Tolerance", str(self.flex_tol)))
00658         test_params.append(TestParam("Flex Max", str(self.flex_max)))
00659         test_params.append(TestParam("Flex SD", str(self.flex_sd)))
00660         test_params.append(TestParam("Flex P Gain", str(self.flex_p_gain)))
00661         test_params.append(TestParam("Flex I Gain", str(self.flex_i_gain)))
00662         test_params.append(TestParam("Flex D Gain", str(self.flex_d_gain)))
00663         test_params.append(TestParam("Flex I Clamp", str(self.flex_i_clamp)))
00664 
00665         return test_params
00666 
00667 ##\brief Analyzes flex effort during wrist difference test
00668 ## 
00669 ## The nominal flex effort during a wrist roll is zero. This 
00670 ## helps determine wrist symmetry.
00671 ## Flex effort values must be within tolerance:
00672 ##  * flex_max - Maximum allowable flex effort value
00673 ##  * flex_sd  - Std. dev. of the flex effort
00674 ##  * flex_avg - Average value of flex effort
00675 ## All measured values must be less than their corresponding parameters.
00676 ## All parameters given in units of effort (Nm)
00677 ##\return  HysteresisAnalysisResult : Result of test
00678 def wrist_flex_analysis(params, data):
00679     flex_max = numpy.average(data.pos_flex_effort)
00680     flex_max_sd = numpy.std(data.pos_flex_effort)
00681     flex_min = numpy.average(data.neg_flex_effort)
00682     flex_min_sd = numpy.std(data.neg_flex_effort)
00683     flex_max_val = max(numpy.max(data.pos_flex_effort), numpy.max(data.neg_flex_effort))
00684     flex_min_val = min(numpy.min(data.pos_flex_effort), numpy.min(data.neg_flex_effort))
00685     
00686     max_msg = '<div class="pass">OK</div>'
00687     if abs(flex_max) > params.flex_tol:
00688         max_msg = '<div class="error">FAIL</div>'
00689         
00690     min_msg = '<div class="pass">OK</div>'
00691     if abs(flex_min) > params.flex_tol:
00692         min_msg = '<div class="error">FAIL</div>'
00693         
00694     max_val_msg = '<div class="pass">OK</div>'
00695     if abs(flex_max_val) > params.flex_max:
00696         max_val_msg = '<div class="error">FAIL</div>'
00697 
00698     min_val_msg = '<div class="pass">OK</div>'
00699     if abs(flex_min_val) > params.flex_max:
00700         min_val_msg = '<div class="error">FAIL</div>'
00701 
00702     max_sd_msg = '<div class="pass">OK</div>'
00703     if abs(flex_max_sd) > params.flex_sd:
00704         max_sd_msg = '<div class="error">FAIL</div>'
00705         
00706     min_sd_msg = '<div class="pass">OK</div>'
00707     if abs(flex_min_sd) > params.flex_sd:
00708         min_sd_msg = '<div class="error">FAIL</div>'
00709         
00710     ok = abs(flex_max) < params.flex_tol and abs(flex_min) < params.flex_tol
00711     ok = ok and abs(flex_min_val) < params.flex_max and abs(flex_max_val) < params.flex_max
00712     
00713     if ok:
00714         html = ['<p>Wrist flex effort is OK.</p>']
00715     else:
00716         html = ['<p>Wrist flex effort failed. Check graphs.</p>']
00717         
00718     html.append('<p>Flex effort maximum values. Allowed maximum: %.2f</p>' % (params.flex_max))
00719     html.append('<table border="1" cellpadding="2" cellspacing="0">')
00720     html.append('<tr><td></td><td><b>Effort</b></td><td><b>Status</b></td></tr>')
00721     html.append('<tr><td><b>Max Value</b></td><td>%.2f</td><td>%s</td></tr>' % (flex_max_val, max_val_msg))
00722     html.append('<tr><td><b>Min Value</b></td><td>%.2f</td><td>%s</td></tr>' % (flex_min_val, min_val_msg))
00723     html.append('</table>')
00724     
00725     html.append('<br>')
00726     html.append('<p>Flex effort average and noise, both directions. Allowed absolute average: %.2f. Allowed noise: %.2f</p>' % (params.flex_tol, params.flex_sd))
00727     html.append('<table border="1" cellpadding="2" cellspacing="0">')
00728     html.append('<tr><td><b>Effort Avg</b></td><td><b>Status</b></td><td><b>Effort SD</b></td><td><b>SD Status</b></td></tr>')
00729     html.append('<tr><td>%.2f</td><td>%s</td><td>%.2f</td><td>%s</td></tr>' % (flex_max, max_msg, flex_max_sd, max_sd_msg))
00730     html.append('<tr><td>%.2f</td><td>%s</td><td>%.2f</td><td>%s</td></tr>' % (flex_min, min_msg, flex_min_sd, min_sd_msg))
00731     html.append('</table>')
00732     
00733     result = HysteresisAnalysisResult()
00734     result.html = '\n'.join(html)
00735     result.result = ok
00736     
00737     result.values = [
00738         get_test_value('Flex Pos Average', flex_max, '', params.flex_tol),
00739         get_test_value('Flex Pos SD', flex_max_sd, '', params.flex_sd),
00740         get_test_value('Flex Neg Average', flex_min, '', params.flex_tol),
00741         get_test_value('Flex Neg SD', flex_min_sd, '', params.flex_sd),
00742         get_test_value('Flex Max Val', flex_max_val, '', params.flex_max),
00743         get_test_value('Flex Min Val', flex_min_val, '', -1 * params.flex_max) 
00744         ]
00745 
00746     return result
00747 
00748 ##\brief Plots effort of wrist flex during qual
00749 ##\return pr2_self_test_msgs.Plot : 
00750 def plot_flex_effort(params, data):
00751     # Can move to separate plotting function
00752     # Plot the analyzed data
00753     flex_max_tol = params.flex_max
00754     
00755     fig = plt.figure(1)
00756     axes1 = fig.add_subplot(211)
00757     axes2 = fig.add_subplot(212)
00758     axes2.set_xlabel('Roll Position')
00759     axes1.set_ylabel('Effort (+ dir)')
00760     axes2.set_ylabel('Effort (- dir)')
00761     
00762     axes1.plot(data.positive.position, data.pos_flex_effort, 'b--', label='_nolegend_')
00763     axes1.axhline(y = flex_max_tol, color = 'y', label='Max Value')
00764     axes1.axhline(y = - flex_max_tol, color = 'y', label='_nolegend_')
00765     axes1.axhline(y = 0, color = 'r', label='_nolegend_')
00766     
00767     axes2.plot(data.negative.position, data.neg_flex_effort, 'b--', label='_nolegend_')
00768     axes2.axhline(y = flex_max_tol, color = 'y', label='Max Value')
00769     axes2.axhline(y = - flex_max_tol, color = 'y', label='_nolegend_')
00770     axes2.axhline(y = 0, color = 'r', label='_nolegend_')
00771     
00772     fig.text(.4, .95, 'Wrist Flex Effort')
00773     
00774     stream = StringIO()
00775     plt.savefig(stream, format = "png")
00776     image = stream.getvalue()
00777     
00778     p = Plot()
00779     p.title = "wrist_diff_flex_effort"
00780     p.image = image
00781     p.image_format = "png"
00782     
00783     plt.close()
00784     
00785     return p
00786 
00787 ##\return str : Summary
00788 def make_wrist_test_summary(roll_stat, flex_stat):
00789     if flex_stat and roll_stat:
00790         return "Wrist Difference check OK. Motors are symmetric."
00791     if flex_stat and not roll_stat:
00792         return "Wrist roll hysteresis failed. Wrist flex effort OK."
00793     if not flex_stat and roll_stat:
00794         return "Wrist roll hysteresis OK. Wrist flex effort failed."
00795     return "Wrist roll hystereis and flex effort failed."
00796 
00797 ##\brief Checks wrist data before analysis
00798 ##\return bool : True if OK to proceed
00799 def wrist_hysteresis_data_present(data):
00800     return data.positive.position.size > 100 and data.negative.position.size > 100


qualification
Author(s): Kevin Watts (watts@willowgarage.com), Josh Faust (jfaust@willowgarage.com)
autogenerated on Sat Dec 28 2013 17:57:34