aboutsummaryrefslogtreecommitdiffstats
path: root/scripts/calibrate_shaper.py
diff options
context:
space:
mode:
authorDmitry Butyugin <dmbutyugin@google.com>2020-10-15 02:08:10 +0200
committerGitHub <noreply@github.com>2020-10-14 20:08:10 -0400
commitf8c4f90c049995b5ec8c15389a5065d1ae705030 (patch)
tree80c5366a7a892b95b0100e03e1cc3451172dab0f /scripts/calibrate_shaper.py
parentfac4e53e86b4677c6745a9df915d6e3ac21b4855 (diff)
downloadkutter-f8c4f90c049995b5ec8c15389a5065d1ae705030.tar.gz
kutter-f8c4f90c049995b5ec8c15389a5065d1ae705030.tar.xz
kutter-f8c4f90c049995b5ec8c15389a5065d1ae705030.zip
resonance_tester: Resonance testing and input shaper auto-calibration (#3381)
Signed-off-by: Dmitry Butyugin <dmbutyugin@google.com>
Diffstat (limited to 'scripts/calibrate_shaper.py')
-rwxr-xr-xscripts/calibrate_shaper.py166
1 files changed, 166 insertions, 0 deletions
diff --git a/scripts/calibrate_shaper.py b/scripts/calibrate_shaper.py
new file mode 100755
index 00000000..22b32b23
--- /dev/null
+++ b/scripts/calibrate_shaper.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python2
+# Shaper auto-calibration script
+#
+# Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
+# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
+#
+# This file may be distributed under the terms of the GNU GPLv3 license.
+from __future__ import print_function
+import optparse, os, sys
+import numpy as np, matplotlib
+sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)),
+ '..', 'klippy', 'extras'))
+from shaper_calibrate import CalibrationData, ShaperCalibrate
+
+def parse_log(logname):
+ with open(logname) as f:
+ for header in f:
+ if not header.startswith('#'):
+ break
+ if not header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'):
+ # Raw accelerometer data
+ return np.loadtxt(logname, comments='#', delimiter=',')
+ # Parse power spectral density data
+ data = np.loadtxt(logname, skiprows=1, comments='#', delimiter=',')
+ calibration_data = CalibrationData(
+ freq_bins=data[:,0], psd_sum=data[:,4],
+ psd_x=data[:,1], psd_y=data[:,2], psd_z=data[:,3])
+ calibration_data.set_numpy(np)
+ # If input shapers are present in the CSV file, the frequency
+ # response is already normalized to input frequencies
+ if 'mzv' not in header:
+ calibration_data.normalize_to_frequencies()
+ return calibration_data
+
+######################################################################
+# Shaper calibration
+######################################################################
+
+# Find the best shaper parameters
+def calibrate_shaper(datas, csv_output):
+ helper = ShaperCalibrate(printer=None)
+ if isinstance(datas[0], CalibrationData):
+ calibration_data = datas[0]
+ for data in datas[1:]:
+ calibration_data.join(data)
+ else:
+ # Process accelerometer data
+ calibration_data = helper.process_accelerometer_data(datas[0])
+ for data in datas[1:]:
+ calibration_data.join(helper.process_accelerometer_data(data))
+ calibration_data.normalize_to_frequencies()
+ shaper_name, shaper_freq, shapers_vals = helper.find_best_shaper(
+ calibration_data, print)
+ print("Recommended shaper is %s @ %.1f Hz" % (shaper_name, shaper_freq))
+ if csv_output is not None:
+ helper.save_calibration_data(
+ csv_output, calibration_data, shapers_vals)
+ return shaper_name, shapers_vals, calibration_data
+
+######################################################################
+# Plot frequency response and suggested input shapers
+######################################################################
+
+def plot_freq_response(calibration_data, shapers_vals,
+ selected_shaper, max_freq):
+ freqs = calibration_data.freq_bins
+ psd = calibration_data.psd_sum[freqs <= max_freq]
+ px = calibration_data.psd_x[freqs <= max_freq]
+ py = calibration_data.psd_y[freqs <= max_freq]
+ pz = calibration_data.psd_z[freqs <= max_freq]
+ freqs = freqs[freqs <= max_freq]
+
+ fontP = matplotlib.font_manager.FontProperties()
+ fontP.set_size('x-small')
+
+ fig, ax = matplotlib.pyplot.subplots()
+ ax.set_xlabel('Frequency, Hz')
+ ax.set_xlim([0, max_freq])
+ ax.set_ylabel('Power spectral density')
+
+ ax.plot(freqs, psd, label='X+Y+Z', color='purple')
+ ax.plot(freqs, px, label='X', color='red')
+ ax.plot(freqs, py, label='Y', color='green')
+ ax.plot(freqs, pz, label='Z', color='blue')
+
+ if shapers_vals:
+ ax.set_title("Frequency response and shapers")
+ else:
+ ax.set_title("Frequency response")
+ ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
+ ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
+ ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
+ ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
+ ax.ticklabel_format(axis='y', style='scientific', scilimits=(0,0))
+ ax.grid(which='major', color='grey')
+ ax.grid(which='minor', color='lightgrey')
+ ax.legend(loc='upper right', prop=fontP)
+
+ if shapers_vals:
+ ax2 = ax.twinx()
+ ax2.set_ylabel('Shaper vibration reduction (ratio)')
+ best_shaper_vals = None
+ for name, freq, vals in shapers_vals:
+ label = "%s (%.1f Hz)" % (name.upper(), freq)
+ linestyle = 'dotted'
+ if name == selected_shaper:
+ label += ' (selected)'
+ linestyle = 'dashdot'
+ best_shaper_vals = vals
+ ax2.plot(freqs, vals, label=label, linestyle=linestyle)
+ ax.plot(freqs, psd * best_shaper_vals,
+ label='After\nshaper', color='cyan')
+ ax2.legend(loc='upper left', prop=fontP)
+
+ fig.tight_layout()
+ return fig
+
+######################################################################
+# Startup
+######################################################################
+
+def setup_matplotlib(output_to_file):
+ global matplotlib
+ if output_to_file:
+ matplotlib.rcParams.update({'figure.autolayout': True})
+ matplotlib.use('Agg')
+ import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager
+ import matplotlib.ticker
+
+def main():
+ # Parse command-line arguments
+ usage = "%prog [options] <logs>"
+ opts = optparse.OptionParser(usage)
+ opts.add_option("-o", "--output", type="string", dest="output",
+ default=None, help="filename of output graph")
+ opts.add_option("-c", "--csv", type="string", dest="csv",
+ default=None, help="filename of output csv file")
+ opts.add_option("-f", "--max_freq", type="float", default=200.,
+ help="maximum frequency to graph")
+ options, args = opts.parse_args()
+ if len(args) < 1:
+ opts.error("Incorrect number of arguments")
+
+ # Parse data
+ datas = [parse_log(fn) for fn in args]
+
+ # Calibrate shaper and generate outputs
+ selected_shaper, shapers_vals, calibration_data = calibrate_shaper(
+ datas, options.csv)
+
+ if not options.csv or options.output:
+ # Draw graph
+ setup_matplotlib(options.output is not None)
+
+ fig = plot_freq_response(calibration_data, shapers_vals,
+ selected_shaper, options.max_freq)
+
+ # Show graph
+ if options.output is None:
+ matplotlib.pyplot.show()
+ else:
+ fig.set_size_inches(8, 6)
+ fig.savefig(options.output)
+
+if __name__ == '__main__':
+ main()